为什么须要 Javascipt 模块化?
前端的倒退突飞猛进,前端工程的复杂度也不可同日而语。原始的开发方式,随着我的项目复杂度进步,代码量越来越多,所需加载的文件也越来越多,这个时候就须要思考如下几个问题:
- 命名问题:所有文件的办法都挂载到
window/global
上,会净化全局环境,并且须要思考命名抵触问题 - 依赖问题:
script
是程序加载的,如果各个文件文件有依赖,就得思考js
文件的加载程序 - 网络问题:如果
js
文件过多,所需申请次数就会增多,减少加载工夫
Javascript
模块化编程,曾经成为一个迫切的需要。现实状况下,开发者只须要实现外围的业务逻辑,其余都能够加载他人曾经写好的模块。
本文次要介绍 Javascript
模块化的 4 种标准: CommonJS
、AMD
、UMD
、ESM
。
CommonJS
CommonJS
是一个更偏差于服务器端的标准。NodeJS
采纳了这个标准。CommonJS
的一个模块就是一个脚本文件。require
命令 第一次加载该脚本时就会执行整个脚本,而后在内存中生成一个对象。
{
id: '...',
exports: {...},
loaded: true,
...
}
id
是模块名,exports
是该模块导出的接口,loaded 示意模块是否加载结束。
当前须要用到这个模块时,就会到 exports
属性上取值。即便再次执行 require
命令,也不会再次执行该模块,而是到缓存中取值。
// utile.js
const util = {
name:'Clearlove'
sayHello:function () {return 'Hello I am Clearlove';}
}
// exports 是指向 module.exports 的一个快捷方式
module.exports = util
// 或者
exports.name = util.name;
exports.sayHello = util.sayHello;
const selfUtil = require('./util');
selfUtil.name;
selfUtil.sayHello();
CommonJS
是同步导入模块CommonJS
导入时,它会给你一个导入对象的正本CommonJS
模块不能间接在浏览器中运行,须要进行转换、打包
因为 CommonJS
是同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。期待模块工夫就是硬盘读取文件工夫,很小。然而,对于浏览器而言,它须要从服务器加载模块,波及到网速,代理等起因,一旦等待时间过长,浏览器处于”假死”状态。
所以在浏览器端,不适宜于 CommonJS
标准。所以在浏览器端又呈现了一个标准—-AMD
。
AMD
AMD
(Asynchronous Module Definition
– 异步加载模块定义)标准,一个独自的文件就是一个模块。它采纳异步形式加载模块,模块的加载不影响它前面语句的运行。
这里异步指的是不梗塞浏览器其余工作(dom
构建,css
渲染等),而加载外部是同步的(加载完模块后立刻执行回调)。
AMD
也采纳 require
命令加载模块,然而不同于CommonJS
,它要求两个参数:
require([module], callback);
第一个参数 [module],是一个数组,外面的成员是要加载的模块,callback
是加载实现后的回调函数,回调函数中参数对应数组中的成员(模块)。
AMD
的规范中,引入模块须要用到办法 require
,因为window
对象上没定义 require
办法,这里就不得不提到一个库,那就是 RequireJS。
官网介绍 RequireJS
是一个 js
文件和模块的加载器,提供了加载和定义模块的 api
,当在页面中引入了RequireJS
之后,咱们便可能在全局调用 define
和require
。
define(id?, dependencies?, factory);
- id:模块的名字,如果没有提供该参数,模块的名字应该默认为模块加载器申请的指定脚本的名字
- dependencies:模块的依赖,已被模块定义的模块标识的数组字面量。依赖参数是可选的,如果疏忽此参数,它应该默认为
["require", "exports", "module"]
。然而,如果工厂办法的长度属性小于 3,加载器会抉择以函数的长度属性指定的参数个数调用工厂办法。 - factory:模块的工厂函数,模块初始化要执行的函数或对象。如果为函数,它应该只被执行一次。如果是对象,此对象应该为模块的输入值。
// 定义一个 moduleA.js
define(function(){
const name = "module A";
return {getName(){return name}
}
});
// 定义一个 moduleB.js
define(["moduleA"], function(moduleA){
return {showFirstModuleName(){console.log(moduleA.getName());
}
}
});
// 实现 main.js
require(["moduleB"], function(moduleB){moduleB.showFirstModuleName();
});
<html>
<!-- 此处省略 head -->
<body>
<!-- 引入 requirejs 并且在这里指定入口文件的地址 -->
<script data-main="js/main.js" src="js/require.js"></script>
</body>
</html>
要通过 script
引入 requirejs
,而后须要为标签加一个属性data-main
来指定入口文件。
后面介绍用 define
来定义一个模块的时候,间接传“模块名”仿佛就能找到对应的文件,这一块是在哪实现的呢?其实在应用 RequireJS
之前还须要为它做一个配置:
// main.js
require.config({
paths: {
// key 为模块名称,value 为模块的门路
"moduleA": "./moduleA",
"moduleB": "./moduleB"
}
});
require(["moduleB"], function(moduleB){moduleB.showFirstModuleName();
});
这个配置中的属性 paths
只写模块名就能找到对应门路,不过这里有一项要留神的是,门路前面不能跟 .js
文件后缀名,更多的配置项请参考 RequireJS 官网。
UMD
UMD
代表通用模块定义(Universal Module Definition
)。所谓的通用,就是兼容了 CmmonJS
和AMD
标准,这意味着无论是在 CmmonJS
标准的我的项目中,还是 AMD
标准的我的项目中,都能够间接援用 UMD
标准的模块应用。
原理其实就是在模块中去判断全局是否存在 exports
和define
,如果存在 exports
,那么以CommonJS
的形式裸露模块,如果存在 define
那么以 AMD
的形式裸露模块:
(function (root, factory) {if (typeof define === "function" && define.amd) {define(["jquery", "underscore"], factory);
} else if (typeof exports === "object") {module.exports = factory(require("jquery"), require("underscore"));
} else {root.Requester = factory(root.$, root._);
}
}(this, function ($, _) {
// this is where I defined my module implementation
const Requester = {// ...};
return Requester;
}));
这种模式,通常会在 webpack
打包的时候用到。output.libraryTarget
将模块以哪种标准的文件输入。
ESM
在 ECMAScript 2015 版本进去之后,确定了一种新的模块加载形式,咱们称之为ES6 Module
。它和前几种形式有区别和相同点:
- 它因为是规范,所以将来很多浏览器会反对,能够很不便的在浏览器中应用
- 它同时兼容在
node
环境下运行 - 模块的导入导出,通过
import
和export
来确定 - 能够和
CommonJS
模块混合应用 CommonJS
输入的是一个 值的拷贝 。ES6 模块输入的是 值的援用, 加载的时候会做动态优化CommonJS
模块是 运行时加载 确定输入接口,ES6 模块是 编译时 确定输入接口
ES6 模块性能次要由两个命令形成:import
和 export
。import
命令用于输出其余模块提供的性能。export
命令用于标准模块的对外接口。
export
的几种用法:
// 输入变量
export const name = 'Clearlove';
export const year = '2021';
// 输入一个对象(举荐)const name = 'Clearlove';
const year = '2021';
export {name, year}
// 输入函数或类
export function add(a, b) {return a + b;}
// export default 命令
export default function() {console.log('foo')
}
import
导入模块:
// 失常命令
import {name, year} from './module.js';
// 如果遇到 export default 命令导出的模块
import ed from './export-default.js';
模块编辑好之后,它有两种模式加载:
浏览器加载
浏览器加载 ES6 模块,应用 <script>
标签,然而要退出 type="module"
属性。
- 外链 js 文件:
<script type="module" src="index.js"></script>
- 内嵌在网页中
<script type="module">
import utils from './utils.js';
// other code
</script>
对于加载内部模块,须要留神:
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块外部的顶层变量,内部不可见
- 模块脚本主动采纳严格模式,不论有没有申明
use strict
- 模块之中,能够应用
import
命令加载其余模块(.js 后缀不可省略,须要提供相对 URL 或绝对 URL),也能够应用export
命令输入对外接口 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层应用this
关键字,是无意义的 - 同一个模块如果加载屡次,将只执行一次
Node 加载
Node 要求 ES6 模块采纳 .mjs
后缀文件名。也就是说,只有脚本文件外面应用 import
或者 export
命令,就必须采纳 .mjs
后缀名。Node.js 遇到 .mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不用在每个模块文件顶部指定use strict
。
如果不心愿将后缀名改成 .mjs
,能够在我的项目的package.json
文件中,指定 type
字段为
{"type": "module"}
一旦设置了当前,该目录外面的 JS 脚本,就被解释用 ES6 模块。
# 解释成 ES6 模块
$ node my-app.js
如果这时还要应用 CommonJS
模块,那么须要将 CommonJS
脚本的后缀名都改成 .cjs
。如果没有type
字段,或者 type
字段为 commonjs
,则.js
脚本会被解释成 CommonJS
模块。
总结为一句话:.mjs
文件总是以 ES6
模块加载,.cjs
文件总是以 CommonJS
模块加载,.js
文件的加载取决于 package.json
外面 type
字段的设置。
留神,ES6
模块与 CommonJS
模块尽量不要混用。require
命令不能加载 .mjs
文件,会报错,只有 import
命令才能够加载 .mjs
文件。反过来,.mjs
文件外面也不能应用 require
命令,必须应用import
。
Node
的 import
命令只反对异步加载本地模块 (file:
协定),不反对加载近程模块。
总结
- 因为
ESM
具备简略的语法,异步个性和可摇树性,因而它是最好的模块化计划 UMD
随处可见,通常在ESM
不起作用的状况下用作备用CommonJS
是同步的,适宜后端AMD
是异步的,适宜前端