前端模塊及依賴管理的新選擇:Browserify

引言

1. manually

之前,我新開一個網頁項目,而後想到要用jQuery,我會打開瀏覽器,而後找到jQuery的官方網站,點擊那個醒目的「Download jQuery」按鈕,下載到.js文件,而後把它丟在項目目錄裏。在須要用到它的地方,這樣用<script>引入它:javascript

<script src="path/to/jquery.js"></script>

2. Bower

後來,我開始用[Bower][]這樣的包管理工具。因此這個過程變成了:先打開命令行用bower安裝jQuery。html

bower install jquery

再繼續用<script>引入它。java

<script src="bower_components/jquery/dist/jquery.js"></script>

3. npm&Browserify

如今,我又有了新的選擇,大概是這樣:node

命令行用npm安裝jQuery。jquery

npm install jquery

在須要用到它的JavaScript代碼裏,這樣引入它:npm

var $ = require("jquery");

沒錯,這就是使用npm的包的通常方法。但特別的是,這個npm的包是咱們熟知的jquery,而它將用在瀏覽器中。json

[Browserify][],正如其名字所體現的動做那樣,讓本來屬於服務器端的Node及npm,在瀏覽器端也可以使用。gulp

顯然,上面的過程還沒結束,接下來是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

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",
        debug: true
    });

    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就能夠獲得結果了,可能像這樣:

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個參數比較複雜,第二、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}。而在key2name.js,它沒有依賴,所以其數組第二個元素是空Object{}

所以,這第1個複雜的參數,攜帶了全部模塊的源碼及其依賴關係,因此叫作模塊map。

包裝

前面提到,原有的文件中的代碼,被包裝了起來。爲何要這樣包裝呢?

由於,瀏覽器原生環境中,並無require()。因此,須要用代碼去實現它(RequireJS和Sea.js也作了這件事)。這個包裝函數提供的3個參數,requiremoduleexports,正是由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.jQuerywindow.$這樣的全局變量來訪問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

相關文章
相關標籤/搜索