专业编程教程与实战项目分享平台

网站首页 > 技术文章 正文

JS模块化 - 浅谈 CommonJS require 函数实现

ins518 2024-10-04 02:10:18 技术文章 13 ℃ 0 评论

最早的前端,没有模块加载规范,只能在HTML中通过<script>来引入js文件,同时无法区分函数来源于哪个js文件,而且要用过多全局变量。而随着前端工程复杂度的提升,使用这种方式已经无法满足日益增长的开发需求,js的模块化应运而生。

CommonJS 是属于 Node.js 的模块化方案,最早是叫 ServerJS,随着 Node.js 的火爆发展而成名的。Module1.0 规范在 Node.js 上实践的很好。

而 JavaScript 在当时(ES6 Modules 规范还未诞生)是没有模块化方案的,所以又更名从 CommonJS,想要统一服务端与客户端的模块加载方案。

但是,require 函数是同步的,在浏览器端由于网络的瓶颈而不适用。于是,AMD 和 CMD 的规范相继涌现,和 CommonJS 一起服务于 JavaScript 模块化。

而正是规则的不统一,这也是目前兼容方案 UMD 会出现的原因。

不过AMD 和 CMD 的浏览器端模块化,有很明显的问题:

  1. 导致浏览器端请求数过多;
  2. 受限于网络,所有模块都成功加载完只是一个承诺。

现如今,当打包这一环节被引入了前端工程化,CommonJS 以与服务端可以类库共用和 NPM(Node Package Manager) 这个后台的优势,成为了 es5 JavaScript 模块化的首选

简介

CommonJS 是一个旨在构建涵盖web服务器端、桌面应用、命令行app和浏览器JS的JS生态系统。

标准

CommonJS 的标准符合 module1.1 规范,暴露给使用者的有三个全局变量:

  1. require 是一个全局方法,用来加载模块
  2. exports 一个全局对象,用来导入模块的属性或方法
  3. module 一个全局对象。涵盖当前模块的必要信息,有一个只读的id属性,有一个uri属性,还有其它的一些命名规范,可以查看 CommonJS 规范的文档。

面向 require 这个切面

本文讲的是如何模拟一个 $require 函数,先来捋一捋 require 函数的主逻辑

require

require是一个基于AMD规范实现的函数,它区别于传统的CommonJS require规范。因为它能够异步地加载动态的依赖,所以,我们对基于require的回调方式有了更多的需求。

API Specification

局部require vs 全局require。局部require可以被解析成一个符合AMD工厂函数规范的require函数。

 define(['require'], function (require) {
 //the require in here is a local require.
 });
 define(function (require, exports, module) {
 //the require in here is a local require.
 });

局部require也支持其他标准实现的API。

全局require函数作用于全局,和define()类似。 全局require和局部require有着相同的行为,包含以下特征:

  • 模块ID应该认为是一个绝对的模块名称,而不是相对另一个模块的ID。
  • 只有在异步的时候,才可以使用require(id, callback?)的回调形式。因为异步加载模块的方式是先发出一个异步请求,然后等主线程代码段执行完毕才能进行异步回调来处理加载好的模块。

实际中,我们经常会遇到一些阻塞模块加载的依赖,如果交互次数很多,需要大量的模块加载,应该采用全局依赖的形式去加载顶层模块。

require(String)

基于以下规范CommonJS Modules 1.1.1 require.根据参数,同步地返回模块ID所代表的模块。

如果模块没有加载或者执行完成,就会抛出错误。特别需要指出的是,在同步加载的回调中,如果模块没有加载完成,禁止动态的获取模块,否则,就会抛出异常。

使用define()定义模块时,依赖项中可以找到一个AMD模块:

 define(function (require) {
 var a = require('a');
 });

工厂方法可以被解析成require('')的调用形式(例如,使用语法解析器或者使用Function.prototype.toString()或者正则表达式)去找到依赖,加载并且执行依赖,然后执行工厂方法内部代码,通过这样的方式,就可以获取到模块。

require(Array, Function)

参数Array是一个由模块ID组成的数组。当模块ID所以代表的模块加载完成且可用时,回调函数Function才开始执行,并且只被执行一次。各个模块按照依赖数组中的位置顺序以参数的形式传入到Function里。

例如:

 define(function (require) {
 require(['a', 'b'], function (a, b) {
 //modules a and b are now available for use.
 });
 }); 

require.toUrl(String)

将形如**[module ID] + '.extension'**这种字符形式转化成URL路径。

require.toUrl()方法采用通用的模块ID路径转化规则,将模块ID字符解析成URL路径.但它不支持以".js"这种扩展形式。所以,我们必须将'.extension'添加到了解析路径里。

例如:

 //cart.js contents:
 define (function(require) {
 // 模块ID名 './templates/a'
 // 扩展名 '.html'
 // 模板路径大致以这样的形式结尾 'modules/cart/templates/a.html'
 var templatePath = require.toUrl('./templates/a.html');
 });

以上是require的具体用法展示。

根据这个逻辑,我们先写一个 main 函数,以及定义一些需要的接口

至此已经有一个不可执行的 require 函数了,逻辑中会依次判断是否有缓存,是否核心模块,加载相应文件以及加载执行模块并缓存。

可以看到没有具体实现,只有接口被定义出来,这种编码方式,同样可以借鉴到在其他的开发需求中:

  1. 在开始编码前,进行尽可能合理的功能模块划分,可以让代码逻辑清晰,减少重复步骤 (DRY),并增强后期的代码可维护性
  2. 定义你需要哪些接口。如果是比较复杂的功能,且不是独立开发的话,这一环节做的好坏,合理地划分与合理地分配,决定团队合作开发是否可以配合恰当

逐一实现接口

这一步骤,主要是对上述过程需要的接口进行实现。

思想上是一个分而治之的思想,实现一个很复杂的东西比较困难,但是实现具体的功能要求,且在一定输入输出的限制下,每个人都能轻易的写出符合需求的算法,并进行调优。

数据结构与工具函数类

栈 -- 存储当前模块的所在目录

function Stack(...args) {
 this._stack = new Array(...args);
}
Stack.prototype = {
 top: function () {
 return this._stack .slice(-1)[0]
 },
 push: function (...args) {
 this._stack.push(...args)
 },
 pop: function () {
 this._stack.pop()
 },
 constructor: Stack
}

这个栈的作用是存放当前模块的所在目录,用于模块内 require 函数传入相对路径时,解析成绝对路径

获取文件所在目录

function getParent(pathname) {
 return path.parse(pathname).dir
}

具体的模块文件查找逻辑

检测模块类型与定位包的位置

这个函数要做下面的事情

  1. 检测模块类型:绝对路径,相对路径 或是 在 node_modules 内
  2. 如果是模块,则需要就近寻找 node_modules 有无这个模块,并且读取 pacakge.json 的 main属性

以下部分,是 getModuleLocation 自身需要实现的接口,往往是开发过程中自行提炼的,其他模块不通用

定位引用的真实路径

  1. 如果是目录,则添加'/index'后缀
  2. 对 '.js','.node', '.json' 可能有的后缀省略进行补齐

_require 函数

这个函数是 CommonJS 模块化的核心体现,理解这个函数,对 module 和 exports 的实际使用也会有帮助

  1. Node.js 的模块化实质是为每个模块的 js 包裹一层 'function module_exports(){}' 用以隔离作用域;
  2. module / exports / require 被作为传参而传入,而 exports 实质是 module.exports 的引用
  3. __dirname / __filename ,其实是这个模块内被先于执行函数定义的变量
  4. JS new Function 的特性常用来动态定义和执行某个方法,这边也同样用来执行模块

以为到这里就完成了吗,不,至少还有一些问题需要考虑:

  1. 如果出现异步 require 的情况,由于当前模块已经执行完,会清空存储模块目录的 stack ,会出现相对路径查找失败的问题,如何解决?
  2. 当发生循环依赖的时候,CommonJS 内部的加载流程是是否会陷入死循环,如果不会那会带来什么其他影响?

链接文章

https://github.com/kaola-fed/blog/issues/16

https://github.com/amdjs/amdjs-api/wiki/require-(中文版)

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表