关于javascript:个人记录前端Module加载实现机制

60次阅读

共计 5645 个字符,预计需要花费 15 分钟才能阅读完成。

1. 什么是前端模块化
模块化开发,一个模块就是一个实现特定性能的文件,有了模块咱们就能够更不便地应用他人的代码,要用什么性能就加载什么模块。
2. 模块化开发的益处
1)防止变量净化,命名抵触
2)进步代码利用率
3)进步维护性
4)依赖关系的治理
3. 浏览器加载

默认状况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到
如果脚本体积很大,下载和执行的工夫就会很长,因而造成浏览器梗塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器容许脚本异步加载,上面就是两种异步加载的语法。
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
https://zhuanlan.zhihu.com/p/…
一旦应用了 async 属性,
ES6 模块也容许内嵌在网页中,语法行为与加载内部脚本完全一致。
对于内部的模块脚本(上例是 foo.js),有几点须要留神:
①代码是在模块作用域之中运行,而不是在全局作用域运行。模块外部的顶层变量,内部不可见。
②模块脚本主动采纳严格模式,不论有没有申明 use strict。
③模块之中,能够应用 import 命令加载其余模块(.js 后缀不可省略,须要提供相对 URL 或绝对 URL),也能够应用 export 命令输入对外接口。
④模块之中,顶层的 this 关键字返回 undefined,而不是指向 window。也就是说,在模块顶层应用 this 关键字,是无意义的。
⑤同一个模块如果加载屡次,将只执行一次。
4.ES6 模块与 CommonJS 模块的差别
①CommonJS 模块输入的是一个值的拷贝,ES6 模块输入的是值的援用。
②CommonJS 模块是运行时加载,ES6 模块是编译时输入接口。
③CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段。
5.Node.js 的模块加载办法
JS 当初有两种模块:一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法下面,两者最显著的差别是,CommonJS 模块应用 require() 和 module.exports,ES6 模块应用 import 和 export。
它们采纳不同的加载计划。从 Node.js v13.2 版本开始,Node.js 曾经默认关上了 ES6 模块反对。
Node.js 要求 ES6 模块采纳.mjs 后缀文件名。也就是说,只有脚本文件外面应用 import 或者 export 命令,那么就必须采纳.mjs 后缀名。Node.js 遇到.mjs 文件,就认为它是 ES6 模块,默认启用严格模式,不用在每个模块文件顶部指定 ”use strict”。
如果不心愿将后缀名改成.mjs,能够在我的项目的 package.json 文件中,指定 type 字段为 module。
一旦设置了当前,该目录外面的 JS 脚本,就被解释用 ES6 模块。
如果这时还要应用 CommonJS 模块,那么须要将 CommonJS 脚本的后缀名都改成.cjs。如果没有 type 字段,或者 type 字段为 commonjs,则.js 脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs 文件总是以 ES6 模块加载,.cjs 文件总是以 CommonJS 模块加载,.js 文件的加载取决于 package.json 外面 type 字段的设置。
留神,ES6 模块与 CommonJS 模块尽量不要混用。require 命令不能加载.mjs 文件,会报错,只有 import 命令才能够加载.mjs 文件。反过来,.mjs 文件外面也不能应用 require 命令,必须应用 import。
6.package.json 的 main 字段
package.json 文件有两个字段能够指定模块的入口文件:main 和 exports。比较简单的模块,能够只应用 main 字段,指定模块加载的入口文件。如果没有 type 字段,index.js 就会被解释为 CommonJS 模块。而后,import 命令就能够加载这个模块。
7.package.json 的 exports 字段
exports 字段的优先级高于 main 字段。它有多种用法:
①子目录别名:package.json 文件的 exports 字段能够指定脚本或子目录的别名
②main 的别名:exports 字段的别名如果是.,就代表模块的主入口,优先级高于 main 字段,并且能够间接简写成 exports 字段的值。
③条件加载:利用. 这个别名,能够为 ES6 模块和 CommonJS 指定不同的入口。目前,这个性能须要在 Node.js 运行的时候,关上 –experimental-conditional-exports 标记。
8.CommonJS 模块加载 ES6 模块
CommonJS 的 require() 命令不能加载 ES6 模块,会报错,只能应用 import()这个办法加载。
require()不反对 ES6 模块的一个起因是,它是同步加载,而 ES6 模块外部能够应用顶层 await 命令,导致无奈被同步加载。
9.ES6 模块加载 CommonJS 模块
ES6 模块的 import 命令能够加载 CommonJS 模块,然而只能整体加载,不能只加载繁多的输入项。
这是因为 ES6 模块须要反对动态代码剖析,而 CommonJS 模块的输入接口是 module.exports,是一个对象,无奈被动态剖析,所以只能整体加载。
加载繁多的输入项,能够写成上面这样:
import packageMain from ‘commonjs-package’;
const {method} = packageMain;
还有一种变通的加载办法,就是应用 Node.js 内置的 module.createRequire()办法。
10. 同时反对两种格局的模块
一个模块同时要反对 CommonJS 和 ES6 两种格局,也很容易。
如果原始模块是 ES6 格局,那么须要给出一个整体输入接口,比方 export default obj,使得 CommonJS 能够用 import()进行加载。
如果原始模块是 CommonJS 格局,那么能够加一个包装层。
Node.js 的内置模块
Node.js 的内置模块能够整体加载,也能够加载指定的输入项。
11. 加载门路
ES6 模块的加载门路必须给出脚本的残缺门路,不能省略脚本的后缀名。import 命令和 package.json 文件的 main 字段如果省略脚本的后缀名,会报错。
为了与浏览器的 import 加载规定雷同,Node.js 的.mjs 文件反对 URL 门路。
目前,Node.js 的 import 命令只反对加载本地模块(file: 协定)和 data: 协定,不反对加载近程模块。另外,脚本门路只反对相对路径,不反对绝对路径(即以 / 或 // 结尾的门路)。
12. 外部变量
ES6 模块应该是通用的,同一个模块不必批改,就能够用在浏览器环境和服务器环境。为了达到这个指标,Node.js 规定 ES6 模块之中不能应用 CommonJS 模块的特有的一些外部变量。
首先,就是 this 关键字。ES6 模块之中,顶层的 this 指向 undefined;CommonJS 模块的顶层 this 指向以后模块,这是两者的一个重大差别。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
__filename
__dirname
13. 循环加载
“循环加载”(circular dependency)指的是,a 脚本的执行依赖 b 脚本,而 b 脚本的执行又依赖 a 脚本。
通常,“循环加载”示意存在强耦合,如果解决不好,还可能导致递归加载,使得程序无奈执行,因而应该避免出现。
然而实际上,这是很难防止的,尤其是依赖关系简单的大我的项目,很容易呈现 a 依赖 b,b 依赖 c,c 又依赖 a 这样的状况。这意味着,模块加载机制必须思考“循环加载”的状况。
对于 JS 语言来说,目前最常见的两种模块格局 CommonJS 和 ES6,解决“循环加载”的办法是不一样的,返回的后果也不一样。
①CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,当前再加载,就返回第一次运行的后果,除非手动革除零碎缓存。
CommonJS 模块的重要个性是加载时执行,即脚本代码在 require 的时候,就会全副执行。一旦呈现某个模块被 ” 循环加载 ”,就只输入曾经执行的局部,还未执行的局部不会输入。
②ES6 解决“循环加载”与 CommonJS 有实质的不同。ES6 模块是动静援用,如果应用 import 从一个模块加载变量(即 import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的援用,须要开发者本人保障,真正取值的时候可能取到值。
\
14. 手写 node.js 的 require 函数

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

// 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._extensionsextname;
}

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);

正文完
 0