模块化历程

  • 利用函数、对象、自执行函数实现分块
  • 模块化规范
    • 模块化是前端走向工程化中的重要一环
    • 早期JavaScript语言层面没有模块化规范(自己利用函数,对象,自执行函数进行代码的分块管理)
    • Commonjs、AMD、CMD、都是模块化规范
    • ES6中将模块化纳入标准规范
    • 当下常用规范是Commonjs与ESM
  • 模块化规范历程
    • Commonjs规范-语言层面上的规范,模块的加载都是同步完成的,并不适合在浏览器端使用
    • AMD规范,异步
    • CMD规范,整合了Commonjs和AMD规范
    • ES modules规范
  • 以 CommonJS 作为模块标准的 Node.js 也紧跟 ES Module 的发展步伐,从 12.20 版本开始正式支持原生 ES Module。也就是说,如今 ES Module 能够同时在浏览器与 Node.js 环境中执行,拥有天然的跨平台能力。 在node中使用ESM,需要在package.json内 type=”module”

Commonjs规范

  • not just for browsers any more!通过这个标准倒逼浏览器做出一些改变。由于浏览器数据一般通过网络进行传输的,存在单线程,阻塞等加载方式,Commonjs不适用于浏览器平台,主要应用于Nodejs中
  • Commonjs是语言层面上的规范,而模块化只是这个规范中的一部分
  1. 模块的引用
  2. 模块定义
  3. 模块标识(模块的id)

    NodeJS与CommonJS

  • 任意一个文件就是一模块,具有独立作用域
  • 使用require导入其他模块
  • 将模块ID传入require实现目标模块定位

    module属性
  • 任意js文件就是一个模块,可以直接使用module属性

  • id: 返回模块标识符,一般是一个绝对路径
  • filename:返回文件模块的绝对路径
  • loaded:返回布尔值,表示模块是否完成加载
  • parent:返回对象存放调用当前模块的模块
  • children:返回数组,存放当前模块调用的其他模块
  • exports:返回当前模块需要暴露的内容
  • paths:返回数组,存放不同目录下的node_modules位置

    module.exports与exports有何区别
  • module.exports是Commonjs提供的

  • exports是nodejs为了方便操作,给每个模块提供的变量,指向module.exports指向的内存地址,不能为exports重新赋值
  • module.exports和exports一开始都是一个空对象{},实际上,这两个对象指向同一块内存。这也就是说module.exports和exports是等价的(有个前提:不去改变它们指向的内存地址)。

    require属性
  • 基本功能是读入并且执行一个模块文件

  • resolve:返回模块文件绝对路径
  • extensions:依据不同后缀名执行解析操作
  • main:返回主模块对象

    Commonjs规范总结

  • Commonjs规范起初为了弥补JS语言模块化缺陷

  • Commonjs是语言层面的规范,当前主要用于Nodejs
  • Commonjs规定模块化分为引入、定义、标识符三个部分
  • Module在任意模块中可直接使用包含模块信息
  • Require接收标识符,加载目标模块
  • Exports接收标识符,加载目标模块
  • exports与module.exports都能导出模块数据(不能为exports重新赋值)
  • Commonjs规范定义模块的加载是同步完成的

    Nodejs与Commonjs代码演示

  • 使用module.exports与require实现模块导入与导出

  • module属性及其常见信息获取
  • exports导出数据及其与module.exports区别
  • CommonJS规范下的模块同步加载 ```javascript // 1. 模块的导入与导出 const age = 18; const addFn = (x, y) => { return x + y; }; module.exports = { age: age, addFn: addFn, };

// module console.log(module); / Module { id: ‘.’, path: ‘E:\测试项目\test\Node\模块化’,
exports: { age: 18, addFn: [Function: addFn] }, parent: null, filename: ‘E:\测试项目\test\Node\模块化\index.js’, loaded: false, children: [], paths: [ ‘E:\测试项目\test\Node\模块化\node_modules’, ‘E:\测试项目\test\Node\node_modules’, ‘E:\测试项目\test\node_modules’, ‘E:\测试项目\node_modules’, ‘E:\node_modules’ ] }
/

// exports exports.name = “aaa” exports = { name: “syy”, age: 20, };

// 同步加载 let name = ‘lg’ module.exports = name console.log(‘index.js被加载导入了’);

  1. ```javascript
  2. // 1. require
  3. let obj = require('./index')
  4. console.log(obj); // { age: 18, addFn: [Function: addFn] }
  5. // 2. module
  6. let obj2 = require('./index')
  7. // 3. exports
  8. let aa = require('./index')
  9. console.log(aa); //{ age: 18, addFn: [Function: addFn] }
  10. // 4. 同步加载
  11. let obj = require('./index')
  12. console.log('commonjs代码执行了');
  13. console.log(require.main === module); // true
  14. // require.main的上级是当前模块,module也是当前模块

模块加载分类及加载流程

  • 内置模块(编译时就加载了)
  • 文件模块(自定义,第三方包)运行时加载

加载流程

  • 路径分析:依据标识符确定模块位置(路径和非路径会当作第三包)
  • 文件定位:确定目标模块中具体的文件及文件类型
    • 导入时无扩展名,node会.js->.json->.node等补足扩展名,或者查找package.json,所以JSON.parse()解析,没有package.json的话,会将index做为目标模块中的具体文件名称
  • 编译执行:采用对应的方式完成文件的编译执行
    • 创建新对象,按路径载入,完成编译执行
      JS文件的编译执行(自执行函数)
  1. 使用fs模块同步读入目标文件内容
  2. 对内容进行语法包装,生成可执行JS函数
  3. 调用函数时传入exports、require、module、filename、dirname属性值
    JSON文件编译执行
  • 将读取的内容通过JSON.parse()进行解析

    缓存优化原则
  • 提高加载速度

  • 当前模块不存在,则经历一次完整加载流程
  • 模块加载完成后,使用路径作为索引进行缓存

    小结(重要)
  • 路径分析:确定目标模块位置

  • 文件定位:确定目标模板中具体文件
  • 编译执行:对模块内容进行编译,返回可用exports对象 ```javascript 打断点,调试源码,vscode Ctrl shift + d 创建启动运行json文件 配置文件 1. 注释掉跳过源码部分 2. 修改路径 // “skipFiles”: [ // “/**” //],

1.在node下,直接打印this,this是空对象的原因,主要在内部做了修改 2.exports是和module.exports指向的同一个内存地址 区别: module.exports和exports一开始都是一个空对象{},实际上,这两个对象指向同一块内存。这也就是说module.exports和exports是等价的(有个前提:不去改变它们指向的内存地址)。

![模块化加载2.png](https://cdn.nlark.com/yuque/0/2022/png/22628793/1651377186310-71d1637e-eed6-42e2-8e0b-408d6375a2d0.png#clientId=ua3539cfe-be0b-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u1cbf1ab8&margin=%5Bobject%20Object%5D&name=%E6%A8%A1%E5%9D%97%E5%8C%96%E5%8A%A0%E8%BD%BD2.png&originHeight=778&originWidth=1535&originalType=binary&ratio=1&rotation=0&showTitle=false&size=715730&status=done&style=none&taskId=u0b685b6a-1e51-44ca-bc73-e9ff18a4327&title=)![node模块化.png](https://cdn.nlark.com/yuque/0/2022/png/22628793/1651377186404-09920004-a328-489d-9f77-20c8f2868fc1.png#clientId=ua3539cfe-be0b-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u3fa4810c&margin=%5Bobject%20Object%5D&name=node%E6%A8%A1%E5%9D%97%E5%8C%96.png&originHeight=906&originWidth=1891&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1178264&status=done&style=none&taskId=ufd09b1b7-efb8-40d2-91fa-59ca361af7a&title=)

- exports和module.exports的区别

![export1.png](https://cdn.nlark.com/yuque/0/2022/png/22628793/1651377421814-79d0736c-904e-4a12-a0ed-870bc4c1199e.png#clientId=ua3539cfe-be0b-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u13fb123c&margin=%5Bobject%20Object%5D&name=export1.png&originHeight=765&originWidth=1750&originalType=binary&ratio=1&rotation=0&showTitle=false&size=751131&status=done&style=none&taskId=u7c68f48d-7a3e-434c-b101-2aa486f7a82&title=)
<a name="Kqt47"></a>
#### Vm模块 沙箱隔离

let age = 18

```javascript
const fs = require('fs')
const vm = require('vm')

let age = 33
let content = fs.readFileSync('test.txt', 'utf-8')

// 读取的是字符串,如何执行字符串呢

// 1. eval
// eval(content)
// console.log(age); // 33

// 2. new Function
// console.log(age); // 33
// let fn = new Function('age', 'return age + 1')
// console.log(fn(age)); // 34

// 在模块中和本环境下都有相同的文件名时,优先加载本文件中的
// vm.runInThisContext(content)
// console.log(age); // 33

// 使用runInThisContext执行字符串的时候,无法使用外部局部变量(把let改成var就OK了)
vm.runInThisContext("age += 10")
console.log(age); // ReferenceError: age is not defined

Nodejs VM虚拟机
深入nodejs Vm虚拟机

模块加载模拟逻辑实现

核心逻辑

  • 路径分析
  • 缓存优化
  • 文件定位
  • 编译执行

步骤:1.确定文件路径 2. 考虑缓存优先,对后缀补足 ,然后把exports返回到当前模块,让其使用,(没有涉及到第三方包,package.json文件的查找)

const name = 'lg'
module.exports = name

路径分析+文件定位实现

const fs = require("fs");
const path = require("path");
const vm = require("vm");

function Module(id) {
  this.id = id;
  this.exports = {};
}
Module._resolveFilename = function (filename) {
  // 利用Path将filename转为绝对路径
  let absPath = path.resolve(__dirname, filename);
  // 还得判断是否是字符串,是否为空,这里不判断,主要是实现主逻辑

  // 判断当前路径对应的内容是否存在(文件or目录)
  if (fs.existsSync(absPath)) {
    // 如果条件成立说明,absPath 对应的内容是否存在
    return absPath;
  } else {
    // 文件定位
    let suffix = Object.keys(Module._extensions);
    // console.log(suffix);
    // 要将拿到的文件后缀名,通过遍历稳定输出
    for(let i = 0; i < suffix.length; i++){
      let newPath = absPath + suffix[i]
      if(fs.existsSync(newPath)){
        return newPath
      }
    }
  }
  throw new Error(`${filename} is not exists`)
};

Module._extensions = {
  ".js"() {},
  ".json"() {},
};

function myRequire(filename) {
  // 1 绝对路径处理
  let mPath = Module._resolveFilename(filename);
  console.log(mPath);
}

let obj = myRequire("./v");
console.log(obj);

实现缓存+js和json文件读取执行(完整代码)

const fs = require("fs");
const path = require("path");
const vm = require("vm");

function Module(id) {
  this.id = id; // 文件标识
  this.exports = {};
}

// 查找文件
Module._resolveFilename = function (filename) {
  // 利用Path将filename转为绝对路径
  let absPath = path.resolve(__dirname, filename);
  // 还得判断是否是字符串,是否为空,这里不判断,主要是实现主逻辑

  // 判断当前路径对应的内容是否存在(文件or目录)
  if (fs.existsSync(absPath)) {
    // 如果条件成立说明,absPath 对应的内容是否存在
    return absPath;
  } else {
    // 文件定位
    let suffix = Object.keys(Module._extensions);
    // console.log(suffix);
    // 要将拿到的文件后缀名,通过遍历稳定输出
    for (let i = 0; i < suffix.length; i++) {
      let newPath = absPath + suffix[i];
      if (fs.existsSync(newPath)) {
        return newPath;
      }
    }
  }
  throw new Error(`${filename} is not exists`);
};

// 定位文件名
Module._extensions = {
  ".js"(module) {
    // 读取
    let content = fs.readFileSync(module.id, "utf-8");
    // 包装
    content = Module.wrapper[0] + content + Module.wrapper[1];
    // console.log(content);

    // VM可以执行字符串,可以调用了
    let compileFn = vm.runInThisContext(content);
    // console.log(compileFn); // [Function (anonymous)]

    // 准备参数值
    let exports = module.exports; // 默认是{}的和global是不一样的
    let dirname = path.dirname(module.id);
    let filename = module.id;
    console.log(exports);

    // 调用
    // 为什么打印exports的时候是一个空对象,在这里已经进行修改了
    // 第一个值传入this指向,exports,然后把定义的5个值传入进去
    compileFn.call(exports, exports, myRequire, module, filename, dirname);
  },
  ".json"(module) {
      let content = JSON.parse(fs.readFileSync(module.id, 'utf-8'))
      module.exports = content
  },
};

// wrapper[0],wrapper[1],代表函数
Module.wrapper = ["(function (exports, require, module, __filename, __dirname){", "})"];

// 缓存
Module._cache = {};

// 加载函数
Module.prototype.load = function () {
  let extname = path.extname(this.id);
  //   console.log(extname);
  Module._extensions[extname](this);
};

function myRequire(filename) {
  // 1 绝对路径处理
  let mPath = Module._resolveFilename(filename);
  // console.log(mPath);

  // 2. 缓存优先(先准备一个对象存储键,然后拿这个绝对路径的值匹配键,如果匹配到就证明之前加载过,直接从缓存中拿)
  let cacheModule = Module._cache[mPath];
  if (cacheModule) return cacheModule.exports; // 如果找到,就返回出去

  // 3 .创建空对象,加载目标模块
  let module = new Module(mPath);

  // 4.缓存已加载过的模块
  Module._cache[mPath] = module;

  // 5. 执行加载,编译执行
  module.load();

  // 6.返回数据
  return module.exports;
}

let obj = myRequire("./v");
console.log(obj.name);