为什么须要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.jsconst 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.jsdefine(function(){ const name = "module A"; return { getName(){ return name } }});// 定义一个moduleB.jsdefine(["moduleA"], function(moduleA){ return { showFirstModuleName(){ console.log(moduleA.getName()); } }});// 实现main.jsrequire(["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.jsrequire.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
是异步的,适宜前端