深入 CommonJs 与 ES6 Module
目前主流的模块规范
UMD
CommonJs
es6 module
umd 模块(通用模块)
[代码](function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.libName = factory());
}(this, (function () { 'use strict';})));
[代码]
如果你在[代码]js[代码]文件头部看到这样的代码,那么这个文件使用的就是 [代码]UMD[代码] 规范
实际上就是 amd + commonjs + 全局变量 这三种风格的结合
这段代码就是对当前运行环境的判断,如果是 [代码]Node[代码] 环境 就是使用 [代码]CommonJs[代码] 规范, 如果不是就判断是否为 [代码]AMD[代码] 环境, 最后导出全局变量
有了 [代码]UMD[代码] 后我们的代码和同时运行在 [代码]Node[代码] 和 [代码]浏览器上[代码]
所以现在前端大多数的库最后打包都使用的是 [代码]UMD[代码] 规范
CommonJs
[代码]Nodejs[代码] 环境所使用的模块系统就是基于[代码]CommonJs[代码]规范实现的,我们现在所说的[代码]CommonJs[代码]规范也大多是指[代码]Node[代码]的模块系统
模块导出
关键字:[代码]module.exports[代码] [代码]exports[代码]
[代码]// foo.js
//一个一个 导出
module.exports.age = 1
module.exports.foo = function(){}
exports.a = 'hello'
//整体导出
module.exports = { age: 1, a: 'hello', foo:function(){} }
//整体导出不能用`exports` 用exports不能在导入的时候使用
exports = { age: 1, a: 'hello', foo:function(){} }
[代码]
这里需要注意 [代码]exports[代码] 不能被赋值,可以理解为在模块开始前[代码]exports = module.exports[代码], 因为赋值之后[代码]exports[代码]失去了 对[代码]module.exports[代码]的引用,成为了一个模块内的局部变量
模块导入
关键字:[代码]require[代码]
[代码]const foo = require('./foo.js')
console.log(foo.age) //1
[代码]
模块导入规则:
假设以下目录为 [代码]src/app/index.js[代码] 的文件 调用 [代码]require()[代码]
[代码]./moduleA[代码] 相对路径开头
在没有指定后缀名的情况下
先去寻找同级目录同级目录:[代码]src/app/[代码]
[代码]src/app/moduleA[代码] 无后缀名文件 按照[代码]javascript[代码]解析
[代码]src/app/moduleA.js[代码] js文件 按照[代码]javascript[代码]解析
[代码]src/app/moduleA.json[代码] json文件 按照[代码]json[代码]解析
[代码]src/app/moduleA.node[代码] node文件 按照加载的编译插件模块dlopen
同级目录没有 [代码]moduleA[代码] 文件会去找同级的 [代码]moduleA[代码]目录:[代码]src/app/moduleA[代码]
[代码]src/app/moduleA/package.json[代码] 判断该目录是否有[代码]package.json[代码]文件, 如果有 找到[代码]main[代码]字段定义的文件返回, 如果 [代码]main[代码] 字段指向文件不存在 或 [代码]main[代码]字段不存在 或 [代码]package.json[代码]文件不存在向下执行
[代码]src/app/moduleA/index.js[代码]
[代码]src/app/moduleA/index.json[代码]
[代码]src/app/moduleA/index.node[代码]
结束
[代码]/module/moduleA[代码] 绝对路径开头
直接在[代码]/module/moduleA[代码]目录中寻找 规则同上
[代码]react[代码] 没有路径开头
没有路径开头则视为导入一个包
会先判断[代码]moduleA[代码]是否是一个核心模块 如[代码]path[代码],[代码]http[代码],优先导入核心模块
不是核心模块 会从当前文件的同级目录的[代码]node_modules[代码]寻找
[代码]/src/app/node_modules/[代码] 寻找规则同上 以导入[代码]react[代码]为例 [代码]先 node_modules 下 react 文件 -> react.js -> react.json -> react.node ->react目录 -> react package.json main -> index.js -> index.json -> index.node[代码] 如果没找到 继续向父目录的[代码]node_modules[代码]中找
[代码]/src/node_modules/[代码]
[代码]/node_modules/[代码]
直到最后找不到 结束
[代码]require wrapper[代码]
[代码]Node[代码]的模块 实际上可以理解为代码被包裹在一个[代码]函数包装器[代码]内
一个简单的[代码]require demo[代码]:
[代码]function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
}
function require(id) {
var cachedModule = Module._cache[id];
if(cachedModule){
return cachedModule.exports;
}
const module = { exports: {} }
// 这里先将引用加入缓存 后面循环引用会说到
Module._cache[id] = module
//当然不是eval这么简单
eval(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname')
return module.exports
}
[代码]
也可以查看:node module 源码
从以上代码我们可以知道:
模块只执行一次 之后调用获取的 [代码]module.exports[代码] 都是缓存哪怕这个 [代码]js[代码] 还没执行完毕(因为先加入缓存后执行模块)
模块导出就是[代码]return[代码]这个变量的其实跟[代码]a = b[代码]赋值一样, 基本类型导出的是值, 引用类型导出的是引用地址
[代码]exports[代码] 和 [代码]module.exports[代码] 持有相同引用,因为最后导出的是 [代码]module.exports[代码], 所以对[代码]exports[代码]进行赋值会导致[代码]exports[代码]操作的不再是[代码]module.exports[代码]的引用
循环引用
[代码]// a.js
module.exports.a = 1
var b = require('./b')
console.log(b)
module.exports.a = 2
// b.js
module.exports.b = 11
var a = require('./a')
console.log(a)
module.exports.b = 22
//main.js
var a = require('./a')
console.log(a)
[代码]
运行此段代码结合上面的[代码]require demo[代码],分析每一步过程:
[代码]执行 node main.js -> 第一行 require(a.js)[代码],([代码]node[代码] 执行也可以理解为调用了require方法,我们省略[代码]require(main.js)[代码]内容)
[代码]进入 require(a)方法: 判断缓存(无) -> 初始化一个 module -> 将 module 加入缓存 -> 执行模块 a.js 内容[代码],(需要注意 是先加入缓存, 后执行模块内容)
[代码]a.js: 第一行导出 a = 1 -> 第二行 require(b.js)[代码](a 只执行了第一行)
[代码]进入 require(b) 内 同 1 -> 执行模块 b.js 内容[代码]
[代码]b.js: 第一行 b = 11 -> 第二行 require(a.js)[代码]
[代码]require(a) 此时 a.js 是第二次调用 require -> 判断缓存(有)-> cachedModule.exports -> 回到 b.js[代码](因为[代码]js[代码]对象引用问题 此时的 [代码]cachedModule.exports = { a: 1 }[代码])
[代码]b.js:第三行 输出 { a: 1 } -> 第四行 修改 b = 22 -> 执行完毕回到 a.js[代码]
[代码]a.js:第二行 require 完毕 获取到 b -> 第三行 输出 { b: 22 } -> 第四行 导出 a = 2 -> 执行完毕回到 main.js[代码]
[代码]main.js:获取 a -> 第二行 输出 { a: 2 } -> 执行完毕[代码]
以上就是[代码]node[代码]的[代码]module[代码]模块解析和运行的大致规则
es6 module
[代码]ES6[代码] 之前 [代码]javascript[代码] 一直没有属于自己的模块规范,所以社区制定了 [代码]CommonJs[代码]规范, [代码]Node[代码] 从 [代码]Commonjs[代码] 规范中借鉴了思想于是有了 [代码]Node[代码] 的 [代码]module[代码],而 [代码]AMD 异步模块[代码] 也同样脱胎于 [代码]Commonjs[代码] 规范,之后有了运行在浏览器上的 [代码]require.js[代码]
[代码]es6 module[代码] 基本语法:
export
[代码]export * from 'module'; //重定向导出 不包括 module内的default
export { name1, name2, ..., nameN } from 'module'; // 重定向命名导出
export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重命名导出
export { name1, name2, …, nameN }; // 与之前声明的变量名绑定 命名导出
export { variable1 as name1, variable2 as name2, …, nameN }; // 重命名导出
export let name1 = 'name1'; // 声明命名导出 或者 var, const,function, function*, class
export default expression; // 默认导出
export default function () { ... } // 或者 function*, class
export default function name1() { ... } // 或者 function*, class
export { name1 as default, ... }; // 重命名为默认导出
[代码]
[代码]export[代码] 规则
[代码]export * from ''[代码] 或者 [代码]export {} from ''[代码],重定向导出,重定向的命名并不能在本模块使用,只是搭建一个桥梁,例如:这个[代码]a[代码]并不能在本模块内使用
[代码]export {}[代码], 与变量名绑定,命名导出
[代码]export Declaration[代码],声明的同时,命名导出, Declaration就是: [代码]var[代码], [代码]let[代码], [代码]const[代码], [代码]function[代码], [代码]function*[代码], [代码]class[代码] 这一类的声明语句
[代码]export default AssignmentExpression[代码],默认导出, AssignmentExpression的 范围很广,可以大致理解 为除了声明[代码]Declaration[代码](其实两者是有交叉的),[代码]a=2[代码],[代码]i++[代码],[代码]i/4[代码],[代码]a===b[代码],[代码]obj[name][代码],[代码]name in obj[代码],[代码]func()[代码],[代码]new P()[代码],[代码][1,2,3][代码],[代码]function(){}[代码]等等很多
import
[代码]// 命名导出 module.js
let a = 1,b = 2
export { a, b }
export let c = 3
// 命名导入 main.js
import { a, b, c } from 'module'; // a: 1 b: 2 c: 3
import { a as newA, b, c as newC } from 'module'; // newA: 1 b: 2 newC: 3
// 默认导出 module.js
export default 1
// 默认导入 main.js
import defaultExport from 'module'; // defaultExport: 1
// 混合导出 module.js
let a = 1
export { a }
const b = 2
export { b }
export let c = 3
export default [1, 2, 3]
// 混合导入 main.js
import defaultExport, { a, b, c as newC} from 'module'; //defaultExport: [1, 2, 3] a: 1 b: 2 newC: 3
import defaultExport, * as name from 'module'; //defaultExport: [1, 2, 3] name: { a: 1, b: 2, c: 3 }
import * as name from 'module'; // name: { a: 1, b: 2, c: 3, default: [1, 2, 3] }
// module.js
Array.prototype.remove = function(){}
//副作用 只运行一个模块
import 'module'; // 执行module 不导出值 多次调用module.js只运行一次
//动态导入(异步导入)
var promise = import('module');
[代码]
[代码]import[代码] 规则
[代码]import { } from 'module'[代码], 导入[代码]module.js[代码]的命名导出
[代码]import defaultExport from 'module'[代码], 导入[代码]module.js[代码]的默认导出
[代码]import * as name from 'module'[代码], 将[代码]module.js的[代码]的所有导出合并为[代码]name[代码]的对象,[代码]key[代码]为导出的命名,默认导出的[代码]key[代码]为[代码]default[代码]
[代码]import 'module'[代码],副作用,只是运行[代码]module[代码],不为了导出内容例如 polyfill,多次调用次语句只能执行一次
[代码]import('module')[代码],动态导入返回一个 [代码]Promise[代码],[代码]TC39[代码]的[代码]stage-3[代码]阶段被提出 tc39 import
[代码]ES6 module[代码] 特点
[代码]ES6 module[代码]的语法是静态的
[代码]import[代码] 会自动提升到代码的顶层
[代码]export[代码] 和 [代码]import[代码] 只能出现在代码的顶层,下面这段语法是错误的
[代码] //if for while 等都无法使用
{
export let a = 1
import defaultExport from 'module'
}
true || export let a = 1
[代码]
[代码]import[代码] 的导入名不能为字符串或在判断语句,下面代码是错误的
[代码]import 'defaultExport' from 'module'
let name = 'Export'
import 'default' + name from 'module'
[代码]
静态的语法意味着可以在编译时确定导入和导出,更加快速的查找依赖,可以使用[代码]lint[代码]工具对模块依赖进行检查,可以对导入导出加上类型信息进行静态的类型检查
####[代码]ES6 module[代码]的导出是绑定的 ####
使用 [代码]import[代码] 被导入的模块运行在严格模式下
使用 [代码]import[代码] 被导入的变量是只读的,可以理解默认为 [代码]const[代码] 装饰,无法被赋值
使用 [代码]import[代码] 被导入的变量是与原变量绑定/引用的,可以理解为 [代码]import[代码] 导入的变量无论是否为基本类型都是引用传递
[代码]// js中 基础类型是值传递
let a = 1
let b = a
b = 2
console.log(a,b) //1 2
// js中 引用类型是引用传递
let obj = {name:'obj'}
let obj2 = obj
obj2.name = 'obj2'
console.log(obj.name, obj2.name) // obj2 obj2
// es6 module 中基本类型也按引用传递
// foo.js
export let a = 1
export function count(){
a++
}
// main.js
import { a, count } from './foo'
console.log(a) //1
count()
console.log(a) //2
// export default 是无法 a 的动态绑定 这一点跟 CommonJs 有点相似 都是值的拷贝
let a = 1;
export default a
// 可以用另一种方式实现 default 的动态绑定
let a = 1;
export { a as default }
export function count(){
a++
}
// 就跟上面 main.js 一样
[代码]
上面这段代码就是 [代码]CommonJs[代码] 导出变量 和 [代码]ES6[代码] 导出变量的区别
es module 循环引用
[代码]// bar.js
import { foo } from './foo'
console.log(foo);
export let bar = 'bar'
// foo.js
import { bar } from './bar'
console.log(bar);
export let foo = 'foo'
// main.js
import { bar } from './bar'
console.log(bar)
[代码]
[代码]执行 main.js -> 导入 bar.js[代码]
[代码]bar.js -> 导入 foo.js[代码]
[代码]foo.js -> 导入 bar.js -> bar.js 已经执行过直接返回 -> 输出 bar -> bar is not defined, bar 未定义报错[代码]
我们可以使用[代码]function[代码]的方式解决:
[代码]// bar.js
import { foo } from './foo'
console.log(foo());
export function bar(){
return 'bar'
}
// foo.js
import { bar } from './bar'
console.log(bar());
export function foo(){
return 'foo'
}
// main.js
import { bar } from './bar'
console.log(bar)
[代码]
因为函数声明会提示到文件顶部,所以就可以直接在 [代码]foo.js[代码] 调用还没执行完毕的[代码]bar.js[代码]的 [代码]bar[代码] 方法,不要在函数内使用外部变量,因为变量还未声明([代码]let,const[代码])和赋值,[代码]var[代码]
CommonJs 和 ES6 Module 的区别
其实上面我们已经说到了一些区别
[代码]CommonJs[代码]导出的是变量的一份拷贝,[代码]ES6 Module[代码]导出的是变量的绑定([代码]export default[代码] 是特殊的)
[代码]CommonJs[代码]是单个值导出,[代码]ES6 Module[代码]可以导出多个
[代码]CommonJs[代码]是动态语法可以写在判断里,[代码]ES6 Module[代码]静态语法只能写在顶层
[代码]CommonJs[代码]的 [代码]this[代码] 是当前模块,[代码]ES6 Module[代码]的 [代码]this[代码] 是 [代码]undefined[代码]
易混淆点
模块语法与解构
[代码]module语法[代码]与[代码]解构语法[代码]很容易混淆,例如:
[代码]import { a } from 'module'
const { a } = require('module')
[代码]
尽管看上去很像,但是不是同一个东西,这是两种完全不一样的语法与作用,ps:两个人撞衫了,穿一样的衣服你不能说这俩人就是同一个人
[代码]module[代码] 的语法: 上面有写 [代码]import/export { a } / { a, b } / { a as c} FromClause[代码]
[代码]解构[代码] 的语法:
[代码]let { a } = { a: 1 }
let { a = 2 } = { }
let { a: b } = { a: 1 }
let { a: b = 2, ...res } = { name:'a' }
let { a: b, obj: { name } } = { a: 1, obj: { name: '1' } }
function foo({a: []}) {}
[代码]
他们是差别非常大的两个东西,一个是模块导入导出,一个是获取对象的语法糖
导出语法与对象属性简写
同样下面这段代码也容易混淆
[代码]let a = 1
export { a } // 导出语法
export default { a } // 属性简写 导出 { a: 1 } 对象
module.exports = { a } // 属性简写 导出 { a: 1 } 对象
[代码]
[代码]export default[代码] 和 [代码]module.exports[代码] 是相似的
ES6 module 支持 CommonJs 情况
先简单说一下各个环境的 [代码]ES6 module[代码] 支持 [代码]CommonJs[代码] 情况,后面单独说如何在不同环境中使用
因为 [代码]module.exports[代码] 很像 [代码]export default[代码] 所以 [代码]ES6模块[代码] 可以很方便兼容 [代码]CommonJs[代码]
在[代码]ES6 module[代码]中使用[代码]CommonJs[代码]规范,根据各个环境,打包工具不同也是不一样的
我们现在大多使用的是 [代码]webpack[代码] 进行项目构建打包,因为现在前端开发环境都是在 [代码]Node[代码] 环境原因,而 [代码]npm[代码] 的包都是 [代码]CommonJs[代码] 规范的,所以 [代码]webpack[代码] 对[代码]ES6[代码]模块进行扩展 支持 [代码]CommonJs[代码],并支持[代码]node[代码]的导入[代码]npm[代码]包的规范
如果你使用 [代码]rollup[代码],想在[代码]ES Module[代码]中支持[代码]Commonjs[代码]规范就需要下载[代码]rollup-plugin-commonjs[代码]插件,想要导入[代码]node_modules[代码]下的包也需要[代码]rollup-plugin-node-resolve[代码]插件
如果你使用 [代码]node[代码],可以在 [代码].mjs[代码] 文件使用 [代码]ES6[代码],也支持 [代码]CommonJs[代码] 查看 nodejs es-modules.md
在浏览器环境 不支持[代码]CommonJs[代码]
node 与 打包工具[代码]webpack,rollup[代码]的导入 [代码]CommonJs[代码] 差异
[代码]// module.js
module.export.a = 1
// index.js webpack rollup
import * as a from './module'
console.log(a) // { a: 1, default: { a:1 } }
// index.mjs node
import * as a from './module'
console.log(a) // { default: { a:1 } }
[代码]
[代码]node[代码] 只是把 [代码]module.exports[代码] 整体当做 [代码]export default[代码]
打包工具除了把 [代码]module.export[代码] 整体当做 [代码]export default[代码],还把 [代码]module.export[代码] 的每一项 又当做 [代码]export[代码] 输出,这样做是为了更加简洁
[代码]import defaultExport from './foo'[代码], [代码]defaultExport.foo()[代码]
[代码]import { foo } from './foo'[代码], [代码]foo()[代码]
使用 ES6 Module
可以在 es6module example 仓库中获取代码在本地进行测试验证
浏览器中使用
你需要起一个[代码]Web服务器[代码]来访问,双击本地运行 [代码]index.html[代码] 并不会执行 [代码]type=module[代码] 标签
我们可以对 [代码]script[代码] 标签的 [代码]type[代码] 属性加上 [代码]module[代码]
先定义两个模块
[代码]// index.js
import module from './module.js'
console.log(module) // 123
// module.js
export default 123
[代码]
在[代码]html[代码]中内联调用
[代码]<!-- index.html -->
<script type="module">
import module from './module.js'
console.log(module) // 123
</script>
[代码]
在[代码]html[代码]中通过 [代码]script[代码] 的 [代码]src[代码] 引用
[代码]<!-- index.html -->
<script type="module" src="index.js"></script>
// 控制台 123
[代码]
浏览器导入路径规则
[代码]https://example.com/apples.mjs[代码]
[代码]http://example.com/apples.js[代码]
[代码]//example.com/bananas[代码]
[代码]./strawberries.mjs.cgi[代码]
[代码]../lychees[代码]
[代码]/limes.jsx[代码]
[代码]data:text/javascript,export default 'grapes';[代码]
[代码]blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f[代码]
补充:
不加 后缀名 找不到具体的文件
后端可以修改接口[代码]/getjs?name=module[代码]这一类的,不过后端要返回 [代码]Content-Type: application/javascript[代码] 确保返回的是[代码]js[代码],因为浏览器是根据 [代码]MIME type[代码] 识别的
因为 [代码]ES6 Module[代码] 在浏览器中兼容并不是很好兼容性表,这里就不介绍浏览器支持情况了,我们一般不会直接在浏览器中使用
Nodejs中使用
nodejs es-modules.md
在 [代码]Node v8.5.0[代码] 以上支持 [代码]ES Module[代码],需要 [代码].mjs[代码]扩展名
NOTE: DRAFT status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement ESM. At which time this draft would be moved to ACCEPTED.
(上面链接可以知道 [代码]ES Module[代码]的状态是 [代码]DRAFT[代码], 属于起草阶段)
[代码]// module.mjs
export default 123
// index.mjs
import module from './module.mjs'
console.log(module) // 123
[代码]
我们需要执行 [代码]node --experimental-modules index.mjs[代码] 来启动
会提示一个 [代码]ExperimentalWarning: The ESM module loader is experimental.[代码]该功能是实验性的(此提示不影响执行)
[代码]ES Module[代码] 中导入 [代码]CommonJs[代码]
[代码]// module.js
module.exports.a = 123 // module.exports 就相当于 export default
// index.mjs
import module from './module.js'
console.log(module) // { a: 123 }
import * as module from './module.js'
console.log(module) // { get default: { a: 123 } }
import { default as module } from './module.js';
console.log(module) // { a: 123 }
import module from 'module'; // 导入npm包 导入规则与 require 差不多
[代码]
导入路径规则与[代码]require[代码]差不多
这里要注意 [代码]module[代码] 扩展名为 [代码].js[代码],[代码].mjs[代码]专属于 [代码]es module[代码],[代码]import form[代码]导入的文件后缀名只能是[代码].mjs[代码],在 [代码].mjs[代码]中 [代码]module[代码]未定义, 所以调用 [代码]module.exports,exports[代码] 会报错
[代码]node[代码]中 [代码]CommonJs[代码] 导入 [代码]es module[代码] 只能使用 [代码]import()[代码] 动态导入/异步导入
[代码]// es.mjs
let foo = {name: 'foo'};
export default foo;
export let a = 1
// cjs
import('./es').then((res)=>{
console.log(res) // { get default: {name: 'foo'}, a: 1 }
});
[代码]
webpack中使用
从 [代码]webpack2[代码] 就默认支持 [代码]es module[代码] 了,并默认支持 [代码]CommonJs[代码],支持导入 [代码]npm[代码]包, 这里 [代码]import[代码] 语法上面写太多 就不再写了
rollup中使用
[代码]rollup[代码] 专注于 [代码]es module[代码],可以将 [代码]es module[代码] 打包为主流的模块规范,注意这里与 [代码]webpack[代码] 的区别,我们可以在 [代码]webpack[代码] 的 [代码]js[代码] 中使用 [代码]Commonjs[代码] 语法, 但是 [代码]rollup[代码] 不支持,[代码]rollup[代码]需要 [代码]plugin[代码] 支持,包括加载 [代码]node_modules[代码] 下的包 [代码]form 'react'[代码] 也需要 [代码]plugin[代码] 支持
可以看到 [代码]es module[代码] 在[代码]浏览器[代码]与[代码]node[代码]中兼容性差与实验功能的
我们大多时候在 打包工具 中使用
Tree-shaking
在最后我们说一下经常跟 [代码]es module[代码] 一起出现的一个名词 [代码]Tree-shaking[代码]
[代码]Tree-shaking[代码] 我们先直译一下 树木摇晃 就是 摇晃树木把上面枯死的树叶晃下来,在代码中就是把没有用到的代码删除
[代码]Tree-shaking[代码] 最早由 [代码]rollup[代码] 提出,之后 [代码]webpack 2[代码] 也开始支持
这都是基于 [代码]es module[代码] 模块特性的静态分析
rollup
下面代码使用 [代码]rollup[代码] 进行打包:
[代码]// module.js
export let foo = 'foo'
export let bar = 'bar'
// index.js
import { foo } from './module'
console.log(foo) // foo
[代码]
在线运行 我们可以修改例子与导出多种规范
打包结果:
[代码]let foo = 'foo';
console.log(foo); // foo
[代码]
可以看到 [代码]rollup[代码] 打包结果非常的简洁,并去掉了没有用到的 [代码]bar[代码]
是否支持对导入 [代码]CommonJs[代码] 的规范进行 [代码]Tree-shaking[代码]:
[代码]// index.js
import { a } from './module'
console.log(a) // 1
// module.js
module.exports.a = 1
module.exports.b = 2
[代码]
打包为 [代码]es module[代码]
[代码]var a_1 = 2;
console.log(a_1);
[代码]
可以看到去掉了未使用的 [代码]b[代码]
webpack
我们下面看看 [代码]webpack[代码] 的支持情况
[代码]// src/module.js
export function foo(){ return 'foo' }
export function bar(){ return 'bar' }
// src/index.js
import { foo } from './module'
console.log(foo())
[代码]
执行 [代码]npx webpack -p[代码](我们使用webpack 4,0配置,-p开启生成模式 自动压缩)
打包后我们在打包文件搜索 [代码]bar[代码] 没有搜到,[代码]bar[代码]被删除
我们将上面例子修改一下:
[代码]// src/module.js
module.exports.foo = function (){ return 'foo' }
module.exports.bar = function (){ return 'bar' }
// src/index.js
import { foo } from './module'
console.log(foo())
[代码]
打包后搜索 [代码]bar[代码] 发现[代码]bar[代码]存在,[代码]webpack[代码] 并不支持对[代码]CommonJs[代码] 进行 [代码]Tree-shaking[代码]
pkg.module
[代码]webpack[代码] 不支持 [代码]Commonjs[代码] [代码]Tree-shaking[代码],但现在[代码]npm[代码]的包都是[代码]CommonJs[代码]规范的,这该怎么办呢 ?如果我发了一个新包是 [代码]es module[代码] 规范, 但是如果代码运行在 [代码]node[代码] 环境,没有经过打包 就会报错
有一种按需加载的方案
全路径导入,导入具体的文件:
[代码]// src/index.js
import remove from 'lodash/remove'
import add from 'lodash/add'
console.log(remove(), add())
[代码]
使用一个还好,如果用多个的话会有很多 [代码]import[代码] 语句
还可以使用插件如 [代码]babel-plugin-lodash, & lodash-webpack-plugin[代码]
但我们不能发一个库就自己写插件
这时就提出了在 [代码]package.json[代码] 加一个 [代码]module[代码] 的字段来指向 [代码]es module[代码]规范的文件,[代码]main -> CommonJs[代码],那么[代码]module - es module[代码] pkg.module
[代码]webpack[代码] 与 [代码]rollup[代码] 都支持 [代码]pkg.module[代码]
加了 [代码]module[代码] 字段 [代码]webpack[代码] 就可以识别我们的 [代码]es module[代码],但是还有一个问题就是 [代码]babel[代码]
我们一般使用 [代码]babel[代码] 都会排除 [代码]node_modules[代码],所以我们这个 [代码]pkg.module[代码] 只是的 [代码]es6 module[代码]必须是编译之后的 [代码]es5[代码] 代码,因为 [代码]babel[代码] 不会帮我们编译,我们的包就必须是 拥有 es6 module 规范的 es5 代码
如果你使用了 [代码]presets-env[代码] 因为会把我们的代码转为 [代码]CommonJs[代码] 所以就要设置 [代码]"presets": [["env", {"modules":false}][代码] 不将[代码]es module[代码] 转为 [代码]CommonJs[代码]
[代码]webpack[代码] 与 [代码]rollup[代码] 的区别
[代码]webpack[代码] 不支持导出 [代码]es6 module[代码] 规范,[代码]rollup[代码] 支持导出 [代码]es6 module[代码]
[代码]webpack[代码] 打包后代码很多冗余无法直接看,[代码]rollup[代码] 打包后的代码简洁,可读,像源码
[代码]webpack[代码] 可以进行代码分割,静态资源处理,[代码]HRM[代码],[代码]rollup[代码] 专注于 [代码]es module[代码],[代码]tree-shaking[代码]更加强大的,精简
如果是开发应用可以使用 [代码]webpack[代码],因为可以进行代码分割,静态资源,[代码]HRM[代码],插件
如果是开发类似 [代码]vue[代码],[代码]react[代码] 等类库,[代码]rollup[代码] 更好一些,因为可以使你的代码精简,无冗余代码,执行更快,导出多种模块语法
结语
本文章介绍了 [代码]Commonjs[代码] 和 [代码]ES6 Module[代码],导入导出的语法规则,路径解析规则,两者的区别,容易混淆的地方,在不同环境的区别,在不同环境的使用,[代码]Tree-shaking[代码],与 [代码]webpack[代码],[代码]rollup[代码] 的区别
希望您读完文章后,能对前端的模块化有更深的了解
参考链接
ECMAScript® 2015 Language Specification sec-imports/sec-exports
MDN import
github nodejs lib/module
github nodejs node-eps/002-es-modules
nodejs docs modules
Understanding ECMAScript 6
ECMAScript 6 入门
es6-modules-final