一、思考如下的代码潜在的问题

Node - 004 - 模块化 - 图1

  • 变量污染
  • 代码复用性不高
  • 代码可维护性不高
  • 依赖关系管理不方便

二、模块化

1. 什么是模块化

一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块。

2. 模块化带来的好处

  • 避免变量污染
  • 提供代码的复用率
  • 提高代码的可维护性
  • 依赖关系清晰

三、前端模块化的发展过程

四、规范与实现

规范与实现是一个相互促进的关系:
Node - 004 - 模块化 - 图2

  • NodeJs - CommonJs规范
  • RequireJS - AMD规范
  • SeaJS - CMD规范


五、CommonJS规范

参考阮一峰大神文章

1. 概述

每个文件就一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其它文件不可见。

CommonJS规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports)是对外的接口。加载某个模块,其实就是加载该模块的 module.exports 属性。

require方法用于加载模块。

CommonJS模块的特点如下:

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

    2. module对象

    每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性:
    • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
    • module.filename 模块的文件名,带有绝对路径。
    • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
    • module.parent 返回一个对象,表示调用该模块的模块。
    • module.children 返回一个数组,表示该模块要用到的其他模块。
    • module.exports 表示模块对外输出的值。

module.exports

module.exports 属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取 module.exports

exports

每个模块还提供有一个 exports 变量,指向 module.exports 。这等于在每个模块头部,有一行隐藏的如下代码:

  1. var exports = module.exports;

造成的结果是,在对外暴露模块接口时,可以直接向 exports 添加属性和方法。

  1. var name = '张三';
  2. exports.name = name;
  3. exports.sayHi = () => {
  4. console.log(`Hello, My Name is ${name}`);
  5. }

注意,不能直接将exports变量指向一个值,因为这样等于切断了 exportsmodule.exports 的联系。

  1. // a.js 切断了与module.exports的联系
  2. exports = function(x) {console.log(x)};
  3. // b.js hello方法没有被暴露,因为module.exports重新赋值了
  4. exports.hello = function() {
  5. return 'hello';
  6. };
  7. module.exports = 'Hello world';

如果你觉得,exportsmodule.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用 exports,只使用 module.exports

3. require命令

1. 基本用法

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的module.exports对象。如果没有发现指定模块,会报错。

2. 加载规则

  • 后缀名默认为 .js

    1. var foo = require('foo');
    2. // 等同于
    3. var foo = require('foo.js');
  • 如果参数字符串以“/”开头(linux)或者以 “D:/”盘符开头(windows),则表示加载的是一个位于绝对路径的模块文件。比如,require(‘/home/marco/foo.js’) 将加载 /home/marco/foo.js

  • 如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require(‘./circle’) 将加载当前脚本同一目录的 circle.js
  • 如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
  • 如果参数字符串不以“./“或”/“开头,而且是一个路径,比如 require(‘example-module/path/to/file’),则将先找到 example-module 的位置,然后再以它为参数,找到后续路径。
  • 如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

3. 目录的加载规则

通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让 require 方法可以通过这个入口文件,加载整个目录。

在目录中放置一个 package.json 文件,并且将入口文件写入 main 字段。下面是一个例子。

  1. // package.json
  2. {
  3. "name" : "some-library",
  4. "main" : "./lib/some-library.js"
  5. }

require 发现参数字符串指向一个目录以后,会自动查看该目录的 package.json 文件,然后加载 main 字段指定的入口文件。如果 package.json 文件没有 main 字段,或者根本就没有 package.json 文件,则会加载该目录下的index.js 文件 或者 index.json 文件 或者 index.node 文件

4. 模块的加载机制

CommonJS 模块的加载机制是,输出拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。

  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4. counter++;
  5. }
  6. module.exports = {
  7. counter: counter,
  8. incCounter: incCounter,
  9. };

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。
然后,加载上面的模块。

  1. // main.js
  2. var counter = require('./lib').counter;
  3. var incCounter = require('./lib').incCounter;
  4. console.log(counter); // 3
  5. incCounter();
  6. console.log(counter); // 3

上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。