持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
核心描述
- 模块化定义:将复杂的程序根据一定的规则或规范,封装成几个模块或文件,并组合在一起。其中模块的内部数据与实现是私有的,只是向外部暴露一些接口或方法与外部其它模块通信。
- 模块化的好处:
- 避免命名冲突(减少命名空间污染,私有变量)
- 更好的分离,按需加载
- 提高代码复用性
- 提高代码可维护性
JS 经过不断的发展,模块化规范与实现也逐步趋于稳定,对于实际应用而言,我们只需要掌握 ES6 的模块化规范(ESM),以及 Nodejs 中的 CommonJS 规范即可。
- ES6 模块化(ESM)
- 描述:ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
- 浏览器端应用
- 通过 import 引入模块文件,通过 export 、export default 导出模块文件
- 项目中的实际应用,项目需要在发布前,通过 babel 、webpack 等构建工具将 ES6 的语法打包成 ES5 的语法,让浏览器正常运行
- 浏览器中也支持原生的 import/export、export default 的语法,只是有兼容性问题,而且对大型项目支持还不够友好,使用示例如下
- html 文件
// html 文件,注意 script 中的 type 需要设置为 module <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <h1>module test</h1> <script type="module" src="./index.js"></script> </body> </html>
- index.js 文件
import modue1 from "./modue1.js"; console.log(modue1)
- modue1.js 文件
export default { a: 'hahahahaha' }
- 服务器端
- 当 Nodejs 版本小于 14.x 时,需要利用 babel 将 ES6 转换为 ES5 ,然后在 Nodejs 环境中执行
- 当 Nodejs 版本大于 14.x 时,可以通过以下方式使用 ES6
- 方式一:设置 package.json 的 type 为 module
- 方式二:可以用 .mjs 的后缀来书写 ES6 的代码文件
- CommonJS:Node 应用由模块组成,采用 CommonJS 模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
- 在服务器端,模块的加载是运行时同步加载的
- 在浏览器端,模块需要提前编译打包处理
- 其他注意点:
- 引入模块:require(xxx)
- 导出模块:module.exports xxx 或 exports.xxx
- export 导出的值的特点,见代码注释:
- node.js 文件
// 导出模块的文件 var myobj = { a:1, b:2 } var myTotal = 1 var myNum = 1 function addNum(){ myNum += 100 } module.exports = { myobj, myTotal, addNum, myNum }
- node1.js 文件
// 引用模块 node,同时导出可以操作模块 node 中变量的方法 var obj = require('./node') function add(){ obj.myobj.a += 2; } function addTotal(){ obj.myTotal +=10 } module.exports = { add, addTotal }
- index.js 文件
// 分别引入模块 node 和模块 node1 var node1 = require('./node1') var node = require('./node') // 打印初始值 console.log(node.myobj,node.myTotal,node.myNum) // 调用模块 node1 中的方法,用于改变模块 node 的引用类型的变量值 node1.add() // 调用模块 node1 中的方法,用于改变模块 node 的值类型的变量值 node1.addTotal() // 调用模块 node 中的方法,用于改变模块 node 的值类型的变量 node.addNum() console.log(node.myobj,node.myTotal,node.myNum)
- 结论:
- 在 Nodejs 中,如果一个引用类型的变量被多个模块引用,且改变其值,则该变量的值会改变
- 如果一个值类型的变量被多个模块引用,且改变值,则该变量的值会改变
- 如果一个值类型的变量被单个模块引用,且改变值,则该变量的值并不会被改变
- 如果一个引用类型的变量被单个模块引用,且改变值,则该变量的值仍然不会被改变(可以自己尝试一下)
知识拓展
- 没有模块化规范之前,在项目中可以使用如下方式进行模块化的实现:
- 全局 function 模式 : 将不同的功能封装成不同的全局函数
- namespace 模式 : 简单对象封装
- 立即执行函数(IIFE)模式:匿名函数自调用(闭包)
- Object 对象 + prototype 原型拓展
- 演变过程中的模块化规范
- AMD:
- 采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。推崇依赖前置
- RequireJS 是 AMD 规范最热门的实现
- CMD:
- CMD 是通用模块加载,要解决的问题与 AMD 一样,只不过是对依赖模块的执行时机不同 ,推崇就近依赖
- Sea.js 是 CMD 规范的一个实现代表库
- UMD:
- UMD是AMD和CommonJS的糅合
- 核心实现:
- 先判断是否支持Node.js模块(exports是否存在),存在则使用Node.js模块模式
- 再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
- 前两个都不存在,则将模块公开到全局(window或global)。
- 代码示例:
// 在很多开源库中,都会有类似的支持逻辑 (function (window, factory) { if (typeof exports === 'object') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define([],factory); } else { window.eventUtil = factory(); } })(this, function () { return {}; })
- AMD:
- 个人理解:随着 JS 的不断发展,我们没有必要再去深入研究曾经的模块化方案,但是我们有必要对齐有一个概览的认识,当我们遇到类似的祖传项目时,往往可以方便我们去定位、解决问题。同时也应该将精力放到更成熟的新的规范中去,就目前而言,浏览器方向的 ES6(ESM) 的
import xxx from xxx
和export xxx
、export default xxx
的组合,以及 Nodejs 方向的require(xxx)
和modules.export
、exports.xxx
的组合,才是我们项目中最应该掌握的。
参考资料
- 前端模块化详解(完整版):segmentfault.com/a/119000001…
- Node.js 如何处理 ES6 模块:www.ruanyifeng.com/blog/2020/0…
- JavaScript 模块:developer.mozilla.org/zh-CN/docs/…
- 一文彻底搞懂JS前端5大模块化规范及其区别:一文彻底搞懂JS前端5大模块化规范及其区别
浏览知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。