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 模块这里单独提下,
我们回忆下让字符串执行的方法有哪些?
- eval
let a = 1;
eval('console.log(a)')
- new Function
let strFn = new Function('console.log(a)')
strFn();
vue 模板引擎的实现原理就是 new Function + with
- 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 模式,就可以查看源码,面板如下所示
3.2 require 执行过程
- 加载时 先看一下模块是否被缓存过 第一次没有缓存过
- Module._resolveFilename 解析出当前引用文件的绝对路径
- 是否是内置模块,不是就创建一个模块 模块有两个属性 一个叫 id = 文件名, exports = {}
- 将模块放到缓存中
- 加载这个文件 Module.load
- 拿到文件的扩展名 findLongestRegisteredExtension() 根据扩展名来调用对应的方法
- 会读取文件 差一个加一个自执行函数,将代码放入
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. 模块查找方式
有几种模块:
- 内置模块 fs path vm
- 文件模块 自定义模块 './'
- 第三方模块 (bluebird....)必须安装才能使用, 用法和内置模块是一样的
方式:
- 先查找当前文件下的文件存不存在,不存在 添加 .js .json 后缀 找到后就结束
- 找不到后会找对应的文件夹,默认找索引文件,如果有package.json ,有这个文件,会查找 main对应的入口文件,去进行加载
- 按照包的方法查找 多个文件组成一个包 npm init -y
- 除了文件的查找方式 第三方模块的查找方式
let r = require('xxx') // xxx表示的是第三方文件夹的名字,找到名字后会找package.json ,如果找不到向上找,找不到就报错。
console.log(module.paths); // 查看效果