前言
在使用 vue、react、node 的时候,常常会看到 module.exports,export default,require,import等字段,因为我对这些字段的概念非常模糊,所以导致我在写代码的时候,在node项目里混用了 export default,在 vue 的项目里写 module.exports。
那么今天就来梳理一下有关模块化的知识。
ESM的模块
语法
ESM(ECMA Script Modules)模块主要由两个命令构成:export 和 import。
暴露模块:export default {} , export {} , export function(){} 引入模块:import {xxx} from 'path' 复制代码
注意
import 的大括号里面指定要从其他模块导入的变量名,如果 export 命令没有写 default,那么 import 大括号里面的变量名,必须与 export 导出的名称相同。
// test.js
export {foo}
// main.js
import { foo } from './test.js'
// 如果想为输入的变量重新取一个名字,要使用as关键字,将输入的变量重命名
import { foo as bar } from './test.js';
复制代码
default 的作用
default 为模块指定默认输出,这样在引入时就不必关心模块输出的名字。
// test.js
function foo() {};
export default {foo}
// main.js
import bar from './test.js' 复制代码
本质上,export default 就是输出一个叫做 default 的变量或方法,然后系统允许你为它取任意名字。
有关 ESM 模块的语法,可以阅读 阮一峰 的文章,这里不详细写出所有写法。
加载机制
我们在使用 import 和 export 的时候,常常看到的是在顶层作用域使用 export 和 import,不会在块级作用域内看到 import 和 export,这是为什么呢?
因为 ESM 模块的设计思想是尽量静态化,编译时就能确定模块的依赖关系,以及输入和输出的变量,如果处于块级作用域内,就没法做静态优化了,违背了 ES6 模块的设计初衷。
但是我明明在 vue router 的使用中看到了这样的写法:
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
复制代码
此时 import 的写法和常规写法不一样,是import(), 并且确实出现在了块级作用域内。
ESM有一个提案,建议引入import()函数,完成动态加载。现在支持动态加载的比如:vue router、webpack。
vue router 动态加载
vue router 的 路由懒加载
webpack 动态导入
webpack 动态导入
Node.js 的模块
语法
暴露模块:module.exports = value 或 exports.xxx = value
引入模块:require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
复制代码
Node 的模块输出和引入的方式与ESM不同,Node 采用的是 CommonJS 模块规范。
CommonJS 规范规定,在每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(module.exports)是对外的接口。
为什么采用 CommonJS 规范呢?
Node.js 主要用于服务端项目,CommonJS 规范也主要用于服务端编程,所以 Node 的模块设计采用 CommonJS 规范很合适。
模块化发展的历史
为什么会有 AMD CMD 规范?
服务端模块的加载是同步的,但是浏览器资源是异步加载,同步意味着阻塞,在没有ESM模块之前,浏览器想做模块化怎么办呢?
AMD CMD解决方案。
实际上AMD (Asynchronous Module Definition) 是 RequireJS 在推广过程中对模块定义规范化的产出。
CMD (Common Module Definition) 是 SeaJS 在推广过程中对模块定义的规范化产出。
SeaJS 和 requireJS 解决的都是模块化问题,只不过在模块定义方式和模块加载(可以说运行、解析)时机上有所不同。
以下例子引用自 WEB 前端模块化都有什么
AMD
// hello.js
define(function() {
console.log('hello init');
return {
getMessage: function() { return 'hello'; } }; }); // world.js define(function() { console.log('world init'); }); // main define(['./hello.js', './world.js'], function(hello) { return { sayHello: function() { console.log(hello.getMessage()); } }; }); // 输出 // hello init // world init 复制代码
CMD
// hello.js
define(function(require, exports) {
console.log('hello init');
exports.getMessage = function() {
return 'hello'; }; }); // world.js define(function(require, exports) { console.log('world init'); exports.getMessage = function() { return 'world'; }; }); // main define(function(require) { var message; if (true) { message = require('./hello').getMessage(); } else { message = require('./world').getMessage(); } }); // 输出 // hello init 复制代码
CMD 的输出结果中,没有打印"world init", 并是不 world.js 文件没有加载。
AMD CMD 规范加载机制
AMD 与 CMD 都是在页面初始化时加载完成所有模块,区别是 CMD 就近依赖, 是当模块被 require 时才会触发执行, AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
同样都是异步加载模块,AMD 在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行。
CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的。
这也是很多人说AMD用户体验好,因为没有延迟,依赖模块提前执行了,CMD性能好,因为只有用户需要的时候才执行。
UMD 规范解决了什么问题?
AMD & CMD,CommonJS 规范是两类规范,AMD & CMD 用于浏览器模块化,CommonJS 用于服务端模块化,但是大家的期望有一个统一的规范来支持这两种规范。于是,UMD规范诞生了。
UMD (Universal Module Definition),它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD / AMD 的项目中运行,同一个 JavaScript 包在浏览器 / 服务端只需要遵守同一个写法就可以了。
UMD 没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身,UMD 先判断是否支持 Node 模块格式(exports是否存在),存在则使用 Node 模块格式,再判断是否支持 AMD(define是否存在),存在则使用 AMD 方式加载模块,如果前两个都不存在,则将模块公开到全局(window或global)。
更早之前
立即执行函数实现模块化(IIFE,Immediately-Invoked Function Expression)
使用立即执行函数,表达式中的变量不能从外部访问。现在项目中已经看不到这样的写法了。
例如:
(function(){
var count = 0;
return count;
})();
复制代码
模块化方案总结
ESM | CommonJS | AMD | CMD | UMD | |
---|---|---|---|---|---|
加载机制 | 编译时 | 运行时 | 提前预加载 | 编译时 & 运行时按需加载 | - |
同步/异步 | 异步 | 同步 | 异步 | 异步,有延迟执行的情况 | - |
适用场合 | 浏览器、服务端 | 服务端 | 浏览器 | 浏览器 | 浏览器、服务端 |
是否常见 | ☆☆☆ | ☆☆☆ | ☆ | ☆ | ☆ |
ESM 在语言标准的层面上,成为浏览器和服务端通用的模块解决方案。
工具时代
webpack 在定义模块上,支持上面提到的所有模块声明方式,只需要在 webpack 的 output 中添加 libraryTarget: 'commonjs/amd/umd'即可。
模块化的好处
- 避免命名冲突,每个模块内的变量仅对自己可见,外部获取依赖模块输出
- 按需加载
- 解耦、复用、高可维护性
参考资料
ECMAScript 6 入门
前端模块化详解(完整版)
AMD与CMD区别
SeaJS 和 RequireJS 的差异
作者:Serendipity96
链接:https://juejin.im/post/5d8c1fc26fb9a04e10438388
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。