CD's blog CD's blog
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

CD_wOw

内卷的行情,到不了的梦
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 基础原理及工具方法

    • JavaScript知识点
    • Javascript 事件流
    • 设计模式应用
    • 原型和继承
    • PromiseA+规范的实现
    • 前端模块化及进程
      • commonjs
        • module 对象
        • module.exports 属性
        • exports 变量
        • require 命令
        • 模块的缓存
        • 环境变量 NODE_PATH
        • require.main
      • AMD
      • CMD
      • AMD与CMD的区别
      • UMD
      • ESModule
        • ESModule 的加载实现
      • commonjs 和 Es 的区别
      • CommonJS 模块加载 ES6 模块
      • ES6 模块加载 CommonJS 模块
      • 同时支持两种格式的模块
      • ESM 在 webpack 下的打包
    • 前端性能异常监控
    • 工作用得到的一些JS方法
    • 项目代码层面优化方法
  • Babel

  • WebComponent

  • 专项知识汇总

  • JavaScript笔记
  • 基础原理及工具方法
CD
2020-12-20
目录

前端模块化及进程

# commonjs (opens new window)

commonjs 关系 nodejs 书写环境,更详细点击 title 打开文档查看。

Nodejs 是基于 v8 引擎,事件驱动 I/O 的服务端 JS 运行环境,推出 CommonJS 规范主要就是 module.exports = {} 导出 require() 导入。使用以下例子可以发现:

// a.js
const i = require("./c");
require("./b");
console.log(i);

// b.js
var i = require("./c");
i = "12321" + i;
setTimeout(() => console.log(i), 1000);

// c.js
var i = Date.now();
module.exports = i;
i = 123;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行以上代码我们可以看到:

  1. 实现了模块化,即使各模块内部会有相同的变量名,也没有造成冲突,并且其输出的值没有发生改变,保证了单例(输入的是被输出的值的拷贝)。
  2. commonjs 采用的是同步的方式进行导入导出并按照顺序执行,倘若导出的是一个非引用类型的对象,即使在后面对其造成了修改,其导出的数值依旧没有发生改变,倘若返回了一个 object 类型的对象,其内的值发生了改变,会影响到其他引入它的模块,在 commonjs 中,其导出的其实是这个对象的引用。
  3. 但是 commonjs 只能在 nodejs 的环境中才能运行,其会解析 js 环境中的导入导出方法,在其他的环境中运行这样的代码就会报错。其实际上是通过参数去找到要引入的文件的物理路径,从而通过系统调用从硬盘中读取文件内容,解析后返回文件的结果。这个过程是以同步执行来进行的,会阻塞接下来的代码的执行,这并不适用于浏览器端。

# module 对象

Node 内部提供一个 Module 构建函数。所有模块都是 Module 的实例,每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。
// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);
// 输出
{ id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/path/to/example.js',
  loaded: false,
  children:
   [ { id: '/path/to/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/path/to/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Object] } ],
  paths:
   [ '/home/user/deleted/node_modules',
     '/home/user/node_modules',
     '/home/node_modules',
     '/node_modules' ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

如果在命令行下调用某个模块,比如 node something.js,那么 module.parent 就是 null。如果是在脚本之中调用,比如 require('./something.js'),那么 module.parent 就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

if (!module.parent) {
  // ran with `node something.js`
  app.listen(8088, function () {
    console.log("app listening on port 8088");
  });
} else {
  // used with `require('/.something.js')`
  module.exports = app;
}
1
2
3
4
5
6
7
8
9

# module.exports 属性

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports 变量。

# exports 变量

为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports。

// 这等同在每个模块头部,有一行这样的命令
var exports = module.exports;
// 错误:不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。而对于module.exports,则需要接受一个引用类型的值。
exports = function (x) {
  console.log(x);
};
1
2
3
4
5
6

# require 命令

Node 使用 CommonJS 模块规范,内置的 require 命令用于加载模块文件。require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。

require 命令是 CommonJS 规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的 module.require 命令,而后者又调用 Node 的内部命令 Module._load。

Module._load = function (request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};
1
2
3
4
5
6
7
8
9

上面的第 4 步,采用 module.compile()执行指定模块的脚本,逻辑如下。

Module.prototype._compile = function (content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};
1
2
3
4
5
6

上面的第 1 步和第 2 步,require 函数及其辅助方法主要如下。

  • require(): 加载外部模块
  • require.resolve():将模块名解析到一个绝对路径
  • require.main:指向主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据文件的后缀名,调用不同的执行函数

模拟一个 DEMO:

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

function r(filename) {
  const pathToFile = path.resolve(__dirname, filename);
  const content = fs.readFileSync(pathToFile, "utf-8");

  const wrapper = ["(function(require, module, exports) {", "})"];

  const wrappedContent = wrapper[0] + content + wrapper[1];

  const script = new vm.Script(wrappedContent, {
    filename: "index.js",
  });

  const result = script.runInThisContext();

  const m = {
    exports: {},
  };

  result(r, m, m.exports);

  return m.exports;
}

const content = r("./module.js");
console.log(content);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 模块的缓存

第一次加载某个模块时,Node 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的 module.exports 属性, 如果再别的文件修改了引入的变量,输出值并不会发生改变,而是读取其缓存,除非写成一个函数,才能得到内部变动后的值。所有缓存的模块保存在 require.cache 之中,如果想删除模块的缓存,可以像下面这样写。

// 删除指定模块的缓存
delete require.cache[moduleName];

// 删除所有模块的缓存
Object.keys(require.cache).forEach(function (key) {
  delete require.cache[key];
});
1
2
3
4
5
6
7

注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require 命令还是会重新加载该模块。

# 环境变量 NODE_PATH

Node 执行一个脚本时,会先查看环境变量 NODE_PATH。它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node 会去这些路径查找。 可以将 NODE_PATH 添加到.bashrc。

export NODE_PATH
require命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的module.require命令,而后者又调用Node的内部命令Module._load。="/usr/local/lib/node"
1
2

所以,如果遇到复杂的相对路径,比如下面这样。

var myModule = require("../../../../lib/myModule");
1

有两种解决方法,一是将该文件加入 node_modules 目录,二是修改 NODE_PATH 环境变量,package.json 文件可以采用下面的写法。

{
  "name": "node_path",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "NODE_PATH=lib node index.js"
  },
  "author": "",
  "license": "ISC"
}
1
2
3
4
5
6
7
8
9
10
11

NODE_PATH 是历史遗留下来的一个路径解决方案,通常不应该使用,而应该使用 node_modules 目录机制

# require.main

require 方法有一个 main 属性,可以用来判断模块是直接执行,还是被调用执行。

直接执行的时候(node module.js),require.main 属性指向模块本身。

require.main === module; // true
1

调用执行的时候(通过 require 加载该脚本执行),上面的表达式返回 false。

# AMD

AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。目前,主要有两个 Javascript 库实现了 AMD 规范:require.js (opens new window)和 curl.js。

// index.html
 <script src="./require.js"></script>
  <script src="./a.js"></script>

// a.js
require(['b', 'c'], function (moduleB, moduleC) {
  console.log(moduleC)
})

// b.js
define(function (require) {
  var m = require('c')
  setTimeout(() => console.log(m), 1000)
})

// c.js
define(function (require) {
  var m = Date.now()
  return m
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

运行以上代码我们可以看到:

  1. AMD 模块定义默认为文件名,并写入一个回调函数,其函数返回的参数即是模块导出的值。
  2. AMD 的模块不能直接运行在 node 端,因为内部的 define 函数, require 函数都必须配合在浏览器中加载 require,is 这类 AMD 库才能使用。
  3. AMD 规范看起来完美解决了浏览器模块化开发的难题。但是它有一个天生的缺陷,对于依赖的模块无论实际需要与否,都会先加载并执行,加入我们第一个参数写入了 b、c,但是仅仅使用到了 b 的内容,但是 c 仍会被加载,从而引起了浪费。

使用 require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的 paths 属性指定各个模块的加载路径。

require.config({
  // baseUrl: "js/lib", 改变获取地址根路径
  paths: {
    jquery: "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min", // 文件名 、 路径
    underscore: "node_modules/underscore.min",
    backbone: "backbone.min",
  },
});
1
2
3
4
5
6
7
8

# CMD

CMD 也是一种 js 实现的模块化方案,不同之处在于,AMD 规范是依赖前置、模块提前加载并执行;CMD 是依赖后置、模块懒惰加载再执行。其规范的库是Sea.js (opens new window),用法如下

// 定义模块 math.js
define(function (require, exports, module) {
  var $ = require("jquery.js");
  var add = function (a, b) {
    return a + b;
  };
  exports.add = add;
});
// 加载模块
seajs.use(["math.js"], function (math) {
  var sum = math.add(1 + 2);
});
1
2
3
4
5
6
7
8
9
10
11
12

# AMD与CMD的区别

我们从requirejs和seajs中可以看到,由于requirejs与seajs遵循规范不同,requirejs在define函数中可以很容易获得当前模块依赖项。而seajs中不需要依赖声明,所以必须做一些特殊处理才能否获得依赖项。方法将factory作toString处理,然后用正则匹配出其中的依赖项,比如出现require(./a),则检测到需要依赖a模块。由于CMD规范和浏览器环境特点决定了seajs需要对代码进行一些预处理。

对于AMD来说,AMD的factory函数在其依赖的模块都执行完毕后便会执行,而CMD的factory是在被使用时才会执行的,它会等到模块的依赖项的文件完全加载完毕(amd中需要执行完毕,cmd中只需要文件加载完毕,注意这时候的factory尚未执行,当使用require请求该模块时,factory才会执行,所以在性能上seajs逊于requirejs)后执行主模块的factory函数;

# UMD

UMD( Universal ModuleDefinition)作为一种同构( isomorphic)的模块化解决方案出现,它能够让我们只需要在一个地方定义模块内容,并同时兼容 AMD 和 Commons 语法。其原理其实就是根据 AMD 和 COMMONJS 模块化规范的特征,判断当前环境是符合哪种规范的特征,从而使用相应的语法来导出。

(function (self, factory) {
  if (typeof module === "object" && typeof module.exports === "object") {
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    define(factory);
  } else {
    //
    self.umdModule = factory();
  }
})(this, function () {
  return function () {
    return Math.random();
  };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# ESModule

  • 每个 JS 的运⾏环境都有⼀个解析器,否则这个环境也不会认识 JS 语法。它的作⽤就是⽤ ECMAScript 的规范去解释 JS 语法,也就是处理和执⾏语⾔本身的内容,例如按照逻辑正确执⾏ var a = "123";,function func() {console.log("hahaha");} 之类的内容。
  • 在解析器的上层,每个运⾏环境都会在解释器的基础上封装⼀些环境相关的 API。例如 Node.js 中的 global 对象、process 对象,浏览器中的 window 对象,document 对象等等。这些运⾏环境的 API 受到各⾃规范的影响,例如浏览器端的 W3C 规范,它们规定了 window 对象和 document 对象上的 API 内容,以使得我们能让 document.getElementById 这样的 API 在所有浏览器上运⾏正常。
  • 事实上,类似于 setTimeout 和 console 这样的 API,⼤部分也不是 JS Core 层⾯的,只不过是所有运⾏环境实现了相似的结果。setTimeout 在 ES7 规范之后才进⼊ JS Core 层⾯,在这之前都是浏览器和 Node.js 等环境进⾏实现。console 类似 promise,有⾃⼰的规范,但实际上也是环境⾃⼰进⾏实现的,这也就是为什么 Node.js 的 console.log 是异步的⽽浏览器是同步的⼀个原因。
  • 同时,早期的 Node.js 版本是可以使⽤ sys.puts 来代替 console.log 来输出⾄ stdout 的。ESModule 就属于 JS Core 层⾯的规范,⽽ AMD,CommonJS 是运⾏环境的规范。所以,想要使运⾏环境⽀持 ESModule 其实是⽐较简单的,只需要升级⾃⼰环境中的 JS Core 解释引擎到⾜够的版本,引擎层⾯就能认识这种语法,从⽽不认为这是个 语法错误(syntax error) ,运⾏环境中只需要做⼀些兼容⼯作即可。
  • Node.js 在 V12 版本之后才可以使⽤ ESModule 规范的模块,在 V12 没进⼊ LTS 之前,我们需要加上 --experimental-modules 的 flag 才能使⽤这样的特性,也就是通过 node --experimental-modulesindex.js 来执⾏。浏览器端 Chrome 61 之后的版本可以开启⽀持 ESModule 的选项,只需要通过 `` 这样的标签加载即可。这也就是说,如果想在 Node.js 环境中使⽤ ESModule,就需要升级 Node.js 到⾼版本,这相对来说⽐较容易,毕竟服务端 Node.js 版本控制在开发⼈员⾃⼰⼿中。但浏览器端具有分布式的特点,是否能使⽤这种⾼版本特性取决于⽤户访问时的版本,⽽且这种解释器语法层⾯的内容⽆法像 AMD 那样在运⾏时进⾏兼容,所以想要直接使⽤就会⽐较麻烦。

# ESModule 的加载实现

在浏览器加载 js 文件时,默认情况下是会同步加载 js 脚本,即浏览器引擎遇到《script》标签后就会停下来,等到脚本执行完之后再继续向下渲染。如果是外部脚本,还需要同步等待脚本下载的时间。在此,同步的代码可能就会受到脚本过大、下载过慢带来极差用户体验的问题。在此浏览器有两种异步加载的方案:

  1. defer <script src="xxx.js" defer></script> 等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行。
  2. async <script src="yyy.js" async></script> 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。

defer 是“渲染完再执行”,async 是“下载完就执行”。另外,如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的。

浏览器加载 ES6 模块,也使用 script 标签,但是要加入 type="module"属性。浏览器对于带有 type="module"的 script,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了 script 标签的 defer 属性。

<script type="module" src="./foo.js"></script>
// 倘若增加了async属性,esmodule就不会按照页面出现的顺序执行,而是模块加载完成,就执行该模块。
<script type="module" src="./foo.js" async></script>
1
2
3

# commonjs 和 Es 的区别

  • commonJs 是被加载的时候运行,esModule 是编译的时候运行。因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  • commonJs 输出的是值的浅拷贝,esModule 输出值的引用
  • commontJs 具有缓存。在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值
  • commonJs 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段

ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的 import 有点像 Unix 系统的“符号连接”,原始值变了,import 加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

浏览器加载 es 的实现 (opens new window)

# CommonJS 模块加载 ES6 模块

CommonJS 的 require()命令不能加载 ES6 模块,会报错,只能使用 import()这个方法加载。

(async () => {
  await import("./my-app.mjs");
})();
1
2
3

上面代码可以在 CommonJS 模块中运行。

require()不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层 await 命令,导致无法被同步加载。

# ES6 模块加载 CommonJS 模块

ES6 模块的 import 命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项。

// 正确
import packageMain from "commonjs-package";

// 报错
import { method } from "commonjs-package";
1
2
3
4
5

这是因为 ES6 模块需要支持静态代码分析,而 CommonJS 模块的输出接口是 module.exports,是一个对象,无法被静态分析,所以只能整体加载。

加载单一的输出项,可以写成下面这样。

import packageMain from 'commonjs-package';
const { method } = packageMain;
还有一种变通的加载方法,就是使用 Node.js 内置的module.createRequire()方法。

// cjs.cjs
module.exports = 'cjs';

// esm.mjs
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

const cjs = require('./cjs.cjs');
cjs === 'cjs'; // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,ES6 模块通过 module.createRequire()方法可以加载 CommonJS 模块。但是,这种写法等于将 ES6 和 CommonJS 混在一起了,所以不建议使用。

# 同时支持两种格式的模块

一个模块同时要支持 CommonJS 和 ES6 两种格式,也很容易。

如果原始模块是 ES6 格式,那么需要给出一个整体输出接口,比如 export default obj,使得 CommonJS 可以用 import()进行加载。

如果原始模块是 CommonJS 格式,那么可以加一个包装层。

import cjsModule from "../index.js";
export const foo = cjsModule.foo;
1
2

上面代码先整体输入 CommonJS 模块,然后再根据需要输出具名接口。

你可以把这个文件的后缀名改为.mjs,或者将它放在一个子目录,再在这个子目录里面放一个单独的 package.json 文件,指明{ type: "module" }。

另一种做法是在 package.json 文件的 exports 字段,指明两种格式模块各自的加载入口。

"exports":{
  "require": "./index.js",
  "import": "./esm/wrapper.js"
}
1
2
3
4

上面代码指定 require()和 import,加载该模块会自动切换到不一样的入口文件。

# ESM 在 webpack 下的打包

通常我们使用 webpack 对我们的 js 文件进行编译的时候,我们可以通过以下例子看到:

编译前

// ES6
import { firstName, lastName, year } from "./profile";
1
2

编译后

// Babel 编译后
"use strict";
var _profile = require("./profile");
1
2
3

你会发现 Babel 只是把 ES6 模块语法转为 CommonJS 模块语法,然而浏览器是不支持这种模块语法的,所以直接跑在浏览器会报错的,如果想要在浏览器中运行,还是需要使用打包工具将代码打包。

Webpack 打包工具在打包这些 commonjs 语法的过程中,模拟了 module、 exports、 require 等浏览器环境中没有的环境变量,并将原有的 commonjs 的文件再包裹一层函数,将这些模拟好的环境变量传入函数。

编辑 (opens new window)
#JavaScript#Node
上次更新: 2023/03/22, 15:28:00
PromiseA+规范的实现
前端性能异常监控

← PromiseA+规范的实现 前端性能异常监控→

最近更新
01
gsap动画库学习笔记 - 持续~
06-05
02
远程组件加载方案笔记
05-03
03
小程序使用笔记
03-29
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CD | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式