這篇文檔用以說明如何使用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 ,或者把轉換拼接到管道里. 除非有很是充足的理由,不然插件不該該重寫原有的方法.