通过 Browserify 在浏览器中使用 NodeJS 模块

NodeJS 的开发人员已经习惯于使用 require 方法来加载其他模块。这种模块化的机制也为开发人员所熟悉。相对来说,在 Web 开发端的 JavaScript 的模块化管理则比较复杂,存在多种不同的规范和实践。Browerify 把 NodeJS 对模块的管理机制引入到了浏览器端,允许使用同样的方式来加载模块。如果应用的后台是基于 NodeJS 的,在前端采用 Browerify 可以统一模块的管理。即便后台不是基于 NodeJS,也可以复用已有的高质量的 NodeJS 模块。本文详细介绍 Browserify 的使用。

成 富, 软件工程师

成富,毕业于北京大学,获得计算机软件与理论专业硕士学位,曾经在 developerWorks 和 InfoQ 中文站上发表多篇技术文章。著有《深入理解 Java 7:核心技术与最佳实践》一书。个人网站是 http://www.midgetontoes.com。



2015 年 1 月 22 日

NodeJS 把 JavaScript 的使用从浏览器端扩展到了服务器端,使得前端开发人员可以用熟悉的语言编写服务器端代码。这一变化使得 NodeJS 很快就流行起来。在 NodeJS 社区中有非常多的高质量模块可以直接使用。根据最新的统计结果,NodeJS 的 npm 中的模块数量已经超过了 Java 的 Maven Central 和 Ruby 的 RubyGems,成为模块数量最多的社区。不过这些 NodeJS 模块并不能直接在浏览器端应用中使用,原因在于引用这些模块时需要使用 NodeJS 中的 require 方法,而该方法在浏览器端并不存在。Browserify 作为 NodeJS 模块与浏览器端应用之间的桥梁,让应用可以直接使用 NodeJS 中的模块,并可以把应用所依赖的模块打包成单个 JavaScript 文件。通过 Browserify 还可以在应用开发中使用与 NodeJS 相同的方式来进行模块化和管理模块依赖。如果应用的后台是基于 NodeJS 的,那么 Browserify 使得应用的前后端可以使用一致的模块管理方式。即便应用的后端不使用 NodeJS,Browserify 也可以帮助进行前端代码的复用和组织。

NodeJS 的模块管理

在介绍 Browserify 之前,有必要首先介绍一下 NodeJS 中的模块管理机制。NodeJS 使用的是 CommonJS 中定义的模块机制。NodeJS 中的模块系统非常简单。模块与 JavaScript 文件之间是一一对应的关系。每个 JavaScript 文件都是一个模块。在 JavaScript 文件中通过 exports 来声明该模块对外提供的接口,包括属性和方法。除了 exports 中包含的内容,JavaScript 文件中的其他变量都是私有的。这样可以实现很好的模块封装。模块的使用者通过 require 方法来加载其他模块,并使用该模块所提供的公开接口。

代码清单 1 中给出了名为 greet.js 的 JavaS 文件的内容。该文件中的模块对外提供了一个 greet 方法。

清单 1. NodeJS 模块定义示例
exports.greet = function(name) {
 return "Hello, " + name;
};

代码清单 2 中给出了如何使用该模块的示例。通过 require 方法来加载当前目录下的 greet.js 文件,并把模块提供的对象赋值给 greet 变量,再通过 greet.greet 调用模块提供的 greet 方法。

清单 2. NodeJS 模块的使用示例
var greet = require('./greet.js');
console.log(greet.greet('Alex'));

如果模块只提供一个方法,或者只需要通过一个赋值语句就可以提供一个完整的对象,可以直接赋值给 module.exports,如代码清单 3 所示。

清单 3. 使用 module.exports 声明模块
module.exports = function(name) {
 return "Hello, " + name;
}

加载模块使用的是 require 方法。如果 require 方法参数中的模块名称以“/”开头,则认为模块名称表示的是绝对路径。如果模块名称以“./”开头,则认为模块名称是相对于 require 方法调用所在文件的当前目录的相对路径。比如 require(‘./app/greet’)会加载调用 require 方法的文件所在目录的子目录 app 下面的 greet.js 文件。

如果模块名称不以“/”、“./”或“../”开头,并且不是 NodeJS 自带的模块,则会在 node_modules 目录中查找。在 node_modules 目录中进行查找是一个递归的过程,首先从当前模块的父目录开始,一直递归查找,直到文件系统根目录。

除了单个文件之外,也可以把模块相关的文件组织到一个目录中。只需要在目录中创建一个 package.json 文件,并指定模块主文件即可,如代码清单 4 所示。

清单 4. 模块目录中的 package.json 文件示例
{ 
 "name" : "greet",
 "main" : "./lib/greet.js" 
}

如果代码清单 4 中的 package.json 文件在 greet 目录中,当通过 require(“./greet”)来加载模块时,会加载 greet 目录下的 lib/greet.js 文件。如果要加载的模块目录下没有 package.json 文件,则会尝试加载默认的 index.js 文件。如果 index.js 文件不存在,则 require 方法会出错。


Browserify 基本使用

在介绍完 NodeJS 中的模块管理之后,下面介绍 Browserify 的基本用法。Browserify 本身也是一个 NodeJS 模块,通过 npm 进行安装。安装命令是“npm install -g browserify”。安装完成之后可以在命令行使用 browserify 命令。browserify 命令运行时以一个 JavaScript 文件作为输入,通过分析文件中对于 require 方法的调用来递归查找所依赖的其他模块。把输入文件所依赖的所有模块文件打包成单个文件并输出。如“browserify greet.js > bundle.js”把 greet.js 及其所依赖的模块文件打包成单个 bundle.js 文件。

变换处理模块

Browserify 支持在对模块的 JavaScript 文件进行合并之前进行变换处理。比如把 CoffeeScript 文件转换成 JavaScript 文件,或是使用正则表达式替换掉 JavaScript 文件中的某些内容。变换过程使用的是 NodeJS 中的流处理。把输入文件当成一个流,在该流上进行处理,处理之后的结果交由 Browserify 使用。可以同时进行多次变换处理,其效果相当于使用 Linux 操作系统上的管道操作。前一个变换处理的输出是下一个变换处理的输入。

比如变换处理模块 coffeeify 可以把 CoffeeScript 编译成 JavaScript。在运行 browserify 命令时通过“-t”参数来指定需要使用的变换,如“browserify -t coffeeify main.coffee > bundle.js”命令先通过 coffeeify 变换进行编译,再把依赖的模块打包。打包时的输入是 coffeeify 变换的输出。

除了社区中已有的变换处理模块之外,也可以开发自己的变换处理模块。代码清单 5 中给出了一个对 JavaScript 文件中的属性值进行替换的变换处理模块 properties.js。该模块只提供一个方法用来对文件进行处理,返回值是一个对文件内容进行变换的流。在实际的处理中通过正则表达式来查找文件中出现的“${}”模式,并替换成 config.js 中定义的对应属性值。

清单 5. 属性值替换模块示例
var config = require('./config.js');
var through = require('through2');
 
module.exports = function(file) {
 return through(function(buf, enc, next) {
var re = /\$\{(.*?)\}/;
var content = buf.toString('utf8');
while(match = re.exec(content)) {
content = content.replace(match[0], config[match[1]]);
}
this.push(content);
next();
 });
};

config.js 中的内容如代码清单 6 所示。

清单 6. 属性值替换模块中的 config.js 文件
module.exports = {
 name: 'Alex',
 email: 'alex@example.org'
};

代码清单 7 给出了一个要处理的模块 transform_sample.js。

清单 7. 属性值替换模块所处理的文件
module.exports = function() {
 console.log("${name}, ${email}");
};

使用命令“browserify -t ./properties.js transform_sample.js > bundle.js”处理之后的 bundle.js 的重要内容如代码清单 8 所示。原始文件中的两个变量被替换成了属性文件中的对应值。

清单 8. 属性值替换模块处理之后的结果
console.log("Alex, alex@example.org");

社区中也提供了很多完成不同变换的模块。比如 browserify-shim 模块用来把非 CommonJS 的模块转换成 CommonJS 模块,从而可以在 NodeJS 中使用。如果模块使用的是全局对象或是 AMD 规范,则可以通过 browserify-shim 来变换成 CommonJS 模块。

比如模块文件 globalVar.js 中通过全局变量 globalVar 暴露它所提供的公开接口。通过在 package.json 文件中添加相关的变换可以把它转换成 Browserify 可用的模块,如代码清单 9 所示。

清单 9. 通过在 package.json 文件中声明变换
{
 "browserify": {
"transform": "browserify-shim"
 },
 "browserify-shim": {
"globalVar.js": "globalVar"
 }
}

当通过 require('globalVar.js')加载该模块时,得到的是 globalVar.js 中提供的 globalVar 对象,但是全局名称空间并不会被污染。该模块已经被自动转换成 CommonJS 模块。


package.json 文件

NodeJS 中模块的 package.json 文件用来定义模块相关的配置信息。Browserify 扩展了 package.json 并提供了额外的配置项。可以通过 package.json 的“browser”属性指定在浏览器环境中的入口文件。NodeJS 的 package.json 中通过“main”属性来指定入口文件。通过添加新的“browser”属性,使得模块可以兼容 NodeJS 和浏览器两个不同的环境。代码清单 10 给出了一个 package.json 文件的示例。

清单 10. 在 package.json 文件中使用 browser 属性
{
 "name": "sample",
 "version": "1.0.0",
 "main": "main.js",
 "browser": "browser.js"
}

当通过 require('sample')来加载 sample 模块时,如果当前是 NodeJS 环境,则使用的是 main.js 提供的接口;如果是浏览器环境,则使用的是 browser.js 提供的接口。

在有些情况下,NodeJS 和浏览器环境下运行的代码的差别并不大。因此不需要分别指定不同的入口文件,而只需要替换某些模块的实现即可。可以通过“browser”属性来指定要替换的文件,如代码清单 11 所示。

清单 11. 在 package.json 文件中指定单个需要替换的文件
{
 "name": "sample",
 "version": "1.0.0",
 "main": "main.js",
 "browser": {
"demo.js": "browser-demo.js"
 }
}

在浏览器环境下,当需要加载模块 demo.js 时,会被替换成加载 browser-demo.js 文件。

在另外的一些情况下,某些模块仅在 NodeJS 环境中有效,而在浏览器中是不需要的。此时可以选择在浏览器环境中忽略该模块。在代码清单 12 中模块 example 被忽略。当加载 example 模块时,其内容会变成一个空的对象。

清单 12. 在 package.json 文件中忽略模块
{
 "name": "sample",
 "version": "1.0.0",
 "main": "main.js",
 "browser": {
"example": false
 }
}

对于变换处理,除了使用“-t”参数在执行 browserify 命令时指定之外,也可以在 package.json 中声明。如代码清单 13 所示。当模块被加载时,通过 browerify.transform 声明的变换处理会被自动调用。

清单 13. 在 package.json 声明变换处理模块
{
 "name": "sample",
 "version": "1.0.0",
 "main": "main.js",
 "browserify": {
"transform": [ "./properties.js" ]
 }
}

所有这些通过 browser 和 browerify.transform 属性所做的对于模块的修改和替换,都只对 package.json 所在的当前模块有效。


模块组织

一个复杂的 Web 应用可能包含很多个不同的模块。这些模块需要进行合理的组织。首先每个模块需要有自己单独的目录,包含所需的全部文件。在目录里面有模块本身的 package.json 文件,定义该模块所依赖的其他模块。该模块所依赖的其他模块会被放在 node_modules 目录下。所有的模块都按照这样的层次结构来组织。通过这种组织方式,每个模块所依赖的模块是相互独立的,都存放在自己的 node_modules 目录中,不会对其他模块产生影响。这些模块的依赖关系也是自包含的。当不同模块依赖同一模块的不同版本时,也不会存在版本之间的冲突问题。

需要注意的是,在 node_modules 目录中也同样包含了通过 npm 下载的其他模块。由于这些模块的代码不属于当前项目的一部分,因此在源代码仓库中需要忽略。而对于项目本身的模块,则需要保留。如果使用 Git 的话,可以采用代码清单 14 所示的.gitignore 文件来声明。

清单 14. 管理模块推荐的.gitignore 文件
node_modules/*
!node_modules/moduleA
!node_modules/moduleB

对于清单中的.gitignore 文件,node_modules 目录下面的 moduleA 和 moduleB 是项目本身的模块,会被保存到 Git 中,而剩下的其他模块则会被忽略。


创建可复用组件

在 Web 应用开发中,创建可复用组件是重要的一环。由于 NodeJS 和浏览器环境的不同,如果需要创建可以同时在 NodeJS 和浏览器中工作的模块,需要一些额外的处理。Web 组件中会用到一些非 JavaScript 的静态文件,包括 HTML 和 CSS 文件。在 NodeJS 中,这些文件是通过 fs 模块的 readFileSync 方法来读取文件内容并使用的。在浏览器环境中不能访问文件系统,因此需要把对 readFileSync 方法的使用进行变换处理。Browserify 提供了一个变换处理模块 brfs 用来对 readFileSync 方法的调用进行替换,用待读取的文件的实际内容来替代。

比如代码清单 15 中,template.html 的内容是“<p>hello</p>”。

清单 15. 变换处理模块 brfs 示例
var fs = require('fs');
var html = fs.readFileSync(__dirname + '/template.html', 'utf8');

在经过 brfs 变换处理之后,所得到的 JavaScript 文件的内容如代码清单 16 所示。

清单 16. 变换处理模块 brfs 处理之后的文件
var fs = require('fs');
var html = "<p>hello</p>";

对于 CSS 文件,可以使用 insert-css 变换处理模块。代码清单 17 给出了使用示例。

清单 17. 变换处理模块 insert-css 使用示例
var fs = require('fs');
var insertCss = require('insert-css');
var css = fs.readFileSync(__dirname + '/style.css');
insertCss(css);
var elem = document.createElement('p');
elem.appendChild(document.createTextNode('Hello World'));
document.body.appendChild(elem);

对于图片文件,可以 base64 编码之后使用 data url 来表示,如代码清单 18 所示。

清单 18. 对于图片文件的处理
var fs = require('fs');
var imdata = fs.readFileSync(__dirname + '/image.png', 'base64');
var img = document.createElement('img');
img.setAttribute('src', 'data:image/png;base64,' + imdata);
document.body.appendChild(img);

完整的模块示例

下面介绍一个完整的模块的开发。该模块是显示数字用的标签(badge)。首先是该模块的 HTML 内容,如代码清单 19 所示。

清单 19. 模块示例的 HTML 代码
<div><span class="number"></span></div>

代码清单 20 给出了 CSS 文件。

清单 20. 模块示例的 CSS 代码
.number {
 font-weight: bold;
 color: #ccc;
}

HTML 和 CSS 都是标准的静态文件。比较重要的 JavaScript 文件如代码清单 21 所示。

清单 21. 模块示例的 JavaScript 文件
var fs = require('fs');
var domify = require('domify');
var insertCss = require('insert-css');
 
var css = fs.readFileSync(__dirname + '/badge.css', 'utf8');
insertCss(css);
 
var html = fs.readFileSync(__dirname + '/badge.html', 'utf8');
 
module.exports = Badge;
 
function Badge(opts) {
 if (!(this instanceof Badge)) return new Badge(opts);
 this.element = domify(html);
 if (opts.number) {
this.setNumber(opts.number);
 }
}
 
Badge.prototype.setNumber = function (number) {
 this.element.querySelector('.number').textContent = number;
}
 
Badge.prototype.appendTo = function (target) {
 if (typeof target === 'string') target = document.querySelector(target);
 target.appendChild(this.element);
};

模块的 package.json 如代码清单 22 所示。

清单 22. 模块的 package.json 文件
{
 "name": "badge",
 "version": "1.0.0",
 "private": true,
 "main": "badge.js",
 "browserify": {
"transform": [ "brfs" ]
 },
 "dependencies": {
"brfs": "^1.1.1"
 }
}

代码清单 23 给出了如何使用这个模块的示例。

清单 23. 模块的使用示例
var Badge = require('badge');
 
var badge = Badge({
 number: 10
});
 
badge.appendTo('#container');

模块打包

一般情况下,Browserify 会把所有的模块打包成单个文件。单个文件在大多数情况下是适用的,可以减少 HTTP 请求数量,提高性能。不过在其他一些情况下,打包的单个文件可能过大,使得页面的初始加载时间过长。这主要是因为单个文件中包含了全部的模块,其中的某些模块使用得很少,或是在页面初始加载的时候不需要,可以在需要的时候再加载。这个时候可以用 Browserify 的插件来创建不同的打包文件。

处理重复模块

可以使用 factor-bundle 插件来处理重复的模块。factor-bundle 根据多个入口点来打包成多个文件。这些文件所共同依赖的模块会被打包在一个单独的文件中。在使用时,需要先引用包含共同模块的文件,再引用单个入口文件对应的打包之后的文件。

比如有 2 个页面分别依赖不同的模块,其中有些模块是重复的,如代码清单 24 所示。

清单 24. 两个依赖重复模块的模块示例
var _ = require('lodash'),
 $ = require('jquery');
 
var _ = require('lodash'),
 async = require('async');

pageA 依赖于 lodash 和 jquery 两个模块,而 pageB 依赖于 lodash 和 async 两个模块。通过代码清单 25 中的命令可以进行打包。

清单 25. 模块打包的命令行
browserify pageA.js pageB.js -p [ factor-bundle -o bundle/pageA.js -o bundle/pageB.js ]  \
         -o bundle/pageCommon.js

在生成的文件中,pageA 和 pageB 共同依赖的模块 lodash 出现在 bundle 目录下的 pageCommon.js 文件中,而 bundle 目录下的 pageA.js 文件则只包含 jquery 模块,pageB.js 文件则只包含 async 模块。在页面 pageA 中需要引用的是 bundle 目录的 pageCommon.js 和 pageA.js 文件,而 pageB 依赖的 async 模块并不会包含在内。

排除和忽略模块

在使用 Browserify 进行打包时,可以选择忽略或排除某些模块。忽略的含义是把模块原来暴露的接口替换成一个空对象。排除则是完全把模块从依赖关系中去除。这两种方式的区别在于使用 require 来导入一个被忽略的模块时,并不会出现错误;而使用 require 来导入一个被排除的模块时会出现错误。

在使用具体的 browserify 命令时,参数“--ignore”指定要忽略的模块,而“--exclude”指定要排除的模块。

独立的模块

当需要分发某个单独的模块时,可以在运行 browserify 命令时使用”--standalone”参数来指定模块的名称。所产生的模块可以在 NodeJS 和浏览器中使用。对于浏览器来说,如果应用支持 AMD,则使用 AMD 来定义模块;否则把模块暴露为全局对象。如“browserify log.js --standalone log > log-bundle.js”把模块 log.js 打包成名为 log 的独立模块。


NodeJS 模块在浏览器中的使用

由于 NodeJS 和浏览器环境的差别,某些 NodeJS 模块在浏览器中并不能直接使用。比如在浏览器中无法访问文件系统,也无法获取操作系统相关的信息。因此在使用 Browserify 之后可能会出现某些模块无法使用的情况。为了解决这个问题,Browserify 为某些常见的 NodeJS 模块提供了浏览器端的实现。在进行打包时,应用所依赖的包会被自动替换成浏览器上的对应实现。这些模块包括:http、https、os、path、querystring、stream、url、util 和 vm 等。如 stream 模块会被替换成 stream-browserify,vm 模块会被替换成 vm-browserify。Browserify 还提供了_dirname 和__filename 两个属性来表示使用该属性的文件所在的目录名称和文件名称。


小结

在应用开发中,高质量的模块对于提高开发效率是很重要的。NodeJS 社区积累了非常多高质量的模块。通过 Browserify,这些模块可以在 Web 应用的前端开发中来使用。这对于广大前端开发人员来说是一个很好的工具。本文详细介绍了 Browserify 工具的使用,以及如何在 Browserify 的基础上开发出高质量的模块。

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=995831
ArticleTitle=通过 Browserify 在浏览器中使用 NodeJS 模块
publish-date=01222015