开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
前言
模块化开发是前端开发中必定用到的开发方式,之前本人对其只是简单得使用,并没有详细得深入理解,本篇文章是我深入学习之后得一些归纳总结,提供给大家学习,如有写得不准确得地方,欢迎大家指出,相互学习,相互进步!
一. 什么是模块化
在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,因为最初的脚本又小又简单,只需要将JavaScript代码写到<script>标签中即可,所以没必要将其模块化。
但是随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了
-
ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过JavaScript进行前端页面的渲染; -
SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript来实现; -
包括
Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;
因此社区发明了许多种方法来将代码组织到模块中,使用特殊的库按需加载模块。 列举一些(出于历史原因):
- AMD : 最古老的模块系统之一,最初由 require.js 库实现。
- UMD : 另外一个模块系统,建议作为通用的模块系统,它与 AMD 和 CommonJS 都兼容。
- CMD : 是 SeaJS在推广过程中对模块定义的规范化产出
它们都在慢慢成为历史的一部分,但我们仍然可以在旧脚本中找到它们。接下来我们重点详细介绍CommonJS和ES 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
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("------")
}
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): 运行代码,计算值,并且将值填充到内存地址中。