最近要重构一版桌面管理软件。业务需求与WEB版基本一致,但需要用到断点续传等本地功能。最经济的办法当然是移植到Electron环境中。

之前的应用框架主要用到了以下React套餐:

  • React
  • React-Router 路由
  • Mobx 数据管理
  • AntDesign 界面组件

经过一天时间的摸索大致找到了门路,构建完成后写了一个脚手架,欢迎下载。

$ git clone https://github.com/lynx1986/cra-antd-mobx-electron-boilerplate.git my-project
$ cd my-project && npm install
$ npm run electron-dev

下面回顾一下本次环境搭建的过程。

安装AntDesign的脚手架

关于Antd在CRA框架下的使用,官网有一份很详细的操作说明(包括babel-import等的设置),初学者可以跟着一步一步学习。如果你赶时间,也可以直接用它的脚手架

$ git clone https://github.com/ant-design/create-react-app-antd.git my-project
$ cd my-project
$ npm install && npm start

跑起来以后,会自动弹出一个页面 localhost:3000,网页上显示的就是最基础的包括了antd的demo示例。

安装Electron及配置

关于electron的介绍,可以看官网文档,本文不涉及。

完成antd脚手架安装后,在工程目录下安装electron。

$ npm install electron --save-dev

安装完成以后,在根目录下需要做一些设置,包括:

  1. electron启动脚本
  2. npm启动及编译命令
  3. render端使用electron模块的设置

electron启动脚本

在根目录下新建main.js,写入以下内容。(其实与官方例子是一样的)

// Modules to control application life and create native browser window
const {app, BrowserWindow} = require('electron')

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow

function createWindow () {
  // Create the browser window.
  mainWindow = new BrowserWindow({width: 800, height: 600})

  // and load the index.html of the app.
  mainWindow.loadFile('build/index.html')

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()

  // Emitted when the window is closed.
  mainWindow.on('closed', function () {
    // Dereference the window object, usually you would store windows
    // in an array if your app supports multi windows, this is the time
    // when you should delete the corresponding element.
    mainWindow = null
  })
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', function () {
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

app.on('activate', function () {
  // On macOS it's common to re-create a window in the app when the
  // dock icon is clicked and there are no other windows open.
  if (mainWindow === null) {
    createWindow()
  }
})

npm启动及编译命令

在工程根目录的package.json中加入以下内容:

{
  ...
  "main": "main.js",
  "homepage": ".",
  "scripts": {
    "electron-dev": "electron . --env dev",
    "electron-build": "npm run build && electron . --env build",
    "start": "react-app-rewired start",
    ...
  }
}

我们希望在执行npm run electron-dev时,能打开electron应用并展示antd的网页内容,那么需要修改main.js:

  ...
  // and load the index.html of the app.
  // mainWindow.loadFile('index.html')   // 这个是原内容,修改为以下内容
  if (argv && argv[1] === 'dev') {       // 如果npm run electron-dev,加载本地网页
    mainWindow.loadURL('http://localhost:3000/')
  }
  else if (argv && argv[1] === 'build') {
    mainWindow.loadURL(url.format({
      pathname: path.join(__dirname, './build/index.html'),
      protocol: 'file:',
      slashes: true
    }))
  }
...

配置到这里后,打开两个终端,分别cd到项目根目录下。

$ npm start   // 终端1
$ npm run electron-dev   // 终端2

electron界面应该就会展示出来,界面中显示的就是antd的demo内容。

render端使用electron模块

经过以上设置,界面内容已经可以正常展示并可以实时刷新,但还缺少了一个重要功能:electron模块,用以支持本地化操作。

配置的基本思路是,在electron启动过程中在window对象中设置webPreferences的preload属性[文档],将electron模块注册为global对象供renderer侧使用。

...
  // Create the browser window.
  // mainWindow = new BrowserWindow({ width: 800, height: 600 })
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    autoHideMenuBar: true,
    fullscreenable: false,
    webPreferences: {
      javascript: true,
      plugins: true,
      nodeIntegration: false, // 不集成 Nodejs
      webSecurity: false,
      preload: path.join(__dirname, './public/renderer.js') // public目录下的renderer.js
    }
  })
...
// 新建renderer.js,public目录下
const process = require('process');

process.once('loaded', () => {
  global.electron = require('electron')
});

设置成功后,在renderer侧即可引入electron的remote模块。

// 根目录下的App.js
import React from 'react';
import { Form, Select, InputNumber, DatePicker, Switch, Slider, Button } from 'antd';
import './App.css';
const { remote } = window.electron;
console.log(remote);
...

使用mobx

安装mobx及mobx-react

$ npm install mobx mobx-react --save

安装成功后,在App.js中使用:

...
import { observer } from 'mobx-react';
...

@observer
class App extends React.Component {
  ...
}

执行开发命令后,会发现出现了一个编译错误。

原因就在于CRA团队并不倾向于支持所有未正式发布的javascript语言特性,目前要使用装饰圈,均需要使用babel等进行转译。这个问题目前有两个方法:使用eject方式或react-app-rewire-mobx模块。

使用eject方式将会暴露出所有的webpack配置,开发者如果熟悉可以进行自定义的配置。当然,这样做的话create-react-app的意义就大打折扣了。

使用react-app-rewire-mobx则相对方便了许多,只需安装该模块,并在config-overrides.js中加入,即可正常使用。

...
const rewireMobX = require('react-app-rewire-mobx');

module.exports = function override(config, env) {
  ...
  config = rewireMobX(config, env);
  return config;
}

注意

按照上面的方式配置以后,仍然会报错。

./src/index.js Error: The 'decorators' plugin requires a 'decoratorsBeforeExport' option, whose value must be a boolean. If you are migrating from Babylon/Babel 6 or want to use the old decorators proposal, you should use the 'decorators-legacy' plugin instead of 'decorators'.

原因在于,新版的babel7.0的描述换了一种方式。应该将上面的配置改为:

module.exports = function override(config, env) {
  ...
  config = injectBabelPlugin(["@babel/plugin-proposal-decorators", { legacy: true }], config);
  return config;
}

其实在react-app-rewire-mobx中也仅仅就是就是导入这个修饰符,只不过是旧版的写法。但就因为没有跟随babel7.0更新,所以导致了这个错误的发生。

// react-app-rewire-mobx/index.js
const {injectBabelPlugin} = require('react-app-rewired');

function rewireMobX(config, env) {
  return injectBabelPlugin('transform-decorators-legacy', config);
}

module.exports = rewireMobX;