这篇文档用以说明如何使用browserify来构建模块化应用javascript
browserify是一个编译工具,经过它能够在浏览器环境下像nodejs同样使用遵循commonjs规范的模块化编程.css
你可使用browserify来组织代码,也可使用第三方模块,不须要会nodejs,只须要用到node来编译,用到npm来安装包.html
browserify模块化的用法和node是同样的,因此npm上那些本来仅仅用于node环境的包,在浏览器环境里也同样能用.前端
如今npm上有愈来愈多的包,在设计的时候就是想好既能在node环境下用,也能在浏览器环境下用的.甚至还有不少包就是给浏览器环境使用的. npm是为全部的javascript服务的,不管前端后端.java
能够直接安装本手册: node
npm install -g browserify-handbook
而后执行 browserify-handbook 命令,就能够在本地阅读这个文件.固然,你也能够直接在这里阅读.react
在深刻学习了解browserify以前,有必要先了解在nodejs里commonjs模块化是如何工做的.jquery
在nodejs里,有一个require()方法用于加载其余文件或包.git
经过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从你给你的入口文件开始,寻找全部调用require()方法的地方, 而后沿着抽象语法树,经过 detective 模块来找到全部请求的模块. (其实这个意思就是说,它require里还有require,还有require,全部的require像一棵树同样,而后沿着这棵树,经过detective来找到全部的模块)
每个require()调用里都传入一个字符串做为参数,browserify把这个字符串解析成文件的路径而后递归的查找文件直到整个依赖树都被找到.
每一个被require()的文件,它的名字都会被映射到内部的id,最后被整合到一个javascript文件中.
这就意味着最后打包生成了文件已经包含了全部能让你的应用跑起来所须要的东西.
查看更多browserify的用法,能够看文档的编译器管道部分.
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> 标签,只发一次请求.
它没有资源映射表,当报错的时候,你只能看到最后报错的位置,而没法轻易的定位到这个报错在源代码的位置.
不使用 <script> 标签,每一个文件都被包装在 define() 函数和回调里. 这里是AMD(就是requirejs).
第一个参数是一个数组,数组里的的每一个模块对应回调里的每一个参数.当全部的模块都被加载完之后,回调被执行.
define(['jquery'] , function ($) { return function () {}; });
你能够给你的模块取一个名字,这样每一个模块均可以引用它.
commonjs有一个语法糖,它会把每一个回调都字符串化,而后经过正则搜索里面的 require() 方法 (源码)
用这种方式编码,比前面两种方式对顺序的要求要低不少,由于顺序已经明确的指定在了依赖信息里.
为了提高性能,大多数时候,AMD也会在服务端被打包成一个文件.在开发的过程当中,更多的使用到了AMD的异步特性.
若是你既要使用语法糖,也想使用构建打包来提高性能,为何不直接把全部的AMD逻辑整合到一块儿,把commonjs打包呢?
经过打包工具,模块会被解析到对应的地址,而且顺序正确.生产环境和开发环境会更类似,更少产生碎片. node 和 npm 把CJS语法变得更好用,创造了新的开发环境.
你的代码能够无缝的在node和browser之间运行.只须要执行一下构建命令,使用一些工具来生成资源映射表,实现自动从新构建打包.
另外,node独有的寻找模块的算法还能把咱们从版本混乱的崩溃状态中解救出来.咱们能够在同一个应用中请求多个版本不一样的包,也不会有任何问题.为了节省下载量,你能够删除重复数据,这在后面会说到.
整合会有一些缺点,可是使用开发工具彻底能够解决它们.
browserify支持 --debug或者 -d 标识或者 opts.debug 参数来支持资源映射图.资源映射图能在浏览器报错的时候,把在打包后的js报错的行列数映射到打包前这个报错所在的js的文件名和对应的行列数.
资源映射图包含了全部原文件的内容,因此你只须要直接把打包好的文件放到服务器上而不须要把全部的源文件都传到服务器的对应路径上.
把全部的原文件放到内联的资源映射图里的缺点是,打包好的js的大小会比原来大一倍.虽然方便调试,但没有必要把资源映射图传到生产环境. 因此,你可使用 exorcist 来把原本内联的资源映射图放到一个单独的 bundle.map.js 里面:
browserify main.js --debug | exorcist bundle.js.map > bundle.js
每次从新编译都要执行一下命令是一件很烦很费时的事.所幸有不少工具能够解决这个问题.有些工具还在必定程度上支持 live-reloading,有些仍是须要传统的手动刷新.
下面列出一部分比较常见的工具,npm上其实还有不少不少.不一样的工具备各自的侧重点和开发方式. 可能要多作些前期工做来找到最符合你的指望的工具,但它的多样性可让程序员更高效的工做,也提供了更多创新和实验的机会. 我认为,从中长期来讲,多样性的工具加上比较少的browserify核心功能,会比一些听起来更好用,其实只是把更多功能整合到browserify核心功能里(对内核形成位衰减,对有用的版本控制形成破坏)的工具要好.
下面是配置browserify工做流时你能够考虑的一些模块.对于没有列出的工具,也须要留个心.
除非你只输出一次文件,你可使用 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 main.js
它会开始在一个http端口运行.
wzrd 和beefy差很少,可是更轻量级
只须要安装 npm install -g wzrd 就可使用了:
wzrd app.js
而后打开浏览器 http://localhost:9966
若是你正在使用 express, 能够看下 browserify-middleware 或者 enchilada
他们都提供了可供express使用的browserify中间件.
livereactload是一个专门给 react 用的工具,它会在你修改代码的时候自动更新页面状态.
livereactload只是一个普通的browserify转换,你能够经过 -t livereactload 来使用它. 点此查看更多 project readme
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专一于为browserify开发提供增量打包和实时刷新服务 (包括css的注入)
安装:
npm install budo -g
在入口文件执行:
budo app.js
会在 http://localhost:9966 打开一个默认的 index.html 页,而后在文件保存的时候进行增量打包. 请求会被延迟到打包完成之后,因此若是你在打包未完成时刷新页面,不会看到旧的或者空的包.
开启 LiveReload 可让你修改 JS/HTML/CSS 时自动刷新浏览器:
budo app.js --live
你也能够在 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 打包的结果.
If you use grunt, you'll probably want to use the grunt-browserify plugin.
若是你使用 grunt, 你可使用 grunt-browserify 插件
若是你使用gulp, 你能够直接使用browserify API
这里有一个帮助你开始使用 gulp 和 browserify 的例子: a guide for getting started.
这里有一个教你 如何在gulp里使用watchify来加快browserify构建速度 的例子,它来自gulp官网.
为了让更多原本为node而开发的npm模块也能用在浏览器里,browserify提供了许多专门给浏览器使用的node内核的库.
events, stream, url, path, querystring 这些模块在浏览器环境中至关好用.
另外,若是 browserify 监测到你使用了 Buffer, process, global, __filename, __dirname 这些模块, 它会引入适用于浏览器的变量.
因此,即便一个模块里有许多buffer和stream操做,只要它不作任何IO读写,就可能只在浏览器环境运行.
若是你之前历来没有接触过node,这里有一些例子告诉你这些全局变量都能作什么. 注意,这些全局变量只有在你使用了他们,或者你依赖的模块里使用了他们,他们才会被定义.
在node里,全部的文件和网络API都是经过buffer块来处理的. 在browserify里, Buffer API 是 buffer 模块提供的, 它使用了一种高性能的加强型数组,而且在旧的浏览器里也能向下兼容.
这里是一个使用 Buffer 来把base64字符串转换成十六进制的例子:
var buf = Buffer('YmVlcCBib29w', 'base64'); var hex = buf.toString('hex'); console.log(hex);
它会输出:
6265657020626f6f70
在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 是node里的最高做用域, 相似于浏览器里的全局变量是申明在 window 下那样. 在browserify里, global 只是 window 的一个别名.
__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 是当前文件所在的文件夹. 和 __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" 属性 , 这个属性值会告诉 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
咱们能够经过定义 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 文件夹下,由于这样就很差区分从 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,但都不建议这样作.
有许多的 browserify transforms 可供使用来作许多事情. 一般来讲,转换用于把非javascript的东西打包到文件里.
其中一种用于引入任何类型的内容的方式就是使用 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.
把这些如何组织代码的想法结合在一块儿,咱们能够搭建一个可重用的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
测试模块化代码很是简单! 模块化的一个最大优势就是你的接口变得很容易单独实例化,因此,也很容易进行自动化测试.
不幸的是,少有一些测试工具能在模块的外部有不错的表现,它们更趋向于使用隐式的全局变量来调用它们本身特有的接口以及迟钝的流控制来获得清晰的隔离设计.(...(@_@;)什么东西...)
人们常常对模型有很大的不解,但若是你在设计模块的时候把测试考虑进去,那它一般就没有必要了.把IO操做从算法中分离出来,严格的限制模块的使用范围,把回调函数做为参数传给不一样的接口,这些都能让你的代码更容易测试.
举个栗子,若是你有一个库,既能够处理IO读写,又能够输出协议.那么,考虑使用相似 streams 的接口 把IO层从协议层里分离出来.
你的代码会出乎意料的容易的测试,而且能够在不一样的上下文环境里重用.这是一个互相循环的结果: 若是你得代码很难测试,那么,你的抽象就不够好,或者模块化不够充分.测试不该该是最后再考虑的事情,它应该在整个设计环节中被考虑进去,这会帮助你写出更好的接口.
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 断言有如下一些基本的方法:
还有更多! 咱们能够在第三个参数里写入任何想要的描述性内容.
运行咱们的模块很是简单! 想要在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模块也是运行简单测试的一个好选择,虽然有时候有点诡异,不能保证全部的回调都被执行了.
你能够经过一些工具来解决这个问题,好比 macgyver ,但它须要根据状况来DIY.
一个简单的在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.
*因为测试不太用获得,因此这部分都是直接翻译,代码也没有通过本人亲测.
这部分更详细的解释打包.
打包这个步骤,从入口文件开始,全部依赖关系中指定的资源文件都会被找到,而后一块儿打包到一个输出文件.
首先你可能会纠结的事情之一是,经过 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 只能用在单文件为入口或者直接请求一个文件的时候.
根据 browserify 的说法, "忽略" 意味着用一个空对象来替代指定的模块. "排除" 意味着把模彻底从依赖关系中移除.
还有另外一个方法能够达到和使用 忽略 和 排除 同样的效果,那就是经过 package.json 的 browser 属性,这篇文章的其余地方也说到过.
忽略是一个可选的策略,它被设计用于伪造一个空的对象来替换某些代码路径里使用到的仅用于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')
另外一个相关的东西咱们可能会用到的就是把整个模块从输出结果中移除,这样 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 到底应用在什么场景,其实到目前都没有发现.
不幸的是,有些包并不遵循node风格的commonjs的输出写法.对于那些经过全局变量输出函数或者返回AMD格式的模块,有一个包能够帮助你自动把这些麻烦的模块转换成browserify能够读懂的模块.
其中一个自动转换非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 会根据入口文件(两个或以上),把 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 相似于 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); }); });
分区部分的代码没有亲测,不确保代码正确
从版本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)的配置,因此若是你使用到了这些功能,要替换掉它就会很难.这里只是举个栗子,告诉你可以经过编译管道提供的钩子来作哪些事情.
browserify 管道的每一个阶段都有一个标记,容许你在上面放钩子. 须要获取指定的标记,能够经过 .get(name) 方法, 它会在合适的标记处返回一个 labeled-stream-splicer 句柄. 获取这个句柄之后,你可使用 .push(), .pop(), .shift(), .unshift(), 以及 .splice() 方法,在管道里添加你本身的流转换操做或者移除已经存在的流转换操做.
在 record 阶段,你能够捕获在 deps 阶段输入的内容,而后在调用 .bundle() 以后再次重现它. 不一样于之前,版本5能够屡次生成打包文件. 这对于一些在文件被修改后从新编译的工具好比 watchify ,是很是好用的.
deps 阶段须要入口和 require() 的文件或对象做为输入, 调用 module-deps 来生成一个json数据流, 这个流包含了全部依赖关系里的文件.
module-deps 能够经过一些自定义的方式调用,好比:
这个转换会在每一个 .json 扩展名的文件的前面添加 module.exports =
这个转换会移除字节顺序标记,在某些windows系统的文件编辑时,会用指定文件的字节顺序(大端小端). 这个标记会被node忽略,因此browserify为了兼容,也会把它忽略.''
这个转换会经过 syntax-error 检查语法错误,给出错误信息以及错误所在行和列.
这个阶段使用 deps-sort 对写入的行进行排序以肯定最后生成的打包文件.
这个阶段的转换使用了 sort 阶段的 deps-sort 所提供的重复信息, 而后移除内容重复的文件.
这阶段会把每一个可能暴露系统路径的文件的ID进行转换,把原来很大的文件包用整数ID来表明.
label 阶段还会把基于 opts.basedir 和 process.cwd() 的文件路径进行标准化,以防止暴露系统文件路径信息.
这个阶段会在 label 阶段结束后,给每一行触发一个 'dep' 事件
若是在实例化构造函数 browserify() 的时候传入了 opts.debug 参数, 那在这个转换阶段,它会使用 pack 阶段的 browser-pack 来给输入流添加 sourceRoot 和 sourceFile 属性.
这个阶段,会把输入流和 'id', 'source'参数一块儿转换,使用 browser-pack 生成打包后的联合的javascript文件.
这是一个空阶段,在这个阶段你能够很容易的附加自定义转换,而不会妨碍原来的机制.
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 ,或者把转换拼接到管道里. 除非有很是充足的理由,不然插件不该该重写原有的方法.