一、模块化概述
模块化是一种思想。模块化是主流的代码组织方式,通过把复杂代码按照功能的不同划分为不同的模块,单独进行维护的方式提高开发效率,降低维护成本。
二、模块化演变过程
Stage1-文件划分方式
早期将每个功能及状态数据单独存放到不同的文件中,约定每个文件都是独立的模块,使用的时候就是将模块引入到页面中,一个script标签对应一个模块,然后再代码中直接调用全局成员(变量或函数)
引用
缺点:
- 污染全局作用域
- 命名冲突问题
- 无法管理模块依赖关系
Stage2-命名空间方式
每个模块只暴露一个全局对象,所有的模块成员都挂载到对象下
缺点:
- 无私有空间
- 模块内部成员可以被访问修改
- 模块之间依赖关系没有被解决
Stage3-IIFE立即执行函数
- 使用立即执行函数包裹代码,要输出的遍历挂载到一个全局对象上
- 变量拥有了私有空间,只有通过闭包修改和访问变量
- 参数作为依赖声明去使用,使得每个模块的依赖关系变得明显
三、模块化规范的出现
1、CommentJs规范
NodeJs提出的一种标准,以同步模式加载模块
如果浏览器端使用commonJS,会导致效率低下,我们每次页面加载,都会导致大量请求出现,所以早期浏览器没有采用commonJS规范,而是采用AMD
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 使用module.export导出成员
- 通过require函数载入模块
CommonJS规范—是通过module.exports定义的,在前端浏览器里面并不支持module.exports,通过node.js后端使用的。Nodejs端是使用CommonJS规范的,前端浏览器一般使用AMD、CMD、ES6等定义模块化开发的
require引入
2、AMD(Asynchronous Module Definition)异步模块定义规范
AMD -> Require.js
实现了这个规范,是模块加载器
定义一个模块
// 定义一个模块
define('module1', ['jquery', './module2'], function ($, module2) {
return {
start: function () {
$('body').animate({ margin: '200px' })
module2()
}
}
})
载入模块
require(['./module1'], function (module1) {
module1.start()
})
目前绝大多数第三方库都支持AMD规范,但是
- 但是AMD使用起来相对复杂
- 模块JS文件请求频繁,会消耗性能
3、Sea.js + CMD(Common Module Definition)通用模块规范
sea.js库是淘宝推出来的
四、模块化标准规范
node.js -> commonjs
我们主要研究ES Modules
五、ES Modules特性
// 通过给script添加type=module属性,就可以使用ES Module的标准执行其中的JS代码
<script type="module">
console.log(123)
</script>
// ./modulejs
const foo = 'es modules'
export { foo }
// ./app.js
import { foo } from './module.js'
console.log(foo) // => es modules
// export defaults
export default var name = 'es module'
import name form './module.js'
- 自动采用严格模式,忽略’use strict’
- 每个ESM模块都是单独的私有作用域
- ESM是通过CORS去请求外部JS模块的
- ESM的script标签会延迟脚本执行
ES6在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案
- 每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取;
- 每一个模块内声明的变量都是局部变量, 不会污染全局作用域;
- 模块内部的变量或者函数可以通过export导出;
- 一个模块可以导入别的模块
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能
一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取,只可以通过export关键字导出。
var name = 'jack'
var age = 18
var obj = { name, age }
export { name, age } // export后面的双括号是固定写法
如果我们想导出一个对象 用export default,后面可以接一个变量或者一个值
var name = 'jack'
var age = 18
var obj = { name, age }
export default { name, age }
是将值的引用关系给了外部,并不是将值拷贝了一份给外部
module.js
var name = 'jack'
var age = 18
export { name, age }
setTimeout(() => {
name = 'ben'
},1000)
app.js
import { name, age } from './module.js' // 这里的import是固定用法,也不是结构赋值
console.log(name, age) // jack 18
name = 'tom' // 这里会报错,说明暴露出来的成员是只读的
setTimeout(() => {
console.log(name, age) // ben 18
},1500)
一些注意事项:
// import { name } from './module'
import { name } from './module.js' // 不可以省略.js扩展名 不然报错
// console.log(name)
// import { lowercase } from './utils'
import { lowercase } from './utils/index.js' // 在原生ES Module环境 index.js也不能省略
// console.log(lowercase('HHH'))
// import { name } from 'module.js'
import { name } from './module.js' // ./不能省略
// import { name } from '/04-import/module.js'
// import { name } from 'http://localhost:3000/04-import/module.js'
// console.log(name)
// --------------
// import {} from './module.js'
// import './module.js'
// ---------------
import * as mod from './module.js' //如果导出的成员比较多,可以采用 *的形式
// console.log(mod)
// ---------------
// var modulePath = './module.js'
// import { name } from modulePath // 报错
// console.log(name)
// if (true) { // 不能嵌套在if中 报错
// import { name } from './module.js'
// }
// import('./module.js').then(function (module) { // 动态导入的形式
// console.log(module)
// })
// ----------------
// import { name, age, default as title } from './module.js' // 导出默认成员
import title, { name, age } from './module.js'// 导出默认成员
console.log(name, age, title)
//变量
let moduleName = 'hello'
//函数
let f1 = function (msg) {
console.log(msg)
}
export var name2 = 3
export { moduleName, f1 }
import {moduleName as a,f1,name2} from './module.js'
console.log(a) //可以使用別名
f1("hello function")
setTimeout(()=>{
console.log(a) //输出的是hi,是修改过的值
},1300)
a = '234' //导出之后不可进行修改
使用别名
//module.js
let moduleName = 'hello'
export { moduleName }
//app.js
import {moduleName as a } from './module.js'
console.log(a) // hello
类的导出
//module.js
export class Point{
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
//app.js
import {Point} from './module.js'
let points = new Point(1,3)
console.log(points) // Point {x: 1, y: 3}
export default { }
//module.js
let num1 = 1
let num2 = 2
// default后是一个对象
export default {num1,num2}
//app.js
import module from './module.js'
console.log(module.num1)
注意事项
- export { } 是一个固定用法,并非导出了一个对象
- 导出的值是引用关系,是只读成员,不可进行修改
- 导入的时候必须使用完整的名称,不可省略扩展名
导入用法
- 导入时,可以使用绝对路径,相对路径如果是同目录必须使用‘./’,不可省略
- 只加载不提取,可以使用:
import 'module.js'
或者import {} from 'module.js'
- 全部导入,使用:
import * as mod from 'module.js'
- import 关键词只能出现在最外层的作用域
- 动态加载
import('./module.js').then(function(module){
console.log(module)
})
六、ES Modules 浏览器环境Polyfill
针对IE不兼容情况
script标签使用nomodule属性,作用是兼容ES Modules的浏览器不再做加载
七、ES Modules in Node.js - 与 Common.js
运行node --experimental-modules es-module.mjs
- ESModules中可以导入CommentJs模块
- commentJs不可以导入ESModule模块
- CommentJs始终只导出一个默认成员
- 注意 import 不是结构导出对象
新版本node 对ES Modules支持
在package.json里面增加 属性
{
"type":"module"
}
这时就可以以正常的js文件去运行ES Module
如果想使用commonJS把文件名改成.cjs
ES Module中使用commonJS