Browserify + watchify

參考
http://browserify.org/
前端模塊及依賴管理的新選擇:Browserify

NodeJS 把 JavaScript 的使用從瀏覽器端擴展到了服務(wù)器端,使得前端開發(fā)人員可以用熟悉的語言編寫服務(wù)器端代碼。這一變化使得 NodeJS 很快就流行起來。在 NodeJS 社區(qū)中有非常多的高質(zhì)量模塊可以直接使用。根據(jù)最新的統(tǒng)計結(jié)果,NodeJS 的 npm 中的模塊數(shù)量已經(jīng)超過了 Java 的 Maven Central 和 Ruby 的 RubyGems,成為模塊數(shù)量最多的社區(qū)。不過這些 NodeJS 模塊并不能直接在瀏覽器端應(yīng)用中使用,原因在于引用這些模塊時需要使用 NodeJS 中的 require 方法,而該方法在瀏覽器端并不存在。Browserify 作為 NodeJS 模塊與瀏覽器端應(yīng)用之間的橋梁,讓應(yīng)用可以直接使用 NodeJS 中的模塊,并可以把應(yīng)用所依賴的模塊打包成單個 JavaScript 文件。通過 Browserify 還可以在應(yīng)用開發(fā)中使用與 NodeJS 相同的方式來進行模塊化和管理模塊依賴。如果應(yīng)用的后臺是基于 NodeJS 的,那么 Browserify 使得應(yīng)用的前后端可以使用一致的模塊管理方式。即便應(yīng)用的后端不使用 NodeJS,Browserify 也可以幫助進行前端代碼的復用和組織。

一、示例

1.npm install -g browserify

//name.js:
module.exports = "aya";

//main.js:
var name = require("./name");

console.log("Hello! " + name);

使用browserify編譯:

browserify main.js -o bundle.js

現(xiàn)在可以在瀏覽器里直接使用bundle.js了,與在命令行里使用node main.js結(jié)果一致。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>browserify</title>
    <script src="bundle.js"></script>
</head>
<body>
</body>
</html>
二、結(jié)構(gòu)

上面的例子很神奇,看一下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])

請先忽略掉省略號里的部分。然后,它的結(jié)構(gòu)就清晰多了。可以看到,整體是一個立即執(zhí)行的函數(shù)([IIFE][]),該函數(shù)接收了3個參數(shù)。其中第1個參數(shù)比較復雜,第2、3個參數(shù)在這里分別是{}和[1]。

1.模塊map
第1個參數(shù)是一個Object,它的每一個key都是數(shù)字,作為模塊的id,每一個數(shù)字key對應(yīng)的值是長度為2的數(shù)組??梢钥闯?,前面的main.js中的代碼,被function(require, module, exports){}這樣的結(jié)構(gòu)包裝了起來,然后作為了key1數(shù)組里的第一個元素。類似的,name.js中的代碼,也被包裝,對應(yīng)到key2。

數(shù)組的第2個元素,是另一個map對應(yīng),它表示的是模塊的依賴。main.js在key1,它依賴name.js,所以它的數(shù)組的第二個元素是{"./name": 2}。而在key2的name.js,它沒有依賴,因此其數(shù)組第二個元素是空Object{}。

因此,這第1個復雜的參數(shù),攜帶了所有模塊的源碼及其依賴關(guān)系,所以叫做模塊map。

2.包裝
前面提到,原有的文件中的代碼,被包裝了起來。為什么要這樣包裝呢?

因為,瀏覽器原生環(huán)境中,并沒有require()。所以,需要用代碼去實現(xiàn)它(RequireJS和Sea.js也做了這件事)。這個包裝函數(shù)提供的3個參數(shù),require、module、exports,正是由Browserify實現(xiàn)了特定功能的3個關(guān)鍵字。

3.緩存
第2個參數(shù)幾乎總是空的{}。它如果有的話,也是一個模塊map,表示本次編譯之前被加載進來的來自于其他地方的內(nèi)容。現(xiàn)階段,讓我們忽略它吧。

4.入口模塊
第3個參數(shù)是一個數(shù)組,指定的是作為入口的模塊id。前面的例子中,main.js是入口模塊,它的id是1,所以這里的數(shù)組就是[1]。數(shù)組說明其實還可以有多個入口,比如運行多個測試用例的場景,但相對來說,多入口的情況還是比較少的。

5.實現(xiàn)功能

(function() {
    function r(e, n, t) {
        function o(i, f) {
            if (!n[i]) {
                if (!e[i]) {
                    var c = "function" == typeof require && require;
                    if (!f && c)
                        return c(i, !0);
                    if (u)
                        return u(i, !0);
                    var a = new Error("Cannot find module '" + i + "'");
                    throw a.code = "MODULE_NOT_FOUND",
                    a
                }
                var p = n[i] = {
                    exports: {}
                };
                e[i][0].call(p.exports, function(r) {
                    var n = e[i][1][r];
                    return o(n || r)
                }, p, p.exports, r, e, n, t)
            }
            return n[i].exports
        }
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)
            o(t[i]);
        return o
    }
    return r
}
)()

還記得前面忽略掉的省略號里的代碼嗎?這部分代碼將解析前面所說的3個參數(shù),然后讓一切運行起來。這段代碼是一個函數(shù),來自于browser-pack項目prelude.js。令人意外的是,它并不復雜,而且寫有豐富的注釋,很推薦你自行閱讀。

// modules are defined as an array
// [ module function, map of requireuires ]
//
// map of requireuires is short require name -> numeric require
//
// anything defined in a previous bundle is accessed via the
// orig method which is the requireuire for previous bundles

(function() {

function outer(modules, cache, entry) {
    // Save the require from previous bundle to this closure if any
    var previousRequire = typeof require == "function" && require;

    function newRequire(name, jumped){
        if(!cache[name]) {
            if(!modules[name]) {
                // if we cannot find the module within our internal map or
                // cache jump to the current global require ie. the last bundle
                // that was added to the page.
                var currentRequire = typeof require == "function" && require;
                if (!jumped && currentRequire) return currentRequire(name, true);

                // If there are other bundles on this page the require from the
                // previous one is saved to 'previousRequire'. Repeat this as
                // many times as there are bundles until the module is found or
                // we exhaust the require chain.
                if (previousRequire) return previousRequire(name, true);
                var err = new Error('Cannot find module \'' + name + '\'');
                err.code = 'MODULE_NOT_FOUND';
                throw err;
            }
            var m = cache[name] = {exports:{}};
            modules[name][0].call(m.exports, function(x){
                var id = modules[name][1][x];
                return newRequire(id ? id : x);
            },m,m.exports,outer,modules,cache,entry);
        }
        return cache[name].exports;
    }
    for(var i=0;i<entry.length;i++) newRequire(entry[i]);

    // Override the current require with this new one
    return newRequire;
}

return outer;

})()

6.在瀏覽器加載 CommonJS 模塊的原理與實現(xiàn)中,介紹了browser-unpack

browserify main.js > compiled.js

browser-unpack < compiled.js

[
  {
    "id":1,
    "source":"module.exports = function(x) {\n  console.log(x);\n};",
    "deps":{}
  },
  {
    "id":2,
    "source":"var foo = require(\"./foo\");\nfoo(\"Hi\");",
    "deps":{"./foo":1},
    "entry":true
  }
]

7.與require.js沖突的問題
參考模塊(一) CommonJs,AMD, CMD, UMD主模塊會這樣寫:

  // main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });

這樣就會與browserify里面的require沖突,可以參見Using Browserify and RequireJS on the same page?
的解決辦法,就是用browserify-derequire改改名字

This Browserify plugin applies derequire in order to rename all require() calls to dereq() calls in the bundle output.

參見flv.js的gulpfile.js

function doWatchify() {
    let customOpts = {
        entries: 'src/index.js',
        standalone: 'flvjs',
        debug: true,
        transform: ['babelify', 'browserify-versionify'],
        plugin: ['browserify-derequire']
    };

    let opts = Object.assign({}, watchify.args, customOpts);
    let b = watchify(browserify(opts));

    b.on('update', function () {
        return doBundle(b).on('end', browserSync.reload.bind(browserSync));
    });
    b.on('log', console.log.bind(console));

    return b;
}

8.browserify-versionify
Browserify transform to replace placeholder with package version.By default, it replaces VERSION with the version from package.json in your source code.

看一下flv.js有一部分代碼:

Object.defineProperty(flvjs, 'version', {
    enumerable: true,
    get: function () {
        // replaced by browserify-versionify transform
        return '__VERSION__';
    }
});

當我們使用gulp.js打包后,這段代碼就變成了

Object.defineProperty(flvjs, 'version', {
    enumerable: true,
    get: function get() {
        // replaced by browserify-versionify transform
        return '1.4.3';
    }
});

而這個1.4.3正是從package.json中讀取的

{
  "name": "flv.js",
  "version": "1.4.3",
  "description": "HTML5 FLV Player",
  "main": "./dist/flv.js",
...
三、結(jié)合gulp使用
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: "./main.js",
  debug: true
});
 
return b.bundle()
  .pipe(source("bundle.js"))
  .pipe(buffer())
  .pipe(sourcemaps.init({loadMaps: true}))
  .pipe(sourcemaps.write("."))
  .pipe(gulp.dest("./"));
});

安裝完上述腳本里的Gulp插件,就可以使用gulp browserify任務(wù)來生成bundle.js了。

在上面的代碼中,entries指定打包入口文件,debug: true是告知Browserify在運行同時生成內(nèi)聯(lián)sourcemap用于調(diào)試。引入gulp-sourcemaps并設(shè)置loadMaps: true是為了讀取上一步得到的內(nèi)聯(lián)sourcemap,并將其轉(zhuǎn)寫為一個單獨的sourcemap文件。vinyl-source-stream用于將Browserify的bundle()的輸出轉(zhuǎn)換為Gulp可用的[vinyl][](一種虛擬文件格式)流。vinyl-buffer用于將vinyl流轉(zhuǎn)化為buffered vinyl文件(gulp-sourcemaps及大部分Gulp插件都需要這種格式)。

關(guān)于gulp-sourcemaps,可以參考Gulp學習筆記

// 引入gulp
var gulp = require('gulp');
// 引入gulp-concat插件
var concat = require('gulp-concat');
// 引入gulp-uglify插件
var uglify = require('gulp-uglify');
// 引入gulp-sourcemaps插件
var sourceMap = require('gulp-sourcemaps');

gulp.task('sourcemap',function() {
    gulp.src('./src/*.js')  
    .pipe( sourceMap.init() )
    .pipe( concat('all.js') )  
    .pipe( uglify() )  
    .pipe( sourceMap.write('../maps/',{addComment: false}) )
    .pipe( gulp.dest('./dist/') ) 
})
sourcemaps.init({
      loadMaps: true,  //是否加載以前的 .map 
      largeFile: true,   //是否以流的方式處理大文件
})

sourceMap.write( path ),將會在指定的 path,生成獨立的sourcemaps信息文件。如果指定的是相對路徑,是相對于 all.js 的路徑。無法指定路徑為 src 目錄,否則,sourcemaps文件會生成在 dist 目錄下。

addComment : true / false ; 是控制處理后的文件(本例是 all.js ),尾部是否顯示關(guān)于sourcemaps信息的注釋。不加這個屬性,默認是true。設(shè)置為false的話,就是不顯示。

四、watchify

如果你的代碼比較多,可能像上面這樣一次編譯需要1s以上,這是比較慢的。這種時候,推薦使用[watchify][]。它可以在你修改文件后,只重新編譯需要的部分(而不是Browserify原本的全部編譯),這樣,只有第一次編譯會花些時間,此后的即時變更刷新則十分迅速。

1.原理
參考如何在Gulp中提高Browserify的打包速度
在gulp中我們可以把一個完整的任務(wù)拆分成很多個局部任務(wù),然后使用gulp.watch對這些局部任務(wù)進行監(jiān)聽,例如:

gulp.task('build-js1', ...);
gulp.task('build-js2', ...);
gulp.task('build-all-js', ['build-js1', 'build-js2']);

gulp.task('watch-js1', function () {
  gulp.watch('./src/models/**/*.js', ['build-js1']);
});

gulp.task('watch-js2', function () {
  gulp.watch('./src/views/**/*.js', ['build-js2']);
});

//gulp.task('watch-js', function () {
//  gulp.watch('./src/**/*.js', ['build-all-js']);
//});

如上例所示,在監(jiān)測不同局部位置的js文件發(fā)生改動后,則只會自動執(zhí)行相應(yīng)的build-js1或build-js2等局部任務(wù);而如果直接監(jiān)測所有的js文件,就必須每次執(zhí)行build-all-js任務(wù)了。

watchify的提速原理和這個思路有點類似,它可以監(jiān)測個別文件的改動,從而觸發(fā)只將需要更新的文件打包。它須要先執(zhí)行一次完整的打包,首次打包的速度和正常速度是一樣的;然后每次用戶改變某個和browserify關(guān)聯(lián)的js文件時,會自動執(zhí)行打包,而這次打包的速度卻非??臁?/p>

參考watchify和gulp.watch之間的區(qū)別

watchify understands commonjs modules (require(./foo.js) stuff) and will watch for changes for all dependencies. It can then recompile the bundle with the changes needed and only reload the changed files from disk. If you use gulp.watch and manually call browserify, it has to build up the dependency tree every time a change happens. This means a lot more disk i/o and hence it will be much slower.

2.改造我們上面的腳本
參考Gulp中文網(wǎng) 使用 watchify 加速 browserify 編譯

var gulp = require("gulp");
var browserify = require("browserify");
var sourcemaps = require("gulp-sourcemaps");
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');

var watchify = require('watchify');

// gulp.task("browserify", function () {
// var b = browserify({
  // entries: "./main.js",
  // debug: true
// });

// return b.bundle()
  // .pipe(source("bundle.js"))
  // .pipe(buffer())
  // .pipe(sourcemaps.init({loadMaps: true}))
  // .pipe(sourcemaps.write("."))
  // .pipe(gulp.dest("./"));
// });

// 在這里添加自定義 browserify 選項
var customOpts = {
  entries: './main.js',
  debug: true
};
var opts = Object.assign({}, watchify.args, customOpts);
var b = watchify(browserify(opts));

// 在這里加入變換操作
// 比如: b.transform(coffeeify);

// 這樣你就可以運行 `gulp build-all-js` 來編譯文件了
gulp.task('build-all-js', bundle);

function bundle() {
  return b.bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(sourcemaps.init({loadMaps: true}))
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('./'));
}

//啟動watchify監(jiān)測文件改動
gulp.task('watch-js', function() {
  b.on('update', function(ids) {  //監(jiān)測文件改動
    ids.forEach(function(v) {
      console.log('bundle changed file:' + v);  //記錄改動的文件名
    });

    gulp.start('build-all-js');  //觸發(fā)打包js任務(wù)
  });

  return bundle();  //須要先執(zhí)行一次bundle
});

這里為了測試效果,又添加了一個age.js文件:

//name.js
module.exports = "cuixu4";

//age.js
module.exports = "31";

//main.js
var name = require("./name");
var age = require("./age");

console.log("Hello! " + name + ",age:" + age);

使用gulp watch-js啟動監(jiān)視任務(wù)后,無論改age.js還是name.js還是main.js都會觸發(fā)更新

PS E:\node\browserifyDemo> gulp watch-js
[17:17:20] Using gulpfile E:\node\browserifyDemo\gulpfile.js
[17:17:20] Starting 'watch-js'...
[17:17:20] Finished 'watch-js' after 61 ms
bundle changed file:E:\node\browserifyDemo\name.js
[17:17:33] Starting 'build-all-js'...
[17:17:33] Finished 'build-all-js' after 49 ms
bundle changed file:E:\node\browserifyDemo\main.js
[17:20:21] Starting 'build-all-js'...
[17:20:21] Finished 'build-all-js' after 76 ms
bundle changed file:E:\node\browserifyDemo\age.js
[17:20:45] Starting 'build-all-js'...
[17:20:45] Finished 'build-all-js' after 48
五、其它細節(jié)

在本文的第三部分中,關(guān)于browserify的參數(shù),只寫了兩個:

var b = browserify({
  entries: "./main.js",
  debug: true
});

其中,entries指定打包入口文件,debug: true是告知Browserify在運行同時生成內(nèi)聯(lián)sourcemap用于調(diào)試。還有其它一些屬性也需要使用,這里以flv.js的gulpfile.js為例

    let customOpts = {
        entries: 'src/index.js',
        standalone: 'flvjs',
        debug: true,
        transform: ['babelify', 'browserify-versionify'],
        plugin: ['browserify-derequire']
    };

1.standalone
在運行 browserify 命令時使用”--standalone”參數(shù)來指定模塊的名稱。所產(chǎn)生的模塊可以在 NodeJS 和瀏覽器中使用。對于瀏覽器來說,如果應(yīng)用支持 AMD,則使用 AMD 來定義模塊;否則把模塊暴露為全局對象。如“browserify log.js --standalone log > log-bundle.js”把模塊 log.js 打包成名為 log 的獨立模塊。

也就是把打包后的函數(shù)掛在window下指定的名字下,在本例中就是flvjs,看一下DEMO

<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            //"isLive": true,
            url: 'http://192.168.198.102/jay.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>

2.transform
Browserify使用了transform以及配合transform的相應(yīng)插件實現(xiàn)了引入模板、樣式等等文本文件的功能。在解析require調(diào)用之前來轉(zhuǎn)換引入的源代碼,通過這一層類似于中間件的功能,使得browserify在拓展性上大有可為。

Babel 入門教程中,使用babelify模塊:

  "browserify": {
    "transform": [["babelify", { "presets": ["es2015"] }]]
  }
}

放置在配置文件.babelrc中

  {
    "presets": [
      "es2015",
      "react",
      "stage-2"
    ],
    "plugins": []
  }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容