模块化开发之CommonJS和ES Module详解

312 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情

前言

模块化开发是前端开发中必定用到的开发方式,之前本人对其只是简单得使用,并没有详细得深入理解,本篇文章是我深入学习之后得一些归纳总结,提供给大家学习,如有写得不准确得地方,欢迎大家指出,相互学习,相互进步!

一. 什么是模块化

在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,因为最初的脚本又小又简单,只需要将JavaScript代码写到<script>标签中即可,所以没必要将其模块化。

但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了

  • ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过JavaScript进行前端页面的渲染;

  • SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript来实现;

  • 包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;

因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。 列举一些(出于历史原因):

  • AMD : 最古老的模块系统之一,最初由 require.js 库实现。
  • UMD : 另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容。
  • CMD : 是 SeaJS在推广过程中对模块定义的规范化产出

它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。接下来我们重点详细介绍CommonJSES Module

二. CommonJS

CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它 的广泛性,修改为CommonJS

Browserify是CommonJS在浏览器中的一种实现

webpack打包工具具备对CommonJS的支持和转换

Node是CommonJS在服务器端一个具有代表性的实现, 它为Node.js 服务器创建的模块化系统

1. 使用方式

Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

exports、module.exports :负责对模块中的内容进行导出

require函数 :导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

举例:

//foo.js
const name = "foo"
const age = 18
function sayHello() {
  console.log("sayHello")
}

// 1.在开发中使用的很少
exports.name = name
exports.age = age
exports.sayHello = sayHello
//exports.name = "哈哈哈哈1" 


// 2.将模块中内容导出
// 结论: Node导出的本质是在导出module.exports对象
module.exports.name = name
module.exports.age = age
module.exports.sayHello = sayHello
console.log(exports === module.exports) //true

// 3.开发中常见的写法
module.exports = {
  name,
  age,
  sayHello
}
// module.exports.name = "哈哈哈哈1"
//main.js
const foo = require("./foo.js")

console.log(foo.name)
console.log(foo.age)
foo.sayHello()

//require中也有一些node提供给内置模块
const path = require("path")
const http = require("http")

2. module.exports和exports之间的关系

module.exports:

CommonJS中是没有module.exports的概念的,但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module

因为module才是导出的真正实现者,所以在Node中真正用于导出的其实根本不是exports,而是module.exports

exports:

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出。而module对象的exports属性exports对象的一个引用module.exports = exports = main中foo

3. 模块的加载过程总结

  • 1: 模块在被第一次引入时,模块中的js代码会被运行一次
  • 2: 模块被多次引入时,会缓存,最终只加载(运行)一次

为什么只会加载运行一次呢?

每个模块对象module都有一个属性:loaded,为false表示还没有加载,为true表示已经加载

  • 3:加载顺序根据数据结构中的图结构,在遍历的过程中Node采用的是深度优先算法

提示:图结构:(有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search))

下图执行顺序为:main->a->b->c->d->e

image.png

4. CommonJS缺点

CommonJS加载模块是同步的:

同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;

这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;

如果将它应用于浏览器呢?

浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行;

那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;

所以在浏览器中,我们通常不使用CommonJS规范(webpack中使用除外

提示: 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD(现在用得很少了),现代浏览器中都使用ES Modules,可借助于webpack等工具可以实现对CommonJS或者ESModule代码的转换;

三. ES Module

ES Module自动采用严格模式:use strict,利用export和import关键字来实现模块化 在ES6的模块化开发中script中加入type="module"来实现:

<script src="main.js" type="module"></script>

注意:浏览器运行会报跨域错误:请在本地服务器测试

1.export 关键字导出方式

// 1.在语句声明的前面直接加上export关键字
export const age = 18


//2.将所有需要导出的标识符,放到export后面的 {}中
//这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
const age = 18
function sayHello() {
  console.log("sayHello")
}
export {
  age,
  sayHello
}

//3.导出时给标识符起一个别名
 export {
   age as age,
   sayHello
 }

2.import关键字导入方式

//1.import {标识符列表} from '模块'
//这里的{}也不是一个对象,里面只是存放导入的标识符列表内容
import { age, sayHello } from "./foo.js"

//2.导入时给标识符起别名
 import { age as ages, sayHello } from "./foo.js"

//3.通过 * 将模块功能放到一个模块功能对象上
import * as foo from "./foo.js"

注意:过import加载一个模块,是不可以将其放到逻辑代码中

举例:

let flag = true
if (flag) {
  //不允许在逻辑代码中编写import导入声明语法, 只能写到js代码顶层
  import { name, age, sayHello } from "./foo.js" 
  console.log(name, age)

  //如果确实是逻辑成立时, 才需要导入某个模块,可以定义一个变量接收返回的promise来解决
  const importPromise = import("./foo.js")
  importPromise.then(res => {
    console.log(res.name, res.age)
  })
  import("./foo.js").then(res => {
    console.log(res.name, res.age)
  })

  console.log("------")
}

新特性点击 import meta(在ES11(ES2020)中新增的特性)

3.export和import结合使用

//index.js
//一般用于多个export文件,统一在一个index文件导出,方便指定统一的接口规范,也方便阅读
//方式一
 export { age, sayHello } from './foo.js'
 export { formatCount, formatDate } from './format.js'

//方式二
 export * from './foo.js'
 export * from './format.js'

4.默认导出(default export)

//一个模块只能有一个默认导出
//方式一
export default function() {
  return 0
}

//方式二
 function foo() {
  return 0
}
export default foo

5.ES Module的解析流程

根据下方图简单描述流程:

构建(Construction): 根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)

实例化(Instantiation): 对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。

运行(Evaluation): 运行代码,计算值,并且将值填充到内存地址中。

image.png