引言
1. manually
以前,我新開一個網頁項目,然後想到要用jQuery,我會打開瀏覽器,然後找到jQuery的官方網站,點擊那個醒目的“Download jQuery”按鈕,下載到.js
文件,然後把它丟在項目目錄裏。在需要用到它的地方,這樣用<script>
引入它:
<script src="path/to/jquery.js"></script>
2. Bower
後來,我開始用[Bower][]這樣的包管理工具。所以這個過程變成了:先打開命令行用bower
安裝jQuery。
bower install jquery
再繼續用<script>
引入它。
<script src="bower_components/jquery/dist/jquery.js"></script>
3. npm&Browserify
現在,我又有了新的選擇,大概是這樣:
命令行用npm
安裝jQuery。
npm install jquery
在需要用到它的JavaScript代碼裏,這樣引入它:
var $ = require("jquery");
沒錯,這就是使用npm的包的一般方法。但特別的是,這個npm的包是我們熟知的jquery
,而它將用在瀏覽器中。
[Browserify][],正如其名字所體現的動作那樣,讓原本屬於服務器端的Node及npm,在瀏覽器端也可使用。
顯然,上面的過程還沒結束,接下來是Browserify的工作(假定上面那段代碼所在的文件叫main.js
):
browserify main.js -o bundle.js
最後,用<script>
引用Browserify生成的bundle.js
文件。
<script src="bundle.js"></script>
這就是依託Browserify建立起來的第三選擇。
等下,怎麼比以前變複雜了?
CommonJS風格的模塊及依賴管理
其實,在這個看起來更復雜的過程中,require()
具有非凡的意義。
Browserify並不只是一個讓你輕鬆引用JavaScript包的工具。它的關鍵能力,是JavaScript模塊及依賴管理。(這纔是爲師的主業)
就模塊及依賴管理這個問題而言,已經有RequireJS[]這些優秀的作品。而現在,Browserify又給了我們新的選擇。
Browserify參照了Node中的模塊系統,約定用require()
來引入其他模塊,用module.exports
來引出模塊。在我看來,Browserify不同於RequireJS和Sea.js的地方在於,它沒有着力去提供一個“運行時”的模塊加載器,而是強調進行預編譯。預編譯會帶來一個額外的過程,但對應的,你也不再需要遵循一定規則去加一層包裹。因此,相比較而言,Browserify提供的組織方式更簡潔,也更符合CommonJS規範。
像寫Node那樣去組織你的JavaScript,Browserify會讓它們在瀏覽器里正常運行的。
安裝及使用
命令行形式
命令行形式是官方貼出來的用法,因爲看起來最簡單。
Browserify本身也是npm,通過npm的方式安裝:
npm install -g browserify
這裏-g
的參數表示全局,所以可以在命令行內直接使用。接下來,運行browserify
命令到你的.js
文件(比如entry.js
):
browserify entry.js -o bundle.js
Browserify將遞歸分析你的代碼中的require()
,然後生成編譯後的文件(這裏的bundle.js
)。在編譯後的文件內,所有JavaScript模塊都已合併在一起且建立好了依賴關係。最後,你在html裏引用這個編譯後的文件(喂,和引言裏的一樣啊):
<script src="bundle.js"></script>
有關這個編譯命令的配置參數,請參照[node-browserify#usage][]。如果你想要做比較精細的配置,命令行形式可能會不太方便。這種時候,推薦結合Gulp使用。
+ Gulp形式
結合Gulp使用時,你的Browserify只安裝在某個項目內:
npm install browserify --save-dev
建議加上後面的--save-dev
以保存到你項目的package.json
裏。
接下來是gulpfile.js
的部分,下面是一個簡單示例:
var gulp = require("gulp");
var browserify = require("browserify");
var sourcemaps = require("gulp-sourcemaps");
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
gulp.task("browserify", function () {
var b = browserify({
entries: "./javascripts/src/main.js", //入口點js
debug: true //是告知Browserify在運行同時生成內聯sourcemap用於調試
});
return b.bundle()
.pipe(source("bundle.js"))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true}))
.pipe(sourcemaps.write("."))
.pipe(gulp.dest("./javascripts/dist"));
});
可以看到,Browserify是獨立的,我們需要直接使用它的API,並將它加入到Gulp的任務中。
在上面的代碼中,debug: true
是告知Browserify在運行同時生成內聯sourcemap用於調試。引入gulp-sourcemaps
並設置loadMaps:
true
是爲了讀取上一步得到的內聯sourcemap,並將其轉寫爲一個單獨的sourcemap文件。vinyl-source-stream
用於將Browserify的bundle()
的輸出轉換爲Gulp可用的[vinyl][](一種虛擬文件格式)流。vinyl-buffer
用於將vinyl流轉化爲buffered
vinyl文件(gulp-sourcemaps
及大部分Gulp插件都需要這種格式)。
這樣配置好之後,直接運行gulp browserify
就可以得到結果了,可能像這樣:
如果你的代碼比較多,可能像上圖這樣一次編譯需要1s以上,這是比較慢的。這種時候,推薦使用[watchify][]。它可以在你修改文件後,只重新編譯需要的部分(而不是Browserify原本的全部編譯),這樣,只有第一次編譯會花些時間,此後的即時變更刷新則十分迅速。
有關更多Browserify + Gulp的示例,請參考[Gulp Recipes][]。
特性及簡要原理
使用Browserify來組織JavaScript,有什麼要注意的地方嗎?
要回答這個問題,我們先看看Browserify到底做了什麼。下面是一個比較詳細的例子。
項目內現在用到2個.js文件
,它們存在依賴關係,其內容分別是:
name.js
:
module.exports = "aya";
main.js
:
var name = require("./name");
console.log("Hello! " + name);
然後對main.js
運行Browserify,得到的bundle.js
的文件內容是這樣的:
(function e(t, n, r) {
// ...
})({
1: [function (require, module, exports) {
var name = require("./name");
console.log("Hello! " + name);
}, {"./name": 2}],
2: [function (require, module, exports) {
module.exports = "aya";
}, {}]
}, {}, [1])
//# sourceMappingURL=bundle.js.map
請先忽略掉省略號裏的部分。然後,它的結構就清晰多了。可以看到,整體是一個立即執行的函數([IIFE][]),該函數接收了3個參數。其中第1個參數比較複雜,第2、3個參數在這裏分別是{}
和[1]
。
模塊map
第1個參數是一個Object,它的每一個key都是數字,作爲模塊的id,每一個數字key對應的值是長度爲2的數組。可以看出,前面的main.js
中的代碼,被function(require,
module, exports){}
這樣的結構包裝了起來,然後作爲了key1
數組裏的第一個元素。類似的,name.js
中的代碼,也被包裝,對應到key2
。
數組的第2個元素,是另一個map對應,它表示的是模塊的依賴。main.js
在key1
,它依賴name.js
,所以它的數組的第二個元素是{"./name":
2}
。而在key2
的name.js
,它沒有依賴,因此其數組第二個元素是空Object{}
。
因此,這第1個複雜的參數,攜帶了所有模塊的源碼及其依賴關係,所以叫做模塊map。
包裝
前面提到,原有的文件中的代碼,被包裝了起來。爲什麼要這樣包裝呢?
因爲,瀏覽器原生環境中,並沒有require()
。所以,需要用代碼去實現它(RequireJS和Sea.js也做了這件事)。這個包裝函數提供的3個參數,require
、module
、exports
,正是由Browserify實現了特定功能的3個關鍵字。
緩存
第2個參數幾乎總是空的{}
。它如果有的話,也是一個模塊map,表示本次編譯之前被加載進來的來自於其他地方的內容。現階段,讓我們忽略它吧。
入口模塊
第3個參數是一個數組,指定的是作爲入口的模塊id。前面的例子中,main.js
是入口模塊,它的id是1,所以這裏的數組就是[1]
。數組說明其實還可以有多個入口,比如運行多個測試用例的場景,但相對來說,多入口的情況還是比較少的。
實現功能
還記得前面忽略掉的省略號裏的代碼嗎?這部分代碼將解析前面所說的3個參數,然後讓一切運行起來。這段代碼是一個函數,來自於browser-pack項目的[prelude.js][]。令人意外的是,它並不複雜,而且寫有豐富的註釋,很推薦你自行閱讀。
所以,到底要注意什麼?
到這裏,你已經看過了Browserify是如何工作的。是時候回到前面的問題了。首先,在每個文件內,不再需要自行包裝。
你可能已經很習慣類似下面這樣的寫法:
;(function(){
// Your code here.
}());
但你已經瞭解到,Browserify的編譯會將你的代碼封裝在局部作用域內,所以,你不再需要自己做這個事情,像這樣會更好:
// Your code here.
類似的,如果你想用"use strict";
啓用嚴格模式,直接寫在外面就可以了,這表示在某個文件的代碼範圍內啓用嚴格模式。
其次,保持局部變量風格。我們很習慣通過window.jQuery
和window.$
這樣的全局變量來訪問jQuery這樣的庫,但如果使用Browserify,它們都應只作爲局部變量:
var $ = require("jquery");
$("#alice").text("Hello!");
這裏的$
就只存在於這個文件的代碼範圍內(獨立的作用域)。如果你在另一個文件內要使用jQuery,需要按照同樣的方式去require()
。
然而,新的問題又來了,既然jQuery變成了這種局部變量的形式,那我們熟悉的各種jQuery插件要如何使用呢?
browserify-shim
你一定熟悉這樣的jQuery插件使用方式:
<script src="jquery.js"></script>
<script src="jquery.plugin.js"></script>
<script>
// Now the jQuery plugin is available.
</script>
很多jQuery插件是這樣做的:默認window.jQuery
存在,然後取這個全局變量,把自己添加到jQuery中。顯然,這在Browserify的組織方式裏是沒法用的。
爲了讓這樣的“不兼容Browserify”(其實是不兼容CommonJS)的JavaScript模塊(如插件)也能爲Browserify所用,於是有了[browserify-shim][]。
下面,以jQuery插件[jquery.pep.js][]爲例,請看browserify-shim的使用方法。
使用示例
安裝browserify-shim:
npm install browserify-shim --save-dev
然後在package.json
中做如下配置:
"browserify": {
"transform": [ "browserify-shim" ]
},
"browser": {
"jquery.pep" : "./vendor/jquery.pep.js"
},
"browserify-shim": {
"jquery.pep" : { "depends": ["jquery:jQuery"] }
}
最後是.js
中的代碼:
var $ = require("jquery");
require("jquery.pep");
$(".move-box").pep();
完成!到此,經過Browserify編譯後,將可以正常運行這個jQuery插件。
這是一個怎樣的過程呢?
在本例中,jQuery使用的是npm裏的,而jquery.pep.js使用的是一個自己下載的文件(它與很多jQuery插件一樣,還沒有發佈到npm)。查看jquery.pep.js
源碼,注意到它用了這樣的包裝:
;(function ( $, window, undefined ) {
// ...
}(jQuery, window));
可以看出,它默認當前環境中已存在一個變量jQuery
(如果不存在,則報錯)。package.json
中的"depends":
["jquery:jQuery"]
是爲它添加依賴聲明,前一個jquery
表示require("jquery")
,後一個jQuery
則表示將其命名爲jQuery
(賦值語句)。這樣,插件代碼運行的時候就可以正常找到jQuery
變量,然後將它自己添加到jQuery中。
實際上,browserify-shim的配置並不容易。針對代碼包裝(儘管都不兼容CommonJS,但也存在多種情況)及使用場景的不同,browserify-shim有不同的解決方案,本文在此只介紹到這。
關於配置的更多說明,請參照browserify-shim官方文檔[]。此外,如果你覺得browserify-shim有些難以理解或者對它的原理也有興趣,推薦閱讀[這篇Stack Overflow上的回答][]。
當然,對於已經處理了CommonJS兼容的庫或插件(比如已經發布到npm),browserify-shim是不需要的。
其實還有的更多transform
在前面browserify-shim的例子中,"browserify": {"transform": [ "browserify-shim" ]}
其實是Browserify的配置。可以看出,browserify-shim只是Browserify的其中一種transform。在它之外,還有[很多的transform][]可用,分別應對不同的需求,使Browserify的體系更爲完善。
比如,還記得本文引言裏的Bower嗎?[debowerify][]可以讓通過Bower安裝的包也可以用require()
引用。npm和bower同爲包管理工具,Browserify表示你們都是我的翅膀。
一點提示
Browserify是靜態分析編譯工具,因此不支持動態require()
。例如,下面這樣是不可以的:
var lang = "zh_cn";
var i18n = require("./" + lang);
文檔資料
有關Browserify更詳細的說明文檔,請看[browserify-handbook][]。
結語
我覺得Browserify很有趣,它用了這樣一個名字,讓你覺得它好像只是一個Node的瀏覽器端轉化工具。爲此,它還完成了Node中大部分核心庫的瀏覽器端實現。但實際上,它走到了更遠的地方,並在JavaScript模塊化開發這個重要的領域中,創立了一個全新的體系。
喜歡CommonJS的簡潔風格?請嘗試Browserify!
(轉自:http://acgtofe.com/posts/2015/06/modular-javascript-with-browserify)