browserify使用手册
简介
这篇文档用以说明如何使用browserify来构建模块化应用
browserify是一个编译工具,通过它可以在浏览器环境下像nodejs一样使用遵循commonjs规范的模块化编程.
你可以使用browserify来组织代码,也可以使用第三方模块,不需要会nodejs,只需要用到node来编译,用到npm来安装包.
browserify模块化的用法和node是一样的,所以npm上那些原本仅仅用于node环境的包,在浏览器环境里也一样能用.
现在npm上有越来越多的包,在设计的时候就是想好既能在node环境下用,也能在浏览器环境下用的.甚至还有很多包就是给浏览器环境使用的. npm是为所有的javascript服务的,无论前端后端.
目录
- introduction
- table of contents
- node packaged manuscript
- node packaged modules
- development
- builtins
- transforms
- package.json
- finding good modules
- organizing modules
- testing in node and the browser
- bundling
- shimming
- partitioning
- compiler pipeline
- plugins
使用node安装本手册
可以直接安装本手册:
npm install -g browserify-handbook
然后执行 browserify-handbook 命令,就可以在本地阅读这个文件.当然,你也可以直接在这里阅读.
node模块化
在深入学习了解browserify之前,有必要先了解在nodejs里commonjs模块化是如何工作的.
require
在nodejs里,有一个require()方法用于加载其他文件或包.
通过npm安装模块:
npm install uniq
然后在 num.js 里,我们就可以调用 require('uniq');
var uniq = require('uniq'); var nums = [ 5, 2, 1, 3, 2, 5, 4, 2, 0, 1 ]; console.log(uniq(nums));
通过nodejs运行这个程序的结果:
$ node nums.js
[ 0, 1, 2, 3, 4, 5 ]
也可以使用'.'开头的相对路径来请求一个文件,比如,要在main.js里加载一个foo.js文件,在main.js里可以这样写:
var foo = require('./foo.js'); console.log(foo(4));
如果foo.js是在父文件夹里,可以改成 '../foo.js':
var foo = require('../foo.js'); console.log(foo(4));
其他的相对路径都可以以此类推.相对路径的解析总是相对于当前文件的所在路径.
注意,require()方法返回的是一个方法,然后我们把它分配给一个变量'uniq'.变量不一定要叫'uniq',叫什么名字都一样.require()方法返回模块的接口给你指定的名字.
很多模块化方案引入的模块会变成一个全局变量,或者是当前文件的本地变量,并且变量的名字是固定不能修改的.而node的require()方法和他们不同, 任何读代码的人都可以很容易的知道每个功能是从哪里来的,这在应用的规模不断扩大,模块不断增加的时候,还能保持很好的可扩展性.
输出
要把一个文件的接口输出给另一个文件,只需要把接口分配给 module.exports:
module.exports = function (n) { return n * 111 };
现在,让 main.js 的文件加载 foo.js , require('./foo.js')的值就是exports输出的函数:
var foo = require('./foo.js'); console.log(foo(5));
这段程序的打印结果:
555
module.exports 不仅可以输出函数,也可以输出任何类型的值.
比如下面这个例子这样,也完全可以:
module.exports = 555
下面这样也行:
var numbers = []; for (var i = 0; i < 100; i++) numbers.push(i); module.exports = numbers;
下面是另外一种输出接口的方式,把输出的内容分配给一个对象: 使用 exports 来替代 module.exports
exports.beep = function (n) { return n * 1000 } exports.boop = 555
上面这段代码也可以写成这样:
module.exports.beep = function (n) { return n * 1000 } module.exports.boop = 555
因为 module.exports 和 exports 是一样的,它们一开始都是一个空对象.
但是注意,你不能这样写:
// 这样是不行的 exports = function (n) { return n * 1000 }
因为输出值是在module对象里的局部变量,所以,直接给exports赋值而不是给module.exports赋值会覆盖对原来对象的引用.
所以如果你想直接输出一个东西,应该这样做:
// 这样做 module.exports = function (n) { return n * 1000 }
如果你仍然感到困惑,看下下面这个场景里,modules是如何工作的:
var module = { exports: {} }; // 当你请求一个模块的时候, 它都被包装在了一个这样的基本函数里. (function(module, exports) { exports = function (n) { return n * 1000 }; }(module, module.exports)) console.log(module.exports); // 它依然是一个空对象 :(
(ps:关于这点,另外写了一篇文章:nodejs里的module.exports和exports的关系)
大多数时候,你可以通过 module.exports 输出一个函数或者构造函数,因为一个模块最好只做一件事.
一开始 exports 对象才是输出函数的首先, module.exports 是次选, 但实践证明, module.exports 更好用,更直接,更清楚,避免重复声明.
以前,这样的方式被普遍使用:
foo.js:
exports.foo = function (n) { return n * 111 }
main.js:
var foo = require('./foo.js'); console.log(foo.foo(5));
注意到这里的 foo.foo 有点儿多余. 使用 module.exports 能让它看起来更清楚:
foo.js:
module.exports = function (n) { return n * 111 }
main.js:
var foo = require('./foo.js'); console.log(foo(5));
为浏览器打包
要运行node里的某个模块,你需要从某个地方开始
直接在命令行里运行 node 文件名
In node you pass a file to the node
command to run a file:
$ node robot.js
beep boop
同样,browserify也差不多,但不是运行文件,而是把需要合成的js文件生成了一个流,通过 > 操作,输出到你指定的文件
$ browserify robot.js > bundle.js
现在 bundle.js 就包括了所有让 robot.js 运行所需要的javascript. 只需要在html里插入script标签,把bundle.js引入就行了.
<html> <body> <script src="bundle.js"></script> </body> </html>
另外:如果你把script标签放在</body>的前面,你就可以使用页面中的所有dom元素,而不需要等到dom onready事件触发以后.
你还可以通过打包来做很多事.后面会有专门讲打包的部分.
browserify 是如何工作的
Browserify从你给你的入口文件开始,寻找所有调用require()方法的地方, 然后沿着抽象语法树,通过 detective 模块来找到所有请求的模块. (其实这个意思就是说,它require里还有require,还有require,所有的require像一棵树一样,然后沿着这棵树,通过detective来找到所有的模块)
每一个require()调用里都传入一个字符串作为参数,browserify把这个字符串解析成文件的路径然后递归的查找文件直到整个依赖树都被找到.
每个被require()的文件,它的名字都会被映射到内部的id,最后被整合到一个javascript文件中.
这就意味着最后打包生成了文件已经包含了所有能让你的应用跑起来所需要的东西.
查看更多browserify的用法,可以看文档的编译器管道部分.
node_modules 是如何工作的
node解析模块有它独特的,聪明的算法,这在它的竞争对手中是独一无二的.
node不像命令行里的 $PATH 那样搜索模块,它的机制是默认搜索本地.
如果你在 /beep/boop/bar.js 里调用 require('./foo.js'), node会在 /beep/boop/目录下寻找 foo.js. require()方法的参数如果是 ./ 开头,或者是 ../开头的,总是本地文件.
然而如果你请求的参数并不是一个相对路径,比如在 /beep/boop/foo.js 里调用 require('xyz') , node会按照下面的顺序寻找这个模块,直到找到.如果找不到就抛出一个错误:
/beep/boop/node_modules/xyz /beep/node_modules/xyz /node_modules/xyz
找到了名为 xyx 的文件夹,node首先会寻找 xyz/package.json ,看下里面有没有 main 属性. main 属性定义了当 require() 请求这个路径的时候,应该找哪个文件.
举个栗子,如果找到了 /beep/node_modules/xyz 这个文件夹,并且文件夹里有 /beep/node_modules/xyz/package.json 这个文件:
{ "name": "xyz", "version": "1.2.3", "main": "lib/abc.js" }
这样, /beep/node_modules/xyz/lib/abc.js 这个文件就是 require('xyz') 的结果.
如果文件夹下没有 package.json 这个文件,package.json里没有 'main' 这个属性, 那默认就是找 index.js
/beep/node_modules/xyz/index.js
如果有需要,也可以直接到模块里指定要选的那个文件. 举个栗子,要加载 dat 模块下的 lib/clone.js ,你只需要这样做:
var clone = require('dat/lib/clone.js')
node_modules 的递归算法会先按照文件夹层级,找到 dat 模块,然后找到里面的 lib/clone.js 文件.
只要你所在的路径能使用 require('dat') ,也就可以使用 require('dat/lib/clone.js').
node 也有机制允许搜索一个路径数组,但这个机制已经被弃用了,除非你确实有很合理的需求要用到它,否则你还是应该使用 node_modules/
不像其他的平台, node的算法和npm下载包的一个很好的优点就是,,你永远不会遇到版本冲突,npm把每个包的依赖都装到了 node_modules 里.
每个库都有它自己的 node_modules/ 文件夹,用于存放它的依赖,而每个依赖又有自己的 node_modules/文件夹,用于存放它的依赖.............就这么递归下去..............
这就意味着,在一个应用里,每个包可以使用各自不用的版本, 大大减少了api的迭代导致的协作成本.这个特性在npm这种没有专员去发布管理包的系统里是十分有用的.每个人都可以发布包,不用担心包里某个依赖的版本选择会影响到应用中的其他依赖.
你也可以利用 node_modules/ 的工作方式来组织你本地应用的模块. avoiding ../../../../../../.. 这部分会介绍更多相关内容
为什么整合
Browserify是一个在服务器端的构建步骤. 它生成一个打包好的文件,这个文件里包含了所有.
这里还有一些其它的一些在浏览器端使用模块的方式,它们有各自的优势和弱点:
·全局变量
不同于模块化系统,每个文件都把属性定义在全局变量下,或者在内部的命名空间下进行开发.
这种方式的可扩展性不太好,维护起来非常吃力.因为每个文件都需要在html页面中用一个script标签来引入,而且,文件引入的顺序十分重要,因为有些文件里用到的全局变量,是在另一个文件里申明的.
使用这种方式,重构和维护都非常困难. 但是,所有的浏览器都原生支持这种方式,不需要任何服务端的支持.
这种方式也很慢,因为每个script标签都会发起一次HTTP往返请求.
·整合文件
不使用全局变量,而是把所有的脚本在服务端都整合到一起.代码的顺序依然必须按照指定顺序,并且难以维护.但是加载速度要快很多,因为只有一个 <script> 标签,只发一次请求.
它没有资源映射表,当报错的时候,你只能看到最后报错的位置,而无法轻易的定位到这个报错在源代码的位置.
·AMD
不使用 <script> 标签,每个文件都被包装在 define() 函数和回调里. 这里是AMD(就是requirejs).
第一个参数是一个数组,数组里的的每个模块对应回调里的每个参数.当所有的模块都被加载完以后,回调被执行.
define(['jquery'] , function ($) { return function () {}; });
你可以给你的模块取一个名字,这样每个模块都可以引用它.
commonjs有一个语法糖,它会把每个回调都字符串化,然后通过正则搜索里面的 require() 方法 (源码)
用这种方式编码,比前面两种方式对顺序的要求要低很多,因为顺序已经明确的指定在了依赖信息里.
为了提升性能,大多数时候,AMD也会在服务端被打包成一个文件.在开发的过程中,更多的使用到了AMD的异步特性.
·在服务器端打包commonjs
如果你既要使用语法糖,也想使用构建打包来提升性能,为什么不直接把所有的AMD逻辑整合到一起,把commonjs打包呢?
通过打包工具,模块会被解析到对应的地址,并且顺序正确.生产环境和开发环境会更相似,更少产生碎片. node 和 npm 把CJS语法变得更好用,创造了新的开发环境.
你的代码可以无缝的在node和browser之间运行.只需要执行一下构建命令,使用一些工具来生成资源映射表,实现自动重新构建打包.
另外,node独有的寻找模块的算法还能把我们从版本混乱的崩溃状态中解救出来.我们可以在同一个应用中请求多个版本不同的包,也不会有任何问题.为了节省下载量,你可以删除重复数据,这在后面会说到.
开发
整合会有一些缺点,但是使用开发工具完全可以解决它们.
source maps
browserify支持 --debug或者 -d 标识或者 opts.debug 参数来支持资源映射图.资源映射图能在浏览器报错的时候,把在打包后的js报错的行列数映射到打包前这个报错所在的js的文件名和对应的行列数.
资源映射图包含了所有原文件的内容,所以你只需要直接把打包好的文件放到服务器上而不需要把所有的源文件都传到服务器的对应路径上.
exorcist
把所有的原文件放到内联的资源映射图里的缺点是,打包好的js的大小会比原来大一倍.虽然方便调试,但没有必要把资源映射图传到生产环境. 所以,你可以使用 exorcist 来把本来内联的资源映射图放到一个单独的 bundle.map.js 里面:
browserify main.js --debug | exorcist bundle.js.map > bundle.js
auto-recompile
每次重新编译都要执行一下命令是一件很烦很费时的事.所幸有很多工具可以解决这个问题.有些工具还在一定程度上支持 live-reloading,有些还是需要传统的手动刷新.
下面列出一部分比较常见的工具,npm上其实还有很多很多.不同的工具有各自的侧重点和开发方式. 可能要多做些前期工作来找到最符合你的期望的工具,但它的多样性可以让程序员更高效的工作,也提供了更多创新和实验的机会. 我认为,从中长期来说,多样性的工具加上比较少的browserify核心功能,会比一些听起来更好用,其实只是把更多功能整合到browserify核心功能里(对内核造成位衰减,对有用的版本控制造成破坏)的工具要好.
下面是配置browserify工作流时你可以考虑的一些模块.对于没有列出的工具,也需要留个心.
watchify
除非你只输出一次文件,你可以使用 watchify 来替代 browserify , watchify 会写入打包文件然后监测所有依赖表里的文件的变化.当你修改一个文件时,由于缓存机制,新生成打包文件的速度会比第一次快很多.
添加 -v 参数,可以在每次打包时输入一个消息:
$ watchify browser.js -d -o static/bundle.js -v 610598 bytes written to static/bundle.js 0.23s 610606 bytes written to static/bundle.js 0.10s 610597 bytes written to static/bundle.js 0.14s 610606 bytes written to static/bundle.js 0.08s 610597 bytes written to static/bundle.js 0.08s 610597 bytes written to static/bundle.js 0.19s
这里是一个 package.json 的 'scripts' 字段部分,使用watchify和browserify的简单配置
{ "build": "browserify browser.js -o static/bundle.js", "watch": "watchify browser.js -o static/bundle.js --debug --verbose", }
然后在生产环境打包的时候执行 npm run build , 在开发环境打包的时候执行 npm run watch
beefy
如果你宁愿当代码改动的时候启动服务来自动重新编译,那么可以看下 beefy.
给一个使用beefy的入门例子:
beefy main.js
它会开始在一个http端口运行.
wzrd
wzrd 和beefy差不多,但是更轻量级
只需要安装 npm install -g wzrd 就可以使用了:
wzrd app.js
然后打开浏览器 http://localhost:9966
browserify-middleware, enchilada
如果你正在使用 express, 可以看下 browserify-middleware 或者 enchilada
他们都提供了可供express使用的browserify中间件.
livereactload
livereactload是一个专门给 react 用的工具,它会在你修改代码的时候自动更新页面状态.
livereactload只是一个普通的browserify转换,你可以通过 -t livereactload 来使用它. 点此查看更多 project readme
browserify-hmr
browserify-hmr是一个用于实现运行时模块替换(hot module replacement)功能的插件
当文件更新的时候
Files can mark themselves as accepting updates. If you modify a file that accepts updates of itself, or if you modify a dependency of a file that accepts updates, then the file is re-executed with the new code.
举个栗子,有一个 main.js 文件:
document.body.textContent = require('./msg.js') if (module.hot) module.hot.accept()
还有一个 msg.js 文件:
module.exports = 'hey'
我们可以监测 main.js 文件的修改,并加载 browserify-hmr 插件:
$ watchify main.js -p browserify-hmr -o public/bundle.js -dv
然后在 public/ 目录下启动静态文件服务器来提供静态文件的内容
$ ecstatic public -p 8000
打开 http://localhost:8000 ,可以在页面上看到内容 hey
如果我们修改 msg.js
module.exports = 'wow'
几秒钟以后,页面就会自己自动更新,显示 wow
除了使用 module.hot
API . Browserify-HMR 还可以和 react-hot-transform 一起使用来自动更新所有的 React 组件. 不像 livereactload 那样只要有修改就全部重新打包, 它只有修改过的文件才会重新执行.
budo
budo专注于为browserify开发提供增量打包和实时刷新服务 (包括css的注入)
安装:
npm install budo -g
在入口文件执行:
budo app.js
会在 http://localhost:9966 打开一个默认的 index.html 页,然后在文件保存的时候进行增量打包. 请求会被延迟到打包完成以后,所以如果你在打包未完成时刷新页面,不会看到旧的或者空的包.
开启 LiveReload 可以让你修改 JS/HTML/CSS 时自动刷新浏览器:
budo app.js --live
直接使用api
你也可以在 http.createServer()
里直接使用 browserify API
var browserify = require('browserify'); var http = require('http'); http.createServer(function (req, res) { if (req.url === '/bundle.js') { res.setHeader('content-type', 'application/javascript'); var b = browserify(__dirname + '/main.js').bundle(); b.on('error', console.error); b.pipe(res); } else res.writeHead(404, 'not found') });
*简单介绍一下这段代码: http.createServer 创建一个服务器, req 是请求体,当请求的url是 /bundle.js 的时候,就设置响应头的响应类型是 'application/javascript' ; 然后使用browserify来打包 main.js ,然后把 b 这个流输出到响应流里. 这样,当请求 bundle.js 的时候,得到的响应就是 main.js 打包的结果.
grunt
If you use grunt, you'll probably want to use the grunt-browserify plugin.
如果你使用 grunt, 你可以使用 grunt-browserify 插件
gulp
如果你使用gulp, 你可以直接使用browserify API
这里有一个帮助你开始使用 gulp 和 browserify 的例子: a guide for getting started.
这里有一个教你 如何在gulp里使用watchify来加快browserify构建速度 的例子,它来自gulp官网.
内嵌
为了让更多本来为node而开发的npm模块也能用在浏览器里,browserify提供了许多专门给浏览器使用的node内核的库.
- buffer
- console
- constants
- crypto
- domain
- events
- http
- https
- os
- path
- punycode
- querystring
- stream
- string_decoder
- timers
- tty
- url
- util
- vm
- zlib
events, stream, url, path, querystring 这些模块在浏览器环境中相当好用.
另外,如果 browserify 监测到你使用了 Buffer, process, global, __filename, __dirname 这些模块, 它会引入适用于浏览器的变量.
所以,即使一个模块里有许多buffer和stream操作,只要它不做任何IO读写,就可能只在浏览器环境运行.
如果你以前从来没有接触过node,这里有一些例子告诉你这些全局变量都能做什么. 注意,这些全局变量只有在你使用了他们,或者你依赖的模块里使用了他们,他们才会被定义.
Buffer
在node里,所有的文件和网络API都是通过buffer块来处理的. 在browserify里, Buffer API 是 buffer 模块提供的, 它使用了一种高性能的增强型数组,并且在旧的浏览器里也能向下兼容.
这里是一个使用 Buffer 来把base64字符串转换成十六进制的例子:
var buf = Buffer('YmVlcCBib29w', 'base64'); var hex = buf.toString('hex'); console.log(hex);
它会输出:
6265657020626f6f70
process
在node里, process 是一个特殊的对象,用于处理信息,控制进行中的进程,比如环境,信号,和标准的IO流. (...(# ̄~ ̄#)呃... ).
其中最有用的就是在事件循环里使用 process.nextTick() 方法.
在 browserify 里,进程应用是由 process module 来处理的, 它只提供了 process.nextTick() 方法和不多的一些东西.
这个例子说明 process.nextTick() 做了什么:
setTimeout(function () { console.log('third'); }, 0); process.nextTick(function () { console.log('second'); }); console.log('first');
这段代码会输出:
first
second
third
process.nextTick(fn) 类似于 setTimeout(fn,0) , 不过比 setTimeout 要快, 因为为了兼容性原因, setTimeout 方法在javascript引擎里是故意被延迟的.
global
global 是node里的最高作用域, 类似于浏览器里的全局变量是申明在 window 下那样. 在browserify里, global 只是 window 的一个别名.
__filename
__filename 是当前文件的路径,所以在每个文件里它是不一样的.
为了防止公开系统路径信息, 这个路径的根目录是根据你传给 browserify() 的 opts.basedir 参数决定的, 默认是 当前工作文件夹
如果我们有一个 main.js :
var bar = require('./foo/bar.js'); console.log('here in main.js, __filename is:', __filename); bar();
还有一个 foo/bar.js :
module.exports = function () { console.log('here in foo/bar.js, __filename is:', __filename); };
然后在 main.js 入口运行 browserify , 它会输出:
$ browserify main.js | node here in main.js, __filename is: /main.js here in foo/bar.js, __filename is: /foo/bar.js
__dirname
__dirname 是当前文件所在的文件夹. 和 __filename 类似, __dirname 相对的根目录也是 opts.basedir.
这里是一个 __dirname 如何工作的例子:
main.js:
require('./x/y/z/abc.js');
console.log('in main.js __dirname=' + __dirname);
x/y/z/abc.js:
console.log('in abc.js, __dirname=' + __dirname);
输出结果:
$ browserify main.js | node in abc.js, __dirname=/x/y/z in main.js __dirname=/
*需要注意这里的 "| node " , 正常情况下,browserify需要有 > bundle.js 来输出打包后的文件,使用 " | node " 表示用node来执行该文件,而非打包.
转换
browserify 并没有支持一切, 他做的是一个灵活的转换系统,用于就地转换资源文件.
你可以 require() 使用 coffeescript 写的文件或模板文件或一切可以被编译成 javascript 的文件.
以 coffeescript 为例,你可以使用 coffeeify 来转换.
首先安装 coffeeify : npm install coffeeify , 然后:
$ browserify -t coffeeify main.coffee > bundle.js
或者通过API来操作:
var b = browserify('main.coffee'); b.transform('coffeeify');
最完美的是,如果你通过 --debug 或者 opts.debug 开启了资源映射图, bundle.js 会把报错映射回原始的coffee script文件. 这对于firebug和chrome的debugging调试是非常有用的.
自定义
转换(的过程)使用的是一个简单的流的接口. 这是一个把 $CWD 替换成 process.cwd() 的例子:
var through = require('through2'); module.exports = function (file) { return through(function (buf, enc, next) { this.push(buf.toString('utf8').replace(/\$CWD/g, process.cwd())); next(); }); };
转换函数会在每个 file 文件被传入的时候触发,它会返回一个转换后的流. browserify读取文件的原始内容的输出流,将这个流转换后成写入流,获取到新的内容. (关于什么是输出流什么是写入流,参考 stream handbook ) ( Σ( ° △ °|||)︴斟酌再三后才翻译成这样,应该是这个意思吧......)
只要把你写的转换方法保存成一个文件或者一个包,然后在调用的时候加上参数 -t ./your_transform.js
想知道流是什么工作的,查看 stream handbook.
package.json
browser 属性
可以在 package.json 文件里定义一个 "browser" 属性 , 这个属性值会告诉 browserify 在寻找入口文件的时候覆盖 "main" 属性的值. 让同一个模块下(在浏览器和node环境下)可以有各自不同的模块.
如果你的模块在nodejs里有一个入口,它来自 main.js , 但还有一个专为浏览器而设的入口在 browser.js 里 , 你可以这样做:
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browser": "browser.js" }
现在,如果有人在 node 里 require('mypkg') , 他们会得到 main.js 的输出, 但如果是在浏览器环境里 require('mypkg') , 他们会得到 browser.js 的输出.
通过 "browser" 属性来区分是否在浏览器环境是一种很好的检查运行环境的方式,因为在浏览器运行环境和node运行环境下,你可能会想加载不同的模块.
如果你在同一个文件里 require() 了 node环境和浏览器环境, browserify的解析标准会把所有的东西都引进来,不管你用不用.(------w(゚Д゚)w------不理解啊)
browser的值可以是一个对象而不是字符串,对象可以有更多功能.
举个栗子,如果你想让 lib/ 下的某个文件输出一个为浏览器指定的版本,你可以这样做:
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browser": { "lib/foo.js": "lib/browser-foo.js" } }
*这个它说的不是很清楚,其实意思就是,当处于浏览器环境的时候,如果你请求的文件是 lib/foo.js ,那么实际请求的文件就应该是 lib/browser-foo.js , 举个最简单的例子来理解,把上面这段配置改一下:
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browser": { "main.js": "lib/browser-foo.js" } }
这样,当它请求 mypkg 这个模块的时候,它就会找到 main.js 入口文件,但是 "browser" 的配置里把 main.js 配置成了 lib/browser-foo.js , 所以在 browserify 打包时,最后请求的文件就是 lib/browser-foo.js
如果你想在这个包里使用本地的某个包,可以这样:
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browser": { "fs": "level-fs-browser" } }
和上面一样,在 mypkg 这个包里,如果在浏览器环境里请求了 fs 模块, 会得到 level-fs-browser 模块.
你可以通过设置 browser 属性值里的某些指定文件的值为 false, 来忽略这些文件(请求时返回一个空的对象).
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browser": { "winston": false } }
browser 属性只作用于当前的包. 你定义的所有规则都不会向上或向下传播到依赖它的包或者它的依赖包.这样把各个模块隔离开来后,在请求模块的时候就不用担心会有系统范围的影响.同样,你也不用担心你本地的配置会深远的影响到整个依赖关系.
关于 package.json 的 browser 属性,可以参考 https://gist.github.com/defunctzombie/4339901
browserify.transform 属性
我们可以通过定义 package.json 的 browserify属性 的 transform 属性,来实现当模块加载时自动进行转换. 下面这个 package.json 文件可以自动执行 brfs 转换
{ "name": "mypkg", "version": "1.2.3", "main": "main.js", "browserify": { "transform": [ "brfs" ] } }
在 main.js 里,我们可以这样写:
var fs = require('fs'); var src = fs.readFileSync(__dirname + '/foo.txt', 'utf8'); module.exports = function (x) { return src.replace(x, 'zzz') };
fs.readFileSync() 方法就会被 brfs 调用,而使用模块的人不需要知道它是怎么调用的. 你在数组里添加任意数量的转换,他们会按照顺序执行.
和 browser 属性一样, package.json 里的 transform 配置只会应用在当前的包.
配置转换
有时候为了使用转换,需要在命令行里带一些参数. 你也可以直接在 package.json 里配置这些参数:
使用命令行:
browserify -t coffeeify \ -t [ browserify-ngannotate --ext .coffee --bar ] \ index.coffee > index.js
在 package.json 配置:
"browserify": { "transform": [ "coffeeify", ["browserify-ngannotate", {"ext": ".coffee", bar: true}] ] }
寻找合适的模块
这里有一些 有用的帮助 来帮助你在npm上寻找到适用于浏览器的合适的模块:
-
可以使用npm来安装它
-
查看这个包的readme文件里面的 require() 代码部分,大致浏览一下,看下如何把这个包整合到现在的项目里.
-
清楚的知道这个包要用在哪些地方,实现什么功能.
-
知道什么时候要用到其他库 - 而不是让这个包去实现所有的功能.
-
写这个包(或维护这个包)的人,他对软件领域,模块化,交互的见解是否和我大致相同. (一般只需要大致看一下,而不需要很仔细的阅读代码或文档)
-
检查有哪些模块是依赖于这个我正在评估的模块的 - 在npm上发布的包的主页里都包含了这个信息.
其它一些指标,比如github上的关注数,项目活跃度,或者是华而不实的界面,都不是很靠谱.
关于模块化的一些观点
过去人们普遍认为输出一堆方便实用的工具函数是程序员们使用的主要方式,这也是各个平台输出和引入模块的主要方式,包括npm也还维持着这种方式.
然而,一个 一劳永逸的想法 产生了: 把一些功能独立,但同一主题的函数放在一个模块里. 这种思想是出现在前github,前npm时代的一个解决模块发布困难的典型产物.
把一堆工具函数放在一个包里输出这种做法虽然很方便,但隐藏着两个很大的问题: 1.领域划分之战 2.应该寻找什么模块去做什么事情.
各个模块都有自己的许多特性,他们浪费了许多时间在裁定哪些特性是自己的哪些不是自己的这件事上.而这个问题本身是没有答案的.(---(°ー°〃)大概是这个意思吧---)答案都是一些人自以为是的观点.
node,npm,browserify不是这样.它是单点的,公然地欢迎大家参与,它欢迎大家有不同的意见,提出许多令人眼花缭乱的新想法新途径,而不是强制大家遵从命名规则,规范或所谓的'最佳实践'
如果一个人想要做一个高斯模糊功能,他不会去想"我应该先去学一下基本数学,统计学,图片处理程序,然后查找类库,看下它是不是有高斯模糊这个功能.它是不是带有统计功能或者图片打包工具或者数学工具,或许底层有这个功能?" 没有人会想这些问题,他们会执行 npm search gaussian ,然后马上发现 ndarray-gaussian-filter 这个模块正是他们想要的,然后他们继续去处理实际问题,而不是迷失在被人忽视的大量杂乱的工具库中.
组织模块
避免 ../../../../../../..
不可能一个应用里所有的东西都刚好在 npm 上都可以找到发布的包, 一般来说,在更多的场合下会更多的需要设置自己的 npm 包或者 git 仓库. 这里是一些建议,帮助你避免出现 ../../../../../../../ 这类相对路径的问题.
创建软链接
最简单的做法就是把你项目的根目录创建软链接到 node_modules/ 下的一个文件夹.
你知道 软链接也可以用在windows系统 么?
把你项目根目录下的 lib/ 文件夹放到 node_modules 里面去,可以这样做:
ln -s ../lib node_modules/app
然后你可以在项目的任何地方来调用 lib/ 文件夹下的文件:
require('app/foo.js') //获取 lib/foo.js
经过实际测试, ln -s ../lib node_modules/app 这句是有问题的. 如果 node_modules 下已经有了 app 这个目录, app 里的内容并不是 lib 文件夹里的内容,而是 lib 文件夹,而如果试图进入 lib 文件夹,它会报错 Too many levels of symbolic links , 如果 node_modules 下没有 app 这个目录,那么会生成一个名为 app 的无法访问的文件(不是文件夹), 正确的做法是进入到 node_modules 目录下, 执行 ln -s ../lib app (此时没有app这个目录).这样才能获得正确的软链接.
node_modules
有时候人们会拒绝把当前应用使用的模块放到 node_modules 文件夹下,因为这样就不好区分从 npm 上下载的第三方模块和自己写的内部模块了.
答案很简单! 如果你在 .gitignore 文件里忽略了 node_modules 部分:
node_modules
你可以通过 ! 来添加例外,把你的内部模块都排除:
node_modules/* !node_modules/foo !node_modules/bar
注意,如果父文件夹已经被忽略了,那就无法再不忽略子文件夹. 所以不能直接忽略 node_modules , 你必须忽略 node_modules 文件夹下的所有文件夹, 可以使用 node_modules/* 这种写法, 然后你就可以添加你的例外.
现在你可以在应用的任何地方使用 require('foo'), 或者 require('bar'), 而不用写很长很破碎的相对路径.
如果你有许多自己的模块,想把它们从npm的第三方模块里分隔出来,你只需要把它们都放在 node_modules 下的一个单独文件夹里,比如 node_modules/app:
node_modules/app/foo
node_modules/app/bar
现在你可以在应用的任何地方使用 require('app/foo') 或者 require('app/bar')
然后在你的 .gitignore 文件里为 node_modules/app 添加一个例外:
node_modules/* !node_modules/app
如果应用的 package.json 里有关于转换的配置, 你需要在 node_modules/foo 或 node_modules/app/foo 组件下单独创建独立的 package.json 来配置 transform 属性.因为 transform 属性不会跨域模块. 这确保了当应用的配置发生的变化的时候,你的模块能更好地适应.另外,模块也更容易重用在其他应用里.
自定义路径
你可能看到过某些地方说到了使用 $NODE_PATH 环境变量或者 opts.paths 来给 node 和 browserify 添加寻找模块时需要寻找的文件夹.
不像其他大多数平台, 在node里,通过 $NODE_PATH 使用shell风格的文件夹数组并没有直接使用 node_modules 文件夹来的好.
这是因为,你的应用越是和运行环境的配置紧密耦合,它的变数就越多.你的应用就只能跑在配置正确的环境里.
node 和 browserify 都支持使用 $NODE_PATH,但都不建议这样做.
非javascript部分
有许多的 browserify transforms 可供使用来做许多事情. 通常来说,转换用于把非javascript的东西打包到文件里.
brfs
其中一种用于引入任何类型的内容的方式就是使用 brfs, 它在node环境和浏览器环境下都可以用.
brfs 在编译的时候使用静态分析的方式解析 fs.readFile() 和 fs.readFileSync() 的调用结果到文件内容里
举个栗子,这是 main.js :
var fs = require('fs'); var html = fs.readFileSync(__dirname + '/robot.html', 'utf8'); console.log(html);
通过 brfs 运行后的结果是这样的:
var fs = require('fs'); var html = "<b>beep boop</b>"; console.log(html);
它非常方便,在node环境和浏览器环境下可以使用完全相同的代码,这让模块的分享和测试都很简单.
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);
如果你希望把css内联到文件里,你同样可以借助一些模块来实现比如 insert-css :
var fs = require('fs'); var insertStyle = require('insert-css'); var css = fs.readFileSync(__dirname + '/style.css', 'utf8'); insertStyle(css);
使用这种方式来插入css,对于一些使用npm来分类的,小型的,可重用的模块是很好用的(不理解...( ̄ε(# ̄)☆╰╮o( ̄皿 ̄///)),因为它是完全可控的.但如果你需要一个更完整的方案来使用browserify管理文件,可以看下 atomify 和 parcelify.
hbsify
jadeify
reactify
可重用的组件
把这些如何组织代码的想法结合在一起,我们可以搭建一个可重用的UI组件.我们可以在应用的多处重用它,也可以用在其他应用里.
下面是很简单的一个空组件模块:
module.exports = Widget; function Widget (opts) { if (!(this instanceof Widget)) return new Widget(opts); this.element = document.createElement('div'); } Widget.prototype.appendTo = function (target) { if (typeof target === 'string') target = document.querySelector(target); target.appendChild(this.element); };
顺便提一下这里的javascript构造函数: 你可以像上面这段代码那样,使用 this instanceof Widget 做一个校验, 这样人们就既可以通过 new Widget 来使用这个模块,又可以通过 Widget() 来使用它. 这样做很棒,你在API里隐式地执行了一个细节,并且通过使用prototype也得到了性能上的提升.
要使用这个组件文件,只需要调用 require() 来加载它,实例化,然后传入一个选择器或者dom元素来调用 .appendTo() 方法:
像这样:
var Widget = require('./widget.js'); var w = Widget(); w.appendTo('#container');
现在你的组件就被插入到DOM元素里了.
通过程序来创建html元素对于简单的内容来说是一种很好的方式.但如果内容很多,它就会变得冗长而不清晰.所幸有许多转换可以帮助你轻松的把html导入到 javascript 模型中.
让我们使用 brfs 模块来扩展刚才的组件例子. 我们也可以使用 domify 来把 fs.readFileSync() 返回的字符串变成html的dom元素.
var fs = require('fs'); var domify = require('domify'); var html = fs.readFileSync(__dirname + '/widget.html', 'utf8'); module.exports = Widget; function Widget (opts) { if (!(this instanceof Widget)) return new Widget(opts); this.element = domify(html); } Widget.prototype.appendTo = function (target) { if (typeof target === 'string') target = document.querySelector(target); target.appendChild(this.element); };
现在,我们的组件会加载 widget.html ,让我们来搞一个:
<div class="widget"> <h1 class="name"></h1> <div class="msg"></div> </div>
事件触发总是很常用的一个功能. 下面是教你如何使用内置的 events 模块或者 inherits 模块来触发事件:
var fs = require('fs'); var domify = require('domify'); var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; var html = fs.readFileSync(__dirname + '/widget.html', 'utf8'); inherits(Widget, EventEmitter); module.exports = Widget; function Widget (opts) { if (!(this instanceof Widget)) return new Widget(opts); this.element = domify(html); } Widget.prototype.appendTo = function (target) { if (typeof target === 'string') target = document.querySelector(target); target.appendChild(this.element); this.emit('append', target); };
现在我们可以在我们的组件实例里监听 append 事件
var Widget = require('./widget.js'); var w = Widget(); w.on('append', function (target) { console.log('appended to: ' + target.outerHTML); }); w.appendTo('#container');
我们可以给这个组件添加更多的方法来给html设置元素:
var fs = require('fs'); var domify = require('domify'); var inherits = require('inherits'); var EventEmitter = require('events').EventEmitter; var html = fs.readFileSync(__dirname + '/widget.html', 'utf8'); inherits(Widget, EventEmitter); module.exports = Widget; function Widget (opts) { if (!(this instanceof Widget)) return new Widget(opts); this.element = domify(html); } Widget.prototype.appendTo = function (target) { if (typeof target === 'string') target = document.querySelector(target); target.appendChild(this.element); }; Widget.prototype.setName = function (name) { this.element.querySelector('.name').textContent = name; } Widget.prototype.setMessage = function (msg) { this.element.querySelector('.msg').textContent = msg; }
如果设置元素的属性和内容变得太冗长,可以看下 hyperglue
最后,我们可以把 widget.js 和 widget.html 丢到 node_modules/app-widget 下. 因为我们的模块使用了brfs 转换,所以可以创建如下的 package.json 文件:
{ "name": "app-widget", "version": "1.0.0", "private": true, "main": "widget.js", "browserify": { "transform": [ "brfs" ] }, "dependencies": { "brfs": "^1.1.1", "inherits": "^2.0.1" } }
现在无论我们在应用的哪个地方执行 require('app-widget') , brfs 转换都会自动被应用到 widget.js ! 我们的组件甚至可以维护自己的依赖.这样我们可以更新这个组件的依赖而不用担心某个破坏性改变会传递到其它组件里.
确保在 .gitignore 文件里把 node_modules/app-widget 排除在外:
node_modules/* !node_modules/app-widget
如果你想要学习更多关于如何使用browserify在node和浏览器里共用渲染视图的逻辑以及一些处理html流的库, 你可以阅读 shared rendering in node and the browser
在node和浏览器里测试
测试模块化代码非常简单! 模块化的一个最大优点就是你的接口变得很容易单独实例化,所以,也很容易进行自动化测试.
不幸的是,少有一些测试工具能在模块的外部有不错的表现,它们更趋向于使用隐式的全局变量来调用它们自己特有的接口以及迟钝的流控制来得到清晰的隔离设计.(...(@_@;)什么东西...)
人们经常对模型有很大的不解,但如果你在设计模块的时候把测试考虑进去,那它通常就没有必要了.把IO操作从算法中分离出来,严格的限制模块的使用范围,把回调函数作为参数传给不同的接口,这些都能让你的代码更容易测试.
举个栗子,如果你有一个库,既可以处理IO读写,又可以输出协议.那么,考虑使用类似 streams 的接口 把IO层从协议层里分离出来.
你的代码会出乎意料的容易的测试,并且可以在不同的上下文环境里重用.这是一个互相循环的结果: 如果你得代码很难测试,那么,你的抽象就不够好,或者模块化不够充分.测试不应该是最后再考虑的事情,它应该在整个设计环节中被考虑进去,这会帮助你写出更好的接口.
测试库
tape
tape从一开始设计的时候就想好要同时支持node和browserify. 假设我们有一个 index.js ,它里面有一个异步的接口:
module.exports = function (x, cb) { setTimeout(function () { cb(x * 100); }, 1000); };
下面是如何使用 tape 来测试这个模块. 让我们这个文件放到 test/beep.js 里:
var test = require('tape'); var hundreder = require('../'); test('beep', function (t) { t.plan(1); hundreder(5, function (n) { t.equal(n, 500, '5*100 === 500'); }); });
由于测试文件在 test/ 目录下,所以我们可以直接通过 require('../') 来请求父文件夹下的 index.js 文件.因为node和browserify在没有package.json文件或者package.json文件里没有main 属性值的时候,会默认寻找 index.js
当通过 npm install tape 安装过tape以后,我们可以像请求其他模块一样使用 require() 方法来请求 tape
字符串 'beep' 是该测试自定义的名字. t.equal() 的第三个参数是一个完全自定义的描述.
t.plan(1) 表示我们希望有一个断言. 如果断言太多或者不足,测试会失败. 断言就是类似于 t.equal() 这样的比较. tape 断言有以下一些基本的方法:
- t.equal(a, b) - 使用 === 来比较a和b
- t.deepEqual(a, b) - 递归的比较a和b
- t.ok(x) - 如果x的结果不为真,结果就是失败
还有更多! 我们可以在第三个参数里写入任何想要的描述性内容.
运行我们的模块非常简单! 想要在node里运行这个模块,只需要执行 node test/beep.js:
$ node test/beep.js TAP version 13 # beep ok 1 5*100 === 500 1..1 # tests 1 # pass 1 # ok
结果就会被打印到标准输出流,退出代码是 0 .
想在浏览器里运行我们的代码,只需要:
$ browserify test/beep.js > bundle.js
然后把 bundle.js 放到 <script> 标签里:
<script src="bundle.js"></script>
然后在浏览器里加载html. 输出结果会在 debug console 面板里看到,可以按 F12 ,或者 ctrl-shift-j, 或者 ctrl-shift-k. 不同浏览器有不同的快捷键.
在浏览器里运行测试有一些麻烦,但你可以通过安装 testling 命令来帮助你. 首先:
npm install -g testling
然后现在只需要执行 browserify test/beep.js | testling:
$ browserify test/beep.js | testling TAP version 13 # beep ok 1 5*100 === 500 1..1 # tests 1 # pass 1 # ok
testling 会在你的系统里启动一个没有头的真实的浏览器来跑测试.
现在,假设我们需要添加另外一个文件: test/boop.js
var test = require('tape'); var hundreder = require('../'); test('fraction', function (t) { t.plan(1); hundreder(1/20, function (n) { t.equal(n, 5, '1/20th of 100'); }); }); test('negative', function (t) { t.plan(1); hundreder(-3, function (n) { t.equal(n, -300, 'negative number'); }); });
现在,我们的测试里有2个 test() 调用. 第二个测试需要等到第一个测试跑完以后才会执行,即使他们是异步的.你甚至可以通过 t.test() 在测试里面嵌套测试 .
在node里,我们可以像运行 test/beep.js 一样去运行 test/boop.js, 但如果我们需要运行这两个测试, tape 给我们提供了一个简写的命令,把 tape 的安装改为:
npm install -g tape
现在我们可以这样跑:
$ tape test/*.js TAP version 13 # beep ok 1 5*100 === 500 # fraction ok 2 1/20th of 100 # negative ok 3 negative number 1..3 # tests 3 # pass 3 # ok
你还可以把 test/*.js 传给browserify以便在浏览器里跑你的测试:
$ browserify test/* | testling TAP version 13 # beep ok 1 5*100 === 500 # fraction ok 2 1/20th of 100 # negative ok 3 negative number 1..3 # tests 3 # pass 3 # ok
为了把这些步骤都放在一起,我们可以配置 package.json 的 script 属性的 test 部分
{ "name": "hundreder", "version": "1.0.0", "main": "index.js", "devDependencies": { "tape": "^2.13.1", "testling": "^1.6.1" }, "scripts": { "test": "tape test/*.js", "test-browser": "browserify test/*.js | testlingify" } }
现在你可以使用 npm test 在node环境里跑测试, 使用 npm run test-browser 在浏览器环境里跑测试. 不用担心在运行 npm run 的时候会使用 -g 的命令进行全局安装,因为npm会自动给项目里的每个本地包设置 $PATH.
如果你有一些测试仅仅运行在node环境,另外一个测试仅仅运行在浏览器环境,你可以在 test/ 目录下设置子文件夹, 比如 test/server , test/browser , 前后端都要运行的测试就直接放在 test/ 下. 然后把相关的文件夹都加到 globs 里去. (关于什么是globs,可以看我的 这篇博文 node-glob学习 )
{ "name": "hundreder", "version": "1.0.0", "main": "index.js", "devDependencies": { "tape": "^2.13.1", "testling": "^1.6.1" }, "scripts": { "test": "tape test/*.js test/server/*.js", "test-browser": "browserify test/*.js test/browser/*.js | testling" } }
现在,除了公共要运行的测试,node端和服务器端还会运行各自的测试.
如果你想要一些更叼的功能,当你掌握了基本的概念以后,可以看下 prova
assert
使用内置的assert模块也是运行简单测试的一个好选择,虽然有时候有点诡异,不能保证所有的回调都被执行了.
你可以通过一些工具来解决这个问题,比如 macgyver ,但它需要根据情况来DIY.
code coverage
coverify
一个简单的在browserify里检查代码覆盖范围的方法是使用 coverify 转换
$ browserify -t coverify test/*.js | node | coverify
或者在浏览器环境里跑你的测试:
$ browserify -t coverify test/*.js | testling | coverify
coverify 通过转换每个包的资源,把每个表达式都通过 __coverageWrap() 函数进行包装.
程序的每个表达式都会得到一个唯一的ID, 表达式第一次执行的时候, __coverageWrap() 函数会输出 COVERED $FILE $ID 这样格式的信息.
在表达式执行之前,coverify 会输出 COVERAGE $FILE $NODES 这样格式的记录, 把表达式在整个文件里每次出现所在的字符范围以数组形式打印出来.
这是一个完整的测试跑下来的结果:
$ browserify -t coverify test/whatever.js | node COVERAGE "/home/substack/projects/defined/test/whatever.js" [[14,28],[14,28],[0,29],[41,56],[41,56],[30,57],[95,104],[95,105],[126,146],[126,146],[115,147],[160,194],[160,194],[152,195],[200,217],[200,218],[76,220],[59,221],[59,222]] COVERED "/home/substack/projects/defined/test/whatever.js" 2 COVERED "/home/substack/projects/defined/test/whatever.js" 1 COVERED "/home/substack/projects/defined/test/whatever.js" 0 COVERAGE "/home/substack/projects/defined/index.js" [[48,49],[55,71],[51,71],[73,76],[92,104],[92,118],[127,139],[120,140],[172,195],[172,196],[0,204],[0,205]] COVERED "/home/substack/projects/defined/index.js" 11 COVERED "/home/substack/projects/defined/index.js" 10 COVERED "/home/substack/projects/defined/test/whatever.js" 5 COVERED "/home/substack/projects/defined/test/whatever.js" 4 COVERED "/home/substack/projects/defined/test/whatever.js" 3 COVERED "/home/substack/projects/defined/test/whatever.js" 18 COVERED "/home/substack/projects/defined/test/whatever.js" 17 COVERED "/home/substack/projects/defined/test/whatever.js" 16 TAP version 13 # whatever COVERED "/home/substack/projects/defined/test/whatever.js" 7 COVERED "/home/substack/projects/defined/test/whatever.js" 6 COVERED "/home/substack/projects/defined/test/whatever.js" 10 COVERED "/home/substack/projects/defined/test/whatever.js" 9 COVERED "/home/substack/projects/defined/test/whatever.js" 8 COVERED "/home/substack/projects/defined/test/whatever.js" 13 COVERED "/home/substack/projects/defined/test/whatever.js" 12 COVERED "/home/substack/projects/defined/test/whatever.js" 11 COVERED "/home/substack/projects/defined/index.js" 0 COVERED "/home/substack/projects/defined/index.js" 2 COVERED "/home/substack/projects/defined/index.js" 1 COVERED "/home/substack/projects/defined/index.js" 5 COVERED "/home/substack/projects/defined/index.js" 4 COVERED "/home/substack/projects/defined/index.js" 3 COVERED "/home/substack/projects/defined/index.js" 7 COVERED "/home/substack/projects/defined/index.js" 6 COVERED "/home/substack/projects/defined/test/whatever.js" 15 COVERED "/home/substack/projects/defined/test/whatever.js" 14 ok 1 should be equal 1..1 # tests 1 # pass 1 # ok
这些 COVERED 和 COVERAGE 状态都会被打印到标准输出流. 另外,他们可以被注入到 coverify 命令中来得到更漂亮的输出:
$ browserify -t coverify test/whatever.js | node | coverify TAP version 13 # whatever ok 1 should be equal 1..1 # tests 1 # pass 1 # ok # /home/substack/projects/defined/index.js: line 6, column 9-32 console.log('whatever'); ^^^^^^^^^^^^^^^^^^^^^^^^ # coverage: 30/31 (96.77 %)
要把代码覆盖添加到你的项目里,你可以在 package.json 文件的 scripts 属性里添加一个入口:
{ "scripts": { "test": "tape test/*.js", "coverage": "browserify -t coverify test/*.js | node | coverify" } }
还有一个 covert 包,可以用来简化 browserify 和 coverify 步骤:
{ "scripts": { "test": "tape test/*.js", "coverage": "covert test/*.js" } }
需要安装 coverify 或者 covert 作为开发环境依赖, 执行 npm install -D coverify 或者 npm install -D covert.
*由于测试不太用得到,所以这部分都是直接翻译,代码也没有经过本人亲测.
bundling
这部分更详细的解释打包.
打包这个步骤,从入口文件开始,所有依赖关系中指定的资源文件都会被找到,然后一起打包到一个输出文件.
节省字节
首先你可能会纠结的事情之一是,通过 npm 安装的文件都被放在哪里了? 如何避免重复?
当你只需要在文件夹下进行一个安装的时候, npm 通常会解析出相似的版本到顶端文件夹,让两个模块可以共享这个依赖. 但是,当你安装许多包的时候,新的包不会被自动解析出公共部分. 但你可以在一个已经安装的包的 node_modules/ 下使用 npm dedupe 命令来解析出公共部分. 如果重复包问题依然存在,你也可以把整个 node_modules/ 删掉重新来过.
browserify 不会把完全相同的文件引入两次, 但兼容的版本可能会有细微的差异.browserify也不怕版本问题,它会使用node的 require() 算法,把对应布局在 node_modules/ 下的这个版本的包引入.
你还可以使用 browserify --list 以及 browserify --deps 命令查看引入了哪些文件,以便检查重复.
独立式文件
你可以通过 --standalone 命令来生成一个 UMD 类型的包,它可以运行在node里,也可以使用全局变量运行在浏览器里,也可以使用在AMD环境里.
只需要在你的打包命令中添加 --standalone NAME
$ browserify foo.js --standalone xyz > bundle.js
这个命令会把 foo.js 的内容引入到一个名为 xyz 的外部模块中. 如果在执行环境中检测到支持模块化,那它就会被按照模块来使用.否则,它会输出一个名为 xyz 的全局变量.
你可以使用 . 来指定命名空间:
$ browserify foo.js --standalone foo.bar.baz > bundle.js
如果在执行环境中的全局下已经有一个 foo 对象或者 foo.bar 对象存在, browserify 会把内容输出给这个对象. AMD 和 module.exports 模块也会一样做.
需要注意, standalone 只能用在单文件为入口或者直接请求一个文件的时候.
external bundles
忽略和排除
根据 browserify 的说法, "忽略" 意味着用一个空对象来替代指定的模块. "排除" 意味着把模完全从依赖关系中移除.
还有另一个方法可以达到和使用 忽略 和 排除 一样的效果,那就是通过 package.json 的 browser 属性,这篇文章的其他地方也说到过.
ignoring
忽略是一个可选的策略,它被设计用于伪造一个空的对象来替换某些代码路径里使用到的仅用于node的模块.(其实就是说,如果代码里请求了一个只能用于node的模块,那么请求的结果用空对象去替代那个只能用于node环境的对象).举个栗子,如果一个模块请求了一个只能用于node环境的库,但这个模块只用在某一块特定的代码里:
var fs = require('fs'); var path = require('path'); var mkdirp = require('mkdirp'); exports.convert = convert; function convert (src) { return src.replace(/beep/g, 'boop'); } exports.write = function (src, dst, cb) { fs.readFile(src, function (err, src) { if (err) return cb(err); mkdirp(path.dirname(dst), function (err) { if (err) return cb(err); var out = convert(src); fs.writeFile(dst, out, cb); }); }); };
browserify 已经 "忽略" 了 'fs' 模块,它的返回结果是一个空对象, 但是这里的 .write() 方法如果在静态分析的时候没有通过transform转换或者执行环境下存储过 fs 抽象, 它是无法执行的.
如果我们确实需要使用 convert() 方法,但却不想在最终的打包文件里看到 mkdirp , 我们可以通过 b.ignore('mkdir') 或者 browserify --ignore mkdirp 来忽略它. 这样代码就依然可以在浏览器环境下运行: 只要不调用 write() 方法, require('mkdirp') 不会报错而只是返回一个空对象.
一般来说,主要用于算法的模块(比如解析,格式化等)不适合自己处理 IO 操作. 但不管怎么样,至少这个技巧可以让你在浏览器里使用这些模块.
通过命令行忽略 foo 模块
browserify --ignore foo
通过 browserify 打包的实例 b 的 api 来忽略 foo 模块:
b.ignore('foo')
excluding
另一个相关的东西我们可能会用到的就是把整个模块从输出结果中移除,这样 require('modulename') 在执行环境里会执行失败. 如果你想把内容拆分到多个打包文件里,顺次加载多个文件,定义了 require() 的文件被延迟加载.
举个栗子,如果我们有一个单独的jquery,作为供应商,它要被单独打包,我们不想它出现在那个主要的包里:
$ npm install jquery
$ browserify -r jquery --standalone jquery > jquery-bundle.js
然后我们想在 main.js 里 require('jquery')
var $ = require('jquery'); $(window).click(function () { document.body.bgColor = 'red' });
在加载完jquery文件之后延迟加载它:
<script src="jquery-bundle.js"></script> <script src="bundle.js"></script>
为了不在 bundle.js 里看到jquery的定义,在编译 main.js 的时候,你可以 --exclude jquery:
browserify main.js --exclude jquery > bundle.js
使用命令行把 foo 模块排除:
browserify --exclude foo
通过 browserify 的实例 b 的api 来排除 foo 模块:
b.exclude('foo')
注: 按照上面的操作,会报错找不到 jquery 模块.为此我在stackoverflow上提了一个 issue(http://stackoverflow.com/questions/34279961/how-to-use-the-exclude-in-browserify/34342779#34342779).得到的结论是,应该使用 external 而不是 exclude .另外如果想要单独把 jquery 或者某个库提取成一个js,并且可以在bundle.js里通过 require() 请求到对应的模块, 可以使用 browserify-shim ,但是使用shim的原理是把原来请求的整个jquery模块替换成请求一个既存的全局变量,和exclude 并没有任何关系. 也可以伪造一个 jquery-fake.js 文件,返回全局变量jquery,然后通过配置,把原来请求到jquery的请求配置成请求 jquery-fake.js. 至于 exclude 到底应用在什么场景,其实到目前都没有发现.
browserify cdn
shimming
不幸的是,有些包并不遵循node风格的commonjs的输出写法.对于那些通过全局变量输出函数或者返回AMD格式的模块,有一个包可以帮助你自动把这些麻烦的模块转换成browserify可以读懂的模块.
browserify-shim
其中一个自动转换非commonjs包的方法就是通过 browserify-shim
browserify-shim 是一个转换工作,它会读取 package.json 文件的 "browserify-shim" 属性.
假设我们需要使用一个麻烦的第三方模块,我们把它放在了 ./vendor/foo.js 里,它输出的是一个全局变量的函数,函数名为 FOO. 我们可以这样配置 package.json 文件:
{ "browserify": { "transform": "browserify-shim" }, "browserify-shim": { "./vendor/foo.js": "FOO" } }
现在,当我们 require('./vendor/foo.js') , 我们可以得到 FOO 变量的结果,这个变量本来是 ./vendor/foo.js 想要输出给全局的,但是这个操作被阻止了,全局变量被放到了一个隔离的上下文里,防止了全局污染.
我们还可以使用 browser field 属性的配置, 让 require('foo') 取代很长的相对路径 require('./vendor/foo.js'), 来获取这个模块.
{ "browser": { "foo": "./vendor/foo.js" }, "browserify": { "transform": "browserify-shim" }, "browserify-shim": { "foo": "FOO" } }
现在, require('foo') 的返回值就是原本 ./vendor/foo.js 想要放到全局的变量 FOO.
分区
大多数时候,默认的打包方式,把所有资源映射图的入口文件都打包输出到一个文件,就已经很足够了,尤其是考虑到打包能够只发送一个http请求就获取全部的javascript组件,从而降低延迟时间.
然后,有时候,这个自带的功能对于某些网页上的大多数用户来说是几乎用不到的,比如后台管理页. 在 ignoring and excluding 部分说到了如何实现分区,但是对于某些大型的,依赖不固定的项目来说,手动地把公共部分提取出来会很痛苦.
所幸,有一些插件可以实现自动把browserify里的公共部分输出到单独的文件里.
factor-bundle
factor-bundle 会根据入口文件(两个或以上),把 browserify 的输出拆分成多个文件.每个入口文件单独生成一个对应的文件. 被两个(或以上)入口文件使用的公共模块会被提取到一个公共的包里.
举个栗子,假设我们有两个页面: /x 和 /y. 每个页面都请求一个入口文件, x.js 被 /x 请求, y.js 被 /y 请求.
然后我们生成了各页面各自使用的 bundle/x.js 和 bundle/y.js 以及一个他们共享的依赖文件 bundle/common.js:
browserify x.js y.js -p [ factor-bundle -o bundle/x.js -o bundle/y.js ] -o bundle/common.js
现在我们就可以简单的把在各个页面里插入两个script标签. 在 /x 页面,我们可以输出:
<script src="/bundle/common.js"></script> <script src="/bundle/x.js"></script>
在 /y 页面:
<script src="/bundle/common.js"></script> <script src="/bundle/y.js"></script>
你也可以通过 ajax 异步地加载包,或者动态创建script标签插入.但 factor-bundle 只关心如何生成文件,而不关心如何加载他们.
partition-bundle
partition-bundle 类似于 factor-bundle,用于把输出文件拆分为多个包, 但是它还包含了一个内置的加载器 loadjs() 函数.
partition-bundle 包含了一个json文件,映射了资源文件和打包后的文件关系:
{ "entry.js": ["./a"], "common.js": ["./b"], "common/extra.js": ["./e", "./d"] }
然后 partition-bundle 会被作为一个插件加载,需要传入映射文件, 输出文件夹, 以及目标url (动态加载需要用到)
browserify -p [ partition-bundle --map mapping.json --output output/directory --url directory ]
现在你可以把它放到页面里了:
<script src="entry.js"></script>
让你的页面加载入口文件. 在入口文件里面,你可以通过 loadjs()函数动态的加载其他的文件.
a.addEventListener('click', function() { loadjs(['./e', './d'], function(e, d) { console.log(e, d); }); });
分区部分的代码没有亲测,不确保代码正确
compiler pipeline
从版本5开始,browserify 通过 labeled-stream-splicer 暴露了它的编译管道.
这意味着可以直接在内部的管道里直接添加或删除转换.这个管道提供了清晰的接口来处理一些高级自定义特性,比如监测文件或者从多个入口文件中提取公共部分进行打包.
举个栗子,内置的标签机制使用的是整数,我们可以把它替换成哈希值ID: 在依赖被解析成哈希资源文件后,注入一个传递的转换. 然后我们可以使用我们捕捉到的哈希值来创建我们自定义的标签来替换内置的标签机制.
var browserify = require('browserify'); var through = require('through2'); var shasum = require('shasum'); var b = browserify('./main.js'); var hashes = {}; var hasher = through.obj(function (row, enc, next) { hashes[row.id] = shasum(row.source); this.push(row); next(); }); b.pipeline.get('deps').push(hasher); var labeler = through.obj(function (row, enc, next) { row.id = hashes[row.id]; Object.keys(row.deps).forEach(function (key) { row.deps[key] = hashes[row.deps[key]]; }); this.push(row); next(); }); b.pipeline.get('label').splice(0, 1, labeler); b.bundle().pipe(process.stdout);
现在,在输出的文件里,我们使用了文件的哈希值ID来取代默认的整数ID:
$ node bundle.js (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({"5f0a0e3a143f2356582f58a70f385f4bde44f04b":[function(require,module,exports){ var foo = require('./foo.js'); var bar = require('./bar.js'); console.log(foo(3) + bar(4)); },{"./bar.js":"cba5983117ae1d6699d85fc4d54eb589d758f12b","./foo.js":"736100869ec2e44f7cfcf0dc6554b055e117c53c"}],"cba5983117ae1d6699d85fc4d54eb589d758f12b":[function(require,module,exports){ module.exports = function (n) { return n * 100 }; },{}],"736100869ec2e44f7cfcf0dc6554b055e117c53c":[function(require,module,exports){ module.exports = function (n) { return n + 1 }; },{}]},{},["5f0a0e3a143f2356582f58a70f385f4bde44f04b"]);
需要注意的是,内置的标签机制还做了其他的事情,比如检查外部文件和排除文件(external和excluded)的配置,所以如果你使用到了这些功能,要替换掉它就会很难.这里只是举个栗子,告诉你能够通过编译管道提供的钩子来做哪些事情.
build your own browserify
阶段标记
browserify 管道的每个阶段都有一个标记,允许你在上面放钩子. 需要获取指定的标记,可以通过 .get(name) 方法, 它会在合适的标记处返回一个 labeled-stream-splicer 句柄. 获取这个句柄以后,你可以使用 .push(), .pop(), .shift(), .unshift(), 以及 .splice() 方法,在管道里添加你自己的流转换操作或者移除已经存在的流转换操作.
record
在 record 阶段,你可以捕获在 deps 阶段输入的内容,然后在调用 .bundle() 之后再次重现它. 不同于以前,版本5可以多次生成打包文件. 这对于一些在文件被修改后重新编译的工具比如 watchify ,是非常好用的.
deps
deps 阶段需要入口和 require() 的文件或对象作为输入, 调用 module-deps 来生成一个json数据流, 这个流包含了所有依赖关系里的文件.
module-deps 可以通过一些自定义的方式调用,比如:
- 为package.json设置browserify转换的属性
- 过滤掉 external, excluded, ignore 的文件
- 通过设置 browserify 构造函数中的 opts.extensions 参数配置默认的文件扩展名,原来是.js和.json文件,添加附加选项,使它支持其他扩展名的文件.
- 配置一个全局的 insert-module-globals 转换来检测和执行 process, Buffer, global, __dirname, __filename.
- 设置一个列表, 包含了那些node内置,然后被改装成为browerisy可用的东西.
json
这个转换会在每个 .json 扩展名的文件的前面添加 module.exports =
unbom
这个转换会移除字节顺序标记,在某些windows系统的文件编辑时,会用指定文件的字节顺序(大端小端). 这个标记会被node忽略,所以browserify为了兼容,也会把它忽略.''
syntax
这个转换会通过 syntax-error 检查语法错误,给出错误信息以及错误所在行和列.
sort
这个阶段使用 deps-sort 对写入的行进行排序以确定最后生成的打包文件.
dedupe
这个阶段的转换使用了 sort 阶段的 deps-sort 所提供的重复信息, 然后移除内容重复的文件.
label
这阶段会把每个可能暴露系统路径的文件的ID进行转换,把原来很大的文件包用整数ID来代表.
label 阶段还会把基于 opts.basedir 和 process.cwd() 的文件路径进行标准化,以防止暴露系统文件路径信息.
emit-deps
这个阶段会在 label 阶段结束后,给每一行触发一个 'dep' 事件
debug
如果在实例化构造函数 browserify() 的时候传入了 opts.debug 参数, 那在这个转换阶段,它会使用 pack 阶段的 browser-pack 来给输入流添加 sourceRoot 和 sourceFile 属性.
pack
这个阶段,会把输入流和 'id', 'source'参数一起转换,使用 browser-pack 生成打包后的联合的javascript文件.
wrap
这是一个空阶段,在这个阶段你可以很容易的附加自定义转换,而不会妨碍原来的机制.
browser-unpack
browser-unpack 可以把编译后的打包文件转换回一个非常类似于 module-deps 输出的格式.
它让你方便于检查或者转换一个已经编译好的文件.
$ browserify src/main.js | browser-unpack [ {"id":1,"source":"module.exports = function (n) { return n * 100 };","deps":{}} , {"id":2,"source":"module.exports = function (n) { return n + 1 };","deps":{}} , {"id":3,"source":"var foo = require('./foo.js');\nvar bar = require('./bar.js');\n\nconsole.log(foo(3) + bar(4));","deps":{"./bar.js":1,"./foo.js":2},"entry":true} ]
这个分解的过程需要使用到一些类似于 factor-bundle 和 bundle-collapser 的工具
插件
加载完以后,插件就有权限获取browserify自身实例
使用插件
插件应该尽量少使用,除非全局的transform没有足够的能力来实现你想要的功能.
你可以在命令行使用 -p 来加载插件:
$ browserify main.js -p foo > bundle.js
你可以加载一个插件 foo. foo 的获取是通过 node 的 require() 方式, 所以如果需要加载一个本地的文件作为插件, 文件的路径要以 ./ 开始. 需要从 node_modules/foo 里加载插件,只需要使用 -p foo.
你可以通过 [ ] 给插件传递参数, [ ]里的是整个插件表达式,包括插件的名字(就是第一个参数)
$ browserify one.js two.js -p [ factor-bundle -o bundle/one.js -o bundle/two.js ] > common.js
命令行语法的解析是通过 subarg 包实现的
要查看browserify插件的列表,请浏览npm官网,查找包的关键词 "browserify-plugin": http://npmjs.org/browse/keyword/browserify-plugin
编写插件
要编写一个插件,只要写一个包,输出一个函数,函数接受两个参数,第一个是browserify实例,另一个是自已定义的参数
// example plugin module.exports = function (b, opts) { // ... }
插件会通过监听事件来直接操作实例 b ,或者把转换拼接到管道里. 除非有非常充足的理由,否则插件不应该重写原有的方法.