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