简单的将所有的 js 文件统统放在一起,然后通过 <script> 标签引入。
优点: 相比于使用一个js文件,这种多个js文件实现最简单的模块化的思想是进步的。 缺点: 污染全局作用域。因为每一个模块都是暴露在全局的,简单的使用,会导致全局变量命名冲突,当然,我们也可以使用命名空间的方式来解决。对于大型项目,各种js很多,开发人员必须手动解决模块和代码库的依赖关系,后期维护成本较高。依赖关系不明显,不利于维护。 比如 main.js 需要使用 jquery,但是,从上面的文件中,我们是看不出来的,如果 jquery 忘记了,那么就会报错。 <!-- 页面内嵌的脚本 --> <script type="application/javascript"> // module code </script> <!-- 外部脚本 --> <script type="application/javascript" src="path/to/myModule.js"> </script>上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此 type=“application/javascript” 可以省略。
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到 <script> 标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。浏览器允许脚本异步加载,下面就是两种异步加载的语法。 <script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>上面代码中,<script> 标签打开 defer 或 async 属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer 与 async 的区别是: defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染一句话,defer 是 “渲染完再执行”,async 是 “下载完就执行”如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的上面代码中,变量 x 和函数 addX,是当前文件 example.js 私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为 global 对象的属性。 global.warning = true上面代码的 warning 变量,可以被所有文件读取。当然,这样写法是不推荐的。
CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。 // example.js var x = 5; var addX = function (value) { return value + x; }; module.exports = { x: x, addX: addX };上面代码通过 module.exports 输出变量 x 和函数 addX。
require 方法用于加载模块 var example = require('./example.js'); console.log(example.x); // 5 console.log(example.addX(1)); // 6CommonJS 规范特点
所有代码都运行在模块作用域,不会污染全局作用域CommonJS 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存CommonJS 模块加载的顺序,按照其在代码中出现的顺序由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用优点:
CommonJS 规范在服务器端率先完成了 JavaScript 的模块化,解决了依赖、全局变量污染的问题,这也是 js 运行在服务器端的必要条件。缺点:
由于 CommonJS 是同步加载模块的,在服务器端,文件都是保存在硬盘上,所以同步加载没有问题,但是对于浏览器端,需要将文件从服务器端请求过来,那么同步加载就不适用了,所以,CommonJS 是不适用于浏览器端的。上面模块会在加载后1秒后,发出 ready 事件。其他文件监听该事件,可以写成下面这样。
var a = require('./a'); a.on('ready', function() { console.log('module a is ready'); });造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
exports.area = function (r) { return Math.PI * r * r; }; exports.circumference = function (r) { return 2 * Math.PI * r; }; 注意,不能直接将 exports 变量指向一个值,因为这样等于切断了 exports 与 module.exports 的联系。 exports.hello = function() { return 'hello'; }; module.exports = 'Hello world';上面代码中,hello 函数是无法对外输出的,因为 module.exports 被重新赋值了
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用 exports 输出,只能使用 module.exports 输出,如下。 module.exports = function (x) { console.log(x); };Node 使用 CommonJS 模块规范,内置的 require 命令用于加载模块文件
require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。如果没有发现指定模块,会报错。 // example.js var invisible = function () { console.log("invisible"); } exports.message = "hi"; exports.say = function () { console.log(message); }运行以下命令输出 exports 对象
var example = require('./example.js'); example // { // message: "hi", // say: [Function] // } 如果模块输出的是一个函数,那就不能定义在 exports 对象上面,而要定义在 module.exports 变量上面。 module.exports = function () { console.log("hello world"); }; require('./example2.js')();这样设计的目的是,使得不同的模块可以将所依赖的模块本地化
在目录中放置一个 package.json 文件,并且将入口文件写入 main 字段。下面是一个例子。
// package.json { "name" : "some-library", "main" : "./lib/some-library.js" } require 发现参数字符串指向一个目录以后,会自动查看该目录的 package.json 文件,然后加载 main 字段指定的入口文件。如果 package.json 文件没有 main 字段,或者根本就没有 package.json 文件,则会加载该目录下的 index.js 文件或 index.node 文件。上面代码中,连续三次使用 require 命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个 message 属性。但是第三次加载的时候,这个 message 属性依然存在,这就证明 require 命令并没有重新加载模块文件,而是输出了缓存。
如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次 require 这个模块的时候,重新执行一下输出的函数。所有缓存的模块保存在 require.cache 之中,如果想删除模块的缓存,可以像下面这样写。
// 删除指定模块的缓存 delete require.cache[moduleName]; // 删除所有模块的缓存 Object.keys(require.cache).forEach(function(key) { delete require.cache[key]; }); 注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,require 命令还是会重新加载该模块。可以将 NODE_PATH 添加到 .bashrc。
export NODE_PATH="/usr/local/lib/node"所以,如果遇到复杂的相对路径,比如下面这样。
var myModule = require('../../../../lib/myModule'); 有两种解决方法,一是将该文件加入 node_modules 目录,二是修改 NODE_PATH 环境变量,package.json 文件可以采用下面的写法。 { "name": "node_path", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "NODE_PATH=lib node index.js" }, "author": "", "license": "ISC" } NODE_PATH 是历史遗留下来的一个路径解决方案,通常不应该使用,而应该使用 node_modules 目录机制。上面代码是三个 JavaScript 文件。其中,a.js 加载了 b.js,而 b.js 又加载 a.js。这时, Node 返回 a.js 的不完整版本,所以执行结果如下。
$ node main.js b.js a1 a.js b2 main.js a2 main.js b2修改 main.js,再次加载 a.js 和 b.js。
// main.js console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x); console.log('main.js ', require('./a.js').x); console.log('main.js ', require('./b.js').x);执行上面代码
$ node main.js b.js a1 a.js b2 main.js a2 main.js b2 main.js a2 main.js b2 上面代码中,第二次加载 a.js 和 b.js 时,会直接从缓存读取 exports 属性,所以 a.js 和 b.js 内部的 console.log 语句都不会执行了。直接执行的时候(node module.js),require.main 属性指向模块本身。
require.main === module // true调用执行的时候(通过 require 加载该脚本执行),上面的表达式返回 false 。
上面代码输出内部变量 counter 和改写这个变量的内部方法 incCounter。然后,在 main.js 里面加载这个模块。
// main.js var com = require('./lib') console.log(com.counter) // 3 com.incCounter() console.log(com.counter) // 3上面代码说明,lib.js 模块加载以后,它的内部变化就影响不到输出的 com.counter 了。这是因为 com.counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
// lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, };上面代码中,输出的 counter 属性实际上是一个取值器函数。现在再执行 main.js ,就可以正确读取内部变量 counter 的变动了。
$ node main.js 3 4上面的第 4 步,采用 module.compile() 执行指定模块的脚本,逻辑如下。
Module.prototype._compile = function(content, filename) { // 1. 生成一个require函数,指向module.require // 2. 加载其他辅助方法到require // 3. 将文件内容放到一个函数之中,该函数可调用 require // 4. 执行该函数 };上面的第 1 步和第 2 步, require 函数及其辅助方法主要如下。
require(): 加载外部模块 require.resolve():将模块名解析到一个绝对路径 require.main:指向主模块 require.cache:指向所有缓存的模块 require.extensions:根据文件的后缀名,调用不同的执行函数一旦 require 函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括 require、module、exports ,以及其他一些参数。
(function (exports, require, module, __filename, __dirname) { // YOUR CODE INJECTED HERE! }); Module._compile 方法是同步执行的,所以 Module._load 要等它执行完成,才会向用户返回 module.exports 的值对于外部的模块脚本(上例是 foo.js ),有几点需要注意。
代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。模块脚本自动采用严格模式,不管有没有声明 use strict 。模块之中,可以使用 import 命令加载其他模块( .js 后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用 export 命令输出对外接口。模块之中,顶层的 this 关键字返回 undefined ,而不是指向 window 。也就是说,在模块顶层使用 this 关键字,是无意义的。同一个模块如果加载多次,将只执行一次。示例
import utils from 'https://example.com/js/utils.js'; const x = 1; console.log(x === window.x); //false console.log(this === undefined); // true利用顶层的 this 等于 undefined 这个语法点,可以侦测当前代码是否在 ES6 模块之中。
const isNotModuleScript = this !== undefined;以之前 2-CommonJS 规范中的例子为例
// 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再举一个例子
// m1.js export var foo = 'bar'; setTimeout(() => foo = 'baz', 500); // m2.js import {foo} from './m1.js'; console.log(foo); setTimeout(() => console.log(foo), 500);