前端系列(2),electron 实践记录

274 阅读3分钟

背景是这样的,公司有一个桌面端应用的开发需求,现需要支持 Win/Mac/Linux 平台,经调研后,采用了 Electron 来进行跨平台开发。

因为是第一次接触 Electron,经验为零,所以可以预料到,接下来会很难过,所以就有了这篇文章,我会将本次开发中所走的弯路都记录下来,希望能给后来者提供一些帮助。

文章会随着项目进度,持续更新到 9 月底项目交付。

因为要在 Electron 中使用 React,所以我直接使用了 electron-react-boilerplate 模版,初期我都会在 Mac 上进行开发,列表见下:

install failed

遇到的第一个问题就是 electron 安装不上啊,经过一顿 google,找到了下面方案,实测有效:

  npm install electron --save-dev
  改为:
  npm install -g cnpm --registry=https://registry.npmmirror.com
  cnpm install --save-dev electron

启动时间长

因为在 package.json 中的 scripts 字段下已经配置了 start 命令,支持我们使用 npm start 在开发模式下打开应用,但每次要等好久,才能启起来。

  {
    "scripts": {
      "start": "electron ."
    }
  }

经排查,与 main.ts 下的 installExtensions 方法有关,该方法在 isDebug 模式下才会执行,目的是安装和配置 Electron 中使用的开发者工具扩展 React Developer Tools。

可能是国内网络导致的特别慢,但我换了几个镜像源后也没解决,看网上资料说,可以手动下载扩展的离线包文件,再通过 session 模块加载,但我没试。我是直接注释掉了,暂时不影响我开发。

const installExtensions = async () => {
  const installer = require('electron-devtools-installer');
  const forceDownload = !!process.env.UPGRADE_EXTENSIONS; // 是否强制下载更新
  const extensions = ['REACT_DEVELOPER_TOOLS']; // 定义要安装的扩展

  return installer
    .default(
      extensions.map((name) => installer[name]), // 将扩展名映射到 installer 中对应的安装函数
      forceDownload,
      // { mirror: 'https://npm.taobao.org/mirrors/chrome/' } 
      // { mirror: 'https://registry.npmmirror.com' }
    )
    .catch(console.log);
};

搭配 .less 文件

在 .erb/configs 下的 webpack.config.renderer.dev.ts 和 webpack.config.renderer.prod.ts 两个文件内,做如下修改,module: { rules: [] } 内:

{
  test: /\.less$/,
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'less-loader',
      options: {
        lessOptions: {
          javascriptEnabled: true,
        },
      },
    },
  ],
},

搭配 Cesium

在 Electron 中使用 Cesium,难点是配置 Webpack。经过一顿 google,终于找到了一个 CesiumGS 下的 cesium-webpack-example 的示例可参考。

在 .erb/configs 下的 webpack.config.renderer.dev.ts 和 webpack.config.renderer.prod.ts 两个文件内,做如下修改,其中 copy-webpack-plugin 下文会用到:

import CopyWebpackPlugin from 'copy-webpack-plugin';
// The path to the CesiumJS source code
const cesiumSource = "node_modules/cesium/Build/Cesium";
const cesiumBaseUrl = "cesiumStatic";

在 output 内添加 sourcePrefix,兼容 CesiumJS 内的多行字符串:

output: {
  ...
  // Needed to compile multiline strings in Cesium
  sourcePrefix: '',
},

module: { rules: [] } 内添加:

{
  test: /\.(png|gif|jpg|jpeg|svg|xml|json)$/,
  type: "asset/inline",
}

module: { plugins: [] } 中添加 CopyWebpackPlugin,将 Cesium Assets、Widgets、ThirdParty 和 Workers 复制到静态目录:

new CopyWebpackPlugin({
  patterns: [
    {
      from: path.join(cesiumSource, "Workers"),
      to: `${cesiumBaseUrl}/Workers`,
    },
    {
      from: path.join(cesiumSource, "ThirdParty"),
      to: `${cesiumBaseUrl}/ThirdParty`,
    },
    {
      from: path.join(cesiumSource, "Assets"),
      to: `${cesiumBaseUrl}/Assets`,
    },
    {
      from: path.join(cesiumSource, "Widgets"),
      to: `${cesiumBaseUrl}/Widgets`,
    },
  ],
}),

module: { plugins: [] } 中再添加 DefinePlugin,定义用于加载资源的相对基本路径:

new webpack.DefinePlugin({
  CESIUM_BASE_URL: JSON.stringify(cesiumBaseUrl),
}),

module: { resolve: {} } 内添加 mainFiles,指定模块的入口文件:

resolve: {
  fallback: { https: false, zlib: false, http: false, url: false },
  mainFiles: ["index", "Cesium"],
},

好了,最后在 React 组件中就能开心的使用 Cesium 了,Cesium 的具体用法就不说了。注意一定要导入 widgets.css,否则最明显的坑就是 Cesium canvas 大小一直是 300 * 150:

import * as Cesium from 'cesium';
import 'cesium/Build/Cesium/Widgets/widgets.css';

搭配使用Mapbox

在 Electron 中使用 mapbox-gl,坑点在于加载 layer 时会导致跨域问题,因为 mapbox 本身是在渲染进程内的。比较正常的思路是拦截 mapbox 的 tile 请求,在主进程中进行数据的获取,在获取数据后,再回到渲染进程并塞给 mapbox,但是吧 实现起来有点麻烦。

比较简单的解决方案是通过 session 针对 mapbox 报跨域的接口单独处理下,代码见下,在 main.ts createWindow() 后添加:

import { session } from 'electron';
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  const { url } = details;
  const allowedOrigins = [
    'aaa/data/tile',
    'bbb/data/tile/',
    'ccc/data/tile',
  ];
  const header = {
    'Access-Control-Allow-Origin': ['*'],
    'Access-Control-Allow-Methods': ['GET, POST, PUT, DELETE, OPTIONS'],
    'Access-Control-Allow-Headers': ['Content-Type, Authorization'],
  };
  if (allowedOrigins.some((item) => url.includes(item))) {
    details.responseHeaders = {
      ...details.responseHeaders,
      ...header,
    };
  }
  callback({
    responseHeaders: details.responseHeaders,
    cancel: false,
  });
});

调用可执行文件

未完待续。