参考
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 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": []
}