微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

用 Electron 打造跨平台前端 App

2016-07-01 21:16 前端大全

(点击上方公众号,可快速关注)

作者:littledu(@小小瘦社)

链接:https://segmentfault.com/a/1190000005744529

前言


现如今,用 HTML、JavaScript、CSS、Node.js 写桌面应用早已不是什么新鲜的事了,作为一名前端,能够使用自己熟悉的语言,快速实现自己想要的桌面应用,是件很让人兴奋的事。

目前常见的有 NW、heX、Electron。今天,就来简单的上手一下 Electron。


Electron 是什么?


Electron 是一款可以利用 Web技术 开发跨平台桌面应用的框架,最初是 Github 发布的 Atom 编辑器衍生出的 Atom Shell,后更名为 Electron。


Electron 能做什么?


Electron 内置了 Chromium 内核 和 Node,因此可以使用 HTML 和 CSS 来实现应用的 GUI 界面,用 JavaScript 调用丰富的原生 API 实现桌面应用。你也可以将 Electron 看作是一个由 JavaScript 控制的一个小型的 Chrome 内核浏览器。


由于内置的 Chromium 内核 和 Node, 因此我们不需要关心前端的兼容问题,你甚至可以写 -webkit- only 的代码; 也不需要关心一些需要编译的 Node 模块兼容问题,因为 Node 版本是固定的。因此,用 Electron 来编写跨平台应用程序是非常合适的。


或许你还不知道,Visual Studio Code 、wordpress 和 slack 等客户端都是基于 Electron 开发的。


下面,先快速上手一下。


快速入门


相信你看到这里都是对 Node 有一定了解的,故这里不再对 Node 的安装进行描述。


我们有如下目录结构:


electron-quick-start/

    ├── package.json

    ├── main.js

    └── index.html


package.json 跟常规 Node 程序一致,将 main.js 作为 程序的启动入口文件,基本内容如下:


{

  "name"    : "electron-quick-start",

  "version" : "1.0.0",

  "main"    : "main.js",

  "scripts" : {

    "start" : "electron main.js"

  },

  "devDependencies": {

    "electron-prebuilt": "^1.2.0"

  }

}


我们用 index.html 作为我们的程序界面,简单的界面代码如下:


<!DOCTYPE html>

<html>

  <head>

    <meta charset="UTF-8">

    <title>Hello World!</title>

  </head>

  <body>

    <h1>Hello World!</h1>

  </body>

</html>


接着是最重要的入口文件 main.js 的编写了,其内容如下:


const electron = require('electron');

const app = electron.app;

const BrowserWindow = electron.BrowserWindow;

let mainWindow;

 

function createWindow () {

  //创建一个 800x600 的浏览器窗口

  mainWindow = new BrowserWindow({width: 800, height: 600});

 

  //加载应用的界面文件

  mainWindow.loadURL(`file://${__dirname}/index.html`);

 

  //打开开发者工具,方便调试

  //mainWindow.webContents.openDevTools();

 

  mainWindow.on('closed', function () {

    mainWindow = null;

  });

}

 

app.on('ready', createWindow);

 

app.on('window-all-closed', function () {

  if (process.platform !== 'darwin') {

    app.quit();

  }

});

 

app.on('activate', function () {

  if (mainWindow === null) {

    createWindow();

  }

});


最后,执行:


npm install && npm start


运行结果如下图:



当程序启动时,Electron 调用在 package.json 中定义的 main.js 文件并执行它。这个过程中,Electron 会创建一个主进程,主进程调用 BrowserWindow 模块创建浏览器窗口,每个浏览器窗口都有自己独立的渲染进程,渲染进程负责渲染 HTML 文件,以作为程序的 GUI 界面。


主进程管理所有页面和与之对应的渲染进程。每个渲染进程都是相互独立的,并且只关心他们自己的网页。



至此,相信你对 Electron 的运行过程已有一定了解了,下面,我将介绍一下我是如何将我们的前端工作流程(tmt-workflow) 封装成桌面应用(WeFlow)的。


应用实践


现状


tmt-workflow : 是一个基于 Gulp(v4.0),通过约定一定的项目结构和配置文件实现高效、跨平台(Mac & Win)、可定制的前端工作流程。


其拥有 4 个任务(gulp task) :


  1. 开发任务(gulp build_dev)

  2. 生产任务(gulp build_dist)

  3. 部署任务(gulp ftp)

  4. 打包任务(gulp zip)


运行时需要先安装(npm install) ,再执行相应任务命令,也可以配合 WebStorm 等编辑器的 gulp 任务管理器 使用。


目标


利用现有的 tmt-workflow, 包装成一个 可视化 界面,不需要安装(npm install) ,直接下载打开即可使用。具体拥有:


  • 可视化的项目管理(新建、打开、配置、删除)

  • 可视化的全局项目配置

  • 可视化的任务执行(开发、生产编译、FTP 部署、Zip 打包)

  • 可视化的 log 日志反馈


设计效果预览



主要由几部分组成:


  1. 第一次打开时的欢迎页

  2. 主窗体,由项目列表和任务列表组成,选择具体项目执行任务流程

  3. 全局设置页

  4. 项目设置页

  5. 关于


实现


核心: 如何将 gulp 程序转换


我们知道,gulp 的任务执行必需在命令行下执行,如: gulp build_dist ,这里的 gulp 是一个命令,是一个全局的 cli。执行时依赖于项目下的 node_modules。


基于 gulp 程序的以上特点,我们的思路如下:


思路 1: 如果我们什么都不改变的话,直接把 tmt-workflow 这个 gulp 工作流封装,那可能的思路就是:


当点击可视化的任务按钮执行时,


  1. 先进入所要执行的项目的目录

  2. 再调用子进程执行 gulp 命令:


let exec = require('child_process').exec;

exec('gulp build_dist', {'cwd': 'projectPath'});


这样子,任何 gulp 流程都不需要改动,直接在其上面套一个壳,这个壳提供一下可视化的交互,然后帮你执行相应的 gulp 任务。

思路貌似挺好的,但跟我们的目标有点冲突,我们之所以要封装打包,为的就是省去用户安装,让用户打开即能用。而这个思路的执行方式需要在用户的项目目录下面执行 gulp 任务,那程序依赖的依然是用户已安装的 node_modules,而安装的过程有些模块(如图形模块)需要本地编译,而编译又依赖于用户系统的 node 版本和相关环境(如 win 下需要 python2.7.3 和 VS2010),这有时候是一个漫长又痛苦的过程。这就是为什么要省去安装的原因了。


所以,我们有了思路 2。


思路 2: 将 gulp 工作流程序 和 node_modules 一起打包进 Electron ,当点击可视化的任务按钮执行时:


  1. 获取项目的路径

  2. 将整个项目传进 Electron 里面打包的工作流执行一遍

  3. 将编译后的文件输出


观察我们的 gulp 任务写法,都有一个固定的结构,如下:


//编译 less

function compileLess() {

    gulp.src(paths.src.less)

        .pipe(less())

        .pipe(gulp.dest(paths.dist.css))

}

 

//注册 build_dist 任务

gulp.task('build_dist', gulp.series(

    delDist,

    compileLess,

    ...

));


就是利用 gulp.src 读取资源,然后经过一系列处理之后再用 gulp.dest 输出。然后再通过 gulp 注册一个 gulp 任务,即可用 gulp 命令调用执行。如果可以把 gulp 从这个过程中去掉,换成普通的程序,则就可以不需要命令行调用,也就可以依赖于当前 Electron 打包的 node_modules ,实现封装的目的。


通过观察 gulp 的实现我们可以看到如下代码:


var vfs = require('vinyl-fs');

 

function Gulp() {

  Undertaker.call(this);

 

  // Bind the functions for destructuring

  this.watch = this.watch.bind(this);

  this.task = this.task.bind(this);

  this.series = this.series.bind(this);

  this.parallel = this.parallel.bind(this);

  this.registry = this.registry.bind(this);

  this.tree = this.tree.bind(this);

  this.lastRun = this.lastRun.bind(this);

}

 

Gulp.prototype.src = vfs.src;

Gulp.prototype.dest = vfs.dest;


我们发现,gulp.src 和 gulp.dest 实际上是 vinyl-fs 模块的实现。而原来 gulp 任务注册的 同步(gulp.parallel) 和 异步(gulp.series) 处理,我们也可以直接用 async 来替代,因此,我们稍微改动可以变成:


const async = require('async');

const vfs = require('vinyl-fs');

 

//编译 less

function compileLess(cb) {

    vfs.src(paths.src.less)

        .pipe(less())

        .pipe(vfs.dest(paths.dist.css))

        .on('end', cb);

}

 

async.series([

    function (next) {

        compileLess(next);

    }

], function (error) {

    if (error) {

        throw new Error(error);

    }

});


这个样子,就跟 gulp 无关了,但相关编译模块都还直接用的原来基于 gulp 的模块,所以,只需要稍加改动,就可以利用现有的 gulp 工作流快速实现 GUI 程序。


解决了核心的 gulp 流程转换,剩下的就是一些逻辑交互处理、配置功能、数据存储、菜单栏和快捷键功能等的实现了。下面对整个项目的相关实现进行介绍。


项目结构


WeFlow/

    ├── about.html              //关于界面

    ├── app.html                //主界面

    ├── assets/                 //资源目录

       ├── css

       ├── img

       └── js

    ├── main.js                 //应用入口文件

    ├── package.json

    ├── src/                    //源文件目录

       ├── _tasks/

       ├── app.js

       ├── common.js

       ├── createDev.js

       └── menu.js

    ├── templates/              //模版目录

       └── project.zip

    └── weflow.config.json      //配置文件


数据存储


WeFlow 需要对用户的一些操作进行记录(新建或打开了多少项目)进行存储,以便下次打开时还原。

Weflow 是一个本地程序,故数据不需要存储在云端,只需要存储在用户本地即可。所以直接使用 localStorage 来存储数据,WeFlow 构造的数据对象如下:


{

    "name": "WeFlow",

    "workspace": "/Users/littledu/WeFlow_workspace",

    "projects": {

        "project": {

            "path": "/Users/littledu/WeFlow_workspace/project",

            "devPath": "/Users/littledu/WeFlow/src/_tasks/tmp_dev/0c0876c4232f1de240f519f0920f2d60.js",

            "pid": 0

        }

    }

}


整个程序运行的过程中都是基于此对象进行操作。打开程序时,会读取此数据,进行界面内容填充。当项目位置或开发状态变动时,也更新数据存储进 localStorage。


菜单栏和快捷键功能设计



menu 模块是一个主进程模块,可以用来创建原生菜单,每个菜单有一个或几个菜单项 menu items,并且每个菜单项可以有子菜单。


Electron 有一个 global-shortcut 模块专门用来设置(注册/注销)各种自定义操作的快捷键。但通过 menu 模块也可以绑定快捷键,代码如下:


const electron = require('electron');

const remote = electron.remote;

const Menu = remote.Menu;

 

var template = [

    {

        label: '文件',

        submenu: [

            {

                label: '新建项目',

                accelerator: 'CmdOrCtrl+N',

                click: function (item, focusedWindow) {

                    newProjectFn();

                }

            },

            {

                label: '打开项目…',

                accelerator: 'CmdOrCtrl+O',

                click: function (item, focusedWindow) {

                    let projectPath = remote.dialog.showOpenDialog({ properties: [ 'openDirectory' ]});

                    if(projectPath && projectPath.length){

                        openProject(projectPath[0]);

                    }

                }

            }

        ]

    }

];


menu 是主进程模块,但在这里想给快捷键绑定渲染进程中的功能。故调用了 remote 模块进行渲染进程和主进程通信。


遇到的问题


1. 浏览器自动刷新监听功能无法中断(browser-sync@2.13.0 之前)


tmt-workflow 使用 browser-sync 实现开发任务的自动刷新功能。常规情况下使用结束时,通过 cmd+c 或 ctrl+c 中断。然而封装后不再是通过命令行方式调用,故无法通过命令行来中断。 browser-sync 也没有提供 API 中断。故 WeFlow 中的 开发任务 跟其他的任务不同,解决方式是:


用子进程 child_process.fork 来执行开发任务的 dev.js,将返回的 PID 保存,即可通过这个 PID 来中断对应的子进程,达到停止开发任务的目的。


原理代码如下:


let childProcess = require('child-process');

function runDevTask(devPath){

    let child = childProcess.fork(devPath, {silent: true});

 

    child.stdout.on('data', function (data) {

        logReply(data.toString());

    });

 

    child.stderr.on('data', function (data) {

        logReply(data.toString());

    });

 

    child.on('close', function (code) {

        if (code !== 0) {

            logReply(`child process exited with code ${code}`);

        }

    });

}

 

function killChildProcess(pid){

    try {

        if(process.platform === 'win32'){

            childProcess.exec('taskkill /pid ' + pid);

        }else{

            process.kill(pid);

        }

    }

}


2. windows 下打包 EXE 后不能使用 process.stdout


官方认为,Electron 实现的都是 GUI 程序,所以理论上不需要这种输出功能。虽然在调试阶段并不影响,但打包的时候记得去掉,要不然会报错。


打包


electron-packager 可以用来打包 Electron 应用。生成各个平台的最终可运行文件,如 .app 和 .exe。


使用命令:


electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]


  • <sourcedir>: 项目的位置

  • <appname>: 应用名

  • --platform=<platform>: 打包的系统(darwin、win32、linux)

  • --arch=<arch>: 系统位数(ia32、x64)

  • --icon=<icon>: 指定应用的图标(Mac 为 .icns 文件,Windows 为 .ico 或 .png)

  • --out <out>: 指定输出的目录

  • --version=<version>: 指定编译的 electron-prebuilt 版本例子:


electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8


我们可以直接在 package.json 的 script 字段中添加脚本,如下:


"scripts": {

    "build:all": "electron-packager . --all --overwrite",

    "build:mac": "electron-packager ./ WeFlow --platform=darwin --arch=x64 --icon=./assets/img/WeFlow.icns --overwrite --out ./dist --version=0.37.8",

    "build:win64": "electron-packager ./ WeFlow --platform=win32 --arch=x64 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8",

    "build:win32": "electron-packager ./ WeFlow --platform=win32 --arch=ia32 --icon=./assets/img/WeFlow.png --overwrite --out ./dist --version=0.37.8 --app-version=1.0.0"

}


注意:不要认为一个系统可以完成所有系统的打包


如果你引用了一些原生模块(如 lwip),它是必需根据目标系统编译生成 .node 文件。遇到这种情况,则无法在一个系统上面打包另一个系统的可执行程序。更好的做法是利用 AppVeyor 和 Travis 来为各平台实现打包自动化。可以通过相应官网进行了解。


electron-packager 打包后的文件可以看到源代码,想更进一步打包可以用 electron-builder 。


下载体验地址


  • tmt-workflow(https://github.com/weixin/tmt-workflow)

  • WeFlow 桌面应用(http://weflow.io/)


参考文档


  • Electron 官方文档

  • 用Electron开发桌面应用

  • electron-packager

  • electron-builder



【今日微信公号推荐↓】

更多推荐请看值得关注的技术和设计公众号


其中推荐了包括技术设计极客 和 IT相亲相关的热门公众号。技术涵盖:Python、Web前端、Java、安卓、iOS、PHP、C/C++、.NET、Linux、数据库、运维、大数据、算法、IT职场等。点击《值得关注的技术和设计公众号》,发现精彩!


 
前端大全 更多文章 44 个 Javascript 变态题解析 (上) 不容错过的 10 篇前端技术热文 44个 Javascript 变态题解析 (下) 来来,一起设计一个简单的活动发布系统 童年的马里奥,其实住在个超级现实的世界
猜您喜欢 【趣文】PHP是世界上最好的编程语言,有例子说明! 亲身参与“引力波”项目之体验 Java究竟是不是纯种的面向对象? 安全行业的第三流派-CSOs |运维帮 和最优秀的人一起,我花一万小时学到了这些