最近开发的一个新项目,构建工具从 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 = 1
var _getId = function () {
console.log(id)
}
// 对外暴露的是返回值
return { getId: _getId }
})()
moduleA._id // undefined
moduleA._getId // undefined
moduleA.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.js
var id = 1;
var getId = function () {
console.log(id);
};
module.exports = { getId };
// ModuleB.js
const moduleA = require("ModuleA.js");
moduleA.id; // undefined
moduleA.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.js
seajs.config({
base: "js",
alias: {
jquery: "jquery/jquery/1.10.1/jquery.js",
},
});
// 加载入口模块
seajs.use("js/main.js");
</script>
通过 define API 定义和引用模块:
// moduleA.js
define(function () {
// 私有变量
var id = 1;
var getId = function () {
console.log(id);
};
// 暴露模块
return { getId };
});
// moduleB.js
define(/** 第一个参数是模块引用列表 */ ["moduleA"], function () {
console.log(moduleA.getId());
});
<!-- 引入 sea.js -->
<script src="sea.js"></script>
<script>
// 配置 sea.js
seajs.config({
base: "js",
alias: {
jquery: "jquery/jquery/1.10.1/jquery.js",
},
});
// 加载入口模块
seajs.use("js/main.js");
</script>
// moduleA.js
define(function (require, exports, module) {
var id = 1;
var getId = function () {
console.log(id);
};
module.exports = { getId };
});
// moduleB.js
define(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.js
var id = 1;
// 可以直接导出变量
export const getId = function () {
console.log(id);
};
// 也可以整体导出模块
export default { getId };
// ModuleB.js
import moduleA from 'moduleA.js'
moduleA.id; // undefined
moduleA.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/