原文发表在个人博客上。最近捣鼓了一下 ES6 的模块化,分享一些经验 :)javascript
Python3 已经发布了九年了,Python 社区却还在用 Python 2.7;而 JavaScript 社区正好相反,你们都已经开始把尚未实现的语言特性用到生产环境中了 (´_ゝ `)前端
虽然这种奇妙状况的造成与 JavaScript 自身早期的设计缺陷以及浏览器平台的特殊性质都有关系,但也确实可以体现出 JavaScript 社区的技术栈迭代是有多么屌快。若是你昏迷个一年半载再去看前端圈,可能社区的主流技术栈已经变得它妈都不认识了(若是你没什么实感,能够看看《在 2016 年学习 JavaScript 是一种怎样的体验》这篇文章,你会感觉到的,你会的)。java
随着 JavaScript 愈来愈普遍的应用,朝着单页应用(SPA)方向发展的网页与代码量的愈发庞大,社区须要一种更好的代码组织形式,这就是模块化:将你的一大坨代码分装为多个不一样的模块。node
可是在 ES6 标准出台以前,因为标准的缺失(连 CSS 都有 @import
,JavaScript 却连个毛线都没),这几年里 JavaScript 社区里冒出了各类各样的模块化解决方案(群魔乱舞),懵到一种极致。主要的几种模块化方案举例以下:webpack
主要用于服务端,模块同步加载(也所以不适合在浏览器中运行,不过也有 Browserify
之类的转换工具),Node.js 的模块化实现就是基于 CommonJS 规范的,一般用法像这样:git
// index.js const {bullshit} = require('./bullshit'); console.log(bullshit()); // bullshit.js function someBullshit() { return "hafu hafu"; } modules.export = { bullshit: someBullshit };
并且 require()
是动态加载模块的,彻底就是模块中 modules.export
变量的传送门,这也就意味着更好的灵活性(按条件加载模块,参数可为表达式 etc.)。es6
即异步模块定义(Asynchronous Module Definition),不是那个平常翻身的农企啦。github
主要用于浏览器端,模块异步加载(仍是用的回调函数),能够给模块注入依赖、动态加载代码块等。具体实现有 RequireJS,代码大概长这样:web
// index.js require(['bullshit'], words => { console.log(words.bullshit()); }); // bullshit.js define('bullshit', ['dep1', 'dep2'], (dep1, dep2) => { function someBullshit() { return "hafu hafu"; } return { bullshit: someBullshit }; });
惋惜不能在 Node.js 中直接使用,并且模块定义与加载也比较冗长。shell
在 ES6 模块标准出来以前,主要的模块化方案就是上述 CommonJS 和 AMD 两种了,一种用于服务器,一种用于浏览器。其余的规范还有:
最古老的 IIFE(当即执行函数);
CMD(Common Module Definition,和 AMD 挺像的,能够参考:与 RequireJS 的异同);
UMD(Universal Module Definition,兼容 AMD 和 CommonJS 的语法糖规范);
等等,这里就按下不表。
ES6 的模块化代码大概长这样:
// index.js import {bullshit} from './bullshit'; console.log(bullshit()); // bullshit.js function someBullshit() { return "hafu hafu"; } export { someBullshit as bullshit };
那咱们为啥应该使用 ES6 的模块化规范呢?
这是 ECMAScript 官方标准(嗯);
语义化的语法,清晰明了,同时支持服务器端和浏览器;
静态 / 编译时加载(与上面俩规范的动态 / 运行时加载不一样),能够作静态优化(好比下面提到的 tree-shaking),加载效率高(不过相应地灵活性也下降了,期待 import()
也成为规范);
输出的是值的引用,可动态修改;
嗯,你说的都对,那我tm到底要怎样才能在生产环境中用上 ES6 的模块化特性呢?
很遗憾,你永远没法控制用户的浏览器版本,可能要等上一万年,你才能直接在生产环境中写 ES6 而不用提心吊胆地担忧兼容性问题。所以,你仍是须要各类各样杂七杂八的工具来转换你的代码:Babel、Webpack、Browserify、Gulp、Rollup.js、System.js ……
噢,我可去你妈的吧,这些东西都tm是干吗的?我就是想用个模块化,我到底该用啥子?
本文正旨在列出几种可用的在生产环境中放心使用 ES6 模块化的方法,但愿能帮到诸位后来者(这方面的中文资源实在是忒少了)。
想要开心地写 ES6 的模块化代码,首先你须要一个转译器(Transpiler)来把你的 ES6 代码转换成大部分浏览器都支持的 ES5 代码。这里咱们就选用最多人用的 Babel(我不久以前才知道原来 Babel 就是巴别塔里的「巴别」……)。
用了 Babel 后,咱们的 ES6 模块化代码会被转换为 ES5 + CommonJS 模块规范的代码,这倒也没什么,毕竟咱们写的仍是 ES6 的模块,至于编译生成的结果,管它是个什么屌东西呢(笑)
因此咱们须要另一个打包工具来将咱们的模块依赖给打包成一个 bundle 文件。目前来讲,依赖打包应该是最好的方法了。否则,你也能够等上一万年,等你的用户把浏览器升级到所有支持 HTTP/2(支持链接复用后模块不打包反而比较好)以及 <script type="module" src="fuck.js">
定义 ( ゚∀。)
因此咱们整个工具链应该是这样的:
而目前来看,主要可用的模块打包工具备这么几个:
Browserify
Webpack
Rollup.js
原本我还想讲一下 FIS3 的,结果去看了一下,人家居然还没原生的支持 ES6 Modules,并且 fis3-hook-commonjs
插件也几万年没更新了,因此仍是算了吧。至于 SystemJS 这类动态模块加载器本文也不会涉及,就像我上面说的同样,在目前这个时间点上仍是先用模块打包工具比较好。
下面分别介绍这几个工具以及如何使用它们配合 Babel 实现 ES6 模块转译。
Browserify 这个工具也是有些年头了,它经过打包全部的依赖来让你可以在浏览器中使用 CommonJS 的语法来 require('modules')
,这样你就能够像在 Node.js 中同样在浏览器中使用 npm 包了,能够爽到。并且我也很喜欢 Browserify 这个 LOGO
既然 Babel 会把咱们的 ES6 Modules 语法转换成 ES5 + CommonJS 规范的模块语法,那咱们就能够直接用 Browserify 来解析 Babel 的转译生成物,而后把全部的依赖给打包成一个文件,岂不是美滋滋。
不过除了 Babel 和 Browserify 这俩工具外,咱们还须要一个叫作 babelify
的东西……好吧好吧,这是最后一个了,真的。
那么,babelify 是拿来干吗的呢?由于 Browserify 只看得懂 CommonJS 的模块代码,因此咱们得把 ES6 模块代码转换成 CommonJS 规范的,再拿给 Browserify 去看:这一步就是 Babel 要干的事情了。可是 Browserify 人家是个模块打包工具啊,它是要去分析 AST(抽象语法树),把那些 reuqire()
的依赖文件给找出来再帮你打包的,你总不能把全部的源文件都给 Babel 转译了再交给 Browserify 吧?那太蠢了,个人朋友。
babelify
(Browserify transform for Babel) 要作的事情,就是在全部 ES6 文件拿给 Browserify 看以前,先把它用 Babel 给转译一下(browserify().transform
),这样 Browserify 就能够直接看得懂并打包依赖,避免了要用 Babel 先转译一万个文件的尴尬局面。
好吧,那咱们要怎样把这些工具捣鼓成一个完整的工具链呢?下面就是喜闻乐见的依赖包安装环节:
# 我用的 yarn,你用 npm 也差很少 # gulp 也能够全局安装,方便一点 # babel-preset 记得选适合本身的 # 最后那俩是用来配合 gulp stream 的 $ yarn add --dev babel-cli babel-preset-env babelify browserify gulp vinyl-buffer vinyl-source-stream
这里咱们用 Gulp 做为任务管理工具来实现自动化(什么,都 7012 年了你还不知道 Gulp?那为何不去问问神奇海螺呢?),gulpfile.js
内容以下:
var gulp = require('gulp'), browserify = require('browserify'), babelify = require('babelify'), source = require('vinyl-source-stream'), buffer = require('vinyl-buffer'); gulp.task('build', function () { return browserify(['./src/index.js']) .transform(babelify) .bundle() .pipe(source('bundle.js')) .pipe(gulp.dest('dist')) .pipe(buffer()); });
相信诸位都能看得懂吧,browserify()
第一个参数是入口文件,能够是数组或者其余乱七八糟的,具体参数说明请自行参照 Browserify 文档。并且记得在根目录下建立 .babelrc
文件指定转译的 preset,或者在 gulpfile.js
中配置也能够,这里就再也不赘述。
最后运行 gulp build
,就能够生成能直接在浏览器中运行的打包文件了。
➜ browserify $ gulp build [12:12:01] Using gulpfile E:\wwwroot\es6-module-test\browserify\gulpfile.js [12:12:01] Starting 'build'... [12:12:01] Finished 'build' after 720 ms
我记得这玩意最开始出来的时候号称为「下一代的模块打包工具」,而且自带了可大大减少打包体积的 tree-shaking
技术(DCE 无用代码移除的一种,运用了 ES6 静态分析语法树的特性,只打包那些用到了的代码),在当时很新鲜。
可是如今 Webpack2+ 已经支持了 Tree Shaking 的状况下,咱们又有什么特别的理由去使用 Rollup.js 呢?不过毕竟也是一种可行的方法,这里也提一提:
# 我也不知道为啥 Rollup.js 要依赖这个 external-helpers $ yarn add --dev rollup rollup-plugin-babel babel-preset-env babel-plugin-external-helpers
而后修改根目录下的 rollup.config.js
:
import babel from 'rollup-plugin-babel'; export default { entry: 'src/index.js', format: 'esm', plugins: [ babel({ exclude: 'node_modules/**' }) ], dest: 'dist/bundle.js' };
还要修改 .babelrc
文件,把 Babel 转换 ES6 模块到 CommonJS 模块的转换给关掉,否则会致使 Rollup.js 处理不来:
{ "presets": [ ["env", { "modules": false }] ], "plugins": [ "external-helpers" ] }
而后在根目录下运行 rollup -c
便可打包依赖,也能够配合 Gulp 来使用,官方文档里就有,这里就不赘述了。能够看到,Tree Shaking 的效果仍是很显著的,经测试,未使用的代码确实不会被打包进去,比起上面几个工具生成的结果要清爽多了:
对,Webpack,就是那个丧心病狂想要把啥玩意都给模块化的模块打包工具。既然人家已经到了 3.0.0
版本了,因此下面的都是基于 Webpack3 的。什么?如今还有搞前端的不知道 Webpack?神奇海螺如下略。
喜闻乐见的依赖安装环节:
# webpack 也能够全局安装,方便一些 $ yarn add --dev babel-loader babel-core babel-preset-env webpack
而后配置 webpack.config.js
:
var path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['env'] } } } ] } };
差很少就是这么个配置,babel-loader
的其余 options
请参照文档,并且这个配置文件的括号嵌套也是说不出话,ZTMJLWC。
而后运行 webpack
:
➜ webpack $ webpack Hash: 5c326572cf1440dbdf64 Version: webpack 3.0.0 Time: 1194ms Asset Size Chunks Chunk Names bundle.js 2.86 kB 0 [emitted] main [0] ./src/index.js 106 bytes {0} [built] [1] ./src/bullshit.js 178 bytes {0} [built]
状况呢就是这么个状况:
Tips: 关于 Webpack 的 Tree Shaking
Webpack 如今是自带 Tree-Shaking 的,不过须要你把 Babel 默认的转换 ES6 模块至 CommonJS 格式给关掉,就像上面 Rollup.js 那样在
.babelrc
中添加个"modules": false
。缘由的话上面也提到过,tree-shaking 是基于 ES6 模块的静态语法分析的,若是交给 Webpack 的是已经被 Babel 转换成 CommonJS 的代码的话那就没戏了。并且 Webpack 自带的 tree-shaking 只是把没用到的模块从
export
中去掉而已,以后还要再接一个 UglifyJS 之类的工具把冗余代码干掉才能达到 Rollup.js 那样的效果。
Webpack 也能够配合 Gulp 工做流让开发更嗨皮,有兴趣的可自行研究。目前来看,这三种方案中,我本人更倾向于使用 Webpack,不知道诸君会选用什么呢?
前几天我在捣鼓 printempw/blessing-skin-server 那坨 shi 同样 JavaScript 代码的模块化的时候,打算试着使用一下 ES6 标准中的模块化方案,并找了 Google 大老师问 ES6 模块转译打包相关的资源,找了半天,几乎没有什么像样的中文资源。全是讲 ES6 模块是啥、有多好、为何要用之类的,没几个是讲到底该怎么在生产环境中使用的(也有多是我搜索姿式不对),说不出话。遂撰此文,但愿能帮到后来人。
且本人水平有限,若是文中有什么错误,欢迎在下方评论区批评指出。