commonJs、AMD、UMD、es6模块化的区别,这篇就够了
前言:相信大家对前端模块化的理解比较模糊,下面我总结了相关知识点,废话不多说,直接进入主题
一、模块化的思想
模块化就是把逻辑代码拆分成独立的块,各自封装,互相独立,每个块自行决定对外暴露什么,同时自行决定引入执行哪些外部代码。
二、commonJs
Node 应用由模块组成,采用 CommonJS 模块规范。也就是说CommonJs是应用在node服务器端的,如果浏览器想使用CommonJs规范的话需要用 browserify库 来进行转化。(后面会有例子)
CommonJs分为两部分:moudle对象和requeire命令
1、moudle对象
Node内部提供一个Module构建函数。所有模块都是Module的实例
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
}
每个模块内部,都有一个module对象,代表当前模块。它有以下属性。
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。
//a.js
module.exports = {
name: 'ysl',
age: '27'
}
let obj = require('./a.js')
console.log(obj) // { name: 'ysl', age: 27 }
除了使用module.exports的方式导出值之外,还可以使用exports导出
//a.js
export.name = 'ysl'
export.age = 27
// 注意不能 export = {name:'ysl',age:27},这种方式是无效的
上面两种方式导出的结果是一致的
2、require命令
上面我们已经将内容导出,现在是怎么把加载导出的内容,node内置的require命令用于加载模块文件
//a.js
module.exports = {name:'ysl',age:27}
//b.js
let obj = require(./a.js)
console.log(obj) // {name:'ysl',age:27}
需要注意点一:require第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。
//a.js
module.exports = {
name: 'ysl',
age: 27
}
//b.js
let obj1 = require('./a.js')
console.log(obj) // {name:'ysl',age:27}
obj1.name = 'abc'
let obj2 = require('./test')
console.log(obj2) // {name:'abc',age:27}
console.log(obj1) // {name:'abc',age:27}
// 从这个例子可以看出node会对模块进行缓存
delete require.cache[require.resolve('./a.js')]
let obj3 = require('./test')
console.log(obj3) // {name:'ysl',age:27}
// 可以使用delete require.cache[moduleName]来删除缓存
需要注意点二:CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
// 上面代码说明,`counter`输出以后,`lib.js`模块内部的变化就影响不到`counter`了。
<script src='../node_modules/requirejs/require.js' data-main='./index'></script>
// data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的index.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把index
三、AMD
因为commonJs加载是同步加载的,在浏览器中如果某个模块加载时间很长,整个应用就会停在那里等,页面会出现卡死的现象。(下面的例子)
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>
// 首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难
AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
require.js类库实现了AMD规范,下面讲讲其基本用法:
1、下载require.js: npm install requirejs
2、引用require.js
<script src='../node_modules/requirejs/require.js' data-main='./index'></script>
// data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的index.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把index
3、编写index.js模块代码,主模块是依赖其他模块的入口,这时就要使用AMD规范定义的的require()函数加载依赖模块
// index.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// some code here
// 可以在这里编写模块加载后的代码
});
// require()函数接受两个参数:
// 第一个参数是一个数组,表示所依赖的模块['moduleA', 'moduleB', 'moduleC']
// 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用
// 注意['moduleA', 'moduleB', 'moduleC']这里面的三个模块与index.js在同一个目录
4、模块的写法,AMD提供define方法,调用define并传入一个函数
// moduleA.js
define(function (){
var add = function (x,y){
return x+y;
};
return {
add: add
};
});
// index.js
require(['moduleA'], function (moduleA){
console.log(moduleA)
//moduleA就是moduleA.js模块传入的函数执行后返回的对象{add:function}
});
以上就是符合AMD规范的require.js基本用法
三、UMD规范
通过对 CommonJs、CMD、AMD 进一步处理,它没有自己专有的规范,是集结了 CommonJs、CMD、AMD 的规范于一身。
它可以通过运行时或者编译时让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。
未来同一个 JavaScript 包运行在浏览器端、服务区端都只需要遵守同一个写法就行了。
// UMD简单实现
((global, factory) => {
//如果 当前的上下文有define函数,并且AMD 说明处于AMD 环境下
if (typeof define === 'function' && define.amd) {
define(["moduleA"], factory);
}
else if (typeof exports === 'object') {//commonjs
let moduleA = require("moduleA")
modules.exports = factory(moduleA)
}
else {
global.moduleA = factory(global.moduleA) //直接挂载成 windows 全局变量
}
})(this, (moduleA) => {
//本模块的定义
return {
}
})
四、ES6模块
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。
es6模块的语法分为两部分:export 模块导出、 import模块导入
1、export 语法
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
上面的例子导出一个对象,包含firstName, lastName, year三个属性。
注意: export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
// profile.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
//上面代码输出变量`foo`,值为`bar`,500 毫秒之后变成`baz`
2、import语法
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同
除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。
// main.js
import * as total from './profile.js';
function setName(element) {
element.textContent = total.firstName + ' ' + total.lastName;
}
3、export default 语法
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export default { firstName, lastName, year };
// main.js
import total from './profile.js';
function setName(element) {
element.textContent = total.firstName + ' ' + total.lastName;
}
注意:export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。
// 正是因为`export default`命令其实只是输出一个叫做`default`的变量,所以它后面不能跟变量声明语句。
export default var a = 1; // 会报错
4、export与import复用
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
5、import()使用场景
import()返回一个 Promise 对象
(1)按需加载,路由按需加载就是调用此方法
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
(2)条件加载
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
CommonsJs与ES6之间的差别
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
五、 node和浏览器分别加载es6与commonJs的方式
node加载es6模块的方式
1、 ES6 模块采用.mjs后缀文件名。
2、 项目的package.json文件中,指定type字段为module。(注意:如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。)
总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。
node默认采用commonJs模块模块的方式
// a.js
module.exports = {name:'ysl}
// main.js
let obj = require('./a.js')
// node环境可以直接使用
浏览器加载es6模块方式
浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
<script type="module" src="./foo.js"></script>
// 浏览器对于带有`type="module"`的`<script>`,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了`<script>`标签的`defer`属性。
<script type="module" src="./foo.js" defer></script>
// 效果一致
通过<script>标签加载,可能会出现跨域的现象,可以使用express解决
浏览器加载CommonJs
浏览器直接加载CommonJs会报错,因为浏览器不存在module、exports、require这些环境变量,可以使用Browserify,对模块进行转换
上面就是我总结出来的知识点,如果觉得有用可以点个关注加收藏,后面会总结更多前端基础知识点。