什么是模块?
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
模块化进程
- 全局function模式
- 不同功能封装成全局函数
- 问题:污染全局命名空间,容易引起命名冲突,且模块成员之间看不出关系
function f1 () {
console.log('f1');
}
function f2 () {
console.log('f2');
}
- namespace模式
- 模块间有了各自的命名空间,避免了命名冲突
- 问题:模块内的数据会被外部修改
const Module = {
data: '模块化',
f1 () {
console.log('module.f1' + this.data);
},
f2 () {
console.log('module.f2' + this.data);
}
}
Module.data = '修改了'
Module.f1()
- IIFE模式(函数自调用)
- 将数据和行为封装到函数内部,通过给window添加属性向外暴露接口,数据私有外部只能通过暴露的方法操作。
- 问题:如果模块依赖别的模块怎么办?
(function (window) {
let data = 'IIFE模式'
function f1 () {
console.log('IIFE模式f1' + data);
}
function f2 () {
f3()
}
function f3 () {
console.log('IIFE模式f3' + data);
}
window.IIFE = { f1, f2 }
})(window)
IIFE.f1()
IIFE.data = '修改了'
IIFE.f2()
模块化的好处
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
引入多个<script>后出现出现问题
- 请求过多
- 依赖模糊
- 难以维护
模块化规范
1.CommonJS
Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。
特点
- 所有代码运行在模块作用域,不会污染全局作用域
- 模块加载的顺序按照代码中出现的顺序
- 模块可以多次加载,但只会在第一次加载运行,之后只会返回缓存结果。如果想再次运行需要清除缓存
基本语法
- 暴露模块:
module.exports = value或exports.xxx = value - 引入模块:
reuqire('xxx')
每个模块内部module代表当前模块,这个模块是一个对象,它的exports属性是对外的接口。加载某个模块实际是加载模块的module.exports属性
const obj = {
a: 1,
b: 2,
c: {
a: 3
}
}
let com = 4
const exFunc = () => {
com++
obj.b++
obj.c.a++
console.log(com, obj.b, obj.c.a);
}
module.exports = { exFunc, com, obj }
const im = require('./模块化.js')
im.obj.a = 2
im.com = 10
im.exFunc()
可以看到
commonjs输出的是值的拷贝(浅拷贝)
2.ES6模块化
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
语法
export用于规定对外暴露的接口,import引入其他模块提供的功能
const obj = {
a: 1,
b: 2,
c: {
a: 3
}
}
export let com = 4
const exFunc = () => {
com++
obj.b++
obj.c.a++
console.log(com, obj.b, obj.c.a);
}
export default { exFunc, com, obj }
import module2 from './模块化.js'
import { com } from './模块化.js'
module2.exFunc()
console.log(module2.com);
console.log(com)
module2.com = 10
console.log(module2.com);
可以看到export default和export导出的com在更改以后的值并不一样,因为
ES6模块输出的是值的引用,js引擎解析代码时遇到模块import生成一个只读引用,等到脚本执行的时候根据只读的引用到被加载的模块里取值。所以模块内部修改了com的值,export的com值也会相应的改变。而通过export default导出的com可以理解为将变量赋值给了default,基本数据类型com赋值给default做了一层拷贝,所以模块内部修改com的值并不会改变deafult里的com。
import()动态加载
import是加载的模块都是静态的,但是ES同时也提供一种动态加载的方式,即import()函数。允许您仅在需要的时候加载模块而不必提前加载所有模块。
function add() {
modules.exFunc()
if(com === 7) {
import('./模块化2.js').then(res => {
console.log(res);
})
}
}
document.getElementById('btn').onclick = add
async function add() {
modules.exFunc()
if(com === 7) {
const res = await import('./模块化2.js')
}
}
document.getElementById('btn').onclick = add
import()导出的是一个promise对象,所以可以通过.then或者await的方式来获取导出结果,需要注意的是如果是默认导出的方式,那么得到的结果实际是包含在default属性中的。export导出的结果可以通过解构来获取。
路由懒加载
懒加载前提:进行懒加载的子模块(子组件)需要是一个单独的文件。在需要用到组件的时候通过import()把组件动态引入
调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。
——摘自《webpack——模块方法》的import()小节methods/#import-)
也就是说import()导入的子模块被分离出来打成单独的chunk,当然这还不足以实现懒加载,还需要借助函数导出的方式导出import()模块,在需要的地方调用函数引入模块。
const routes = [{
path: '/',
name: 'Home',
// 将子组件加载语句封装到一个function中,将function赋给component
component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
}
]
这是我们在vue路由中懒加载的常见写法,可以看到就是利用了import()来动态加载相应的路由组件并通过函数赋值给component属性。
总结
CommonJS模块是运行时加载,ES6模块是编译时加载ES6在编译的时候就确定了模块间的依赖关系,输入和输出的变量,对外的接口在代码静态解析阶段就生成。所以import必须在顶层作用域上,if(xx) {import xx from 'xxx'}是不能通过编译的。所以CommonJS加载一个对象,对象只有在脚本运行完才生成,所以可以在任何地方引入CommonJS的模块
CommonJS模块输出是值得拷贝(浅拷贝),ES6模块输出的是值的引用CommonJS是同步加载模块,ES6是异步加载模块
结语
这里只总结了一小部分,之后会总结其他的部分。写的不好,各位大佬多多指教,感恩家人🙏