browserify源码解析3 —— pipeline流程
这一节主要解析数据进入pipeline之后到底经历了什么,最后才能得到打包文件。
从上节内容可以知道我们一共向这个pipe写入过两次内容:
self.pipeline.write(row);
self.pipeline.write({
transform: globalTr,
global: true,
options: {}
});
然后这些内容就进入到了pipeline中:
var pipeline = splicer.obj([
// 记录输入管道的数据,重建管道时直接将记录的数据写入。
// 用于像watch时需要多次打包的情况
'record', [ this._recorder() ],
// 依赖解析,预处理
'deps', [ this._mdeps ],
// 处理JSON文件
'json', [ this._json() ],
// 删除文件前面的BOM
'unbom', [ this._unbom() ],
// 删除文件前面的`#!`行
'unshebang', [ this._unshebang() ],
// 语法检查
'syntax', [ this._syntax() ],
// 排序,以确保打包结果的稳定性
'sort', [ depsSort(dopts) ],
// 对拥有同样内容的模块去重
'dedupe', [ this._dedupe() ],
// 将id从文件路径转换成数字,避免暴露系统路径信息
'label', [ this._label(opts) ],
// 为每个模块触发一次dep事件
'emit-deps', [ this._emitDeps() ],
'debug', [ this._debug(opts) ],
// 将模块打包
'pack', [ this._bpack ],
'wrap', []
]);
record操作:
Browserify.prototype._recorder = function (opts) {
var self = this;
var ended = false;
this._recorded = [];
if (!this._ticked) {
process.nextTick(function () {
self._ticked = true;
self._recorded.forEach(function (row) {
stream.push(row);
});
if (ended) stream.push(null);
});
}
var stream = through.obj(write, end);
return stream;
function write (row, enc, next) {
debugger
self._recorded.push(row);
if (self._ticked) this.push(row);
next();
}
function end () {
ended = true;
console.warn('_ticked', self._recorded);
if (self._ticked) this.push(null);
}
};
这一步的操作主要是记录,将deps操作前进入的流都记录下来,然后进行利于进行多次打包。
if (!this._ticked) {
process.nextTick(function () {
self._ticked = true;
self._recorded.forEach(function (row) {
stream.push(row);
});
if (ended) stream.push(null);
});
}
这一步就是Transform流中的流传递到下一步流操作中。
deps 操作
这一步是核心操作,需要入口和 require() 的文件或对象作为输入, 调用 module-deps 来生成一个json数据流, 这个流包含了所有依赖关系里的文件(下一节详细)。
我们可以引入这个包,然后测试一下第一节里的main.js看下能输出什么:
var mdeps = require('module-deps');
var JSONStream = require('JSONStream');
var md = mdeps();
md.pipe(JSONStream.stringify()).pipe(process.stdout);
md.write({ file: __dirname + '/main.js' });
md.end();
得到结果:
[
{"id":"/Users/cyl/workplace/browserify-test/lib/bar.js","source":"module.exports = function (n) { return 111 }","deps":{},"file":"/Users/cyl/workplace/browserify-test/lib/bar.js"}
,
{"id":"/Users/cyl/workplace/browserify-test/w.js","source":"module.exports = {\n c() {console.log(1)}\n} ","deps":{},"file":"/Users/cyl/workplace/browserify-test/w.js"}
,
{"id":"/Users/cyl/workplace/browserify-test/foo.js","source":"var w = require('./w');\nw.c();\n\nmodule.exports = function (n) { return n * 111 }","deps":{"./w":"/Users/cyl/workplace/browserify-test/w.js"},"file":"/Users/cyl/workplace/browserify-test/foo.js"}
,
{"id":"/Users/cyl/workplace/browserify-test/node_modules/gamma/index.js","source":"// transliterated from the python snippet here:\n// http://en.wikipedia.org/wiki/Lanczos_approximation\n\nvar g = 7;\nvar p = [\n 0.99999999999980993,\n 676.5203681218851,\n -1259.1392167224028,\n 771.32342877765313,\n -176.61502916214059,\n 12.507343278686905,\n -0.13857109526572012,\n 9.9843695780195716e-6,\n 1.5056327351493116e-7\n];\n\nvar g_ln = 607/128;\nvar p_ln = [\n 0.99999999999999709182,\n 57.156235665862923517,\n -59.597960355475491248,\n 14.136097974741747174,\n -0.49191381609762019978,\n 0.33994649984811888699e-4,\n 0.46523628927048575665e-4,\n -0.98374475304879564677e-4,\n 0.15808870322491248884e-3,\n -0.21026444172410488319e-3,\n 0.21743961811521264320e-3,\n -0.16431810653676389022e-3,\n 0.84418223983852743293e-4,\n -0.26190838401581408670e-4,\n 0.36899182659531622704e-5\n];\n\n// Spouge approximation (suitable for large arguments)\nfunction lngamma(z) {\n\n if(z < 0) return Number('0/0');\n var x = p_ln[0];\n for(var i = p_ln.length - 1; i > 0;--i) x += p_ln[i] / (z + i);\n var t = z + g_ln + 0.5;\n return .5*Math.log(2*Math.PI)+(z+.5)*Math.log(t)-t+Math.log(x)-Math.log(z);\n}\n\nmodule.exports = function gamma (z) {\n if (z < 0.5) {\n return Math.PI / (Math.sin(Math.PI * z) * gamma(1 - z));\n }\n else if(z > 100) return Math.exp(lngamma(z));\n else {\n z -= 1;\n var x = p[0];\n for (var i = 1; i < g + 2; i++) {\n x += p[i] / (z + i);\n }\n var t = z + g + 0.5;\n\n return Math.sqrt(2 * Math.PI)\n * Math.pow(t, z + 0.5)\n * Math.exp(-t)\n * x\n ;\n }\n};\n\nmodule.exports.log = lngamma;\n","deps":{},"file":"/Users/cyl/workplace/browserify-test/node_modules/gamma/index.js"}
,
{"file":"/Users/cyl/workplace/browserify-test/main.js","id":"/Users/cyl/workplace/browserify-test/main.js","source":"var foo = require('./foo.js');\nvar bar = require('./lib/bar.js');\nvar gamma = require('gamma');\nvar w = require('./w');\n\nvar elem = document.getElementById('result');\nvar x = foo(100) + bar('baz');\nw.c();\nelem.textContent = gamma(x);","deps":{"./lib/bar.js":"/Users/cyl/workplace/browserify-test/lib/bar.js","./w":"/Users/cyl/workplace/browserify-test/w.js","./foo.js":"/Users/cyl/workplace/browserify-test/foo.js","gamma":"/Users/cyl/workplace/browserify-test/node_modules/gamma/index.js"},"entry":true}
]
每个数组元素结构:
{
"id": "/Users/cyl/workplace/browserify-test/foo.js",
"source": "var w = require('./w');\nw.c();\n\nmodule.exports = function (n) { return n * 111 }",
"deps": {
"./w": "/Users/cyl/workplace/browserify-test/w.js"
},
"file": "/Users/cyl/workplace/browserify-test/foo.js"
},
这样其实就分析出来了入口文件的所有依赖关系,每个依赖就一个元素。
- id:该依赖的id。
- source:该依赖的源码
- deps:该依赖所依赖的其他文件所对应的id
- file:该依赖的文件位置
主入口也是一个元素,不同在于他还包含一个属性:entry:true
。
json操作
这个操作就比较简单了,引用的文件可能是json文件,我们为了让他正确执行,其实在source加上module.exports=
即可。
through.obj(function (row, enc, next) {
if (/\.json$/.test(row.file)) {
row.source = 'module.exports=' + sanitize(row.source);
}
this.push(row);
next();
});
unbom操作:
删除文件源码前面的BOM(不然会影响执行)
return through.obj(function (row, enc, next) {
if (/^\ufeff/.test(row.source)) {
row.source = row.source.replace(/^\ufeff/, '');
}
this.push(row);
next();
});
unshebang操作:删除文件源码前面的#!
行
syntax操作:
这个转换会通过调用 syntax-error 检查语法错误,给出错误信息以及错误所在行和列。
Browserify.prototype._syntax = function () {
var self = this;
return through.obj(function (row, enc, next) {
var h = shasum(row.source);
if (typeof self._syntaxCache[h] === 'undefined') {
var err = syntaxError(row.source, row.file || row.id);
if (err) return this.emit('error', err);
self._syntaxCache[h] = true;
}
this.push(row);
next();
});
};
除此之外,还缓存了文件的hash值,有利于多次打包,如果文件没有改动不需要再次检测。
sort 操作:
这个阶段使用 deps-sort 对写入的行进行排序以确定最后生成的打包文件。(没有去看源码,不太清楚基于什么做的sort,但是能够知道的是他能分析出重复的信息,并且将row添加了一个属性indexDeps,之前deps是按照id的形式映射,现在变成了下标的形式)
dedupe操作:基于sort给出的重复信息,对拥有同样内容的依赖去重。
label操作:
这阶段会把每个可能暴露系统路径的文件的ID进行转换,把原来很大的文件包用整数ID来代表。
核心其实就一句:if (row.index) row.id = row.index;
label 阶段还会把基于 opts.basedir 和 process.cwd() 的文件路径进行标准化,以防止暴露系统文件路径信息。
并且记录了入口文件id:
if (row.entry || row.expose) {
self._bpack.standaloneModule = row.id;
}
emit-deps操作
这个阶段会在 label 阶段结束后,给每一行触发一个 'dep' 事件。
return through.obj(function (row, enc, next) {
self.emit('dep', row);
this.push(row);
next();
})
那么这个时候外部b实例就能监听到dep事件,做一些相应操作。
debug操作
如果在实例化构造函数 browserify() 的时候传入了 opts.debug 参数, 那在这个转换阶段,它会使用 pack 阶段的 browser-pack 来给输入流添加 sourceRoot 和 sourceFile 属性。
pack操作:
调用browser-pack包将所有到模块信息打包成一个可在浏览器执行的文件,也就是我们第一节输出的那个文件。感兴趣的可以去看看源码。
下一节主要会解析下依赖分析的过程。
参考文献:
browserify使用手册 -- 原版
browserify使用手册 -- 中文
deps-sort - github
browser-pack - github