通常情况下,按需引入区别于异步加载,但是本文会统一讲述这些「有需要时」才去拿取或剔除相关资源的类似场景,因此标题中的「按需引入」代表了这些做法的泛概念。
随着项目体量提升,如果我们不作干涉,可能会碰到项目首页加载慢、chunk 体积大、各环境配置未相互分离、调试代码需手动剔除等一系列问题,其中有不少问题是构建行为不合理引起的。 本文就这些问题分享一些「按需引入」的实现方式。
import()
import()
语法分为了静态引入以及动态引入。
静态引入无需多说,动态引入也已在 ES6 中得到了实现,import
在支持 ES6 语法的环境中将会是一个全局函数,它接收需要动态引入的脚本地址,返回一个 Promise
,其异步返回值将会是一个对象,对象中包含动态引入模块的导出信息。
为了兼容性,Webpack 将会对 import()
语法作编译处理,将其转换为 WebpackJsonp 的加载方式。Webpack 将引入的模块单独打包为一个 chunk,在需要用到这部分代码时再使用注入 <script>
标签的方式将其请求到本地并运行。 此外,Webpack 还对这个 API 进行了一些能力扩展,比如可以在其中书写 /* webpackChunkName: "xxx" */
这样的注释实现自定义这个模块的 chunk 名称。
babel-plugin-import
babel-plugin-import
是一个处理模块导入的 Babel 插件。
它对三方库进行按需引入的处理在默认配置下有些约定俗成的意味,它要求三方库中拥有 lib
文件夹,并且在 lib
文件夹中拥有和三方库同名(camel-case)的模块文件。抛开默认情况,它也提供了一系列配置让我们可以自定义三方库的导入形式。这个特性可以实现诸如根据环境选择导入不同的模块、组件库的按需引入、工具函数库的按需引入等一系列需求。
配置文件 .babelrc
业务代码
编译结果
我们可以清楚地看到,原来直接从 antd
引入的 Button
组件被编译成了从 antd/lib/button
引入。
配置文件 babel.config.js
我们也能够通过自定义 customName
函数来实现变更引入路径的效果,使用方法和其他配置可以自行查阅 Babel 官网。
插个题外话,我们也能够自己编写一个 Babel 插件来实现针对内部库的按需导入,思路是通过拦截自定义 Babel 插件 visitor
配置中的 ImportDeclaration
节点,根据节点配置决定使用哪个其他模块路径进行替换。
这是我们团队制作的自定义 Babel 插件的一部分,它实现了类似于将 import { abc } from 'xxx';
在编译时转换为 import abc from 'xxx/lib/abc.js
这样的效果,和 babel-plugin-import
实现的效果非常类似。
人一定要做好减法,不断专注聚焦,系统也是如此。相对于按需引入,Tree Shaking 从反方向聚焦于按需剔除。
Tree Shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的「静态结构」特性,例如
import
和export
。这个术语和概念实际上是由 ES2015 模块打包工具 Rollup 普及起来的。
说到「未引用」,这里要先明白「纯净」和「副作用」这两个概念,我们先看下面两段代码。
纯净(pure)的代码
有副作用(side-effect)的代码
「纯净」在文件的维度指的是 纯正的 ES2015 模块,这种模块只包含导出代码,不包含其他有「副作用」的代码,从上面的代码样例我们就可以很容易理解,所谓「副作用」指的就是在模块被导入时会有其他影响当前环境代码被执行。
Tree Shaking 的用途其实就是分析代码,将未导入的并且没有副作用的模块(文件或局部代码)在最终构建结果中删除。
Webpack 4 天然支持了 Tree Shaking,我们必须执行以下操作确保其生效:
import
和 export
);package.json
文件中,添加 sideEffects
属性来标记哪些文件是有副作用的;mode
为 production
的配置项以启用 更多优化项 ,包括压缩代码与 Tree Shaking。回到这个术语「Tree Shaking」的含义上,字面意思是摇晃树。
你可以将应用程序想象成一棵树。绿色表示实际用到的源码和库,是树上活的树叶。灰色表示未引用代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。
接触过 uni-app 的同学对条件编译应该不陌生,下面是使用了 uni-app 的条件编译示例:
demo.html
demo.js
上面的代码示例意思是,当平台为微信或百度小程序时才会将上述 HTML 代码编译进这两个平台的小程序最终代码中,以及当平台为 APP 时才将上述 JavaScript 代码编译进最终代码中。
这就是条件编译的概念,它直接在编译阶段就进行逻辑区分,防止无关代码污染各个输出结果。通过条件编译,我们就能实现一份代码根据不同环境或输出目标来表现不同的展示形式,而不必考虑运行时判断代码的浪费以及相关性能问题。
通过 Webpack 的三方插件,我们能很轻松地实现条件编译这种效果,本文介绍两款三方插件——传统的 uglify-js-plugin 和 js-conditional-compile-loader,下面就简单讲述一下它们的使用方法。
我们对 webpack.config.js
进行配置:
之后在业务代码中我们直接这么写:
上面的代码意思就是当 ENV_TEST
值为 true
时,才将 if
中的代码编译进结果中。
使用 uglify-js-plugin
进行条件编译的方式相当于是曲线救国,它本身不具有条件编译的功能,但是它有一个剔除死代码的功能,死代码的意思如同:
我们很容易看出这段代码永远不会被执行,这就称为死代码。
我们可以通过使用 define-plugin
插件来使一段代码成为死代码,方法就是 if
中的条件写成我们通过 define-plugin
定义的全局变量,而这个变量在某些环境下会是永远的 false
,这就成了死代码。所以说使用 uglify-js-plugin
和 define-plugin
进行条件编译的方式是曲线救国。
js-conditional-compile-loader
是一个专业做条件编译的 Webpack Loader,相比于 uglify-js-plugin
曲线救国的方式,它更灵活,理论上它支持任何文件中任何部分的条件编译,相当于它是将文件当成了字符串进行处理,在某些环境下保留或剔除被特定格式包裹的字符串。
比如我们在一个 Vue 组件中使用它:
它决定是否剔除 注释包裹内容 的方式也是通过它的 Loader 配置实现的,相当于使用了它内部的环境变量,下面我们就来看一下它是如何被配置到 Webpack 中的。
这里需要注意,js-conditional-compile-loader
需要作为处理源文件的第一步,即放在每种文件处理规则中 loader
数组的末尾。原因很容易想,因为避免其他的 Loader 改变文件结构,导致文件内容发生变化而不生效。
import()
babel-plugin-import
customName
ImportDeclaration
uglify-js-plugin
js-conditional-compile-loader
我们总共学习了四大类「按需引入」的方式,相信这些方式能解决我们工作中不少构建问题。我在日常工作中秉承的原则是能不重复造轮子就不重复造轮子,有优秀的三方库就直接使用,当然如果我们有自己的个性化需求,也可以参考他们这些优秀的实现方式来创造我们自己的提效工具。
领取专属 10元无门槛券
私享最新 技术干货