前端模块化就是将复杂程序根据规范拆分成若干模块,一个模块包括输入和输出。而且模块的内部实现是私有的,它通过对外暴露接口与其他模块通信,而不是直接调用。现在在HTML文件中可以引用的script包括脚本和模块,其中模块具有更高的开发效率(可读性强、复用性高),而脚本具有更高的页面性能,因为模块相对文件多,加载速度慢。需要注意的是模块在浏览器中运行会存在兼容性的问题,在script声明type="module"即可使用ES Module模块化规范。
function api(){
return {
data:{
a:1,
b:2
}
}
}
var __Module={
api(){
return {
data:{
a:1,
b:2
}
}
}
}
那么我们可以通过函数作用域加闭包的特性来解决。
(function(){
var a =1;
function getA(){
return a;
}
function setA(a){
window.__module.a = a;
}
window.__module = {
a,
getA,
setA
}
})();
(function(global){
var a =1;
function getA(){
return a;
}
function setA(a){
window.__module.a = a;
}
global.__Module_API = {
a,
getA,
setA
}
})(window)
(function(global,moduleAPI){
global.__Module = {
setA:moduleApi.setA
}
})(window,window.__Module_API)
以上就是早期没有工具和规范的情况下对模块化的落地方式。
通过约定的方式去做模块化,不同的开发者和项目会有不同的差异,我们就需要一个标准去规范模块化的实现方式。针对与模块加载的方式,以上方法都是通过script标签手动引入的方式,模块加载不受代码的控制,时间久了维护会非常麻烦。那么就需要一个模块化的标准和自动加载模块的基础库。
CommonJS规范是Node.js默认模块规范,他规定了每个文件就是一个模块,每个模块有自己的作用域,它的模块加载采用同步加载方式,加载模块的时候必须模块加载完成后再执行后续的代码。它通过require来加载模块,通过exports或module.exports输出模块。
CommonJS要想在浏览器中使用的话需要使用browserify来打包。
browserify打包原理主要是:通过自执行函数实现模块化,将每个模块编号,存入一个对象,每个模块标记依赖模块。它内部实现了require方法,核心是通过call方法调用模块,并传入require、module、exports方法,通过module存储模块信息,通过exports存储模块输出信息。
AMD(Asynchronous Module Definition)规范采用异步加载模块,允许指定回调函数。Node模块通常都位于本地,加载速度快,所以适用于同步加载,但是在浏览器运行环境中,用同步加载会阻塞浏览器渲染,所以在浏览器环境中,模块需要请求获取,适用于异步加载。因此就诞生了AMD规范,用于异步加载,其中require.js就是AMD的一个具体实现库。目前不管是CMD或是AMD用的都很少,在Node开发中通常用CommonJS规范,在浏览器中用ES Module规范。
引用了require.js之后,它会全局定义一个define函数和require函数,所有的模块要用define去定义。define有三个参数:第一个模块名,第二个是数组用于声明模块依赖项,第三个是一个函数,函数参数与第二个参数依赖项一一对应,每一个参数为依赖项导出的成员。函数的作用可以理解为当前模块提供私有的空间,如果要向外导出成员可以通过return来实现。
define('module1',['jquery','./module2'],function($,module2){
return {
}
});
而require函数则用来加载模块,当调用require函数的时候,其内部会自动创建script标签来发送脚本文件的请求,并执行相对应的模块代码。
require(['./module1'],function (module1){
module1.start();
})
目前绝大部分第三方库都支持AMD规范,但是因为要使用频繁使用require、define导致AMD使用起来相对复杂。另外如果模块代码划分的很细致,那么在同一个页面中,JS文件的请求次数相对多,导致页面效率低下。
CMD规范整合了CommonJS和AMD的优点,通过异步加载模块。CMD专门用于浏览器端,其中淘宝的sea.js就是CMD规范的一个具体实现库。
//CMD规范,类似CommonJS规范
define(function(require,exports,module){
//通过require引入依赖
var $ = require('jquery')
//通过exports 或者module.exports对外暴露成员
module.exports = function(){
}
})
目前模块化标准规范已经非常成熟统一了,在Node.js中遵循CommonJS组织模块,在浏览器端使用ESModule规范。目前前端模块化基本统一为CommonJS + ESModule规范。
ESModule规范是目前应用最为广泛的模块化规范,它的设计理念是在编译时就确定模块依赖关系及输入输出。而CommonJS和AMD由于采用闭包的形式必须在运行时才能确定依赖和输入、输出。ESM通过import加载模块,通过export输出模块。
使用ESModule,通过给script标签添加type=module属性,就可以以ESModule的标准执行JS代码。
<script type="module">script>
ESModule有如下几个特性:
在ESM中 export {}
它只是导出成员的语法,不是导出字面量对象,import {} from 'xxx'
,也是语法,不是解构对象,如果想导出对象可以使用export default {}
,使用import xxx from 'xxx'
获取对象。其次通过export {name}
导出的name值是引用的不是拷贝,它的值会受到导出模块内部修改的影响,而且name只是可读不能修改。
.js
扩展名,相对路径要是用./
,可以使用绝对路径和url的方式。import {name} from './module.js';
import {name} from '/xxx/module.js';
import {name} from 'http://localhost:3000/module.js';
import './xxx.js';
import * as mod from './xxx.js';
import('./xxx.js').then((module)=>{
console.log(module);
});
//a.js
...
export {name,age}
export default 'title'
//b.js
import {name,age,default as title} from './a.js';
//或者
import title,{name,age} from './a.js';
通过export将目录下散落的模块在index文件中导出,方便外部使用
export {name,age} from './module.js';
如果要使用commonjs规范的话就要将对应的js文件改为.cjs
后缀名。
在Node.js新版本中,在package.json中添加type="module"
字段就可以使用ESModule规范了。
模块化解决了我们在复杂应用当中的代码组织问题,但是随着引入模块化,又产生了新的问题。例如以下问题:
针对第一个问题,我们需要一个工具将代码进行编译,在开发阶段将新特性的代码转换为兼容绝大多数环境的代码。
针对第二个问题,将模块化的文件打包到一块。
针对第三个问题,将其他资源通过代码的方式进行控制,统一去维护。
针对以上问题就需要模块打包工具来解决,例如webpack、parcel和rollup。
通过使用webpack就可以将零散的代码打包到js文件中,对于那些有环境兼容问题的代码就可以通过模块加载器loader去做兼容转换,他还具有代码拆分的能力,可以按我们的需要去打包,不用担心将所有的代码打包到一块,导致文件比较大的问题。我们可以将应用加载过程中初次运行所必须的模块打包到一起,其他的模块单独存放,等到应用运行过程中需要某个模块,再异步加载这个模块,从而实现增量加载或者渐进式加载。
对于资源文件webpack支持在JS中以模块化的方式去载入任意类型的资源文件。打包工具解决的是前端整体的模块化,并不单指JavaScript模块化。
看完觉得对您有所帮助别忘记关注呦