模块化开发有助于我们将代码进行拆分,便于开发和维护,但如果不清楚模块化规范,就会在开发时不知道该用 require 还是 import,导出时该用 export 还是 module.exports,所以我们必须搞清除它们的区别和事情的来龙去脉。
本篇主要内容是 CommonJS 和 ES Module 规范。其它还有 AMD、CMD、UMD规范,感兴趣的小伙伴可以自行了解一下。
随着前端项目越做越大,功能越来越多,我们不能把所有代码写在一个 js 中,而是把代码按照不同的功能进行划分,但是代码越来越多,代码之间的引用嵌套越来越深,我们又不得不花费大量时间去管理和维护,如何提高代码的管理效率?就是通过模块化。
模块化不但是一种代码组织形式,也是一种思想,我们根据代码的不同功能,来划分不同模块,目的是方便管理代码,从而提升开发效率。
模块化规范不是一夜之间突然出现,而是像时代一样,有着演进过程:
script 标签引入 js 文件,并且约定,一个文件代表一个模块,这种方式很好理解,但存在很多问题// 蒸汽机时代:使用 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导入导出
requiremodule.exportsexportsES Module导入导出
importexportexport defaultimport 还有几种特殊用法,接着往下看。CommonJS 首先帮我们解决了 script 标签引入的问题,只需要提供一个 script 标签作为入口文件,模块之间的引用可以交给 CommonJS。
我们来快速了解一下 CommonJS 规范:
CommonJS 源自社区CommonJS 的出现早于 ES Module 规范CommonJS 被大量使用在 node.js 中module.exports 导出模块,使用 require 导入模块exports 也可以导出模块,它的本质还是引用了 module.exportsCommonJS 是同步加载模块,这点与 ES Module 不同可以导出任意类型
// module.js
module.exports = {
name:'banana',
age:18,
eat:function(){
console.log('I like eating bananas')
}
}
module.exports.userName = 'admin'
// 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 的原因是代码本身就存储于服务器,加载模块就是读取磁盘文件,这个过程会快很多,不用担心阻塞的问题。
所以浏览器加载模块只能使用异步加载,这就是 AMD 规范的诞生背景。
CommonJS 虽然很好,但是不适用于浏览器,于是 ES Module 应运而生。
再来了解一下 ES Module:
ES Module 是 ES6 之后新增的模块化规范,它从 Javascript 本身的语言层面,实现了模块化ES Module 想要完成浏览器端、服务端的模块化大一统,成为通用解决方案export 导出模块,使用 import 导入模块as 关键词,对导出对象重命名,也可以通过 as 对导入对象重命名可以导出任意类型
// module.js
const obj = {
name:'banana',
age:18,
eat:()=>{
console.log('I like eating bananas')
}
}
const userName = 'admin'
export { obj,userName }
// app.js
import { obj,userName } from './module.js'
// module.js
const userName = 'admin'
const passWorld = '密码是我生日'
export {
userName as name,
passWorld as pass
}
// app.js
import { name,pass } from './module.js'
默认导出一个成员
// 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
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' // 简写的方式,将默认成员放在最前面
<script type="module">
console.log(this) //undefined
script>
<script type="module">
const name = 'banana'
script>
<script type="module">
console.log(name) //undefined
script>
<script type="module" src="http://www.baidu.com" /> // 报错
<!-- ESM 的 script 标签会延迟执行,当 html 加载完毕后,再执行 script,相当于添加了 defer 属性 -->
<script>
// 阻塞下面的 p 标签显示
alert('hello')
script>
<p>内容1p>
<script type="module">
// 不会阻塞下面的 p 标签显示
alert('hello')
script>
<p>内容2p>
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