深切 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是不是是一个中心模块pathhttp,优先导入中心模块
不是中心模块 会从当前文件的同级目次的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赋值一样, 基本范例导出的是援用范例导出的是援用地点
  • exportsmodule.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,剖析每一步历程:

  1. 实行 node main.js -> 第一行 require(a.js),(node 实行也能够明白为挪用了require要领,我们省略require(main.js)内容)
  2. 进入 require(a)要领: 推断缓存(无) -> 初始化一个 module -> 将 module 到场缓存 -> 实行模块 a.js 内容,(须要注重 是到场缓存后实行模块内容)
  3. a.js: 第一行导出 a = 1 -> 第二行 require(b.js)(a 只实行了第一行)
  4. 进入 require(b) 内 同 1 -> 实行模块 b.js 内容
  5. b.js: 第一行 b = 11 -> 第二行 require(a.js)
  6. require(a) 此时 a.js 是第二次挪用 require -> 推断缓存(有)-> cachedModule.exports -> 回到 b.js(由于js对象援用题目 此时的 cachedModule.exports = { a: 1 }
  7. b.js:第三行 输出 { a: 1 } -> 第四行 修正 b = 22 -> 实行终了回到 a.js
  8. a.js:第二行 require 终了 猎取到 b -> 第三行 输出 { b: 22 } -> 第四行 导出 a = 2 -> 实行终了回到 main.js
  9. main.js:猎取 a -> 第二行 输出 { a: 2 } -> 实行终了

以上就是nodemodule模块剖析和运转的大抵划定规矩

es6 module

ES6 之前 javascript 一向没有属于本身的模块范例,所以社区制订了 CommonJs范例, NodeCommonjs 范例中自创了头脑因而有了 Nodemodule,而 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为导出的定名,默许导出的keydefault
  • import 'module',副作用,只是运转module,不为了导出内容比方 polyfill,屡次挪用次语句只能实行一次
  • import('module'),动态导入返回一个 PromiseTC39stage-3阶段被提出 tc39 import

ES6 module 特征

ES6 module的语法是静态的

import 会自动提升到代码的顶层

exportimport 只能出如今代码的顶层,下面这段语法是毛病

 //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)
  1. 实行 main.js -> 导入 bar.js
  2. bar.js -> 导入 foo.js
  3. 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.jsbar 要领,不要在函数内运用外部变量,由于变量还未声明(let,const)和赋值,var

CommonJs 和 ES6 Module 的区分

实在上面我们已说到了一些区分

  • CommonJs导出的是变量的一份拷贝,ES6 Module导出的是变量的绑定(export default 是特别的)
  • CommonJs是单个值导出,ES6 Module能够导出多个
  • CommonJs是动态语法能够写在推断里,ES6 Module静态语法只能写在顶层
  • CommonJsthis 是当前模块,ES6 Modulethisundefined

易殽杂点

模块语法与解构

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 defaultmodule.exports 是相似的

ES6 module 支撑 CommonJs 状况

先简朴说一下各个环境的 ES6 module 支撑 CommonJs 状况,背面零丁说如安在差别环境中运用

由于 module.exports 很像 export default 所以 ES6模块 能够很轻易兼容 CommonJs
ES6 module中运用CommonJs范例,依据各个环境,打包东西差别也是不一样的

我们如今大多运用的是 webpack 举行项目构建打包,由于如今前端开辟环境都是在 Node 环境缘由,而 npm 的包都是 CommonJs 范例的,所以 webpackES6模块举行扩大 支撑 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中经由过程 scriptsrc 援用

<!-- 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 moduleimport form导入的文件后缀名只能是.mjs,在 .mjsmodule未定义, 所以挪用 module.exports,exports 会报错

nodeCommonJs 导入 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 的区分,我们能够在 webpackjs 中运用 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

webpackrollup 都支撑 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

webpackrollup 的区分

  • webpack 不支撑导出 es6 module 范例,rollup 支撑导出 es6 module
  • webpack 打包后代码许多冗余没法直接看,rollup 打包后的代码简约,可读,像源码
  • webpack 能够举行代码支解,静态资本处置惩罚,HRMrollup 专注于 es moduletree-shaking越发壮大的,精简

假如是开辟运用能够运用 webpack,由于能够举行代码支解,静态资本,HRM,插件
假如是开辟相似 vuereact 等类库,rollup 更好一些,由于能够使你的代码精简,无冗余代码,实行更快,导出多种模块语法

结语

本文章引见了 CommonjsES6 Module,导入导出的语法划定规矩,途径剖析划定规矩,二者的区分,轻易殽杂的处所,在差别环境的区分,在差别环境的运用,Tree-shaking,与 webpackrollup 的区分
愿望您读完文章后,能对前端的模块化有更深的相识

参考链接

    原文作者:李十三
    原文地址: https://segmentfault.com/a/1190000017878394
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞