一、什么是模块化
将一个复杂的程序安装一定的规则拆分成一个个独立的“块”,“块”的内部数据与实现是私有的,向外部暴露一些接口供使用。
二、模块化的作用
- 避免命名冲突
- 更好地分离,按需加载
- 更高的复用性
- 可维护性好
三、模块化的发展
- js在最初始阶段,代码量少,不需要分模块
- 随着互联网的发展,交互增多,js代码量增多,将部分相同的js代码抽离或将复杂的功能拆分成一个个相对简单的功能,更有利于代码的维护。在依赖于html文件的基础上,将js代码抽离出去。可以通过闭包(立即执行函数),做到全局变量的命名冲突,模块之间可以相互依赖,但是问题在于,依赖之间的加载顺序必须要有前后关系,如果B模块依赖于A模块,那么B模块需要在A模块后引入
- Nodejs的诞生带来了前所未有的模块化体验,通过
module.exports导出模块,通过require导入模块。js文件的加载是同步的,在Node上运行,在require时,该模块会被缓存。require引入进来的模块会被自动处理成局部变量 - AMD通过define定义模块,通过require导入模块,基于require.js,前置依赖:等待所有依赖的模块加载完毕,回调函数执行,所以依赖的前后顺序无所谓。它是在script标签上添加了async属性
- CMD(阿里)通过define定义模块,通过require导入模块,基于seajs,也是前置依赖,但是
依赖就近,按需加载,这是和AMD本质上的不同,效率上高于AMD - ES6的模块化,ESMA官方的模块化。commonJS模块引入时,是值的拷贝,和导出文件中的值没有关系;import引入时,是值的引用,和导出文件中的值是同一个。commonJS在运行时加载,ES6模块在编译时加载
四、模块化使用案例
1、commonJS规范
- 暴露模块
- 导入模块
至此,第一种模块化方案出现。commonJS是同步加载,用于服务端,服务端模块存放在硬盘中,读取速度就是硬盘读取文件的时间,很快;而浏览器端必须是异步加载,如果采用同步加载,由于网速、代理、模块过大等问题,容易出现请求超时。
commonJS如何运行在浏览器环境中?
- 安装browserify
npm i browserify -D
- package.json中添加脚本,-o意思为output
"build": "browserify ./index.js -o ./dist/bundle.js"
- 执行npm run build生成dist
2、AMD-requirejs
在AMD出现之前,市场上的commonJS不适用于浏览器端,于是js开发者采用的“模块化”方案由立即执行函数将作用域隔绝,通过挂载到window上达到模块之间的相互依赖
模块的导入
<script src="./js/dataService.js"></script>
<script src="./js/alerter.js"></script>
<script src="./app.js"></script>
这种方式本质上还是同步加载,依赖之间有严格的前后关系,同步加载最大的问题——请求超时还是没有解决。浏览器的模块,不能采用“同步加载”,只能采用“异步加载”,这就是AMD诞生的背景
- 暴露模块
define的参数:
1、模块名,字符串,可选
2、依赖的模块,数组,可选
3、回调,函数/对象,必须。如果是函数,需要通过return暴露模块;如果是对象,此对象为模块的输出值
- 导入模块
;(function () {
require.config({
// baseUrl: './js/', // 加上baseUrl,'./js/dataServer'就可以写成'dataServer'
paths: {
dataServer: './js/dataServer', // 路径后不加.js后缀,alerter.js中使用的dataServer模块名就是在这里映射的
alerter: './js/alerter'
}
})
require(['alerter'], function (alerter) {
alerter.showMsg()
})
})()
- 在浏览器端使用,依赖requirejs。html中不需要像之前那样依次引入模块了,在require.config()中已经做了模块的映射
<script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"></script>
<script src="./index.js"></script>
或
<script data-main="./index.js" src="./libs/require.js"></script>
如果要问requirejs实现原理,用promise可以实现
3、CMD-seajs
AMD推崇依赖前置,CMD推崇依赖就近、按需加载,这是CMD和commonJS/AMD本质上的区别
- 暴露模块
define中只有一个函数,该函数有3个形参
1、require
2、exports
3、module
- 导入模块
define(function (require, exports, module) {
// 异步导入
require.async('./a.js', function (moduleA) {
console.log(moduleA, moduleA.getName())
})
// 同步导入
const moduleB = require('./b.js')
console.log(moduleB, moduleB.getName())
})
- 加载模块
// 加载模块,加载完执行回调,回调中的参数和加载顺序对应
seajs.use(['./js/b', './js/c'], function (moduleB, moduleC) {
console.log(moduleB, moduleC)
})
- html中使用
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/runtime.js"></script>
<script src="./index.js"></script>
4、ES6的模块化
- 暴露模块
- 导入模块
import a, { add as add1 } from './a'
import { add, reduce, multiply } from './b'
a()
console.log(add1(10, 20))
console.log(add(1, 2))
console.log(reduce(1, 2))
console.log(multiply(1, 20))
浏览器不支持ES6语法,需要通过babel将ES6转为ES5,再通过browserify编译
- 安装插件
npm i babel-cli babel-preset-es2015 browserify -D
- .babelrc
{ "presets": ["es2015"] }
- package.json
"babel": "babel js -d babel",
"bro": "browserify babel -o dist/index.js"
- 执行npm run babel和npm run bro分别生成babel文件夹和dist文件夹
- html中使用
<script src="./dist/index.js"></script>
五、commonJS和ES6模块化的区别
| 区别 | commonJS | ES6 |
|---|---|---|
| 输出不同 | 输出值的拷贝,原来模块中的值改变不会影响到已经加载的值 | 输出值的引用,原来的值改变了,引用的值跟着改变 |
| 加载时机不同 | 运行时(require导入时)加载 | 编译时加载,通过webpack或别的工具编译时就已经加载好了,比commonJS加载时机更前 |
| this指向 | 指向当前模块 | 指向undefined |
| 加载内容 | 加载整个模块,所有的接口都会加载进来 | 可以单独加载其中某个接口 |
| 同步异步 | 同步,适用于服务端 | 异步,适用于客户端 |
六、script标签中async和defer的异同
GPU渲染进程和js引擎线程是互斥的,当js引擎执行时GPU线程会被挂起,GPU更新会被保存在一个队列中等待js引擎空闲时立即被执行,如果js执行的时间过长,会导致页面渲染被阻塞。
相同点
- 只用于外联script
- script下载和html解析并行
不同点
| 区别 | async | defer |
|---|---|---|
| 脚本执行时机 | ①下载脚本时执行脚本,可能在html解析完之前执行,如果脚本中操作了DOM可能会出错 | ①等待html解析完执行脚本 |
| 适用场景 | 脚本中不需要操作DOM,并且与其他脚本互不依赖 | 需要操作DOM的脚本,如jQuery。将脚本放到body底部和defer都是在html解析完加载,区别是defer会在html解析的过程中下载脚本 |
| 优先级 | 同时使用async、defer,async的优先级更高 |