js require 导入文件_深入理解前端模块加载机制,手写 node.js 的 require 函数

本文介绍了JavaScript模块化的演进,从CommonJS规范到Node.js中的核心模块如fs、path和vm。详细阐述了require()的实现过程,包括模块解析、缓存、加载和执行,并探讨了module.exports与exports的关系以及模块查找方式。同时,文章还涉及到手写require()源码的实践。
摘要由CSDN通过智能技术生成

06891f671e7dbd3850fd5705a4b50bc5.png

1. 模块化的演进

模块化特点的特点是什么? 防止变量污染

在以前的开发中,如果不用模块化,我们无法处理依赖关系,所以代码很难维护

那最早的时候我们是使用 单例 的方式解决,但是有个缺点,我们不能保证变量的唯一性,而且单例可能导致调用时浮躁,命名过长等问题, 如下方代码所示:

const a = {
  m: 'test',
  get: function() {
    ...
  },
  ...
}

const a1234455 = {
  ....
}

当然,我们还有用到了 IFFE(立即执行函数)来处理这个问题

(function(a){
  console.log('ddd', a);
  .....
})()

再近点,就出现了一些解决模块化的第三方库,下面两种,在现在未前后端分离的项目(也就是 mvc 模式下 jquery 项目)还是会见到的。

  • seajs库 cmd(就近依赖,只有在用到某个模块的时候再去require)
  • requirejs库 amd(依赖前置, 在定义模块的时候就要声明其依赖的模块)

这里不多说了,感兴趣的可以百度了解下这两个库。

那当下 vue, react 框架流行下,我们是如何来处理模块化的呢?

结论是: umd(统一模块化),常见的是 es6 module(node 中无法使用) 和 commonjs 规范

如果想要在 node 中使用 es module,也就是 import 和 export,怎么办呢?
node 官网提供了一个 ECMAScript模块,它需要将文件后缀改成 .mjs,来支持 import 导入的方式,但是目前还是实验版。
还有一种方式,就是使用 babel-node 来转化 es6 模块。

1.1 commonjs 规范

特点是啥,往下看?

  • 每个 js 文件都是一个模块
  • 每个文件如果需要用到别的模块 require()
  • 想把代码给别人使用 需要导出模块 module.exports

那文件之间是怎么隔离的?

其实很简单,就是给当前文件代码加了个闭包来隔离

(function(){
  let module = {
    exports: {}
  }
  module.exports = 'hello'
  return module.exports
})()
let  file = require('./a.js')

// 大概效果如下
let  file = require((function(){
  let module = {
    exports: {}
  }
  module.exports = 'hello'
  return module.exports
})())

2. fs path vm 等 node 模块

这里为什么单独介绍下这几个模块,因为这几个是核心模块,看过源码,你就知道, require 的实现就需要用到这几个模块。

  • path

path 模块提供了一些实用工具,用于处理文件和目录的路径

具体可看:path 文档

  • fs

fs 模块可用于与文件系统进行交互(以类似于标准 POSIX 函数的方式)。

具体可看:fs 文档

  • vm

vm 模块可在 V8 虚拟机上下文中编译和运行代码。 vm 模块不是安全的机制。 不要使用它来运行不受信任的代码。

具体可看:vm 文档

vm 模块这里单独提下,

我们回忆下让字符串执行的方法有哪些?

  1. eval
let a = 1;
eval('console.log(a)')
  1. new Function
let strFn = new Function('console.log(a)')
strFn();
vue 模板引擎的实现原理就是 new Function + with
  1. vm

vm 沙箱,创造一个干净的执行上下文环境,不会向上查找

vm.runInThisContext(str)

3. require 实现过程

3.1 断点调试

这里简单提下如何断点调试,不方便展示,大家可以网上翻阅下文档

我用的 vscode, 需要配置下 launch.json 文件,下面是我的配置

{
  // 使用 IntelliSense 了解相关属性。
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
      {
          "type": "node",
          "request": "launch",
          "name": "Client",
          "program": "${workspaceFolder}/app.js"
      }
  ]
}

然后运行 debug 模式,就可以查看源码,面板如下所示

b8339e505dd0197482772526baa45bc8.png

3.2 require 执行过程

  1. 加载时 先看一下模块是否被缓存过 第一次没有缓存过
  2. Module._resolveFilename 解析出当前引用文件的绝对路径
  3. 是否是内置模块,不是就创建一个模块 模块有两个属性 一个叫 id = 文件名, exports = {}
  4. 将模块放到缓存中
  5. 加载这个文件 Module.load
  6. 拿到文件的扩展名 findLongestRegisteredExtension() 根据扩展名来调用对应的方法
  7. 会读取文件 差一个加一个自执行函数,将代码放入

3.3 手写 require 源码

// a.js 文件
module.exports = 'hello';
console.log('加载了一次');

// require.js 文件
let fs = require('fs');
let path = require('path');
let vm = require('vm');

function Module(id) {
  this.id = id; // 文件名
  this.exports = {}; // exports 导出对象
}

Module._resolveFilename = function(filename) {
  // 应该去依次查找 Object.keys(Module._extensions)
  // 默认先获取文件的名字
  filename = path.resolve(filename);
  // 获取文件的扩展名 并判断是否有,若没有就是.js,若有,就采用原来的名字
  let flag = path.extname(filename);
  let extname = flag ? flag : '.js';
  return flag ? filename : (filename + extname);
}

Module._extensions = Object.create(null);

Module.wrapper = [
  '(function(module,exports,require,__dirname,__filename){',
  '})'
]

Module._extensions['.js'] = function(module) { // id exports
  // module.exports = 'hello'
  let content = fs.readFileSync(module.id, 'utf8')
  let strTemplate = Module.wrapper[0] + content + Module.wrapper[1];
  // console.log('111', strTemplate);
  // 希望让这个函数执行,并且,我希望吧exports 传入进去
  let fn = vm.runInThisContext(strTemplate);
  // 模块中的 this 就是 module.exports的对象
  fn.call(module.exports, module, module.exports, requireMe);
}

// json 就是直接将结果放到 module.exports 上
Module._extensions['.json'] = function(module) {
  let content = fs.readFileSync(module.id, 'utf8');
  module.exports = JSON.parse(content);
}

Module.prototype.load = function() {
  // 获取文件的扩展名
  let extname = path.extname(this.id);
  Module._extensions[extname](this);
}

Module._cache = {}; // 缓存对象

function requireMe(filename) {
  let absPath = Module._resolveFilename(filename);
  // console.log(absPath);
  if (Module._cache[absPath]) { // 如果缓存过了,直接将exports 对象返回
    return Module._cache[absPath].exports;
  }
  let module = new Module(absPath);
  // 增加缓存模块
  Module._cache[absPath] = module;
  // 加载
  module.load();
  return module.exports; // 用户将结果赋予给 exports 对象上 默认 require 方法会返回 module.exports 对象
}

let str = requireMe('./a');
str = requireMe('./a');
console.log('===', str);

4. module.exports 与 exports 关系

exports 是 module.exports 一个简写

  • 常用的导出方式

http://exports.xxx = xxx module.exports module.exports.a = xxx global.a = xxx(可以,但不会用,全局污染) exports = xxx(错误)

5. 模块查找方式

有几种模块:

  1. 内置模块 fs path vm
  2. 文件模块 自定义模块 './'
  3. 第三方模块 (bluebird....)必须安装才能使用, 用法和内置模块是一样的

方式:

  • 先查找当前文件下的文件存不存在,不存在 添加 .js .json 后缀 找到后就结束
  • 找不到后会找对应的文件夹,默认找索引文件,如果有package.json ,有这个文件,会查找 main对应的入口文件,去进行加载
  • 按照包的方法查找 多个文件组成一个包 npm init -y
  • 除了文件的查找方式 第三方模块的查找方式
let r = require('xxx') // xxx表示的是第三方文件夹的名字,找到名字后会找package.json ,如果找不到向上找,找不到就报错。

console.log(module.paths); // 查看效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值