Skip to content

代替 Electron 的几种方案

electron 的劣势

  1. 安装包体积大,每装一个 app 等同于装一个 chrome
  2. electron 的窗口是自绘的,因此无法自适应系统主题、控制按钮扩展以及透明效果
  3. electron 的主进程也是 js 写的,涉及到 CPU 较为密集的场景性能烂

代替方案一: tauri

相对于 electron 的优势

  1. tauri 使用了系统自带的 WebView,安装体积非常小
  2. tauri 的窗口仍然是系统的原生窗口
  3. tauri 主进程是 rust 写的,对于 CPU 密集的场景也没有性能问题

tauri 的劣势

  1. rust 有一定的学习成本,大部分前端不会去接触 rust。

tauri 示例

rs
// src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// 定义一个greet方法暴露给前端
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

// 定义一个greet方法暴露给前端
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
vue
<script setup>
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/tauri";

const greetMsg = ref("");
const name = ref("");

async function greet() {
  // 调用 rust 定义的 greet 方法
  greetMsg.value = await invoke("greet", { name: name.value });
}
</script>

<template>
  <form class="row" @submit.prevent="greet">
    <input id="greet-input" v-model="name" placeholder="Enter a name..." />
    <button type="submit">Greet</button>
  </form>

  <p>{{ greetMsg }}</p>
</template>
<script setup>
import { ref } from "vue";
import { invoke } from "@tauri-apps/api/tauri";

const greetMsg = ref("");
const name = ref("");

async function greet() {
  // 调用 rust 定义的 greet 方法
  greetMsg.value = await invoke("greet", { name: name.value });
}
</script>

<template>
  <form class="row" @submit.prevent="greet">
    <input id="greet-input" v-model="name" placeholder="Enter a name..." />
    <button type="submit">Greet</button>
  </form>

  <p>{{ greetMsg }}</p>
</template>

代替方案二: dart + webview

dart 不是只能写 flutter,通过 ffi 调用 win32 与 webview2 也是一种方案。

相对于 electron 的优势

  1. dart + webview 使用了系统自带的 WebView,安装体积非常小
  2. dart 是调用 win32 的窗体
  3. 主进程是 dart 写的,通过 compile 可以编译成 native 程序

dart + webview 的劣势

  1. 没有完整的一套解决方案,很多轮子需要自己造
  2. dart 语言学习成本(不过我感觉比 rust 容易学很多,而且可以用于 flutter)

dart + webview 示例

安装依赖

bash
dart pub add webview_dart
dart pub add webview_dart

yaml
dependencies:
  webview_dart: ^1.0.0
dependencies:
  webview_dart: ^1.0.0

文件结构

创建如下的结构:

 myapp
      |
      +---- out
      |      |
      |      +--- webview.dll
      |      |
      |      +--- webview2loader.dll
      |
      +--- bin
      |     |
      |     +--- myapp.dart
      |
      +---- all other files
 myapp
      |
      +---- out
      |      |
      |      +--- webview.dll
      |      |
      |      +--- webview2loader.dll
      |
      +--- bin
      |     |
      |     +--- myapp.dart
      |
      +---- all other files

编写主入库

dart
import 'package:webview_dart/webview_dart.dart';

void main() {
    final url = "https://www.google.com";
    Webview(false)
        .setTitle("title")
        .setSize(1280, 800, SizeHint.none)
        .navigate(url)
        .run();
}
import 'package:webview_dart/webview_dart.dart';

void main() {
    final url = "https://www.google.com";
    Webview(false)
        .setTitle("title")
        .setSize(1280, 800, SizeHint.none)
        .navigate(url)
        .run();
}

缺失 dll 的问题

可以从 https://github.com/shreyassanthu77/webview_dart/releases 去下载。

注入 JS 脚本(可选)

通过 webview.init("A valid string of Js") 可以向 webview 注入脚本。

比如可以注入禁用 Web 控制台的脚本

js
(function () {
  const keys = {};
  document.addEventListener("keydown", function (event) {
    keys[event.key] = true;
    const cond = (keys["I"] && keys["Shift"] && keys["Control"]) || keys["F12"];
    !cond || event.preventDefault();
  });
  document.addEventListener("keyup", function (event) {
    keys[event.key] = false;
  });
  document.addEventListener("contextmenu", function (event) {
    event.preventDefault();
  });
})();
(function () {
  const keys = {};
  document.addEventListener("keydown", function (event) {
    keys[event.key] = true;
    const cond = (keys["I"] && keys["Shift"] && keys["Control"]) || keys["F12"];
    !cond || event.preventDefault();
  });
  document.addEventListener("keyup", function (event) {
    keys[event.key] = false;
  });
  document.addEventListener("contextmenu", function (event) {
    event.preventDefault();
  });
})();

如何在 win 上隐藏黑窗口

bash
dart pub add win32
dart pub add win32
dart
import 'package:win32/win32.dart';
//...
ShowWindow(GetConsoleWindow(), SW_HIDE);
//...
import 'package:win32/win32.dart';
//...
ShowWindow(GetConsoleWindow(), SW_HIDE);
//...

绑定原生方法

dart
webview.bind("jsName", (List<dynamic> args) {
    // some code
})
webview.bind("jsName", (List<dynamic> args) {
    // some code
})

除了绑定原生方法,也可以通过 http 服务来与 dart 交互

我使用的是 alfred

yaml
name: dart_webview
description: A sample command-line application.
version: 1.0.0

environment:
  sdk: ">=3.0.0"

dependencies:
  alfred: ^1.1.1
  webview_dart: ^1.0.0
  win32: ^5.0.7

dev_dependencies:
  lints: ^2.0.0
  test: ^1.21.0
name: dart_webview
description: A sample command-line application.
version: 1.0.0

environment:
  sdk: ">=3.0.0"

dependencies:
  alfred: ^1.1.1
  webview_dart: ^1.0.0
  win32: ^5.0.7

dev_dependencies:
  lints: ^2.0.0
  test: ^1.21.0

将主入口改造为:

dart
import 'dart:isolate';

import 'package:webview_dart/webview_dart.dart';
import 'package:alfred/alfred.dart';
import 'package:win32/win32.dart';

void main() {
  Isolate.spawn(init, []).then((value) {
    ShowWindow(GetConsoleWindow(), SW_HIDE);
    final url = "http://127.0.0.1:8080/example";
    Webview(false).setTitle("title").setSize(1280, 800, SizeHint.none).navigate(url).run();
  });
}

void init(List<dynamic> args) async {
  final app = Alfred();

  app.get('/example', (req, res) => 'Hello world');

  await app.listen(8080);
}
import 'dart:isolate';

import 'package:webview_dart/webview_dart.dart';
import 'package:alfred/alfred.dart';
import 'package:win32/win32.dart';

void main() {
  Isolate.spawn(init, []).then((value) {
    ShowWindow(GetConsoleWindow(), SW_HIDE);
    final url = "http://127.0.0.1:8080/example";
    Webview(false).setTitle("title").setSize(1280, 800, SizeHint.none).navigate(url).run();
  });
}

void init(List<dynamic> args) async {
  final app = Alfred();

  app.get('/example', (req, res) => 'Hello world');

  await app.listen(8080);
}

附录

最后编辑时间:

Version 4.0 (framework-1.0.0-rc.20)