为什么要需要前端模块化
随着W3C的规范、浏览器的飞速发展,前端模块化会逐步成为开发的基础措施。通过模块化规范,我们可以开心的造轮子无缝使用轮子。
也正是基于此,衍生出一些库、工具或者实现特殊的功能 譬如:基于ESModule模块化规范的Vite、构建工具的路由懒加载、模块懒加载、code-split等等。
模块化开发的优势
-
避免因作用域污染出现的变量命名冲突等问题。
-
假设老李跟老王的开发任务在一个作用域下
老李:我要声明一个pageData用哈
老王:真巧我也想声明一个pageData
老李:emmmm 老王你小子不讲武德怎么把我声明的变量覆盖了!!!
-
-
模块分离实现模块的按需加载,提升项目性能。
-
假设你的项目有首屏加载的性能问题
老板:小王,咱的页面为啥加载时间那么长,有什么优化方案么?
小王:老板,我试下吧。我先实现下路由页面的懒加载,然后用code-split把不经常更新的工具库单独打包成一个bundle.js并做Http缓存处理 这样不用后面不用重复请求资源了还有…
老板:不错小王,速度确实提升了不少。年底奖金大大滴(你他喵,有这些大招不早点放)
-
-
对业务功能进行模块封装,提高代码的可复用性。
-
业务维护不需要在整个业务代码进行,只在某个具体功能模块进行维护,提高项目的可维护性。
前端模块化成长之路
JavaScript被设计出来时 只是为了进行页面交互、表单处理等简单逻辑。但是随着互联网技术的发展,前端开发也变得越来越复杂。开始出现命名冲突、繁琐的依赖文件等与模块化规范相关的问题,为了解决这些问题,前端模块化开始踏上探索之路。
IIFE模式
IIFE(Immediately Invoked Function Expression)立即调用函数表达式,由于JS的scope方面的缺陷。JS只有全局作用域和块级作用域,到了ES6才有块级作用域的概念。在此之前,开始尝试用IIFE的方式弥补这种缺陷,实现作用域的隔离。
// moduleA.js
(function(window) {
// 私有变量
var minAge = 18
// 私有函数
function goInternetBar(person) {
if(person.age<minAge){
console.wran("未成年人禁止去网吧")
}else {
// 去网吧玩,还想学习成绩好 天底下哪有那么多好事 5_5
person.grade--
}
}
// 暴露模块私有函数
window.plugTool = {goInternetBar}
})(window);
// moduleB.js
(function(window,tool) {
var student = {
name:"小明",
age:19,
grade:99
}
// 使用moduleB暴露的goInternetBar方法
tool.goInternetBar(student)
console.log("小明去过网吧后,成绩:"+student.grade)
})(window,plugTool)
复制代码
优雅标准的模块化规范
IIFE 的出现解决了作用域混淆污染的问题,但是它仍然有很多不足同时写法也不够优雅
- 如何安全的包装一个模块的代码(实现作用域隔离,不污染模块外的任何代码)
- 如何唯一的标记一个模块(便于模块的缓存的等操作)
- 如何优雅的暴露模块的API而非全局变量的形式
- 如何清晰展示模块之间的依赖关系
CommonJS
CommonJS最初是服务端应用在Node环境中的模块化规范,它规定在Node中,每个文件都是一个模块都拥有各自的作用域。
语法
global
:对象,挂载全局变量module
:对象,当前模块对象require
:函数,输入依赖module.exports
:对象,模块向外暴露的接口exports
:对象,默认指向module.exports
,不可以覆盖该属性的值,只能在该属性上挂载变量或者方法
// moduleA.js
function eat(weight, food) {
if (food === '米饭') {
weight++
} else if (food === '馒头') {
weight += 2
}
return weight
}
// 暴露方式1:
module.exports = { eat }
// 暴露方式2:
// exports.eat = eat
// 错误的暴露方式:
// exports = { eat }
// moduleB.js
const tool = require("./moduleA.js")
const person = {
name: "张三",
weight: 100
}
const weight = tool.eat(person.weight, "馒头")
console.log("小明吃过馒头后,体重为:" + weight)
复制代码
特性
-
模块同步加载
CommonJS模块化规范模块是同步加载机制,加载的资源都在本地服务器上,时间和速度都没问题。但在浏览器端,受限于网络及性能的原因CommonJS显然不适合。 -
模块暴露出去的值为浅拷贝
当暴露的值为简单数据类型时,模块内外的数据是独立的,即使模块内部数据改变 暴露的值也不变
当暴露的值为复杂数据类型时,模块内外引用的为同一个对象,在模块内部修改该对象属性会影响到外部。// moduleA.js // 复杂数据 var person = { name: "张三", age: 18 } // 简单数据 var maxAge = 10 function ageAdd() { person.age++ } function maxAgeAdd() { maxAge++ } console.log("moduleA is loading"); module.exports = { person, maxAge, ageAdd, maxAgeAdd } // main.js var { person, maxAge, ageAdd, maxAgeAdd } = require("./moduleA.js") console.log(`============= init ============`); console.log("person.age:" + person.age + ",maxAge:" + maxAge); // 修改模块内部的简单、复杂数据 ageAdd() maxAgeAdd() console.log(`============= age add ============`); console.log("person.age:" + person.age + ",maxAge:" + maxAge); 复制代码
AMD
AMD(Asynchronous Module Definition):异步加载模块定义。
鉴于浏览器环境的特性,我们需要一个异步加载机制的模块化规范。AMD则是最早实践者的其中之一。
AMD语法规范
声明模块
define([依赖模块],function(依赖模块暴露对象) {...})
// 声明无依赖的AMD模块
define(function(){
return 向外暴露的对象
})
// 声明有依赖的AMD模块
define(["jquery"],function($){
return 向外暴露的对象
})
复制代码
导入依赖
require([依赖模块],function(依赖模块暴露对象) {...})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#app").css("background","pink")
});
复制代码
require.js
require.js是AMD规范的最佳实践,它主要解决了在使用AMD规范中触发以下几个问题:
- 实现js文件的异步加载,避免网页失去响应
- 管理模块之间的依赖性,便于代码的编写和维护。
AMD规范特性
- 依赖前置,提前执行
在AMD中,输入依赖的语句都会提升到最前面。并且立即执行所有输入依赖的模块,然后再执行其他内容// moduleA.js define(function () { console.log('modelA is loading'); return { getName: function () { return 'modelA'; } }; }); // moduleB.js define(function () { console.log('modelB is loading'); return { getName: function () { return 'modelB'; } }; }); // main.js define(function (require) { var modelA = require('./a'); console.log(modelA.getName()); var modelB = require('./b'); console.log(modelB.getName()); }); // index.html <body> <!-- 引入require.js --> <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script> <script> // 加载main.js requirejs(['main']); </script> </body> 复制代码
CMD
CMD(Common Module Definition):通用模块定义。
它是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。
CMD语法规范
声明模块
// 声明没有依赖的模块
define(function(require, exports, module){
// 暴露方式1:
exports.xxx = value
// 暴露方式2:
module.exports = value
})
复制代码
// 声明有依赖的模块
define(function(require, exports, module) {
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {...})
//暴露模块
exports.xxx = value
})
复制代码
导入依赖
define(function (require) {
var mA = require('./moduleA')
var mB = require('./moduleB')
mA.sayHello()
mB.sayHello()
})
复制代码
CMD规范特性
- 依赖就近、延迟执行
在当前模块中,需要依赖的时候才进行声明导入。等导入成功后当前模块代码才会继续向下执行。
// moduleA.js
define(function (require, exports, module) {
console.log("moduleA is loading");
module.exports = {
getName: function () {
return "moduleA"
}
}
});
// moduleB.js
define(function (require, exports, module) {
console.log("moduleB is loading");
module.exports = {
getName: function () {
return "moduleB"
}
}
});
// main.js
define(function (require, exports, module) {
var moduleA = require("./moduleA")
console.log(moduleA.getName());
var moduleB = require("./moduleB")
console.log(moduleB.getName());
});
// index.html
<body>
<script src="https://cdn.bootcss.com/seajs/3.0.3/sea.js"></script>
<script>
seajs.use('./main.js')
</script>
</body>
复制代码
前端模块化旧时代落幕
前端模块化由IIFE — AMD — CMD — UMD(他的出现为了解决前面几种规范的兼容性问题,本文不对其进行详解介绍),这些模块化规范是前端开发者自己不断探索演变的,直到ES6推出新的模块化标准 ES Modules,老的模块化规范开始过时走向衰弱。
有两个有意思的文章可以看下:
ES Modules
ES Modules(ESM)是 JavaScript 官方的标准化模块系统。 作为JS ESMA标准它会逐步被全部浏览器支持。它可以在NodeJS中运行。
语法
模块暴露
export default
:默认导出
export
:按需导出
var num= 1
function numAdd() {
num++
}
var count = 10
var name = "张三"
export default {num,numAdd}
export count
export name
或者
export {name,count}
复制代码
导入依赖
// 默认导入
import tool from './moduleA.js';
// 按需导入
import { count } from './moduleA.js';
// 复合用法
import tool,{ count } from './moduleA.js';
复制代码
特性
-
import
命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。// moduleB.js console.log("moduleB is loading"); import * as tool from './moduleA.js'; console.log("tool", tool); // moduleA.js console.log("moduleA is loading"); export var num = 1 // index.html <body> <script type="module" src="./moduleB.js"></script> </body> 复制代码
-
导出值为模块中值的引用
// moduleA.js
// 复杂数据
var person = { name: "张三", age: 18 }
// 简单数据
var maxAge = 10
function ageAdd() { person.age++ }
function maxAgeAdd() { maxAge++ }
console.log("moduleA is loading");
export {
person, maxAge, ageAdd, maxAgeAdd
}
// main.js
import { person, maxAge, ageAdd, maxAgeAdd } from './moduleA.js';
console.log(`============= init ============`);
console.log("person.age:" + person.age + ",maxAge:" + maxAge);
ageAdd()
maxAgeAdd()
console.log(`============= age add ============`);
console.log("person.age:" + person.age + ",maxAge:" + maxAge);
// index.html
<body>
<script type="module" src="./main.js"></script>
</body>
复制代码
ES Modules 与CommonJS的区别
暴露值的区别
ES Modules
暴露的为一个值的引用,在模块外调用模块内的方法修改模块内的值 模块暴露的值也会改变
CommonJS
暴露的为一个值的浅拷贝,当模块内这个值发生变化,如果这个值为简单数据则暴露的值不会发生变化,如果这个值为复杂数据则暴露的值也会跟着改变。
加载机制不同
CommonJS
的模块为动态导入,运行时加载。会先将整个模块加载进来,然后生成一个对象 从这个对象上取暴露的数据。
ES Modules
是JS静态编译时加载,import
语句优先执行。在编译时import
去指定加载暴露的数据