模块
- es6Module 静态导入 在编译的时候就可以知道使用了哪些变量 可以实现tree-shaking
- commonjs 动态导入 不支持tree-shaking
commonjs规范
- 想使用哪个模块就require谁 (后缀可以省略,默认会查找.js文件,没有js找.json)
- 想被别人使用需要导出 module.exports
- 在node中每一个js/json文件就是一个模块
- 一个包中包含多个模块(每个包都必须配置一个package.json文件)
原理: 读取文件 => 包装自执行函数,设置参数 => 默认返回module.exports对象
运行流程分析
let a = require("./del");
- mod.require(path) -> Module.prototype.require
- Module._load 加载模块
- Module._resolveFilename 方法就是把路径变成绝对路径 添加后缀名
- 实现模块的缓存(根据绝对路径进行模块的缓存)
- module.exports 会在第一次缓存起来(导出的结果如果是一个对象内部属性变了会跟着变, 普通值不会变)后续再去使用的话会取上次的返回值
- es6模块使用export {} 导出的是一个变量,如果内部对应的值发生变化 导出的是会跟着变的
- 会尝试加载是不是一个原生模块,如果带相对路径或者绝对路径就不是核心模块
- 拿到绝对路径,创造一个模块 new Module。 几个重要的属性: this.id this.exports = {} this.path 父路径
- module.load 对模块进行加载
- 根据文件后缀 去做策略加载
- 用的是同步读取
- 增加一个函数的壳子,并让函数执行,让 module.exports 作为了 this
- 用户会默认拿到 module.exports 的返回值
- 最终返回的是 exports 对象
node 中 commonjs 规范的简单实现
const fs = require('fs');
const path = require('path');
const vm = require('vm');
function Module(id) {
// 传入的id是文件的绝对路径
this.id = id;
this.exports = {};
}
Module._cache = {};
Module._extensions = {
'.js'(module) {
// 读取脚本
let script = fs.readFileSync(module.id, 'utf8');
// 包装函数,生成字符串
let template = `(function(exports, module, require, __dirname, __filename){${script}})`;
// 字符串变成函数
let fn = vm.runInThisContext(template); // 相当于 new Function
// 函数执行,将this指向module.exports
let exports = module.exports;
let thisValue = exports; // this = module.exports = exports
let filename = module.id;
let dirname = path.dirname(filename);
// 函数执行,调用了模块,也就完成了 module.exports的赋值
fn.call(thisValue, exports, module, req, dirname, filename);
},
'.json'(module) {
// 获取文件内容
let script = fs.readFileSync(module.id, 'utf8');
// 导出
module.exports = JSON.parse(script);
},
};
// 返回require文件的绝对路径
Module._resolveFilename = function (id) {
let filePath = path.resolve(__dirname, id);
console.log(filePath, 'filePath');
// 通过判断filePath这个路径存不存在来看传入的路径有没有加后缀
let isExists = fs.existsSync(filePath);
if (isExists) return filePath;
// 不存在 则尝试添加后缀
let keys = Reflect.ownKeys(Module._extensions); // 拿到所有的key
for (let i = 0; i < keys.forEach.length; i++) {
let newPath = filePath + keys[i];
if (fs.existsSync(newPath)) return newPath;
}
throw new Error('module not found');
};
// 加载模块,让用户给module.exports赋值
Module.prototype.load = function () {
// 不需要传任何参数,this就指向创建的module实例
// 先取文件后缀名
let ext = path.extname(this.id);
// 根据后缀名,采用不同的策略去加载
Module._extensions[ext](this);
};
/*
自实现的require方法
filename是传入的文件路径
*/
function req(filename) {
// 返回绝对路径,并且加上后缀
filename = Module._resolveFilename(filename);
if (Module._cache[filename]) {
return Module._cache[filename].exports;
}
// 创造一个模块
const module = new Module(filename);
// 增加缓存
Module._cache[filename] = module;
// 对模块进行加载
module.load(); // 就是让用户给module.exports赋值
// 最终导出module.exports
return module.exports; // 默认是空对象
}
// let a = require("./del");
let a = req('./del');
console.log(a, '输出');
⚠️注意点
this = module.exports = exports 指向的是同一个引用地址 如果将exports更改了 module.exports不会变
- 导出的时候 module.exports = "aaa"
- 不能 exports = "aaa";
- 但是可以这样导出 exports.a = "111" this.b = "222";(因为没有改变引用地址,和 module.exports 指向同样的引用地址)
- 小结:最终用户使用的结果都是来自于
module.exports; 不要同时使用exports和module.exports否则会以module.exports结果为基准
npm
- 全局模块:安装在电脑的 npm 下,仅仅在命令行中使用,做一些工具
全局安装的模块只能在命令行中使用 npm 默认在电脑的环境变量里,所以可以直接使用。安装的全局模块都在 npm 下生成了一个快捷方式,所以也可以直接用
- 默认会安装在c盘下的npm/node_modules, npm root -g 可查看;npm i xxx -g安装完成后除了将文件放到全局目录中之外,还会默认生成软链(快捷命令)
- 全局安装的模块包为什么要放到npm目录下:因为npm是添加在系统环境变量里的,所以当执行模块包的命令的时候,会去npm目录下查找
自实现一个全局模块
- 包必须要初始化 npm init
- 现在需要让这个包运行在当前的命令行内,需要在package.json中配置“bin”
// package.js
{
"name": "zhuzhu",
"version": "1.0.0",
"description": "",
"main": "index.js",
"bin": "./1.js"
}
// 表示当在命令行执行zhuzhu这个命令,实际调用的就是"./1.js"这个文件
- 希望把这个包放到全局安装npm目录下:在开发时可以临时将这个包放到全局下,通过npm link命令创建软链
- 在bin指定的文件1.js头部加上
#! /usr/bin/env node,这个话的含义是告诉在命令行执行“zhuzhu”命令的时候不是把这个文件打开,而是用系统的环境变量node来执行文件里的内容 - (需要重新npm link(npm link --force)一下,才能生效)
- 真正使用的时候会将包发布到npm上,就可以通过 npm i xxx -g来正常使用
nrm
- 切换源
- nrm use cnpm/taobao/npm
nvm
- 切换node版本号
依赖
- devDependencies
- dependencies
- peerDependencies 依赖的模块
- optionalDependencies
- bundleDependencies
- npm pack 打包成压缩包
- package.json 中配置:"bundleDependencies":["bootstrap"] 压缩打包的时候就会把 node_modules bootstrap 打包进去
命令执行问题 -- 将模块安装到项目里,该如何使用
- 如webpack 一般安装本地 不安装全局,因为安装到全局每个人用到的webpack版本可能不一样,安装到本地可以锁定webpack的版本,所以是为了保证版本一致所以安装到本地
- 在本地安装mime
- 配置"script": {"mime":"mime a.js"}
- npm run mime 可以执行 在命令行里直接 mime 却不可以
- npm run 时把当前项目下的node_modules/.bin目录放到了环境变量里,所以就可以间接执行配置了的mime a.js 如果直接用 npm run script 方式默认在执行命令之前会将其添加到全局的环境变量,所以可以使用,但是命令执行完毕之后会删除掉对应的 path
- npx 和 npm run 类似,可以 npx mime a.js 使用,但是一般用 npm run npx 的好处是: 如果模块不存在会先安装再使用,使用后自动删除掉 如果当前项目下有就直接复用,如果没有就先安装 好处:永远是最新的
版本号
- 管理的方式 semver major.minor.patch
- ^2.2.4 第一位只能是 2
- ~2.2.4 只能是2.2,第三位比4大都可以
- 特殊的
- alpha 内部测试
- beta 公测版本
- rc 最终测试版本
- 最后发布正式版本
- 包的发布
- 看包是否重名
- 切换到本地 npm 源
- 登录账号 发布
模块查找机制
- 一般 require 都是相对路径
- 会判断路径是不是核心模块/或者第三方模块,是核心模块就不做后面的事情了
- 默认先查找同名文件,如果没找到尝试添加.js/.json 后缀查找文件
- 如果没有,查找同名文件夹(当成一个包),查找 package.json(找设置的入口文件), 如果没有就找 index.js
- 如果没有就报错
- 第三方模块
- 默认会沿着当前目录向上查找,查找 node_modules 下的同名文件夹,根据(package.json 中的 main)-> index.js 中查找
- 如果没有找到向上查找 查找上级的 node_modules
- 如果到根路径还没有找到就报错了