目前主流的模块规范javascript
(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
规范html
Nodejs
环境所使用的模块系统就是基于CommonJs
规范实现的,咱们如今所说的CommonJs
规范也大可能是指Node
的模块系统前端
关键字:module.exports
exports
vue
// 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
的引用,成为了一个模块内的局部变量java
关键字:require
node
const foo = require('./foo.js')
console.log(foo.age) //1
复制代码
假设如下目录为 src/app/index.js
的文件 调用 require()
react
./moduleA
相对路径开头在没有指定后缀名的状况下 先去寻找同级目录同级目录:src/app/
webpack
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
git
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
结束es6
/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
以前 javascript
一直没有属于本身的模块规范,因此社区制定了 CommonJs
规范, Node
从 Commonjs
规范中借鉴了思想因而有了 Node
的 module
,而 AMD 异步模块
也一样脱胎于 Commonjs
规范,以后有了运行在浏览器上的 require.js
es6 module
基本语法:
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(){}
等等不少// 命名导出 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 importES6 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
导出变量的区别
// 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
导出的是变量的绑定(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
状况,后面单独说如何在不一样环境中使用
由于 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()
能够在 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
在浏览器中兼容并非很好兼容性表,这里就不介绍浏览器支持状况了,咱们通常不会直接在浏览器中使用
在 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 }
});
复制代码
从 webpack2
就默认支持 es module
了,并默认支持 CommonJs
,支持导入 npm
包, 这里 import
语法上面写太多 就再也不写了
rollup
专一于 es module
,能够将 es module
打包为主流的模块规范,注意这里与 webpack
的区别,咱们能够在 webpack
的 js
中使用 Commonjs
语法, 可是 rollup
不支持,rollup
须要 plugin
支持,包括加载 node_modules
下的包 form 'react'
也须要 plugin
支持
能够看到 es module
在浏览器
与node
中兼容性差与实验功能的 咱们大多时候在 打包工具 中使用
在最后咱们说一下常常跟 es module
一块儿出现的一个名词 Tree-shaking
Tree-shaking
咱们先直译一下 树木摇晃 就是 摇晃树木把上面枯死的树叶晃下来,在代码中就是把没有用到的代码删除 Tree-shaking
最先由 rollup
提出,以后 webpack 2
也开始支持 这都是基于 es module
模块特性的静态分析
下面代码使用 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
的支持状况
// 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
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
的区别 但愿您读完文章后,能对前端的模块化有更深的了解