24.深入浅出模块化(含 tree shaking)(下)

本节课程,让我们继续模块化的话题。在此之前,先来回顾一下这个主题的知识点:


模块化发展历程

在上一讲中,介绍了以下方案实现模块化:

早期命名空间模拟模块化

  • CommonJS
  • AMD
  • CMD
  • UMD

接下来我们来探讨 ES 原生模块化的知识,并就 tree shaking 这个话题展开。

ES 原生时代和 tree shaking

ES 模块化(或称为 ESM)的具体使用方法我们不再具体介绍,请读者先了解相关基础内容。

ES 模块的设计思想是尽量静态化,这样能保证在编译时就确定模块之间的依赖关系,每个模块的输入和输出变量也都是确定的。CommonJS 和 AMD 模块,无法保证前置即确定这些内容,只能在运行时确定。这是 ES 模块化和其他规范的显著不同。第二个差别在于,CommonJS 模块输出的是一个值的拷贝,ES 模块输出的是值的引用。我们来具体看一下:

// data.js
export let data = 'data'
export function modifyData() {
   data = 'modified data'
}

// index.js
import { data, modifyData } from './lib'
console.log(data) // data
modifyData()
console.log(data) // modified data

我们在 index.js 中调用了 modifyData 方法,之后查询 data 值,得到了最新的变化。

而同样的逻辑,在 CommonJS 规范下的表现为:

// data.js
var data = 'data'
function modifyData() {
   data = 'modified data'
}

module.exports = {
   data: data,
   modifyData: modifyData
}

// index.js
var data = require('./data').data
var modifyData = require('./data').modifyData
console.log(data) // data
modifyData()
console.log(data) // data

因为 CommonJS 是输出了值的拷贝,而非引用,因此在调用 modifyData 之后,index.js 的 data 值并没有发生变化,其值为一个全新的拷贝。

ES 模块化为什么要设计成静态的

一个明显的优势是:通过静态分析,我们能够分析出导入的依赖。如果导入的模块没有被使用,我们便可以通过 tree shaking 等手段减少代码体积,进而提升运行性能。这就是基于 ESM 实现 tree shaking 的基础。

这么说可能过于笼统,我们从设计的角度分析这两种规范哲学的利弊。静态性需要规范去强制保证,不像 CommonJS 那样灵活,ES 模块化的静态性带来了限制:

  • 只能在文件顶部 import 依赖
  • export 导出的变量类型严格限制
  • 变量不允许被重新绑定,import 的模块名只能是字符串常量,即不可以动态确定依赖

这样的限制在语言层面带来的便利之一是:我们可以通过作用域分析,分析出代码里变量所属的作用域以及它们之间的引用关系,进而可以推导出变量和导入依赖变量的引用关系,在没有明显引用时,就可以进行去冗余。

tree shaking

上面说到的「在没有明显引用时,就可以进行去冗余」,就是我们经常提到的 tree shaking,它的目的就是减少应用中写出,但没有被实际运用的 JavaScript 代码。这样一来,无用代码的清除,意味着更小的代码体积,bundle size 的缩减,对用户体验起到了积极作用。

在计算机科学当中,一个典型去除无用代码、冗余代码的手段是 DCE,dead code elimination。那么 tree shaking 和 DCE(Dead Code Elemination)有什么区别?

Rollup 的主要贡献者 Rich Harris 做过这样的比喻:假设我们用鸡蛋做蛋糕。显然,我们不需要蛋壳而只需要蛋清和蛋黄,那么如何去除蛋壳呢?DCE 是这样做的:直接把整个鸡蛋放到碗里搅拌,蛋糕做完后再慢慢地从里面挑出蛋壳

相反,与 DCE 不同,tree shaking 是开始阶段就把蛋壳剥离,留下蛋清和蛋黄。事实上,也可以将 tree shaking 理解为广义 DCE 的一种,它在前置打包时即排除掉不会用到的代码。

当然说到底,tree shaking 只是一种辅助手段,良好的模块拆分和设计才是减少代码体积的关键。

Tree shaking 也有局限性,它还有很多不能清除无用代码的场景,比如 Rollup 的 tree shaking 实现只处理函数和顶层的 import/export 导入的变量,不能把没用到的类的方法消除;对于 tree shaking 来说,具有副作用的脚本无法被优化。

更多情况读者可以参考:

tree shaking 使用注意事项

webpack 和 Rollup 构建工具目前都有成熟的方案,但是笔者并不建议马上引入到项目中。事实上,是否要在成熟的项目上立即实施 tree shaking 需要妥善考虑。这里我也提供几篇收藏的文章,介绍了 tree shaking 的使用方法,这些基本操作内容,我们不再展开,可以按照官方文档实施,我也在文档之外推荐这些内容供大家学习。

ES 的 export 和 export default

ES 模块化导出有 export 和 export default 两种。这里我们建议减少使用 export default 导出,原因是一方面 export default 导出整体对象结果,不利于 tree shaking 进行分析;另一方面,export default 导出的结果可以随意命名变量,不利于团队统一管理。

Nicholas C. Zakas 有一篇文章: Why I've stopped exporting defaults from my JavaScript modules ,表达了类似的观点。

未来趋势和思考

个人认为,ES 模块化是未来不可避免的发展趋势,它的优点毫无争议,比如开箱即用的 tree shaking 和未来浏览器兼容性支持。Node.js 的 CommonJS 模块化方案甚至也会慢慢过渡到 ES 模块化上。如果你正在使用 webpack 构建应用项目,那么 ES 模块化是首选;如果你的项目是一个前端库,也建议使用 ES 模块化。这么看来,或许只有在编写 Node.js 程序时,才需要考虑 CommonJS。

在浏览器中快速使用 ES 模块化

目前各大浏览器较新版本都已经开始逐步支持 ES 模块了。如果我们想在浏览器中使用原生 ES 模块方案,只需要在 script 标签上添加一个 type="module" 属性。通过该属性,浏览器知道这个文件是以模块化的方式运行的。而对于不支持的浏览器,需要通过 nomodule 属性来指定某脚本为 fallback 方案:

<script type="module">
  import module1 from './module1'
</script>

<script nomodule>
  alert('你的浏览器不支持 ES 模块请先升级')
</script>

使用 type="module" 的另一个作用是进行 ES Next 兼容性的嗅探。因为支持 ES 模块化的浏览器,都支持 ES Promise 等特性,基于此,应用场景较多。

在 Node.js 中使用 ES 模块化

Node.js 从 9.0 版本开始支持 ES 模块,执行脚本需要启动时加上 --experimental-modules,不过这一用法要求相应的文件后缀名必须为 *.mjs:

node --experimental-modules module1.mjs
import module1 from './module1.mjs'
console.log(module1)

另外,也可以安装 babel-cli 和 babel-preset-env,配置 .babelrc 文件后,执行:

./node_modules/.bin/babel-node

或:

npx babel-node

在工具方面,webpack 本身维护了一套模块系统,这套模块系统兼容了几乎所有前端历史进程下的模块规范,包括 AMD/CommonJS/ES 模块化等,具体分析咱们见后续课程《webpack 工程师 > 前端工程师》(即下一讲的内容)。

总结

通过本课程的学习,我们了解了 JavaScript 模块化的历史,重点分析了不同过渡方案的不同实现以及 ES 模块化标准的细节。希望读者对模块系统有一个更清晰的认识,同时希望大家可以仔细阅读源码,对代码设计有自己的理解和体会。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容