前言
模块化开发有助于我们将代码进行拆分,便于开发和维护,但如果不清楚模块化规范,就会在开发时不知道该用 require
还是 import
,导出时该用 export
还是 module.exports
,所以我们必须搞清除它们的区别和事情的来龙去脉。
本篇是我的学习记录,主要内容是 CommonJS
和 ES Module
规范。其它还有 AMD
、CMD
、UMD
规范,感兴趣的小伙伴可以自行了解一下。
什么是前端模块化
随着前端项目越做越大,功能越来越多,我们不能把所有代码写在一个 js 中,而是把代码按照不同的功能进行划分,但是代码越来越多,代码之间的引用嵌套越来越深,我们又不得不花费大量时间去管理和维护,如何提高代码的管理效率?就是通过模块化。
模块化不但是一种代码组织形式,也是一种思想,我们根据代码的不同功能,来划分不同模块,目的是方便管理代码,从而提升开发效率。
模块化的演进过程
模块化规范不是一夜之间突然出现,而是像时代一样,有着演进过程:
- 石器时代:我们通过
script
标签引入js
文件,并且约定,一个文件代表一个模块,这种方式很好理解,但存在很多问题- 缺少私有空间,也就是模块内部成员可以在外部被修改
- 所有模块作用在全局,容易发生命名冲突,变量污染
- 无法管理模块的依赖关系,如果引用顺序出错,程序将难以运行
- 青铜器时代:使用命名空间模式,就是给每一个模块暴露出一个对象,把模块内的所有成员挂载到这个对象下,这就有点模块化内味儿了,但还是无法解决私有空间的问题,模块成员在外部依然可以被修改
- 蒸汽机时代:使用 IIFE(立即调用函数表达式)提供私有作用域,当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问,也就是形成了闭包,然后再将对象暴露出去,挂载到全局
// 蒸汽机时代:使用 IIFE 提供私有作用域的方式
;(function(){
// 通过闭包,避免私有成员被外部修改
var msg = 'hello world'
function method(){
console.log(msg)
}
// 挂载到全局
window.module = {
method:method
}
})()
通过 IIFE 我们解决了私有作用域的问题,却无法解决
script
标签引入的问题,当index.html
中引入了十几个script
标签,还要维护他们的引入顺序时,那是相当痛苦的。
- 开源时代,百家争鸣。百花齐放,
JavaScript
社区孕育出了CommonJS
规范。
回顾CommonJS和ES Module使用方式
先回顾下 CommonJS
和 ES Module
常用的方式,加深下印象:
CommonJS
导入导出require
module.exports
exports
ES Module
导入导出import
export
export default
把他们归类一下,就好区分了,其中 import
还有几种特殊用法,接着往下看。
CommonJS 规范
CommonJS
首先帮我们解决了 script
标签引入的问题,只需要提供一个 script
标签作为入口文件,模块之间的引用可以交给 CommonJS
。
我们来快速了解一下 CommonJS
规范:
CommonJS
源自社区CommonJS
的出现早于ES Module
规范CommonJS
被大量使用在node.js
中- 使用
module.exports
导出模块,使用require
导入模块 exports
也可以导出模块,它的本质还是引用了module.exports
CommonJS
是同步加载模块,这点与ES Module
不同
CommonJS 导出
可以导出任意类型
// module.js
module.exports = {
name:'banana',
age:18,
eat:function(){
console.log('I like eating bananas')
}
}
module.exports.userName = 'admin'
CommonJS 导入
// app.js
const obj = require('./module.js')
console.log(obj) // { name: 'banana', age: 18, eat: [Function: eat], userName: 'admin' }
// 如果只想导入某个属性,可以使用解构赋值
const { name } = require('./module')
console.log(name) // 'banana'
CommonJS 不适用于浏览器
因为 CommonJS
是同步加载模块,而加载模块就是去服务端获取模块,加载速度会受网络影响,假如一个模块加载很慢,后面的程序就无法执行,页面就会假死。而服务端能够使用 CommonJS
的原因是代码本身就存储于服务器,加载模块就是读取磁盘文件,这个过程会快很多,不用担心阻塞的问题。
所以浏览器加载模块只能使用异步加载,这就是 AMD
规范的诞生背景。
ES Module 规范
CommonJS
虽然很好,但是不适用于浏览器,于是 ES Module
应运而生。
再来了解一下 ES Module
:
ES Module
是ES6
之后新增的模块化规范,它从Javascript
本身的语言层面,实现了模块化ES Module
想要完成浏览器端、服务端的模块化大一统,成为通用解决方案- 使用
export
导出模块,使用import
导入模块 - 通过
as
关键词,对导出对象重命名,也可以通过as
对导入对象重命名
ES Module 导出
可以导出任意类型
// module.js
const obj = {
name:'banana',
age:18,
eat:()=>{
console.log('I like eating bananas')
}
}
const userName = 'admin'
export { obj,userName }
ES Module 导入
// app.js
import { obj,userName } from './module.js'
通过 as 重命名导出
// module.js
const userName = 'admin'
const passWorld = '密码是我生日'
export {
userName as name,
passWorld as pass
}
// app.js
import { name,pass } from './module.js'
default 默认导出
默认导出一个成员
// module.js
const name = 'banana'
export default name
// app.js
import newName from './module.js' // 此时可以用新的变量名接收
默认导出多个成员
// module.js
export default {
name:'banana',
age:18,
eat:()=>{
console.log('I like eating bananas')
}
}
// app.js
import handle from './module.js'
console.log(handle.name) // banana
handle.eat() // I like eating bananas
ES Module 注意点
- 当我们只想运行模块,而不是获取其中变量时,可以这么写
import './module.js'
- 需要导出大量成员时,可以用一个变量来接收
export { name,age,address,tel,gender,...... } // 导出了很多的成员
import * as obj from './module.js' // 使用 obj 来接收
- 同时导出命名成员和默认成员
const name='banana',age=18;
export { name,age }
export default 'default value'
import { name, age, default as title } from './module.js' // 此时默认成员需要用 default as 来接收
import title, { name, age } from './module.js' // 简写的方式,将默认成员放在最前面
使用 ES Module 执行 JS 代码
- 通过给
script
标签添加type="module"
属性,可以用ES Module
的标准来执行JS
代码 - 使用
ES Module
的JS
,会延迟执行,有点类似于defer
属性
<!-- 默认使用严格模式 -->
<script type="module">
console.log(this) //undefined
</script>
<!-- 每个 ES Modules 都是一个私有作用域 -->
<script type="module">
const name = 'banana'
</script>
<script type="module">
console.log(name) //undefined
</script>
<!-- 外部文件是通过 CORS 的方式请求的,需要后端加请求头,否则无法加载 js -->
<script type="module" src="http://www.baidu.com" /> // 报错
<!-- ESM 的 script 标签会延迟执行,当 html 加载完毕后,再执行 script,相当于添加了 defer 属性 -->
<script>
// 阻塞下面的 p 标签显示
alert('hello')
</script>
<p>内容1</p>
<script type="module">
// 不会阻塞下面的 p 标签显示
alert('hello')
</script>
<p>内容2</p>
node 对 ES Module 的支持
node@8.0
之前的版本还不支持 ES Module
,不过可以通过 babel 来解决
// 安装 babel 插件
yarn add @babel/node @babel/core @babel/preset-env -D
// 运行babel
yarn babel-node
// 运行文件和插件
yarn babel-node index.js --persets=@babel/preset-env
以上就是我的学习记录,其中大部分内容来自网上优秀文章的讲解,并加入个人理解总结,如果有错误的地方希望大佬指正,如果对你有帮助,请帮忙点个赞