目录

3Node.js的模块管理下

上一节课我们说过,Node.js 诞生的时候,模块管理采用CommonJS规范。CommonJS规范是一套和ES Modules不同的模块管理方式。因为 Node.js 的ES Modules还处在实验特性阶段,大部分 Node 模块还是用CommonJS规范实现的。

这一节课我们就来看一下,如果用CommonJS规范实现ziyue模块该怎么做。

我们还是先写一个ziyue.js文件,内容如下:

const ziyue = (text) => `
                 __._                                   
                / ___)_                                 
               (_/Y ===\\                                __ 
               |||.==. =).                                | 
               |((| o |p|      |  ${text}
            _./| \\(  /=\\ )     |__                    
          /  |@\\ ||||||||.                             
         /    \\@\\ ||||||||\\                          
        /   \\  \\@\\ ||||||//\\                        
       (     Y  \\@\\|||| // _\\                        
       |    -\\   \\@\\ \\\\//    \\                    
       |     -\\__.-./ //\\.---.^__                        
       | \\    /  |@|__/\\_|@|  |  |                         
       \\__\\      |@||| |||@|     |                    
       <@@@|     |@||| |||@|    /                       
      / ---|     /@||| |||@|   /                                 
     |    /|    /@/ || |||@|  /|                        
     |   //|   /@/  ||_|||@| / |                        
     |  // \\ ||@|   /|=|||@| | |                       
     \\ //   \\||@|  / |/|||@| \\ |                     
     |//     ||@| /  ,/|||@|   |                        
     //      ||@|/  /|/||/@/   |                        
    //|   ,  ||//  /\\|/\\/@/  / /                      
   //\\   /   \\|/  /H\\|/H\\/  /_/                     
  // |\\_/     |__/|H\\|/H|\\_/                         
 |/  |\\        /  |H===H| |                            
     ||\\      /|  |H|||H| |                            
     ||______/ |  |H|||H| |                             
      \\_/ _/  _/  |L|||J| \\_                          
      _/  ___/   ___\\__/___ '-._                       
     /__________/===\\__/===\\---'                     
                                                        
`;

module.exports = ziyue;

然后写一个index.js文件:

const ziyue = require('./ziyue');

const argv = process.argv;
console.log(ziyue(argv[2] || '有朋自远方来,不亦乐乎!'));

运行node index.js就可以得到我们要的输出了。可以看到,实际上CommonJSES Modules类似,和ES Modules不同的地方是,CommonJS采用module.exports/requireES Modules则采用export/import

同样,CommonJS 规范可以导出多个接口:

// abc.js
const a = 1;
const b = 2;
const c = () => a + b;

module.exports = {
  a, b, c
};
const {a, b, c} = require('./abc.js');
console.log(a, b, c()); // 1 2 3

ES Modules不同的是,module.exports导出的是真正的对象,所以我们可以这样给 API 别名:

// abc.js
const a = 1;
const b = 2;
const c = () => a + b;

module.exports = {
  d: a,
  e: b,
  f: c
};
const {d, e, f} = require('./abc.js'); // 这里要用d、e、f了
console.log(d, e, f()); // 1 2 3

在 CommonJS 规范中,有两种导出模块 API 的方式:module.exportsexports。这两个变量默认情况下都指向同一个初始值{}。因此,除了使用module.exports我们也可以使用exports变量导出模块的 API:

// abc.js
exports.a = 1;
exports.b = 2;
exports.c = () => a + b;

但是,这种用法不能module.exports混用。因为module.exports = 新对象改写了module.exports的默认引用,而引擎默认返回的是modeule.exports,从而导致exports指向的初始空间无效了。这样一来,原先用exports导出的那些数据就不会被导出了。

exports.a = 1;
exports.b = 2;
exports.c = () => a + b;
const d = 'foobar';
moudle.exports = {d};

上面的代码只导出了d:foobar而没有导出a、b、c,因为module.exports = {d}覆盖了默认的初始空间,这让之前 exports 变量上增加的属性(a、b、c)不会再被导出。

CommonJS规范有 1、2 两个版本,exports.属性名 = ...的用法属于早期,也就是 CommonJS 1 的用法。module.exports用法属于CommonJS 2的用法,我们应该尽量用module.exports,避免用exports.属性名 = 的写法。

ES Modules的向下兼容

在 Node.js 环境中,ES Modules 向下兼容 CommonJS,因此用import方式可以引入 CommonJS 方式导出的 API,但是会以 default 方式引入。

因此以下写法:

// abc.js 这是一个 CommonJS 规范的模块
const a = 1;
const b = 2;
const c = () => a + b;

module.exports = {a, b, c};

它可以用ES Modulesimport引入:

import abc from './test.js';
console.log(abc.a, abc.b, abc.c()); // 1 2 3

但是不能用

import {a, b, c} from './test.js';

因为 module.exports = {a, b, c} 相当于:

const abc = {a, b, c};
export default abc;

ES ModulesCommonJS的主要区别

ES ModulesCommonJS的主要有四个区别。第一个区别前面也提到过,如果要在导出时使用别名ES Modules要写成:

export {
  a as d,
  b as e,
  c as f,
}

而对应的CommonJS的写法是:

module.exports = {
  d: a,
  e: b,
  f: c,
}

第二个区别是,CommonJSrequire文件的时候采用文件路径,并且可以忽略 .js 文件扩展名。也就是说,require('./ziyue.js')也可以写成require('./ziyue')。但是,ES Modulesimport的时候采用的是 URL 规范就不能省略文件的扩展名,而必须写成完整的文件名import {ziyue} from './ziyue.mjs'.mjs的扩展名不能省略。

💡如果你使用 Babel 编译的方式将ES Modules编译成CommonJS,因为 Babel 自己做了处理,所以可以省略文件扩展名,但是根据规范还是应该保留文件扩展名。

第三个区别是,ES Modulesimportexport都只能写在最外层,不能放在块级作用域或函数作用域中。比如:

if(condition) {
  import {a} from './foo';
} else {
  import {a} from './bar';
}

这样的写法,在ES Modules中是不被允许的。但是,像下面这样写在CommonJS中是被允许的:

let api;
if(condition) {
  api = require('./foo');
} else {
  api = require('./bar');
}

事实上,CommonJS 的 require 可以写在任何语句块中。

第四个区别是,require 是一个函数调用,路径是参数字符串,它可以动态拼接,比如:

const libPath = ENV.supportES6 ? './es6/' : './';
const myLib = require(`${libPath}mylib.js`);

但是ES Modulesimport语句是不允许用动态路径的。

import() 动态加载

ES Modules不允许import语句用动态路径,也不允许在语句块中使用它。但是,它提供了一个异步动态加载模块的机制 —— 将import作为异步函数使用。

所以我们可以这样动态加载:

(async function() {
  const {ziyue} = await import('./ziyue.mjs');
  
  const argv = process.argv;
  console.log(ziyue(argv[2] || '巧言令色,鮮矣仁!'));
}());

同样地,我们也可以向下兼容地加载 CommonJS 模块,module.exports导出的对象会被作为default对象加载进来。

一般来说,在写 Node.js 模块的时候,我们更多采用静态的方式引入模块。但是动态加载模块在一些复杂的库中比较有用,尤其是跨平台开发中,我们可能需要针对不同平台的环境加载不同的模块,这个时候采用动态加载就是必须的了。

到这里,我们基本上介绍完了 Node.js 模块管理的 ES Modules 和 CommonJS 的基本用法,在后续的课程中,我们将会经常用到它们。关于 CommonJS 还有更多的用法,如果你想了解,可以参考Node.js 官方 API 文档

总结

这一节课,我们介绍了 CommonJS 规范创建和引入模块的语法。与 ES Module 语法不同,CommonJS 规范采用 module.export/require 的方式引出和引入模块。由于 ES Module 的向下兼容,遵循 CommonJS 规范的模块可以被 ES Module 规范以 default 方式引入。

此外,我们还要注意,CommonJS 和 ES Module 规范两者有四个不同:

  1. 重命名导出的公共 API 的方式不同;

  2. CommonJS 在require文件的时候可以忽略 .js 文件扩展名,而 ES Module 在import文件的时候不能忽略 .mjs 的扩展名;

  3. CommonJS 规范的require是可以放在块级作用域中的,而 ES Module 的import则不能放在任何语句块中;

  4. CommonJS 的require是一个函数,可以动态拼接文件路径,而ES Modulesimport语句是不允许用动态路径的。

虽然 ES Module 规范不允许import语句动态拼接路径,但是它允许import作为异步函数异步动态加载模块。