Browserify + watchify

参考
http://browserify.org/
前端模块及依赖管理的新选择:Browserify

NodeJS 把 JavaScript 的使用从浏览器端扩展到了服务器端,使得前端开发人员可以用熟悉的语言编写服务器端代码。这一变化使得 NodeJS 很快就流行起来。在 NodeJS 社区中有非常多的高质量模块可以直接使用。根据最新的统计结果,NodeJS 的 npm 中的模块数量已经超过了 Java 的 Maven Central 和 Ruby 的 RubyGems,成为模块数量最多的社区。不过这些 NodeJS 模块并不能直接在浏览器端应用中使用,原因在于引用这些模块时需要使用 NodeJS 中的 require 方法,而该方法在浏览器端并不存在。Browserify 作为 NodeJS 模块与浏览器端应用之间的桥梁,让应用可以直接使用 NodeJS 中的模块,并可以把应用所依赖的模块打包成单个 JavaScript 文件。通过 Browserify 还可以在应用开发中使用与 NodeJS 相同的方式来进行模块化和管理模块依赖。如果应用的后台是基于 NodeJS 的,那么 Browserify 使得应用的前后端可以使用一致的模块管理方式。即便应用的后端不使用 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

现在可以在浏览器里直接使用bundle.js了,与在命令行里使用node main.js结果一致。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>browserify</title>
    <script src="bundle.js"></script>
</head>
<body>
</body>
</html>
二、结构

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

请先忽略掉省略号里的部分。然后,它的结构就清晰多了。可以看到,整体是一个立即执行的函数([IIFE][]),该函数接收了3个参数。其中第1个参数比较复杂,第2、3个参数在这里分别是{}和[1]。

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。

2.包装
前面提到,原有的文件中的代码,被包装了起来。为什么要这样包装呢?

因为,浏览器原生环境中,并没有require()。所以,需要用代码去实现它(RequireJS和Sea.js也做了这件事)。这个包装函数提供的3个参数,require、module、exports,正是由Browserify实现了特定功能的3个关键字。

3.缓存
第2个参数几乎总是空的{}。它如果有的话,也是一个模块map,表示本次编译之前被加载进来的来自于其他地方的内容。现阶段,让我们忽略它吧。

4.入口模块
第3个参数是一个数组,指定的是作为入口的模块id。前面的例子中,main.js是入口模块,它的id是1,所以这里的数组就是[1]。数组说明其实还可以有多个入口,比如运行多个测试用例的场景,但相对来说,多入口的情况还是比较少的。

5.实现功能

(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个参数,然后让一切运行起来。这段代码是一个函数,来自于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 模块的原理与实现中,介绍了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",
...
三、结合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任务来生成bundle.js了。

在上面的代码中,entries指定打包入口文件,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-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 ),尾部是否显示关于sourcemaps信息的注释。不加这个属性,默认是true。设置为false的话,就是不显示。

四、watchify

如果你的代码比较多,可能像上面这样一次编译需要1s以上,这是比较慢的。这种时候,推荐使用[watchify][]。它可以在你修改文件后,只重新编译需要的部分(而不是Browserify原本的全部编译),这样,只有第一次编译会花些时间,此后的即时变更刷新则十分迅速。

1.原理
参考如何在Gulp中提高Browserify的打包速度
在gulp中我们可以把一个完整的任务拆分成很多个局部任务,然后使用gulp.watch对这些局部任务进行监听,例如:

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']);
//});

如上例所示,在监测不同局部位置的js文件发生改动后,则只会自动执行相应的build-js1或build-js2等局部任务;而如果直接监测所有的js文件,就必须每次执行build-all-js任务了。

watchify的提速原理和这个思路有点类似,它可以监测个别文件的改动,从而触发只将需要更新的文件打包。它须要先执行一次完整的打包,首次打包的速度和正常速度是一样的;然后每次用户改变某个和browserify关联的js文件时,会自动执行打包,而这次打包的速度却非常快。

参考watchify和gulp.watch之间的区别

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中文网 使用 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监测文件改动
gulp.task('watch-js', function() {
  b.on('update', function(ids) {  //监测文件改动
    ids.forEach(function(v) {
      console.log('bundle changed file:' + v);  //记录改动的文件名
    });

    gulp.start('build-all-js');  //触发打包js任务
  });

  return bundle();  //须要先执行一次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启动监视任务后,无论改age.js还是name.js还是main.js都会触发更新

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
五、其它细节

在本文的第三部分中,关于browserify的参数,只写了两个:

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

其中,entries指定打包入口文件,debug: true是告知Browserify在运行同时生成内联sourcemap用于调试。还有其它一些属性也需要使用,这里以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”参数来指定模块的名称。所产生的模块可以在 NodeJS 和浏览器中使用。对于浏览器来说,如果应用支持 AMD,则使用 AMD 来定义模块;否则把模块暴露为全局对象。如“browserify log.js --standalone log > log-bundle.js”把模块 log.js 打包成名为 log 的独立模块。

也就是把打包后的函数挂在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的相应插件实现了引入模板、样式等等文本文件的功能。在解析require调用之前来转换引入的源代码,通过这一层类似于中间件的功能,使得browserify在拓展性上大有可为。

Babel 入门教程中,使用babelify模块:

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

放置在配置文件.babelrc中

  {
    "presets": [
      "es2015",
      "react",
      "stage-2"
    ],
    "plugins": []
  }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容