最近开发的一个新项目,构建工具从 Webpack 转换到了 Vite,开发体验有了惊人的提升:
-
冷启动从 10+s 下降至 2s(有缓存时 1s 以内)
-
热更新从 2s 下降至 1s 以内
Vite 为何如此神奇?官网对 Vite 的介绍很简单:Native-ESM powered web dev build tool. It's fast.
看来重点是 Native-ESM。那么究竟什么是 Native-ESM?为什么 Native-ESM 速度如此之快?让我们一探究竟。
什么是 Native-ESM?
ESM 即 ES Module,全称 EcmaScript Modules,是 EcmaScript 组织制定的模块化规范。
Native 代表原生支持。至于被什么原生支持,这里简单介绍下 EcmaScript、JavaScript、浏览器/Node.js 之间的关系:
-
EcmaScript 是规范
-
JavaScript 是 EcmaScript 的编程语言实现
-
Node.js 和浏览器是 JavaScript 的两个运行时
Vite 是运行在浏览器上的 JavaScript 库,因此这里的 Native-ESM 指的是被浏览器原生支持、符合 EcmaScript Modules 标准的 JavaScript 模块。
在详细探索 ES Module 之前,我们先了解下 JavaScript 模块化的历史。
Javascript 模块化发展历程
编程语言中的模块通常是为了避免全局污染,同时更好地组织程序逻辑,方便代码复用。我们在讨论模块时一般考虑两个方面:
-
模块定义:如何定义私有和公开的变量、属性、函数、方法
-
模块加载:如何在运行环境中使用模块,模块之间如何相互引用
有些语言天然支持模块,例如 Java 中的 Class 就是天然的模块:
// import 引入模块import ClassA from 'ClassA';// Class 类即模块Class B {// 私有属性private Int id;// 公有方法public getId() {return this.id;}}
但 Javascript 可以理解为一个「一切皆为 function」的语言,并没有提供类似 Java Class 这种完善的模块化语法。为了实现模块化,JS 大致走过了以下历程:
命名空间
为了不污染全局变量,我们可以把想要访问的变量用一个「命名空间」包裹。实际上就是新建一个包含属性的对象,避免属性直接挂在在全局变量(Window/Global)上。但这个对象可任意修改,没有封闭的作用域,所以并非真正的模块。
var moduleA = {id: 1,name: 'test'}console.log(moduleA.id)
立即执行函数表达式(IIFE)
利用立即执行函数表达式创建一个闭包。函数内的变量均为私有,对外暴露函数的返回值。这样这个闭包就成为了一个模块。代码示例如下:
var moduleA = (function () {var _id = 1var _getId = function () {console.log(id)}// 对外暴露的是返回值return { getId: _getId }})()moduleA._id // undefinedmoduleA._getId // undefinedmoduleA.getId // 1
这种方式除了能够完成完整的模块定义,还可以通过给匿名函数传参实现模块引用。在 jQuery 时代,这样的方式被广泛应用:
var moduleB = (function ($, _) {var _body = $('body')var _bodyCopy = _.clone(_body)var _getBodyCopy = function () {console.log(_bodyCopy)}return {getBodyCopy: _getBodyCopy,}})(jQuery, lodash)moduleB.getBodyCopy()
但是上面的代码中,jQuery 和 lodash 从哪里来呢?——还是全局变量。所以 IIFE 只在一定程度上解决了全局污染的问题,并没有根除。同时编写的方式也不太优雅。
CommonJS
JavaScript 的运行时起初只有浏览器,Node.js 的出现让 JavaScript 也可以在服务器上运行。似乎服务端的开发者更注重模块化,Node.js 社区推出了 CommonJS 规范。
CommonJS 中一个 js 文件就是一个模块,文件中的所有变量均为私有变量,通过 module.exports 导出一个对象,里面包含的则是公开变量;引用时通过 require 获取模块导出的对象。
// ModuleA.jsvar id = 1;var getId = function () {console.log(id);};module.exports = { getId };// ModuleB.jsconst moduleA = require("ModuleA.js");moduleA.id; // undefinedmoduleA.getId(); // 1
因为 CommonJS 是 Node.js 的默认模块规范,Node.js 中包含原生的 require 等 API,无需引用其它包获取。
AMD & CMD
CommonJS 的出现让 JavaScript 有了真正的模块规范。但由于其起源于 Node.js 社区,面向服务端开发场景,CommonJS 使用同步的方式加载模块,即等待所有模块加载完毕后再执行代码。
在服务端无需考虑网速的场景下是没问题的,但在浏览器上,同步加载的方式可能会导致较长的网络加载时间。由此诞生了面向浏览器,异步加载的模块方案:AMD、CMD
AMD 和 CMD 放在一起是因为这两个方案同为异步加载模块,只是在模块执行时间的设计理念有所差异。并且区别于 CommonJS,这两个方案均是社区推出,而非浏览器官方标准,因此需要引入各自的包来提供模块 API。
AMD
AMD 是 require.js 推出的模块规范。
在 html 中引入 require.js,并进行配置:
<!-- 引入 sea.js --><script src="sea.js"></script><script>// 配置 sea.jsseajs.config({base: "js",alias: {jquery: "jquery/jquery/1.10.1/jquery.js",},});// 加载入口模块seajs.use("js/main.js");</script>
通过 define API 定义和引用模块:
// moduleA.jsdefine(function () {// 私有变量var id = 1;var getId = function () {console.log(id);};// 暴露模块return { getId };});// moduleB.jsdefine(/** 第一个参数是模块引用列表 */ ["moduleA"], function () {console.log(moduleA.getId());});
<!-- 引入 sea.js --><script src="sea.js"></script><script>// 配置 sea.jsseajs.config({base: "js",alias: {jquery: "jquery/jquery/1.10.1/jquery.js",},});// 加载入口模块seajs.use("js/main.js");</script>
// moduleA.jsdefine(function (require, exports, module) {var id = 1;var getId = function () {console.log(id);};module.exports = { getId };});// moduleB.jsdefine(function (require, exports, module) {const moduleA = require("moduleA");console.log(moduleA.getId());});
AMD 和 CMD 的区别在于依赖引入的时机。AMD 会将全部依赖前置,只要加载完成就会执行代码;CMD 则推荐将依赖就进处理,需要时再引入。
// AMD 推荐define(["a", "b"], function (a, b) {a.doSomething(); // 依赖前置,提前执行b.doSomething();});// CMD 推荐define(function (require, exports, module) {var a = require("a");a.doSomething();var b = require("b");b.doSomething(); // 依赖就近,延迟执行});
(function (window, factory) {// Node.js 环境if (typeof exports === "object") {module.exports = factory();// 浏览器环境,支持 AMD} else if (typeof define === "function" && define.amd) {define(factory);// 浏览器环境,不支持 AMD} else {window.eventUtil = factory();}})(this, function () {//module ...});
-
CommonJS 看上去是实现方式最好的,但仅支持 Node.js(Node.js 亲儿子) -
浏览器端的方案,IIFE 已经过时了,AMD 和 CMD 支持异步加载适合浏览器,但缺乏浏览器原生 API 支持(不是亲儿子担心维护性),使用方式也没有 CommonJS 来得优雅
// ModuleA.jsvar id = 1;// 可以直接导出变量export const getId = function () {console.log(id);};// 也可以整体导出模块export default { getId };// ModuleB.jsimport moduleA from 'moduleA.js'moduleA.id; // undefinedmoduleA.getId(); // 1
-
Node.js: 模块文件的后缀名为 .mjs -
浏览器: script 标签
-
ECMAScript Code: AST 语法树 -
需要请求的模块 -
moduleA.js -
moduleB.js -
请求模块的入口 -
import CustomModule from 'moduleA.js': CustomModule 就是模块入口 -
其它属性和方法
-
构造(Construction) -
实例化(Instantiation) -
求值(Evaluation)
-
找到包含模块的文件 -
下载文件 -
将文件解析为 Module Record
-
用 Vite 加速你的生产力:https://segmentfault.com/a/1190000040422503 -
【THE LAST TIME】深入浅出 JavaScript 模块化:https://github.com/Nealyang/PersonalBlog/issues/61 -
前端模块化开发那点历史:https://github.com/seajs/seajs/issues/588 -
ES modules: A cartoon deep-dive:https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/