前端的导入导出:「CommonJS」「ES Module」模块化规范

20,342 阅读6分钟

前言

模块化开发有助于我们将代码进行拆分,便于开发和维护,但如果不清楚模块化规范,就会在开发时不知道该用 require 还是 import,导出时该用 export 还是 module.exports,所以我们必须搞清除它们的区别和事情的来龙去脉。

本篇是我的学习记录,主要内容是 CommonJSES Module 规范。其它还有 AMDCMDUMD规范,感兴趣的小伙伴可以自行了解一下。

什么是前端模块化

随着前端项目越做越大,功能越来越多,我们不能把所有代码写在一个 js 中,而是把代码按照不同的功能进行划分,但是代码越来越多,代码之间的引用嵌套越来越深,我们又不得不花费大量时间去管理和维护,如何提高代码的管理效率?就是通过模块化。

模块化不但是一种代码组织形式,也是一种思想,我们根据代码的不同功能,来划分不同模块,目的是方便管理代码,从而提升开发效率。

模块化的演进过程

模块化规范不是一夜之间突然出现,而是像时代一样,有着演进过程:

  1. 石器时代:我们通过 script 标签引入 js 文件,并且约定,一个文件代表一个模块,这种方式很好理解,但存在很多问题
    • 缺少私有空间,也就是模块内部成员可以在外部被修改
    • 所有模块作用在全局,容易发生命名冲突,变量污染
    • 无法管理模块的依赖关系,如果引用顺序出错,程序将难以运行
  2. 青铜器时代:使用命名空间模式,就是给每一个模块暴露出一个对象,把模块内的所有成员挂载到这个对象下,这就有点模块化内味儿了,但还是无法解决私有空间的问题,模块成员在外部依然可以被修改
  3. 蒸汽机时代:使用 IIFE(立即调用函数表达式)提供私有作用域,当函数变成立即执行的函数表达式时,表达式中的变量不能从外部访问,也就是形成了闭包,然后再将对象暴露出去,挂载到全局
// 蒸汽机时代:使用 IIFE 提供私有作用域的方式
;(function(){
  // 通过闭包,避免私有成员被外部修改
  var msg = 'hello world' 
  function method(){ 
      console.log(msg)
  }
  // 挂载到全局
  window.module = {
      method:method
  }
})()

通过 IIFE 我们解决了私有作用域的问题,却无法解决 script 标签引入的问题,当 index.html 中引入了十几个 script 标签,还要维护他们的引入顺序时,那是相当痛苦的。

  1. 开源时代,百家争鸣。百花齐放,JavaScript 社区孕育出了 CommonJS 规范。

回顾CommonJS和ES Module使用方式

先回顾下 CommonJSES 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 ModuleES6 之后新增的模块化规范,它从 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 ModuleJS,会延迟执行,有点类似于 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

以上就是我的学习记录,其中大部分内容来自网上优秀文章的讲解,并加入个人理解总结,如果有错误的地方希望大佬指正,如果对你有帮助,请帮忙点个赞