node模块及模块引入实现原理

1,240 阅读3分钟

一、es6Module和CommonJS规范


1) 静态和动态
es6Module静态模块,可以在编译的时候分析依赖,支持tree-shaking
CommonJS动态模块,在代码执行的时候才知道依赖,不支持tree-shaking
2)esModule输入的是值的引用。CommonJS输出的是值得拷贝。
3)esModule模块中顶层的this指向undefinedCommonJS模块中顶层this指向当前模块。

二、node中的模块

1. 内置模块

1) fs模块
let r = fs.readFileSync('./1.js');
let exists = fs.existsSync('./2.js');
2) path模块
path.resolve(__dirname, 'a.js');  # 当前模块文件被解析过后的所在文件夹的路径(绝对路径)
path.resolve(__filename, 'b.js'); # 当前模块文件被解析过后的文件路径(绝对路径)

path.resolve(__dirname, 'a', 'b', '/');  # c:\ 如果有`/`,会返回根目录。
path.join(__dirname, 'a', 'b', '/'); #/Users/Desktop/demo/a/b/  路径拼接,遇到`/`也会拼在一起
// 取扩展名
path.extname('a.min.js'); // .js
// 根据后缀取文件名
path.basename('a.js', '.js') // a
// 获取相对路径
path.relative('a/b/c/1.js', 'a'); // ../../..
// 获取目录名
path.dirname('a/b/c'); // a/b
3) vm模块
// new Function() 执行代码
global.a = 100;
new Function('b', 'console.log(a, b)')('b'); // 100 b
// 在当前上下文中执行
const vm = require('vm');
global.a = 100;
vm.runInThisContext('console.log(a)'); // 100

2. 文件模块

文件模块查找规范:

  • 默认先查找同名文件,尝试添加.js和.json文件。
  • 找不到同名文件,就找同名文件夹(把文件夹当成包)。如果文件夹中有package.json,则根据package.json中的main指定的文件查找。
  • 如果没有package.json,在找同名文件夹下的index.js。

3. 第三方模块

第三方模块引用,分为全局模块安装引用和代码中引用。

第三方模块查找规范:

  • 默认沿着当前目录向上查找,查找node_modules下的同名文件夹。根据package.json中的main查找,如果找不到,则找index.js。
  • 如果没找到,一直向上级的node_modules查找。找到根路径仍旧没找到,说明没有这个包,报错。

三、node中require的实现原理


CommonJS的require的核心思路是:引入进来的模块包装一层自执行函数,实现模块隔离。

看下面的例子,帮助理解怎么包装的:

// a.js
var a = 100;
module.exports = '1';
// b.js

// 引入a.js
let a = require('./a');

// 相当于
let a = (function(exports, module, require, __dirname, __filename) {
    var a = 100;
    module.exports = '1';
    
    return module.exports;
})(exports, module, require, __dirname, __filename);

实现流程:

  1. Module.resolveFilename 根据用户输入的相对引用路径,生成绝对路径。
  2. new Module 根据文件路径创建模块。
  3. module.load 调用模块的load事件,加载模块,执行脚本给module.exports赋值。
  4. return module.exports 返回exports对象, 用户会拿到module.exports的返回结果。
  5. cacheModule = Module._cache[filename] 根据文件名缓存模块。
// 1.js

var a = 100;
console.log(this === module.exports) // true
module.exports = a;
// require.js

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

function Module(id) {
    this.id = id;
    this.exports = {};
}
Module._cache = [];
Module.prototype.load = function() {
    // 获取文件后缀名, 走策略模式
    let ext = path.extname(this.id);
    Module._extensions[ext](this);
}
// 策略模式,不同后缀
Module._extensions = {
    '.js'(module) {
        // 读取脚本
        let script = fs.readFileSync(module.id, 'utf8');
        // 包装成函数,runInThisContext类似于 new Function(),将templateFn字符串转成函数
        let templateFn = `(function(exports, module, require, __dirname, __filename){${script}})`;
        let fn = vm.runInThisContext(templateFn);
        // console.log(fn.toString());

        // this = module.exports = exports
        let exports = module.exports;
        let thisValue = exports;
        let filename = module.id;
        let dirname = path.dirname(filename);
        
        // 函数调用,调用完后,module.exports = 100
        fn.call(thisValue, exports, module, req, dirname, filename);
    },
    '.json'(module) {
        let script = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(script);
    }
}
// 把路径变为绝对路径,添加后缀名
Module._resolveFilename = function(id) {
    let filePath = path.resolve(__dirname, id);
    let isExists = fs.existsSync(filePath);
    if (isExists) return filePath;

    // 尝试添加后缀
    let keys = Reflect.ownKeys(Module._extensions); // ['.js', '.json']
    for (let i = 0; i < keys.length; i++) {
        let newPath = filePath + keys[i];
        if (fs.existsSync(newPath)) return newPath;
    }
    throw new Error('module not found');
}

function req(filename) {
    // 1. 创建一个绝对引用路径,方便后续读取
    filename = Module._resolveFilename(filename);
    // 缓存中有,直接取缓存
    let cacheModule = Module._cache[filename];
    if (cacheModule) return cacheModule.exports;
    // 2. 根据路径创建一个模块
    const module = new Module(filename);
    // 根据文件名缓存模块
    Module._cache[filename] = module;
    // 3. 让用户给module.exports赋值
    module.load();

    return module.exports;
}

// let a = req('a.json');
let a = req('./1.js');
console.log(a);

四、第三方模块的安装及发布

1)全局安装

全局安装,只能在命令行中使用,全局安装的模块会安装到npm目录下。

自己实现全局安装包,需要:

  1. package.json中配置bin命令。
  2. 添加执行方式 #!/usr/bin/env node
  3. 将此包放到npm下,可以全局安装,也可以临时sudo npm link
2)代码安装

npm install 模块 --save --save-dev

3)依赖关系
  1. 开发依赖:--save-dev
  2. 生产依赖:--save
  3. 同等依赖:peerDependencies,一个第三方模块依赖于另外一个模块,安装时提示需要你安装同等依赖。
  4. 可选依赖:optionalDependencies。
  5. 打包依赖:bundledDependencies。npm pack打压缩包时,希望把某个包打进去就在这里配。(默认npm pack 打压缩包不会打node_modules。)
4)npm run xxx 为什么可以执行?

使用npm run xxx,默认在命令执行之前,会将.bin环境变量添加到全局环境变量下,这时候命令就可以执行了。待命令执行完毕后,会删掉对应的path。

npxnpm run类似,npx如果模块不存在会先安装再使用,使用后可以自动删除。

5)版本管理

版本管理方式:semver (major 、minor 、 patch);

^2.2.4  => 第一位只能是2。  
~2.2.4  => 第二位不能低于2,大版本不能超过2版本,即 2.2.x`>=`    => 大于等于某个版本
`<=`    => 小于等于某个版本

指定版本:npm install jquery@2.2.4  
指定版本:npm install jquery@2  不低于2的版本
6)包发布
  1. 切换源
nrm use npm
  1. 登录npm
npm addUser
  1. 发布
npm publish

更新版本

npm version major  # 大版本号加 1,其余版本号归 0
npm version minor  # 小版本号加 1,修订号归 0
npm version patch  # 修订号加 1
npm publish