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

- 变量污染
- 代码复用性不高
- 代码可维护性不高
- 依赖关系管理不方便
二、模块化
1. 什么是模块化
一个模块就是一个实现特定功能的文件,有了模块我们就可以更方便的使用别人的代码,要用什么功能就加载什么模块。
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 。这等于在每个模块头部,有一行隐藏的如下代码:
var exports = module.exports;
造成的结果是,在对外暴露模块接口时,可以直接向 exports 添加属性和方法。
var name = '张三';exports.name = name;exports.sayHi = () => {console.log(`Hello, My Name is ${name}`);}
注意,不能直接将exports变量指向一个值,因为这样等于切断了 exports 与 module.exports 的联系。
// a.js 切断了与module.exports的联系exports = function(x) {console.log(x)};// b.js hello方法没有被暴露,因为module.exports重新赋值了exports.hello = function() {return 'hello';};module.exports = 'Hello world';
如果你觉得,exports 与 module.exports 之间的区别很难分清,一个简单的处理方法,就是放弃使用 exports,只使用 module.exports。
3. require命令
1. 基本用法
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的module.exports对象。如果没有发现指定模块,会报错。
2. 加载规则
后缀名默认为 .js
var foo = require('foo');// 等同于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 字段。下面是一个例子。
// package.json{"name" : "some-library","main" : "./lib/some-library.js"}
require 发现参数字符串指向一个目录以后,会自动查看该目录的 package.json 文件,然后加载 main 字段指定的入口文件。如果 package.json 文件没有 main 字段,或者根本就没有 package.json 文件,则会加载该目录下的index.js 文件 或者 index.json 文件 或者 index.node 文件。
4. 模块的加载机制
CommonJS 模块的加载机制是,输出拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。
// lib.jsvar counter = 3;function incCounter() {counter++;}module.exports = {counter: counter,incCounter: incCounter,};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。
然后,加载上面的模块。
// main.jsvar counter = require('./lib').counter;var incCounter = require('./lib').incCounter;console.log(counter); // 3incCounter();console.log(counter); // 3
上面代码说明,counter输出以后,lib.js模块内部的变化就影响不到counter了。
