携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情。
今天整理了一下 JavaScript 模块化的内容,与大家一起分享!
1. 什么是模块化
我们先来说一下项目中的模块化开发,那什么是模块化开发呢?模块化开发就是将程序划分成一个个小的结构,每个结构中有属于自己的逻辑代码和作用域,并且不会影响到其他结构。这个结构也可以导出自己的变量、函数和对象等给其他结构使用。这里所说的结构就是模块,按照这种结构划分开发程序的过程,就是模块化开发的过程。
2. 发展历程
我们必须承认,Brendan Eich 用了 10 天写出 JavaScript 的时候,它就不可避免地存在缺陷,比如 var 定义变量作用域以及没有模块化的问题等。
2.1 早期的 JavaScript
早期只是作为一门脚本语言,做一些简单的表单验证或动画实现等,后来随着前端和 JavaScript 的快速发展,JavaScript 代码变得越来越复杂了:
- (1)ajax 的出现,前后端开发分离,意味着后端返回数据后,我们需要通过 JavaScript 进行前端页面的渲染;
- (2)SPA 的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过 JavaScript 来实现;
- (3)包括 Node 的实现,JavaScript 编写复杂的后端程序,没有模块化是致命的硬伤
- (4)早期没有模块化带来了很多的问题:比如命名冲突的问题
解决方案:立即函数调用表达式。 因为函数是有作用域的,所以只需要将 JS 文件中的代码用一个函数包裹起来自调用,同时可以将变量或方法 return 出去让别的 JS 文件调用,这样就实现了不同 JS 文件中命名冲突的问题,但是这样写很混乱,并且需要记住每个 JS 文件返回对象的命名。
2.2 2015 年 ES6 出现之前:CommonJS
-
Node 中对 CommonJS 进行了支持和实现;在 Node 中每一个 JS 文件都是一个单独的模块,这些模块中包括 CommonJS 规范的核心变量:exports、module.exports、require
-
exports 和 module.exports:负责对模块中的内容进行导出
- exports:
- 本质上是一个对象,会在堆内存中开辟一个空间,用来存储导出的内容
- 想要导出谁,就将谁作为 exports 中的一个属性导出
- 每个 JS 文件都会有一个默认的 exports 对象,如果没有进行导出,那它就是一个空对象
- module.exports:
- 每一个 JS 文件就是一个 Module 实例
- 两者之间的联系:
- Module.exports = exports(源码中的操作),exports 实际上是 Module 类中的一个属性,他们都指向同一个内存地址,所以本质上是 Module.exports 在导出
- 一旦使用 module.exports = {},会新开辟一个内存空间,跟 exports 就没有关系了
- require:导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
- require() 本质上是一个函数,返回的就是 exports 对象。(严格意义上来说,require() 就是指向了 exports 对象的堆内存地址,相当于就是浅拷贝了一层(引用赋值))
- require 的查找规则: 参考:nodejs.org/dist/latest…
总结比较常见的查找规则: 导入格式如下:require(X)
情况一:X 是一个核心模块,比如 path、http ————>直接返回核心模块,并且停止查找
情况二:X 是以 ./ 或 ../ 或 /(根目录)开头的
- 第一步:将 X 当做一个文件在对应的目录下查找;
- 1.如果有后缀名,按照后缀名的格式查找对应的文件
- 2.如果没有后缀名,会按照如下顺序:直接查找文件 X —> 查找 X.js 文件 —> 查找 X.json 文件 —> 查找 X.node 文件
- 第二步:没有找到对应的文件,将 X 作为一个目录
- 查找目录下面的 index 文件 —> 查找 X/index.js 文件 —> 查找 X/index.json 文件 —> 查找X/index.node 文件
- 如果没有找到,那么报错:not found
情况三:直接是一个 X(没有路径),并且 X 不是一个核心模块
- /Users/coderwhy/Desktop/Node/TestCode/04_learn_node/0 5_javascript-module/02_commonjs/main.js中编写 require('why’)
如果上面的路径中都没有找到,那么报错:not found
- 模块的加载过程
- 结论一:模块在第一次被引入的时候,模块中的代码会加载一次(同步加载)
- 不用担心阻塞的问题,因为 NodeJS 常用于服务端开发,而资源往往都是存放在同一个服务器中的,可以直接获取到
- 结论二:如果一个模块被多个模块多次引用时,会缓存,并且最终只会(加载)执行一次
- 原因:每个模块对象 module 中都有一个属性:loaded,为 false 表示还没有加载,为 true 表示已经加载过了
- 结论三:如果有循环加载,那么最终加载的顺序是按照深度优先算法进行加载的
- CommonJS 的规范缺点: CommonJS 载模块是同步的,意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行,这个在服务器不会有什么问题,因为服务器加载的 js 文件都是本地文件,加载速度非常快。但是如果用于浏览器,浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行。一旦发生阻塞,就意味着后续的 js 代码都无法正常运行。所以在浏览器中,我们通常不使用 CommonJS 规范。但是借助于 webpack 等工具可以实现对 CommonJS 或者 ES Module 代码的转换。
2.3 AMD 和 CMD
也是模块化的两种规范,有兴趣可以了解一下,这里只做简单介绍。
-
AMD 主要是应用于浏览器的一种模块化规范
- AMD 是 Asynchronous Module Definition(异步模块定义)的缩写;
- 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;
- 它采用的是异步加载模块;
- AMD 实现的比较常用的库是 require.js 和 curl.js。
-
CMD 规范也是应用于浏览器的一种模块化规范:
- CMD 是 Common Module Definition(通用模块定义)的缩写;
- 它也采用了异步加载模块,但是它将 CommonJS 的优点吸收了过来;
- CMD 也有自己比较优秀的实现方案:SeaJS。
3. ES Module 的产生
JavaScript 没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD 等, 所以在 ES 推出自己的模块化系统时,大家也是兴奋异常。与 CommonJS 的不同之处在于:
- 使用 import 关键字从其他模块导入内容;
- 使用 export 关键字将模块中的内容导出;
- 使用的是关键字,意味着需要 JS 引擎来进行解析;
- ES Module 自动采用严格模式(MDN 上的解析:developer.mozilla.org/zh-CN/docs/… )
- ES Module 加载过程:加载 JS 文件的过程是在编译时(解析)时加载的,是异步的。异步意味着,JS 引擎在遇到 import 时会获取这个 JS 文件,获取的过程是异步的,并不会阻塞主线程继续执行。在引入的 script 标签中,需要设置 type="module",相当于是添加了 async。如果 script 标签后面还有普通的 script 标签以及对应的代码,那么 ESModule 对应的 JS 文件和代码不会阻塞他们的执行。
方式一:
导出:export const foo (){}
导入:import {} from “./路径”
方式二:
导出:export { 放置要导出的变量的引用列表,注意这不是一个对象 }
导入:import { FName as name } from "./路径" 修改导入变量的名称,as 后面的是修改后的名称
方式三:
导出:export { name as FName, age as FAge } 修改导出变量的名称,as 后面的是修改后的名称
导入:import * as foo from "./路径" 导入的内容作为 foo 的属性使用
方式四:
导出:export default const foo(){} 默认导出 (每个模块中只能有一个默认导出)
导入:默认导出的导入,可以自定义任何名称,import fooooo from "./路径"
import 和 export 的结合使用:export {} from “./路径”
使用场景:在开发和封装一个功能库的时候,通常希望将暴露的借口放到一个文件中。比如 A.js 中封装了三个方法,B.js 中封装了两个方法,在 index.js 中需要将这些方法在一个文件中全部暴露出去,就可以在 index.js 文件中这样使用
注意点:
- import 不能写在逻辑代码中,比如写在 if 语句中,因为 ES Module 被 JS 引擎解析时就必须知道它的依赖关系(在解析成 AST 语法树时就需要已经确定依赖关系);
- 确实有需要动态导入的情况存在,这个时候可以使用动态导入。如果是在 webpack 环境下,使用函数 require();如果实在纯 ES Module 环境下,使用函数 import(),进行异步加载,返回的是一个 Promise 在 .then() 中就可以获取到导入的内容,这步操作,到时候 webpack 打包时会单独打包,进行分包优化,会加快首屏的渲染时间。
4. CommonJS 和 ES Module 的交互
-
结论一:通常情况下,CommonJS 不能加载 ES Module
- CommonJS 是同步加载的,使用的是 require(),本质上是函数,所以在代码运行阶段;
- ES Module 需要先在 parse 阶段进行解析(静态分析),然后才能加载 js 代码;
- 在 Node 中是不支持的,但是如果在其他平台先解析了,也可能会支持
-
结论二:多数情况下,ES Module 是可以加载 CommonJS 的