初探webpack之单应用多端构建
在现代化前端开发中,我们可以借助构建工具来简化很多工作,单应用多端构建就是其中应用比较广泛的方案,webpack
中提供了loader
与plugin
来给予开发者非常大的操作空间来操作构建过程,通过操作中间产物我们可以非常方便地实现多端构建,当然这是一种思想而不是深度绑定在webpack
中的方法,我们也可以借助其他的构建工具来实现,比如rollup
、vite
、rspack
等等。
描述
首先我们先来聊聊多端构建,实际上单应用多端构建的思想非常简单,就是在同一个项目中我们可以通过一套代码来构建出多个端的代码,例如小程序的跨平台兼容、浏览器扩展程序的跨平台兼容、海内外应用资源合规问题等等,这些场景的特点是核心代码是一致的,只不过因为跨平台的原因会有接口调用或者实现配置的差异,但是差异化的代码量是非常少的,在这种场景下借助构建工具来实现单应用多端编译是非常合适的。
在这里需要注意的是,我们是在编译的过程中处理掉单应用跨平台造成的代码冗余情况,而例如在浏览器中不同版本的兼容代码是需要执行动态判断的,不能够作为冗余处理,因为我们不能够为每个版本的浏览器都分发一套代码,所以这种情况不属于我们讨论的多端构建场景。实际上我们也可以理解为因为我们能够绝对地判断代码的平台并且能够独立分发应用包,所以才可以在构建的过程中将代码分离,兼容平台的代码不会消失只会转移,相当于将代码中需要动态判断平台的过程从运行时移动到了构建时机,从而能够获得更好的性能与更小的包体积。
接下来实现多端构建就需要借助构建工具的能力了,通常构建工具在处理代码资源压缩时会有清除DEAD CODE
的能力,即使构建工具没有预设这个能力,通常也会有插件来组合功能,那么我们就可以借助这个方法来实现多端构建。那么具体来说,我们可以通过if
条件,配合代码表达式,让代码在编译的过程中保证是绝对的布尔值条件,从而让构建工具在处理的过程中将不符合条件的代码处理掉DEAD CODE
即可。此外由于我们实际上是处理了DEAD CODE
,那么在一些场景下例如对内与对外开放的SDK
有不同的逻辑以及包引用等,就可以借助构建工具的TreeShaking
实现包体积的优化。
if ("chromium" === "chromium") {
// xxx
}
if ("gecko" === "chromium") {
// xxx
}
process.env
我们在平时开发的过程中,特别是引入第三方Npm
包的时候,可能会发现打包之后会有出现ReferenceError: process is not defined
的错误,这也算是经典的异常了,当然这种情况通常是发生在将Node.js
代码应用到浏览器环境中,除了这种情况之外,在前端构建的场景中也会需要使用到process.env
,例如在React
的入口文件react/index.js
中就可以看到如下的代码:
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
当然在这里是构建时发生的,实际上还是运行在Node
环境中的,通过区分不同的环境变量打包不同的产物,从而可以区分生产环境与开发环境的代码,从而提供开发环境相关的功能和警告。那么类似的,我们同样也可以借助这种方式作为多端构建的条件判断,通过process.env
来判断当前的平台,从而在构建的过程中将不符合条件的代码处理掉。类似于React
的这种方式来做跨平台编译当然是可行的,只不过看起来这似乎是commonjs
的模块化管理方式,而ES Module
是静态声明的语句,也就是说导入导出语句必须在模块的顶层作用域中使用,而不能在条件语句或循环语句等代码块中使用,所以这段代码通常可能需要手动维护或者需要借助工具自动生成。
那么在ES Module
静态声明中,我们就需要借助共建工具来完成跨端编译的方案了。回到刚开始时提到的那个process is not defined
的问题,除了上述的两种情况,还有一种常见的情况是process
这个变量代码本身就存在于代码当中,而在浏览器在runtime
执行的时候发现并没有process
这个变量从而抛出的异常。在最开始的时候,我还是比较纳闷这个Node
变量为什么会出现在浏览器当中,所以为了解决这个问题我可能会在全局声明一下这个变量,那么在现在看来当时我可能产生了误用的情况,实际上我们应该借助于浏览器构建工具来处理当前的环境配置。那么我们来举个例子,假设此时我们的环境变量是process.env.NODE_ENV
是development
,而我们的源码中是这样的,那么在借助打包工具处理之后,这个判断条件就会变成"development" === "development"
,这个条件永远为true
,那么else
的部分就会变成DEAD CODE
进而被移除,由此最后我们实际得到的url
是xxx
,同理在production
的时候得到的url
就会变成xxxxxx
。
let url = "xxx";
if (process.env.NODE_ENV === "development") {
console.log("Development Env");
} else {
url = "xxxxxx";
}
export const URL = url;
// 处理后
let url = "xxx";
if ("development" === "development") {
console.log("Development Env");
}// else {
// url = "xxxxxx";
// }
export const URL = url;
实际上这是个非常通用的处理方式,通过指定环境变量的方式来做环境的区分,以便打包时将不需要的代码移除,例如在Create React App
脚手架中就有custom-environment-variables
相关的配置,也就是必须要以REACT_APP_
开头的环境变量注入,并且NODE_ENV
环境变量也会被自动注入,当然值得注意的是我们不应该把任何私钥等环境变量的名称以REACT_APP_
开头,因为这样如果在前端构建的源码中有这个环境变量的使用,则会造成密钥泄漏的风险,这也是Create React App
约定需要以REACT_APP_
开头的环境变量才会被注入的原因。
那么实际上这个功能看起来是不是非常像字符串替换,而webpack
就提供了开箱即用的webpack.DefinePlugin
来实现这个能力https://webpack.js.org/plugins/define-plugin/
,这个插件可以在打包的过程中将指定的变量替换为指定的值,从而实现我们要做的允许跨端的的不同行为,我们直接在webpack
的配置文件中配置即可。此外,使用ps -e
或systemctl status
查看进程pid
,并配合cat /proc/${pid}/environ | tr '