首发于酷家乐前端博客,作者@摘星(segmentfault @StinsonZhao)
我们能从很多地方学习到怎么起一个 Electron 项目,有些还会介绍怎么打包或构建你的代码,但距离「真正地发行一款 Electron 产品」这一目标,还有很多工作需要做...
这是 Electron 系列文章的第二篇,这一篇文章将和大家分享我是怎么去构建自动化的 Electron 开发构建工程的,说白了,就是怎么把敲的代码变成一个用户可以下载安装的包,当然随着之后应用复杂度的提升和技术再选型,工程体系可能随时会重构或演进,但至少可以给大家一些参考,欢迎留言交流。
这是一篇很长的文章(手册),写得比较「唐僧」(知我者可以说我写得比较用心),至少会花你一天时间(没开玩笑),适用场景是「用 Electron 打造 Windows 或 Mac 应用」,是的,你没看错,同时会讲清楚兼容 Win 和 Mac 两个系统的流程。文中提及的技术方案绝对不是最佳的(我保证),因为几乎每隔几天我都会发现某个环节可以做得更好,但要明白要唱多大的戏,就先搭多大的台,够用就好,不要为了搭台耽误演出时间。
工程自动化,应该是所有开发者的一种基础追求,当你搭建建好工程体系,以后你将专注于产品功能的开发,而不会花大量不必要的时间去手动构建。作为前端,可能我们已经熟悉了 web 应用的构建和部署,但是客户端程序有其本身的特点,相比较 web 应用最大也是我认为最根本的一点区别在于「你的应用是被用户下载过去安装在用户本地再跑起来的」。
这一区别对工程的影响在于,你不可能把你的代码部署到「用户的电脑」,你需要构建安装包,你需要针对不同的用户系统构建不同的安装包,你需要让你的应用被系统认为是安全的...
本文需要做的是,把客户端的打包构建发行这一流程做到像「把大象放进冰箱」一样的简单:打开命令行,敲一个 npm run xxx
,喝一口咖啡,咪哩嘛哩哄,安装包出现(一开始打造这个流程时,剧本可能是「喝一口咖啡,啪,Error 了,又 Error 了」,take it easy,生活需要慢慢品味 —— 来自一位25岁的仙风道骨白胡子程序员)。
本文将分以下小节和大家分享「从本地的代码到云端可下载的安装包」这一路的风景,你会有漫步月球般的感觉(因为月球全是坑啊,还没氧气):
- 第一节是关于目录结构的讨论,合适的目录结构会是一个良好的开端
- 第二节是之后几个小节的概述,阐述了怎么把这一整个过程分成多个环节,每个环节又大致要做什么事
- 第三到七节分别详细描述了「配置」、「打包」、「代码签名」、「构建安装包」、「发行安装包」这几个环节要做哪些事,有什么讲究
- 第八节是简述一些可进一步研究或优化的点
- 附:这样设计的 gulpfile 文件结构
下面一一展开进行阐述,再次强调,文中很多依赖的技术或包,你都可以尝试替换成自己相中的,不必在意是选「翠花」还是「桂花」,多处处就知道了。
一、目录结构
以下目录结构供参考,没有很详细地展开,因为每个应用可能不同,最想表达的是这是一个「双 package.json 结构」,你可以看到根目录下有一个package.json
,app
目录下还有一个package.json
。
/ // 项目根目录
├── app/ // 应用源码目录,打包就针对app目录进行打包
│ ├── assets/ // 应用需要的图片、icon等资源
│ ├── config/ // 配置文件存放
│ ├── consts/ // 应用运行需要的常量,如ipcChanel
│ ├── lib/ // 引入的库文件,如jquery
│ ├── plugins/ // 应用运行需要的插件,如flash
│ ├── utils/ // 常用的工具方法
│ ├── view/ // 视图,html、css和js
│ ├── app_config.js // 整个app的配置,引入config文件夹下文件
│ ├── main.js // 应用入口
│ ├── package.json // 内部的package,定义应用的版本、运行依赖等
│ └── yarn.lock
├── build_resource/ // 构建需要的一些工具、资源或者脚本的目录
├── config/ // 环境配置文件目录,会选择一个写入到app/config
├── deploy/ // 部署脚本,用户部署文件到cdn或上传文件到OSS
├── reserve/ // 保留目录,存放一些文件用于写入到app内
├── dist/ // 打包和构建的目标目录
├── release/ // 发行的目标目录
├── .gitignore
├── gulpfile.js // gulp配置
├── package.json // 外部package.json,用于定义开发依赖和脚本
└── yarn.lock
这是因为,我们的应用在运行时需要一些第三方依赖,这些依赖我们需要打包到应用内,也就是说/app/node_modules
目录内的内容是要被打包到应用内的,用户使用的时候才不会缺失「运行时依赖」,而如果我们只有一个package.json
,那么所有的依赖都被下载和安装到同一个node_modules
文件夹下,我们没法把我们需要打包进去的依赖树提取出来。所以这样双 package.json
的结构最清晰明了和简单易用,dependencies
和devDependencies
有了明确划分。
再大致解释下其他目录的作用:
-
app
目录:是我们应用的源码目录,我们所说的打包针对的就是这个目录,其他目录和文件不会被打包进去,而app
目录内的子目录和文件就见仁见智了,在不同的复杂度下有不同的设置,这里还有一些东西是需要从外面复制进来的,因为不同的平台下你可能需要打包进去的东西是不同的。-
config
:配置文件目录,可能因为你想打包的应用所处的阶段(开发、内测、众测、正式发行)和平台(Windows、Mac),那么可能需要不同的配置,比如一些资源的名称和路径等,这里你可以把不同情况下都一样的配置写到一个配置文件,而根据情况不一样的配置文件是从外部脚本写进来的,这就是为什么你会在app
目录外面看到一个config文件夹的原因 -
plugins
:是插件文件夹,你可能需要给自己的应用加一些插件,比如 flash,而一个 flash 插件有 40M 左右,Win(32bit)、Win(64bit) 和 Mac 需要的 flash 插件文件都是不一样的,所以如果全部打包进你的应用,再用「if - else」去选显然是不科学的,Mac 下的应用肯定是用不到 Win 版本的插件的,所以这里的文件也是从外面脚本写进来的 -
view
:是视图文件夹,也可以说是渲染进程对应的代码文件夹
-
-
build_resource
:构建资源或工具文件夹,这个文件夹下放打包到发行这一流程中需要用到的资源和工具,比如程序主图标、构建安装包的配置脚本(win)、代码签名工具等 -
deploy
:存放部署脚本的文件夹,这里的脚本负责把你的应用安装包上传到云存储(OSS),我们会在 gulp 中的发行环节引入这里写的脚本进行自动上传安装包 -
dist
和release
:前者是打包和构建安装包这两步的 output 目录,后者是最终我们会上传到云端的安装包目录,构建和发行环节的差别我们后面会讲到
二、把整个流程拆分成段
这个部分没法正向推导,我是从一个乱七八糟的 windows 开发流程开始的,然后修改成一个合适的 windows 开发流程,再因为要兼容 Mac 的开发,再改成现在这样的流程设计的,所以我没法从一开始就说因为什么所以要考虑什么,然后慢慢构建出一个合适的工作流,这是上帝视角,这个偏实践经验的过程一定是实践越多,感受越多的。
所以我会先说我的做法,再说这么做的好处,所用的工具是 gulp(如果不熟悉,可以去 gulp 官网看一下,很容易上手),利用 gulp 的 task 串起整条流程,我把工程中的一个阶段称为一个环节,是为了和应用本身的阶段(开发、内测、众测或正式发行)做一个区分,不然都不知道说的阶段是指啥:
- 配置环节:设定需要打包构建针对的系统、位数(Mac 版不考虑 32 bit)和这个版本所处的阶段(开发、内存、众测或正式发行)这些变量,然后把相关配置写入配置文件模板,再导入 app 文件夹内相应位置,把其他相应的文件也写入 app 文件夹内相应位置,如此 app 文件夹就 Ready 了。
- 打包环节:根据不同的平台打出不同的可执行程序,这一步输出的是可运行的程序
- 代码签名环节:客户端特殊的一步,你的应用需要被系统所信任,那就需要代码签名,获取对应平台下的代码签名 CA 然后进行应用签名,这样你的应用才能被系统信任
-
构建安装包环节:根据不同的系统利用不同的技术和依赖构建安装包,Windows 下的
.exe
和 Mac下的.dmg
,并且对这两个安装包也需要代码签名,这一步后你的应用可以被分发安装啦 - 发行环节:对构建的安装包进行最后一步修饰,比如修改合适的文件名,然后上传到云存储服务器,获取到可下载的链接,如此,你的应用已经可以经获取到的 url 访问进行下载安装了
以上每一步,Mac 版和 Windows 版的开发都需要经历,只是所用的方法不同,这样做的好处,一是统一了 Mac 和 Win 下开发工作流的生命周期,二是简单和直观,每一环节目的是什么,输出是什么很明确。
如此,我在package.json
中的script
就可以这么写:
... ...
"start": "cross-env NODE_ENV=dev gulp dev",
"packDev": "cross-env NODE_ENV=dev gulp pack",
"buildDev": "cross-env NODE_ENV=dev gulp build",
"releaseDev": "cross-env NODE_ENV=dev gulp release",
... ...
当然这里的NODE_ENV
你也可以写成命令行参数(我只是习惯了用这个),利用这个参数去指定需要针对的应用阶段,像以上这样就配好了「dev」阶段的相关脚本,可以用npm run packDev -- --platform="xxxx" --arch="xxxx" --sign
这样形式的命令行去执行不同的 gulp 任务,后面的参数,是需要我们在 gulpfile 文件中解析的,以上3个参数分别表示「系统平台」、「系统位数」、「是否需要代码签名」,我们可以在 gulpfile 文件中给这些参数合适的默认值,使操作更人性化。
三、配置环节
目的:一是为之后的环节初始化工作流参数,二是准备好应用文件夹内容(即要打包的目标文件夹 —— app)
做的事:解析命令行参数,初始化工作参数,填充配置文件,把配置文件和相关依赖文件导入到app
文件夹内合适的地方
1. 初始化工作参数
所用工具:yargs
yargs 是一款优秀的命令行参数解析工具,我们要初始化的工作参数包括以下 3 个:「系统平台」、「系统位数」、「需不需要签名」,你也可以把应用的所处阶段(开发、内测、众测、正式)设计成参数。
// 以下 3 个变量在 gulpfile 内全局声明
// 这里的 detectPlatform() 需要自己写,利用 node 的 os 模块去检测开发机环境从而给出
// 为了理解上直观一些,把 32 位的 win 写成 win32,64 位的 win 写成 win64
// node os.platform() 没有 win64 的返回的,只有在返回 win32 基础上,你再使用 os.arch() 去确定是否是win64
// 可能的合法值:darwin、win64、win32
platform = yargs.argv.platform || detectPlatform() || 'win32';
// 系统位数,如果是 Mac OS X,不考虑 32位
// 可能的合法值:x64、ia32
arch = platform === 'darwin' ? 'x64' : (yargs.argv.arch || 'ia32');
// 布尔值,指定是否需要代码签名
needSign = yargs.argv.sign || process.env.NODE_ENV === 'prod' || platform === 'darwin';
看到上面的参数初始化,可能会有疑问,既然已经在platform
中区分了 win32(32bit) 和 win64(64bit),而且darwin
下不考虑 32bit(因为 OS X 10.6 之后就全是 64 位的),arch
参数是否多余?这是可以认为是多余的,但是有的话更完整,而且如果你以后又想兼容 linux 了呢?
2. 填充并导入配置文件
所用工具:gulp API、gulp-replace、gulp-rename
首先我会在根目录下的 config 文件夹下放几个不同的配置文件模板,分别对应应用不同的阶段的配置(比如dev.js、alpha.js、beta.js、prod.js),然后利用gulp-replace
去替换掉里面的一些占位字符串(也就是填充模板),最后利用gulp-rename
重命名为比如env.js
后,利用gulp.dest
写入文件到 app/config 目录下,于是配置文件 Ready。
3. 二进制文件导入(以 flash 为例)
所用工具:gulp API、del
以 flash 插件为例,首先你要找到需要的插件文件,electron 官网所说的打开chrome://plugins
已经没法用了,从 chrome 的某个版本开始,chrome://plugins
Is Not Available。
所以用系统的搜索功能吧,记得先装下 chrome 浏览器,Mac 搜索「PepperFlashPlayer.plugin」,Windows 搜索「pepflashplayer」,Windows下如果搜到多个,记得选择和 chrome 目录有关的那个「.dll」文件,此外 win32bit 和 win64bit 所用的 flash 也是不同的,Mac 下的「PepperFlashPlayer.plugin」本质是一个文件夹,整个文件夹都需要。所有的3个插件放进根目录下 reserve 文件夹。
接下来需要做的就是,根据不同的平台读不同的 flash 插件( .dll 文件或 .plugin 文件夹)到 app/plugin 文件夹下。
这里有一个需要注意的是,每次你构建时,如果 app/plugin 下的 flash 不是你要的,那么你需要先删除那个旧的,否则你的 app/plugin 文件夹下会躺着一个你不会用的 flash 插件,但会被打包进去,你的文件大小突然多了 40M,我这里用的删除工具是 del。
经过配置环节,app
文件夹已经准备就绪,所以以开发模式(不需要打包)运行应用也就没啥大问题,可以另写一个「dev」的 gulp task,利用 node 的child_process
模块下的exec
调用下electron app --debug
就可以运行应用了,没啥可以多说的,我们继续进入下一步 —— 打包。
四、打包环节
目的:产出一个可执行程序,简单来说,就是能有一个应用,双击能运行起来
做的事:利用electron-packager
打包,补充应用信息(only for win)
1. 利用electron-packager
打包
利用electron-packager
打包,只需要针对不同系统平台给出不同的配置,然后调用其 API 就可以了。
// Mac
const options = {
dir: './app',
name: '应用名字',
platform: 'darwin',
arch: arch, // 这就是工作参数 arch
overwrite: true,
appVersion: 'Copyright(C) 2017 Qunhe',
asar: {
unpackDir: 'plugins' // plugins 内的文件我们不希望打进 asar 格式包内
},
out: './dist',
icon: './build_resource/logo.icns' // Mac 下 icon 格式是 .icns
};
// Win
const options = {
dir: './app',
platform: 'win32', // 不管是 32bit 还是 64bit 的 win,这里都是 win32
arch: arch, // 这里依靠 x64 或 ia32 去区分位数
overwrite: true,
asar: {
unpackDir: 'plugins'
},
out: './dist',
icon: './build_resource/logo.ico' // Win 下 icon 格式是 .ico
};
Mac 下各处(Dock、任务栏、进程名等地)展示的应用名字只要指定了name
选项,就是处处一样的,所以你可以用 name 指定一个中午名字,而且 Mac 下默认编码都是 UTF-8,问题不大。
而对于 Windows,首先其中文默认编码是 GBK 的,而所以如果指定中文名字可能会有奇怪的问题,所以 Windows 应用一般我不填name
项,这样它会去找你 app 目录下的 package.json 文件中的productName
或name
字段值,这个字段一般设置是英文的,第二个不去设置中文的原因是,Windows 下应用的展示名字是 exe 主程序的FileDescription
配置项决定的,如果不去设置,那么可能你的应用用任务管理器打开,显示的进程是「Electron」,而不是你的应用名字。
关于应用的实际名字和展示名字,Win 和 Mac 下都有自己的一套,这里不细展开。而基于目前的实践,我给的建议是,Mac 下的开发,你可以直接指定name
为一个你要的中文应用名,而对于 Win,你最好像下面那样操作。
2. 补充应用信息(for win)
所用工具:rcedit
Command line tool to edit resources of exe file on Windows. 翻译过来就是一个用于编辑 exe 文件信息的windows 命令行工具,当然它已经有了 node 版本,叫 node-rcedit,也就是说你可以用 node 子进程的exec
去执行,也可以调用 node 版本的 API。
可以这么用:
execSync(`
.\\node_modules\\rcedit\\bin\\rcedit // 调用rcedit
./dist/xxxxxx.exe // 目标文件(刚打包出来的主程序)
--set-version-string "LegalCopyright" "Copyright(C) 2017 Health" // 版权信息
--set-version-string "CompanyName" "仙风道骨养生俱乐部" // 公司名字
--set-version-string "ProductName" "养生" // 产品名字
--set-version-string "FileDescription" "养生宝典" // 这个很重要,因为这个就是你打开任务管理器看到的进程名字
`);
大部分信息,你可以右键主程序(.exe)文件,「属性 —— 详细信息」中看到,这么做还有一个考虑是,这样你的应用看上去会更加规范。
这里肯定有人说,为什么不用electron-builder
,因为我首先接触到的是electron-packager
,我觉得够用(因为我有一台 win 和一台 mac,跨平台打包,不存在的),第二,electron-packager
完成打包的事就够了,后面构建安装包等过程可以让我们有更多的选择,符合本文的工作流设定,每个环节做每个环节该做的事就好,当然你也可以选择electron-builder
,能达到目的就好。
五、代码签名环节
目的:使应用被系统所认可,能正常安装
做的事:给应用进行代码签名
1. 为什么需要代码签名,没有会怎样
代码签名的目的就是为了安全,你的应用一旦经过了代码签名,如果发行过程中被篡改,你的用户会看到系统给出的警告提示,而对于发行方而言,代码签名后,应用才能被系统认可,很大概率不会被杀毒软件做掉,而且如果你要提交一些软件市场,一些软件市场要求应用需要有合法的代码签名。
而如果作为铁头娃的你铁定不签名,这应用就不能跑了么?不是的,还是可以跑的,只不过对你的用户来说很不友好。
1.1 Windows 下有和没有代码签名的差别
Windows 下代码签名的限制没有 Mac 那么严,你选择「是」都是可以安装使用的,但是从你产品的用户角度,有一个代码签名会更可靠,此外,这样的没有签名的安装包在一些软件市场可能都提交不上去。
1.2 Mac 下有和没有代码签名的差别
Mac 下有和没有代码签名的差别就很大了,没有合法的代码签名,你的 .dmg 安装包根本没法打开。
如果没有代码签名,Mac 下的 .dmg 安装包打开,首先会提示你「该应用来自身份不明的开发者,是否确认打开」,然后你点「确认」,再根据你的安全设定(系统偏好设置 —— 安全和隐私 —— 允许从以下位置的应用下的设置)去决定,而绝大部分的 Mac 用户都是勾选「App Store 和 被认证的开发者」,于是就算你点了「打开」,直接会告诉你「打不开XXX,因为它来自身份不明的开发者」,这个时候只能去改变「系统偏好设置 —— 安全和隐私 —— 允许从以下位置的应用下的设置」才能打开。
典型的盗版软件安装方式啊,所以作为一款要发行的产品,我们一定是需要代码签名的。
2. Windows 下的代码签名
总体建议:个人的小项目就不用 Windows 代码签名了,因为很贵,2K+/年,而且 Windows 下代码签名没有问题不是非常大(和 Mac 相比),公司的产品,那就必须要的。
2.1 购买微软代码签名证书
可以向权威的 CA 机构购买代码签名证书,这里就我了解的做一个建议:建议向赛门铁克购买签名普通软件(非驱动)的微软代码签名证书,大概几百刀一年。
背景说明:目前我们用的是沃通的代码签名证书,赛门铁克的只是咨询过,没用过。
就以上的建议做一个解释,为什么我这么建议:
- 我们需要代码签名,进一步,需要把 Windows 代码签名这一环节也做到自动化流程中,这是我们的需求
- 沃通的代码签名证书是封死在 U 盘里,所以可认为这是物理证书,更安全,但很不方便,不可能导出来进行签名的
- 了解到的,赛门铁克颁发的如果是针对普通软件(非驱动的),那么是可以给颁发文件格式的真·电子证书的
- 意味着沃通的证书我们要签名,需要依靠一个物理U盘
- 最坑爹的:沃通的代码签名时,要手输密码,如果一个 Windows 应用我们选择 SHA1 + SHA256 的签名方式,那么应用和安装包,我们需要输4次密码,气到拉闸,他们官方说有自己的命令行,实际是命令行唤起他们的 GUI 图形界面来签名,还不是需要人工操作
- 所以,显然这和我们的「自动化」目标相去甚远,我建议普通的应用,没有涉及到高度安全的,不要选择购买封死在 U 盘中的 Windows 代码签名证书。
2.2 签名
当你购买了证书后,就可以利用signtool
命令行进行签名了,命令怎么写,这些都在你购买证书的 CA 网站上找到或者 google 一下,这里要说的就两点:
- Windows 代码签名我们目前选择 SHA1 签名后再追加 SHA2(SHA256) 签名,这样的组合方式,安全和兼容性最好
- 代码签名可以在 gulpfile 文件中封装成一个方法(参数是需要签名的文件路径),因为我们会多次调用
2.3 查看签名信息
查看 Windows 代码签名信息很简单,右键你签名的文件,签名后的文件,属性打开会有一个「数字签名」的 tab,点击切换到「数字签名」可以看到代码签名信息。
3. Mac 下的代码签名
总体建议:Mac 下应用要代码签名,因为很方便,也不是很贵,个人开发者 99 USD 一年,如果公司有 Apple Develop Team,你可以直接加入,关键是 Mac 下如果你不进行可供分发的代码签名,你的应用很难被他人安装啊。
3.1 利用 Xcode 申请证书,各个证书间差别
证书是可以在 Xcode 下申请的,Xcode —— Preference —— Account 下,选择一个Team(之前要先加入),如果是独立开发者,就选自己 Apple ID 的那个,点击「Manage Certificates」,弹出的弹窗中左下角点加号,可以选择需要的证书。
我看到之后的第一反应是:尼玛,哪些是我要的啊。下面简单说明下(摘自Mac App 发布的最后 1km):
-
Developer Certificate
- Mac Development :这个只用来开发,Debug,不是正式发布的版本
-
Production Certificate
-
Mac App Store
- Mac App Distribution :这个用于 Xcode 自己把 .app 文件上传到 Mac App Store
- Mac Installer Distribution :这个没用过,但可以肯定的,也是上传 Mac App Store 用的
-
Developer ID
- Developer ID Application:这个用于开发者使用开发者帐号签名,导出一个线下发布版本的 .app 文件,脱离了苹果的 Mac App Store。
- Developer ID Installer:用于开发者打包,同时加上开发者帐号签名,打包工具在下面介绍。
-
我们主要需要的就是「Developer ID Application」这个类型的证书,「Mac Development」只是用于开发的,而前者可以供分发,也就是签名后,别人下载安装,就是来自「被认证的开发者」的应用啦。
如果是在一个 Team 中,不是个人独立开发者,那么这个「Developer ID Application」证书的申请你是没有权限的,就算你们 Team 的 Agent 设置你为 admin(管理员),你还是没有权限的,因为一个「Developer ID Application」只有一个 Team 的 agent(owner) 才能申请,你需要做的是利用你 Mac 上的钥匙串工具(具体怎么做,google 下就可以了),生成「CertificateSigningRequest」(简称 CSR),然后发给你的 team agent,让他帮你生成证书,发回给你,你再安装到自己机子上,搞定。
你可以在终端调用security find-identity -p codesigning -v
来看一下你可用的代码签名证书,其中那个Developer ID Application
开头的就是我们要的。
3.2 签名
所用工具:electron-osx-sign
Mac 下的签名简直是红红火火开开心心嘿嘿哈哈啊,你可以从electron-osx-sign 指导这里获得完全的指导,你在这个页面右边可以根据你的项目进行填写,页面最后会根据你的配置,给你一段你都可以直接复制的签名代码,完美。
而且签名还能集成到打包阶段,不过我建议还是拿出来好,比较清真。
3.3 查看签名信息
Mac 下查看文件签名信息,你可以终端运行codesign --display --verbose=4 "文件路径"
。
六、构建安装包环节
目的:使你的应用可以被安装(如果没有这一步,你能怎么办,压缩整个应用文件夹,然后分发这个压缩包,呃,你能接受也可以啊)
做的事:把经历了打包和签名环节后的应用程序文件夹(Mac 下的.app
其实也是文件夹)打成一个安装包文件
为什么要构建安装包,这有很多的原因,可能你也会想到很多,其中值得强调的两点,一是构建安装包会直接便利于应用的自动更新,具体我们下一篇文章里再说,二是 Win 下安装包的体积相比原先的文件夹,体积明显小很多,在硬盘容积很大的时代,下载体积才是最影响用户体验的,而安装后的体积不是最需要考虑的体积。
安装包这个事和代码签名类似,两个不同的系统(Win 和 Mac)实现完全不同,Windows 下我们习惯.exe
或.msi
这样的安装包格式,习惯点下一步到完成或一键安装,而 Mac 下除了 Store 下载安装的,我们习惯的.dmg
格式的,挂载后打开,将里面的应用拖入到Application
文件夹就完成了安装。
这里我们实现的就是经典的 Windows exe 安装和 Mac dmg 安装,相比较而言,Windows 下的繁琐得多得多。
1. Windows 下利用 inno setup 进行安装包构建
1.1 为什么用这个 inno setup
最终说服我使用 inno setup 来构建应用安装包的理由是,VS Code 也是这么做的。因为按照程序这个领域离一个小前端已经很遥远了,对于跨度大的未知东西,一般都会做充足的调研,最后发现 VS Code 也是这么做的,好,干!
而使用了一段时间后,我可以说几点不后悔的理由(当然我没使用过其他的安装包构建工具,所以仅一些偏见):
- inno setup 应该是 windows 下构建安装程序的老牌工具了,你可以去进他们的官网,一股「老牌可靠」的风格扑面而来,可靠
- 它有 GUI 和 命令行工具,有 unicode 版本(意味着完全支持中文),gulp 有别人写好的现成的插件(对于中文应用需要修改)
- 基本使用的话,学习成本不大,基本去找一些案例配置文件去学一下就可以了
- 进阶使用,需要写 pascal 脚本,但是功能是真的强大
- 还有一点我感受很好的是,这个工具的支持很好,stackoverflow 上有足够的问答资源,如果还是没有你满意的,官网有一个看上去很很很简陋的论坛,但是很有用啊,我问过 2 个问题,睡一觉起来都有回应了
1.2 怎么学习 inno setup
先可以自己去搜一下 inno setup,进入官网逛一逛,下载安装一下(记得安装 unicode 版本,即括号里有 u 的版本),浏览后有几个基本认知需要具备:
- inno setup 是完全根据配置文件(.iss)来构建安装程序的,你用 GUI 其实也是去编写 .iss 文件,然后利用这个配置构建的
- inno setup 可以用 pascal 脚本控制安装向导的行为,这是进阶的使用方式,足够你安装自己的设想优化安装程序了
- inno setup 构建出来的安装包运行时可以添加参数,使安装有不同的表现,比如完全静默的后台安装(Amazing,这里的参数对于自动更新很有用)
有了上面的几点认知,可以给出「学习和使用 inno setup 路径」的建议:
- 下载安装后,找几篇 inno setup GUI 使用教程,尝试构建一个安装包(要可以安装的)
- 找一些 inno setup 配置文件的案例,对于 inno setup 配置方式有一个印象,分多个[section],每个[section]有很多配置项,每个配置项可能有多个字段
- 可以把 inno setup 官方文档 浏览一遍,跳过「pascal scripting」部分
- 到这里,你应该能看得懂他人的 .iss 文件里除了 [code] 这个 section 外的配置了
- 把安装向导的语言换成中文(先要导入中文语言包,再改配置,具体做法也有一些文章说到了,不多说,这一步对于你之后步骤也是有用的)
- 可以尝试正式结合到你的 gulp 工作流了
1.3 怎么结合到 gulp 工作流中
所用工具:修改后的 gulp-inno
如果按照之前的步骤花了个把小时大概学习了下 inno setup 的话,那么到这里你应该可以尝试把 inno setup 构建安装包做到你的 gulp 工作流中了,如果还不熟悉 inno setup 配置文件,没关系,你可以从仿照开始,不要怂,就是干,都到这一步了,谁怂谁尴尬。
配置文件的详解不是这里的重点,所以不再展开,把 inno setup 整合进脚本中,因为它本身提供命令行工具,勤快和好学的你可以根据官方或其他渠道的指导自己封装一个 node 模块,而我就比较懒了,搜到一个已有的 gulp 插件 —— 「gulp-inno」,高兴地一匹。
然而,事情总不会那么顺利,该吃的shi躲不掉,该经历的坑绕不过,这才叫「历shi」。我利用「gulp-inno」根据其指导怎么都不能正确编译,大概提示是有不合法的字符的意思。
明白了,绝壁是「gulp-inno」里包的 inno setup 不是 unicode 版本,所以一旦有中文等字符,就出错了,我看到这个包里的 inno 文件夹完全就是和我的 inno setup 文件夹没差嘛,于是我把我本地安装的 inno setup 文件夹里内容复制替换到 gulp-inno 的 inno 的文件夹内,问题解决。
因为我之前导入过中文语言包,所以我复制过去的时候,中文语言包也复制过去了,可以愉快地配置安装向导界面为中文了。
一旦修改好「gulp-inno」包(替换成 unicode 版本 & 加入简体中文语言包),就可以怎么操作:
// 1. 准备 iss 文件:填充你的 iss 配置文件模板,并输出到 dist 目录下
const appInfo = require('./app/package.json'); // 所有和应用相关的信息从 package.json 读取
const bom = require('gulp-bom'); // 这是为了解析中文的
const outputName =
`${appInfo.name}-${platform}-${appInfo.version}-${process.env.NODE_ENV}`;
const outputIssName =
`${appInfo.name}-${platform}-${process.env.NODE_ENV}.iss`
gulp
.src(`./build_resource/installer_win_config_${platform}.iss`)
.pipe(bom())
.pipe(replace('${version}', appInfo.version))
.pipe(replace('${appExe}', `${appInfo.name}.exe`))
.pipe(replace('${sourcePath}', `${appInfo.name}-${platform}`))
.pipe(replace('${outputName}',outputName))
.pipe(rename(outputIssName))
.pipe(gulp.dest('./dist'))
.on('end', () => {
// .iss file is ready
})
// 2. 交给 inno setup
const inno = require('my-gulp-inno'); // 修改后的 gulp-inno
gulp
.src(`./dist/${outputIssName}`)
.pipe(inno())
.on('end', () => {
// you have an installer now
});
1.4 未来可以做什么
当时还有一个看中 inno setup 的理由是,它可以让我们定制我们的安装向导步骤和外观,也就是说你可以让你的应用也像其他一些优秀的产品一样,在安装的时候可以定制酷炫的外观,可以优化安装流程,支持一键安装,inno setup 还是可以玩出一些花样的,enjoy。
1.5 对安装包也进行代码签名
同样的,安装包也需要代码签名,利用之前封装的签名方法进行签名就行了。
2. Mac 下的构建 dmg 安装包
所用工具:appdmg
相比于 windows 的安装包构建,Mac 下的构建安装包又是美滋滋啊,你看我下面小标题都没有就知道了。
// 因为 appdmg 在 windows 下不能下载安装的,所以放在外部 package.json 的 optionalDependencies 下
// 在 gulp 脚本中需要做 try...catch 处理,否则当你回到 windows 下使用这份 gulp 时会出报错
let appdmg;
try {
appdmg = require('appdmg');
} catch (err) {
appdmg = null;
}
const dmg = appdmg({
// 打出的目标 dmg
target: `dist/balabala.dmg`,
// 基准目录,以下的资源都基于这个目录
basepath: __dirname,
// 具体的选项
specification: {
// dmg 打开后的窗口名字
// 注意不要给中文,给中文会导致下面的 background 无效,不明白, github 上也有人提了这个 issue
title: `myapp`,
// dmg 挂载后的图标,出现在桌面上
icon: "xxx.icns",
// 背景图,如果同时存在 bg.png 和 bg@2x.png,appdmg 会根据用户屏幕自己找合适的图
background: "bg.png",
// 里面所有icon的尺寸
'icon-size': 96,
// 窗口设置
window: {
size: {
width: 550,
height: 320
}
},
// 里面的内容,x 是指这个 icon 中心距离窗口最左边的距离,y 是指这个 icon 中心距离窗口顶部的距离
// 这里可以指定一个name项,不要给中文,会导致图标异常
contents: [
{ "x": 400, "y": 128, "type": "link", "path": "/Applications" },
{ "x": 150, "y": 128, "type": "file", "path": "你的应用.app" }
],
// 对 dmg 进行代码签名
'code-sign': {
'signing-identity': '你的代码签名证书'
}
}
});
dmg.on('finish', function () {
// you have a dmg now
});
dmg.on('error', function (err) {
// error
});
其余的配置和所以配置影响的内容可以参加 appdmg githug 主页,然后就是自己试试看了。
七、发行环节
目的:使应用可以被下载(上一步只是能被安装,但并不能被下载)
做的事:重命名应用安装包供发行,上传应用安装包到云存储服务器供下载
这一步根据每个人使用的云存储方式不同而需要利用卖方提供的 API 编写合适的脚本去上传你的安装包,因此具体的脚本不做展开,只是有几点最佳实践可以参考:
- 上传前,把你的安装包文件重命名成符合一定规范的,可能是「应用名-版本-阶段-系统-尾数」,可能是「应用名-版本-系统-构建号」,可能是...这个就自己定,但一定要有一个合适的命名,这样一看到名字就知道这个是啥,不会弄错
- 你的 OSS 服务器上要针对应用安装包的不同阶段建立不同的文件夹,一方面可以方面管理,另一方面也便于做权限管理
当你上传了你的安装包后,也就意味着这个安装包有了一个下载链接,你可以分发这个链接供用户下载啦,至此终于走完了「代码」到可下载「安装包」的过程,鼓掌。
八、路漫漫
这一路走来看上去已经很有成就感,但实际上还有许多事可以做得更好,不过工程化的东西,逻辑清晰、流程自动化、能满足需求就可以了,而搭好工程,我们需要开始专注于 Electron 应用的功能开发了,才刚刚要迈上红地毯,路还有很长,下期见。
附:gulp 文件和脚本看上去会是怎样的
对之前的工作流做一个小结(如果遇到有一些旧文件覆盖不了,可以自己加一个清理环节或方法,去清理旧文件)
/* gulpfile.js START */
// 此处省略一堆需要引入的依赖
// 工作参数
let platform = 'win32';
let arch = 'ia32';
let needSign = false;
// 配置环节
gulp.task('env', (cb) => {
// ...
});
// 开发调试
gulp.task('dev', ['env'], (cb) => {
exec('electron app --debug', (err) => {
if (err) return cb(err);
cb();
});
});
// 打包环节
gulp.task('pack',['env'], (cb) => {
if (platform === 'darwin') {
// ...
} else {
// ...
}
});
// 签名环节
gulp.task('sign-pack', ['pack'], (cb) => {
if (needSign) {
if (platform === 'win32' || platform === 'win64') {
// ...
} else if (platform === 'darwin') {
// ...
}
} else {
cb();
}
});
// 构建环节
gulp.task('build', ['sign-pack'], (cb) => {
if (platform === 'darwin') {
// ...
} else {
// ...
}
});
// 发行环节
gulp.task('release', ['build'], (cb) => {
// ...
});
const codeSignForWin = (filePath) => {...};
const codeSignForMac = (filePath) => {...};
const detectPlatform = () => {...};
/* gulpfile.js END */
// package.json 中配脚本
"scripts": {
"yarnall": "yarn && (cd app && yarn)",
"start": "cross-env NODE_ENV=dev gulp dev",
"packDev": "cross-env NODE_ENV=dev gulp pack",
"packAlpha": "cross-env NODE_ENV=alpha gulp pack",
"packProd": "cross-env NODE_ENV=prod gulp pack",
"buildDev": "cross-env NODE_ENV=dev gulp build",
"buildAlpha": "cross-env NODE_ENV=alpha gulp build",
"buildProd": "cross-env NODE_ENV=prod gulp build",
"releaseDev": "cross-env NODE_ENV=dev gulp release",
"releaseAlpha": "cross-env NODE_ENV=alpha gulp release",
"releaseProd": "cross-env NODE_ENV=prod gulp release"
}
// 可选命令行参数:
// sign: 是否签名
// platform: 系统平台
// arch: 系统位数
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。