webpack插件 vuecli-electron-bytenode-webpack-plugin 中文文档

webpack插件 vuecli-electron-bytenode-webpack-plugin 中文文档
gy这里是:webpack插件 vuecli-electron-bytenode-webpack-plugin 中文文档。
1 简介
1.1 背景介绍
随着electron框架的广泛应用,应用程序的安全性和性能正在逐渐受到重视。把js文件编译成字节码,不仅使得逆向分析难以进行,也可以省去V8的字节码编译步骤,可能带来一定的加载性能提升。
vuecli3-electron-bytenode-webpack-plugin是一个发布在npm和yarn上的webpack插件,致力于在构建流程中自动且可靠地进行上述工作,受到electron-bytenode-webpack-plugin的启发。我在其基础上增加了对vue-cli的支持,以及更多、更灵活的配置项,使得开发者可以根据自己的需求进行定制。
1.2 适用条件
以下条件同时满足,是本插件可以正常发挥作用的充分不必要条件:
最终代码运行环境:Electron,且渲染进程开启了Node.js环境集成
构建工具:Webpack
脚手架:Vue-cli
1.3 为什么我要开发这个插件?
虽然已经有了electron-bytenode-webpack-plugin这个先例,但是这个插件(以及其他同类插件)依然存在以下缺点:
- 无法与Vue-cli兼容
- 无法处理使用
webpackChunkName标记的懒加载模块 - 可配置性差
- 作者已经不维护
- 构建产物较大,冗余度高
2 使用
2.1 安装
在你的项目根目录中执行以下命令:
1 | |
2.2 引入
在你的项目配置文件中(一般是vue.config.js),引入本插件。
1 | |
2.3 配置
按照第三节的说明,进行配置。其中lazyBundleNames必须填写。
2.4 启动构建流程
配置好之后就可以使用了。默认只会在production环境下进行字节码的编译。
1 | |
3 配置API
按照如下的配置格式来进行插件配置:
1 | |
3.1 lazyBundleNames
- 描述:懒加载chunk的名称。为保证所有.jsc模块的正常引入,务必全部列出来。比如,举一个Vue项目中最常见的例子:在用户的router.ts内,下面代码使用了特殊注释
/* webpackChunkName */进行模块的拆分,会把名字为settings的组件从app.jsc中拆分为一个独立的单文件settings.jsc,那么你在配置本插件的时候就要在lazyBundleNames里面插入'settings'。
1 | |
- 类型:
Array<string> - 默认值:
[] - 示例:
1 | |
3.2 environments
- 描述:指定本插件在何种
process.env.NODE_ENV下运行。若不满足匹配条件,本插件不会在apply钩子被调用之后做任何事情。 - 类型:
Array<string> - 默认值:
["production"] - 示例:
1 | |
3.3 compileAsModule
- 描述:是否把.jsc文件编译成模块。建议不要随便乱动,保留默认值
true。 - 类型:
boolean - 默认值:
true
3.4 keepSource
- 描述:是否在构建产物中保留未被编译成字节码的代码。由于保留了源码引用,开启这一配置项之后
Function.prototype.toString就能正常使用了。 - 类型:
boolean - 默认值:
false
3.5 logLevel
- 描述:控制日志等级。本插件运行过程中,会向控制台输出日志信息,使用这个配置项可以选择只输出大于特定级别的日志,有助于在不同阶段或需求下更清晰地查看日志信息。
- 类型:
'debug' | 'info' | 'warning' | 'error' | 'silent',从左至右等级逐渐升高,调试信息的数量逐渐减少 - 默认值:
'info' - 示例:
1 | |
4 实现原理
4.1 核心原理
本插件主要依赖bytenode模块,借助vm.Script().createCachedData()获得V8字节码缓存,经过一些二次处理(如html内容修改、字节码模块化包装、loader代码嵌入等)后,将生成的内容emit到最终的构建结果里。
4.2 执行流程
4.2.1 初始化。webpack构建流程启动后,本插件会在apply(compiler)中注册compiler.hooks.emit钩子。当构建进行到输出asset到output目录的阶段时,这个钩子被触发,劫持当前compilation,将assets列表中符合字节码编译条件的js或html文件写入到指定目录(如果这个目录中已经存在文件,会先将其清空),并创建资源列表raw_assets.json。随后创建一个nodejs子进程,执行electron cli.js命令。这个命令会启动一个完整的electron环境,为接下来的字节码编译做好准备。
4.2.2 字节码编译。根据资源列表raw_assets.json,遍历指定目录,逐个编译文件,将编译结果同样写入到这个目录中,并创建编译结果列表compiled_assets.json。整个编译过程都是在electron环境中完成,由渲染进程的V8引擎负责字节码的转换。
4.2.3 模块包装。对字节码使用Module.wrap()进行包装,生成nodejs模块。
**4.2.4 ** 注入运行时代码。注入运行时代码,主要是注入loader代码。修改html中的script标签内容,使得字节码文件能够像普通JS文件一样被正常引入。对于使用webpackChunkName标记的懒加载模块,本插件提供了特殊支持,替换为loader代码,把js文件作为跳板去加载字节码。
bytenode是一个特殊的node模块,因为它既是构建时的编译工具,又是运行时的依赖库。这说明,如果我只是想在运行时使用bytenode来加载字节码文件,我其实是用不到bytenode的编译功能的。所以,我对运行时注入的bytenode模块进行了定制裁剪,去除了编译功能,保留了字节码加载功能,并且使用Uglify.js进行压缩。这一套操作下来,构建产物的大小从20+KB降为3KB左右。
注入的bytenode模块使用了IIFE进行包裹(~function(){/*裁剪和压缩过的bytenode模块*/}()),不会污染全局作用域。
5 注意事项
5.1 兼容性
与常见的JavaScript源码不同,v8的字节码属于js引擎的内部实现(internal implements),没有相应的规范来约束,往往与特定的平台或操作系统相关,而且不同版本的v8所生成的字节码也不完全一致。例如,如果你的App是在AMD或ARM平台的自动构建服务器上完成构建和分发的,那么可能没办法在用户的Intel设备上正常运行。注意,我说的是“可能”,因为影响兼容性的因素很多、很复杂,你需要自己去做实验。实践才是检验真理的唯一标准。
下面这张图,展示了不兼容的字节码文件,被V8引擎拒绝执行的情景:

所以,如果你想提高构建平台与目标平台的兼容性,请确保以下因素一致或者尽可能接近:
- Node.js版本
- CPU架构、指令集
- 操作系统
- Electron版本(虽然本插件编译字节码时使用的electron环境与运行时一致,理论上electron版本不会影响兼容性,但是我在实际项目中还是遇到了不兼容问题。我最后通过降低并锁定electron版本到v27.1.2解决了问题。)
另外,如果你使用Github Actions,你可以在工作流脚本里配置jobs.<job_id>.runs-on,指定容器的操作系统和CPU架构。这也许对提高兼容性有所帮助。详戳↓
5.2 开启node集成(nodeIntegration)
我们知道,Electron创建窗口时提供了nodeIntegration选项,决定渲染进程是否可以使用node API。然而本插件的注入代码高度依赖完整的node环境,如果主进程在创建渲染进程时设置了nodeIntegration: false,那么渲染进程内就完全无法进行字节码模块的加载,本插件将起不到任何正面作用。所以,请开启node集成。当然,这么做破坏了上下文保护机制,必定会导致额外的安全问题,需要你自己去做出权衡。
5.3 Function.prototype.toString问题
Any code depends on Function.prototype.toString function will break, because Bytenode removes the source code from .jsc files and puts a dummy code instead. See #34. For a workaround, see #163
本插件依赖Bytenode,借助new vm.Script执行.jsc文件。new vm.Script强制要求传入源代码作为第一个参数,因此这里Bytenode直接向其传递若干零宽度空格(’\u200b’)糊弄过去了:
1 | |
这就导致了Function.prototype.toString无法正常执行。请检查你的代码以及production依赖中是否调用了Function.prototype.toString方法,若存在这个情况,请将这个方法覆写。(有一些库会调用toString来判断一个function是不是class的构造函数。)







