历史
我们都知道,js在刚被创建的时候,只是为了在网页上写一些小脚本而已,比如网页特效,表单验证等等,创立者也许没觉悟到以后的js会发展到如此规模。这是web1.0时代。
在web 2.0时代,各种前端库,前端框架被开发出来,jquery,angular就是代表。此时js的功能也就从写写小特效啥的跃迁到了应用开发的级别上。可以说,js经历了工具类库,组件库,前端框架,前端应用的变迁。
于是在越来越广泛的应用中,js暴露了它先天就缺乏的一项功能:模块。在其它高级语言,都有模块的定义,java有类,python有import机制,ruby有require,php有require和include。而js此时还单纯的用script标签引入,用命名空间来约束代码,杂乱无章,于是,commonjs规范便应运而出。
commonJS的模块规范
commonjs规范的出发点就是让js在任何地方都能运行。它弥补了js此时的几点缺陷:
- 没有模块概念
- 标准库较少
- 没有标准接口
- 没有包管理系统
当然,如今commonjs规范已经解决了大部分问题,而且涵盖了模块,二进制,Buffer,字符集编码,I/O流,单元测试,web服务网关接口,包管理,等。
commonjs对模块的定义很简单,包含了模块定义,模块引用,模块标识3个部分。
- 模块引用
var xx = require('xxx')
关键字require来接受模块标识,引入这个模块的API到上下文中。
- 模块定义
一个例子来解释:
//add.js
function add(a,b){
return a+b;
}
// 这样导出的 add是作为 exports 的一个方法被导出的
exports.add = add;
// main.js
var Add = require('add');
console.log(Add.add(1,2));
//Add是require引入的模块名,add是方法名。
node在编译的时候,会把代码封装成如下的样子
// require 是对 Node.js 实现查找模块的 Module._load 实例的引用
// __finename 和 __dirname 是 Node.js 在查找该模块后找到的模块名称和模块绝对路径
(function(exports,require,module,__filename,__dirname){
function add (a,b){
return a+b;
}
exports.add = add;
})
为了将函数直接导出成一个模块,而不是一个方法,用到了全局变量module,下面就是我们常见的样子了:
// add.js
function add (a,b){
return a+b ;
}
module.exports = add;
// main.js
var add = require('add');
console.log(add(1,2));
- 模块标识
模块标识就是require()里的参数,必须是小驼峰命名的字符串,或者是路径(./ 或../)。可以没有后缀.js
Node的模块机制
Node并不是完全按照commonjs规范来实现,而是进行了一些取舍,并增加了自己的一些特性。
Node中引入模块,经历3个步骤:
- 路径分析
- 文件定位
- 编译执行
Node中模块分为2种,核心模块(Node提供的)和文件模块(用户自己编写的)
Node对引入过的模块会进行缓存,就像前端浏览器会缓存静态脚本来提高性能一样。require()方法在对同一模块的二次加载一律采用缓存优先的方式。但是对核心模块的缓存检查优先于对文件模块的缓存检查。
- 路径分析
就是对模块标识的分析呗。
模块标识符在Node中分以下几类:
- 核心模块,如http,path
- ./ ../相对路径
- / 绝对路径
- 非路径形式的文件模块。
- 文件分析
如果模块标识符没有后缀,默认补上后缀从.js,.json,.node来次序查找。
- 模块编译
js编译上面已经提到了。Node对JS文件进行了包装,在头部添加了(function (exports, require, module, __filename, __dirname) {...})。这样每个模块都进行了作用域隔离。包装之后通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局)返回一个function。然后将上述参数传给这个function执行。
json编译更简单,Node直接用JSON.parse()方法编译json内容,得到的对象赋给exports。
包和NPM
commonjs的包规范包含2个组成部分,包结构和包描述文件
包描述文件:package.json
包结构:
- package.json 包描述文件
- bin: 存放可执行二进制文件的目录
- lib 存放js代码的目录
- doc. 存放文档的目录
- test: 存放单元测试用例的代码
NPM的用法就不多说了。
前后端公用模块
自从Node出来以后,js也可以运用在后端。但是前后端的JS扮演的角色不同,浏览器端的js需要经历从同一个服务器分发到多个客户端执行。服务端的js则是相同的代码多次执行。前者的瓶颈在于带宽。后者的瓶颈在于CUP和内存。前者需要通过网络加载。后者从磁盘中加载。
因为Node基于commonjs规范来同步的加载模块的。前端若用同步方式来加载模块,在用户体验上会造成很大的问题。UI在初始化的时候需要等待很长时间来加载js脚本。所以提出了异步模块定义AMD和CMD。这在我的另一篇博客中有提到。就不多说了。
为了写个能兼容前后端的模块规范,类库的开发者要把代码包装在一个 闭包里。这样就能兼容Node,AMD,CMD和常见的浏览器。