node.js 后端之路
写在开始: node 后端工作多年, 积累了一些经验, 可比较散乱, 想在这里起一个系列文章, 把这些知识点细细理一遍, 查漏补缺. 基础牢固了才能够继续进步. 个人文档, 思路比较意识流.
node.js 模块化的几种模式
前言
为什么需要模块化? 它解决了什么问题?
一切都是从Google公司发明了 chrome 开始的, chrome 极大的提升了 js 的效率, 这让一个页面可以加载的 js 代码的行数爆炸式的增长了. 这就造成了一个问题: 不同公司和个人书写的代码块都运行在一个 js 上下文中, 它们可能就会有命名冲突, 从而造成运行错误.
这就需要有一种解决方案, 可以实现大规模的 js 集成; 可以让不同公司的代码各自运行在自己的一个私有上下文中, 这些代码可以方便的相互依赖, 但不会造成命名冲突.
以上说的情形虽然是前端浏览器, 但后端的 node.js 其实脱胎于 chrome 的 v8 引擎, 你可以理解它是一个无头浏览器, 所以对于 node.js 后端开发, 面临的问题是一样的.
一、CommonJS
node.js 默认的模块化方案:
// hello.js
exports.sayHello = function(name) {
console.log('Hello' + name);
};
// index.js
const { sayHello } = require('./hello.js');
sayHello('Jack');
网上讲 module exports, require 导出的是复制不是引用的文章很多. 这里我们换个思路: 假设 require 是由下面的代码实现的, 就容易理解多了. 代码是示意作用, 别在意细节.
const ModuleMap = new Map();
/**
* 这里我们假定全局的 require 是这样实现的
*/
function require(filePath) {
const absPath = getAbsoultPath(filePath); // 伪代码
const m = ModuleMap.get(absPath); // 真实 node.js 不一定是用绝对路径做 key 的, 这里只是示意
// 如果模块已经加载了, 那么返回缓冲的实例
if (m) {
return m.exports;
}
// 加载新的模块
const body = readFileFromDisk(filePath); // 伪代码
const moduleFunction = new Function('exports, require, module, __filename, __dirname', body);
const module = { exports: {} };
const __filename = 'theFileNameYouAreRequiring'; // 文件名, 例如 hello.js
const __dirname = 'fileDir'; // 文件路径
// moduleFunction 是拼接出来的一个函数, 其中函数的 body 就是你写的模块文件的内容.
// moduleFunction 执行后模块导出的结果一定会绑定到 module.exports, 所以此函数不需要返回值
moduleFunction(module.exports, require, module, __filename, __dirname);
ModuleMap.set(absPath, module);
return module.exports
}
// index.js
const { sayHello } = require('./hello.js');
sayHello('Jack');
看过上面的代码, 你就理解了 module.exports = xxx 是替换掉了默认的 exports. 模块加载其实就是构造了一个闭包, 把你自己的代码放在这个闭包内, 这样就可以隔绝不同模块之间的相互影响了. 模块的导出值就是 require 函数调用后绑定在 module.exports 上的属性, require 执行完毕后, module.exports 就不会改变了, 所以这些属性只可能是复制. 其他模块通过下面的方式引入这些属性时会再次生成拷贝, 例如:
// myMath.js
exports.PI = 3.1415;
// main.js
const { PI } = require('./myMath');
// PI 在 main.js 的上下文再次复制, 它相当于:
// const MyMath = require('./myMath');
// const PI = MyMath.PI;
// 即使你执行 MyMath.PI = 3; 当前的 PI 也没法改变了
二、ES6模块
CommonJS 方案走在了时代的前面. CommonJS 由 Mozilla 工程师 Kevin Dangoor 于 2009 年 1 月创立,最初命名为ServerJS。2009 年 8 月,该项目更名为CommonJS。旨在解决 Javascript 中缺少模块化标准的问题。Node.js 后来也采用了 CommonJS 的模块规范。由于 CommonJS 并不是 ECMAScript 标准的一部分,所以 类似 module 和 require 并不是 JS 的关键字,仅仅是对象或者函数而已,意识到这一点很重要。
js 语言的后续版本 ES6 从语言层面上支持了模块(模块就是代码可以写在多个文件中). 这里我们只列举 ES6 模块化同 CommonJS 的不同即可.
1.加载时间不同
CommonJS 是运行时加载, ES6 模块是编译时加载的. CommonJS 其实就是函数调用, 所以你动态的加载所需模块, 动态拼接路径. 也可以在代码的任意位置运行 require . 例如:
function loadImplement(name) {
return require(`./${name}`);
}
const publicImpl = require('local');
但 ES6 import 时不能添加 if else 条件, 且只能在文件的头部
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
2.拷贝和引用的区别
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。原因:CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
CommonJS:
// lib.js
let counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
const mod = require('./lib');
console.log(mod.counter); // 3, mod 其实是一个 json, 在执行 module.exports 时就已经确定了
mod.incCounter();
console.log(mod.counter); // 3 lib.js 上下文的 counter 改变了, 但 module.exports 已经确定了幷不受影响
ES6模块:
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
//ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。
三、AMD模块化
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD多用在前端, 因为后端代码加载是同步的, 文件系统的访问速度和可靠性都远大于通过网络加载的
以 requirejs 为例:
/**
* require使用指南!
* 第一步:建立一个符合Require CMD模块化的标准目录结构,去官方查看!
* 第二步:在html文件中指定主js文件:<script data-main="./my_modules/app.js" src="./lib/require.js"></script>
* 第三步:配置requirejs.config路径;
* 第四步:每一个文件都是一个模块对象,默认对象名就是文件名,要依赖哪个模板就difine(["模块名1","模块名"2...],回调函数);
*
*/
requirejs.config({
baseUrl:'./',
// 注:路径后面不能加.js,因为系统自动加上.js的。这里给本地文件起了别名, 例如 require001
paths: {
require001:'my_modules/require001',
require002:'my_modules/require002',
jquery:'lib/jquery-3.3.1'
}
});
// 入口逻辑, 格式 requirejs([依赖模块名称数组], function(依赖模块加载后的exports数组,顺序同前面定义的依赖数组的顺序相同) {
...
})
requirejs(['require001','require002', 'jquery'], function (require001,require002,$) {
require001.test001();
require002.test002();
$("body").css("background","red");
});
requirejs 模块定义:
// package/lib 是依赖模块的名字. function(lib) 这里的 lib 是 package/lib 加载后的对象
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
参考
山水有轻音
链接:https://juejin.cn/post/7000168161370701837
比特币爱好者007
链接:https://blog.csdn.net/weixin_43343144/article/details/85329875