node.js 模块化的几种方法

本文详细介绍了Node.js后端开发中的模块化,包括CommonJS、ES6模块和AMD(异步模块定义)的原理与区别。CommonJS是同步加载,适合服务器环境;ES6模块则是静态加载,支持编译时加载,避免了命名冲突;AMD主要应用于前端,如RequireJS,采用异步加载方式。
摘要由CSDN通过智能技术生成

node.js 后端之路

写在开始: node 后端工作多年, 积累了一些经验, 可比较散乱, 想在这里起一个系列文章, 把这些知识点细细理一遍, 查漏补缺. 基础牢固了才能够继续进步. 个人文档, 思路比较意识流.



前言

为什么需要模块化? 它解决了什么问题?
一切都是从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


总结

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wlpsjgs

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值