问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

详解Js中的模块化是如何实现的

发布网友 发布时间:2023-08-05 10:23

我来回答

1个回答

热心网友 时间:2023-11-04 08:49



由于 Js 起初定位的原因(刚开始没想到会应用在过于复杂的场景),所以它本身并没有提供模块系统,随着应用的复杂化,模块化成为了一个必须解决的问题。本着菲麦深入原理的原则,很有必要来揭开模块化的面纱


一、模块化需要解决的问题

要对一个东西进行深入的剖析,有必要带着目的去看。模块化所要解决的问题可以用一句话概括

在没有全局污染的情况下,更好的组织项目代码


举一个简单的栗子,我们现在有如下的代码:

function doSomething () {
const a = 10;
const b = 11;
const add = function (a + b) {
return a + b
}
add (a + b)
}
在现实的应用场景中,doSomething 可能需要做很多很多的事情,add 函数可能也更为复杂,并且可以复用,那么我们希望可以将 add 函数独立到一个单独的文件中,于是:

// doSomething.js 文件
const add = require('add.js');
const a = 10;
const b = 11;
add(a+ b);
// add.js 文件
function add (a, b) {
return a + b;
}
mole.exports = add;
这样做的目的显而易见,更好的组织项目代码,注意到两个文件中的 require 和 mole.exports,从现在的上帝视角来看,这出自 CommonJS 规范(后文会有一个章节来专门讲规范)中的关键字,分别代表导入和导出,抛开规范而言,这其实是我们模块化之路上需要解决的问题。另外,虽然 add 模块需要得到复用,但是我们并不希望在引入 add 的时候造成全局污染

二、引入的模块如何运行

在上述的例子中,我们已经将代码拆分到了两个模块文件当中,在不造成全局污染的情况下,如何实现 require,才能使得例子中的代码做到正常运行呢?

先不考虑模块文件代码的载入过程,假设 require 已经可以从模块文件中读取到代码字符串,那么 require 可以这样实现

function require (path) {
// lode 方法读取 path 对应的文件模块的代码字符串
// let code = load(path);
// 不考虑 load 的过程,直接获得模块 add 代码字符串
let code = 'function add(a, b) {return a+b}; mole.exports = add';
// 封装成闭包
code = `(function(mole) {$[code]})(context)`
// 相当于 exports,用于导出对象
let context = {};
// 运行代码,使得结果影响到 context
const run = new Function('context', code);
run(context, code);
//返回导出的结果
return context.exports;
}
这有几个要点:


1) 为了不造成全局污染,需要将代码字符串封装成闭包的形式,并且导出关键字 mole.exports ,mole 是与外界联系的唯一载体,需要作为闭包匿名函数的入参,与引用方传入的上下文 context 进行关联


2) 使用 new Function 来执行代码字符串,估计大部分同学对 new Function 是不熟悉的,因为一般情况下定义一个函数无需如此,要知道,用 Function 类可以直接创建函数,语法如下:

var function_name = new function(arg1, arg2, ..., argN, function_body)
在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。也就是说,可以使用它来执行字符串代码,类似于 eval,并且相比 eval, 还可以通过参数的形式传入字符串代码中的某些变量的值


3)如果曾经你有疑惑过为什么规范的导出关键字只有 exports 而我们实际使用过程中却要使用mole.exports(写过 Node 代码的应该不会陌生),那在这段代码中就可以找到答案了,如果只用 exports 来接收 context,那么对 exports 的重新赋值对 context 不会有任何影响(参数的地址传递),不信将代码改成如下形式再跑一跑:



演示结果

三、代码载入方式

解决了代码的运行问题,还需要解决模块文件代码的载入问题,根据上述实例,我们的目标是将模块文件代码以字符串的形式载入

在 Node 容器,所有的模块文件都在本地,只需要从本地磁盘读取模块文件载入字符串代码,再走上述的流程就可以了。事实证明,Node 非内建、核心、c++ 模块的载入执行方式大体如此(虽然使用的不是 new Function,但也是一个类似的方法)

在 RN/Weex 容器,要载入一个远程 bundle.js,可以通过 Native 的能力请求一个远程的 js 文件,再读取成字符串代码载入即可(按照这个逻辑,Node 读取一个远程的 js 模块好像也无不可,虽然大多数情况下我们不需要这么做)

在浏览器环境,所有的 Js 模块都需要远程读取,尴尬的是,受限于浏览器提供的能力,并不能通过 ajax 以文件流的形式将远程的 js 文件直接读取为字符串代码。前提条件无法达成,上述运行策略便行不通,只能另辟蹊径

这就是为什么有了 CommonJs 规范了,为什么还会出现 AMD/CMD 规范的原因


那么浏览器上是怎么做的呢?在浏览器中通过 Js 控制动态的载入一个远程的 Js 模块文件,需要动态的插入一个 <script> 节点:

// 摘抄自 require.js 的一段代码
var node = config.xhtml ?
document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true;
node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremole', moleName);
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
要知道,设置了 <script> 标签的 src 之后,代码一旦下载完成,就会立即执行,根本由不得你再封装成闭包,所以文件模块需要在定义之初就要做文章,这就是我们说熟知的 AMD/CMD 规范中的 define,开篇的 add.js 需要重新改写一下

// add.js 文件
define ('add',function () {
function add (a, b) {
return a + b;
}
return add;
})
而对于 define 的实现,最重要的就是将 callback 的执行结果注册到 context 的一个模块数组中:

context.moles = {}
function define(name, callback) {
context.moles[name] = callback && callback()
}
于是 require 就可以从 context.moles 中根据模块名载入模块了,是不是有了一种自己去写一个 “requirejs” 的冲动感

具体的 AMD 实现当然还会复杂很多,还需要控制模块载入时序、模块依赖等等,但是了解了这其中的灵魂,想必去精读 require.js 的源码也不是一件困难的事情

四、Webpack 中的模块化

Webpack 也可以配置异步模块,当配置为异步模块的时候,在浏览器环境同样的是基于动态插入 <script> 的方式载入远程模块。在大多数情况下,模块的载入方式都是类似于 Node 的本地磁盘同步载入的方式

_忘记,Webpack 除了有模块化的能力,还是一个在辅助完善开发工作流的工具,也就是说,Webpack 的模块化是在开发阶段的完成的,使用 Webpack 构筑的工作环境,在开发阶段虽然是独立的模块文件,但是在运行时,却是一个合并好的文件

所以 Webpack 是一种在非运行时的模块化方案(基于 CommonJs),只有在配置了异步模块的时候对异步模块的加载才是运行时的(基于 AMD)

五、模块化规范

通用的问题在解决的过程中总会形成规范,上文已经多次提到 CommonJs、AMD、CMD,有必要花点篇幅来讲一讲规范


Js 的模块化规范的萌发于将 Js 扩展到后端的想法,要使得 Js 具备类似于 Python、Ruby 和 Java 那样具备开发大型应用的基础能力,模块化规范是必不可少的。CommonJS 规范的提出,为Js 制定了一个美好愿景,希望 Js 能在任何地方运行,包括但不限于:

服务器端 Js 应用
命令行工具
桌面应用
混合应用
CommonJS 对模块的定义并不复杂,主要分为模块引用、模块定义和模块标识

模块引用:使用 require 方法来引入一个模块
模块定义:使用 exports 导出模块对象
模块标识:给 require 方法传入的参数,小驼峰命名的字符串、相对路径或者绝对路径


模块示意


CommonJs 规范在 Node 中大放异彩并且相互促进,但是在浏览器端,鉴于网络的原因,同步的方式加载模块显然不太实用,在经过一段争执之后,AMD 规范最终在前端场景中胜出(全称 Asynchronous Mole Definition,即“异步模块定义”)

什么是 AMD,为什么需要 AMD ?在前述模块化实现的推演过程中,你应该能够找到答案

除此之外还有国内玉伯提出的 CMD 规范,AMD 和 CMD 的差异主要是,前者需要在定义之初声明所有的依赖,后者可以在任意时机动态引入模块。CMD 更接近于 CommonJS

两种规范都需要从远程网络中载入模块,不同之处在于,前者是预加载,后者是延迟加载

五、总结

如果有心,可以参照本文的推演,来实现一个 “yourRequireJs”,没有什么比重复造轮子更能让知识沉淀~~

声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
自离后能补办离职证明吗? 我是家里自离的,可以补办辞职手续吗 急救!!!狗狗受凉吃什么就吐什么,该吃什么药 狗狗受凉呕吐吃什么药好得快 公告栏标题里的字如何设定字体及大小 公告栏怎移动啊 我的公告栏 如何设置 怎么让QQ拍拍公告栏文字不滚动 在滚动的公告栏中鼠标放上去时就停止,这种效果怎么做?谢谢了 王者荣耀铂金1和黄金3双排排到的是什么段位的 是药物的VB2好还是保健品的好 用英文简介学校,四年级水平的 本体和实体有什么区别 当归10克川贝6克丹参10克香附10克乌药10克牛夕10克红花6克益母草12克... 古代即兴起舞的故事 以舞相属和即兴起舞的区别 用cool edit拼接音乐时如何消除拼接点的连接痕迹? 为什么我的电脑用HDMI线连接电视机后,鼠标有迟钝现象?电视机是50寸的... 洋马110收割机 油茶是什么东西做的 鸡蛋吃法大全 以后每天做给家人吃 面食琉璃1球的做法 车位在防爆门旁好不好 因网络问题,连续两次申请了电信王卡,用一张,另一张不激活,会自动注销吗... 电信王卡没激活,那钱怎么办? 广州哪里有新书批发? 广州初一下册七科复习资料在哪买 罗胖数学上海合适吗 名侦探柯南通往天国的倒计时的下一集是什么 蛤蟆油发黄水洗在晾干可以吗 C/C++中能否实现在循环中连续声明变量,如下: C语言怎么在循环里面重新定义变量的值 c语言选择排序中为什么一层for循环中要定义变量k,直接把交换那一步... c语言可以在for里面同时定义变量吗? 关于C中for循环内定义的变量的生命周期 佳能cad相机开机响一下就黑屏了 最容易进监狱的专业 高考再见祝福语句 再见后面用祝福语对吗 八月再见祝福文案有哪些 酥香可口的茶香鸡翅应该如何做呢? 正时皮带断了的后果和维修费 炒菜芯的做法,炒菜芯怎么做好吃,炒菜芯的家常做法 美图t8幻影黑怎么卖 开车违法怎么举报 太阳能光伏发电的收益和补贴到底如何 户用光伏补贴下降了吗 中控智慧指纹考勤机哒哒响 泡发好的香茹能保存几天 这是哪部电影?叫什么名字?像是泰国的。