我们调用browserify时通常有两种方式:
一:命令行方式
这个时候调用的文件是:package.json:
"bin": {
"browserify": "bin/cmd.js"
},
然后我们进入该文件:
var b = require('./args')(process.argv.slice(2));
他首先生成了./args
实例,并且把命令行参数传入了:
我们进入./args
:
var browserify = require('../');
module.exports = function() {
var b = browserify(xtend({
// ...
}, opts));
return b;
}
实际上就是把一些命令行参数的默认值和用户传入参数值作为配置信息生成browserify实例,然后根据这些参数值进行一些browserify实例的操作。
然后browserify代码实际上是:package.json:"main": "index.js"
。
然后这些配置参数例如:
-i(–ignore),将依赖的文件以{}空对象替代,这个命令看起来没什么用处啊,依赖一个空的对象,不能提供任何属性和方法,暂时没发现这个命令的使用场景。
-u(–exclude),忽略依赖的对象,该依赖对象不会被打包,依赖的对象可以是第三方的或者是通过-r命令生成的另外一个bundle文件,可以通过-u命令指定一些第三方类库,比如vue的类库。
-x(external),-x指向的文件是通过-r命令生成的另外一个文件,该文件不会被打包。
更多参数参见:browserify
二:api使用的方式,我们在代码中直接调用browserify实例进行打包:
var browserify = require('browserify');
var b = browserify();
b.add('./main.js');
b.bundle().pipe(process.stdout);
调用的就是browserify的index.js源码。
无论什么样的方式调用browserify,我们最终需要理解的是browserify的index.js。 我们由api使用的方式一步步看。
- 首先生成了browserify实例,然后add是添加入口文件。
Browserify.prototype.add = function (file, opts) {
var self = this;
if (!opts) opts = {};
if (isArray(file)) {
file.forEach(function (x) { self.add(x, opts) });
return this;
}
return this.require(file, xtend({ entry: true, expose: false }, opts));
};
- 调用add实际上是调用了
Browserify.prototype.require
,调用require主要的作用是包装文件信息,在这里得到的是:
row:{
"entry":true,
"expose":false,
"file":"/Users/cyl/workplace/browserify-test/main.js",
order: 0
}
然后将他写如流中:
self.pipeline.write(row);
到此,其实b.add('./main.js');
就执行完毕了。
最后就是打包,并输出打包后的文件:
b.bundle().pipe(process.stdout);
- 调用
Browserify.prototype.bundle()
,由于this._pending === 0,触发bundle事件:
if (!this._bundled) {
this.once('bundle', function () {
self.pipeline.write({
transform: globalTr,
global: true,
options: {}
});
});
}
这里又向流中写入一个对象。
然后上次write和这次write就进入了pipeline中,等到打包完this._bundled就为true了,然后会把就过pipeline操作后的流封装成只读流返回,完成打包流程。
browserify采用的是流工作机制,在bundle的过程中经过了好几道流操作,最后把数据写入到writable里,这里的writable就是process.stdout。
要了解browserify的工作机制就必须了解清楚流是如何工作的。
node流:
node流分为可读流和可写流。
如何创建一个可读流:
正常情况下,需要为流实例提供一个_read方法,在这个方法中调用push产生数据。 既可以在同一个tick中(同步)调用push,也可以异步的调用(通常如此)。
var Stream = require('stream')
var source = ['a', 'b', 'c']
var readable = Stream.Readable({
read: function () {
this.push(source.shift() || null)
},
})
这样一个可读流就创建好了,那么我们如何接受数据?
readable.on('end', function() {
console.log('end');
});
readable.on('data', function(data) {
console.log('data: ', data);
});
当read函数每次push当时候,readable.on('data')
就能读取到每次push的数据,当read中this.push(null)
时,就代表流结束了触发end事件。
如何创建一个可写流:
与Readable类似,需要为writable实现一个_write方法, 来实现一个具体的可写流。
在写入数据(调用writable.write(data))时, 会调用_write方法来处理数据。
var Stream = require('stream')
var writable = Stream.Writable({
write: function (data, _, next) {
console.log(data)
next()
},
})
writable.on('finish', function () {
console.log('finish')
})
writable.on('prefinish', function () {
console.log('prefinish')
})
writable.write('a', function () {
console.log('write a')
})
writable.write('b', function () {
console.log('write b')
})
writable.write('c', function () {
console.log('write c')
})
writable.end()
- _write(data, _, next)中调用next(err)来声明“写入”操作已完成, 可以开始写入下一个数据。
- next的调用时机可以是异步的。
- 调用write(data)方法来往writable中写入数据。将触发_write的调用,将数据写入底层。
- 必须调用end()方法来告诉writable,所有数据均已写入。
运行结果:
prefinish
w a
w b
w c
finish
- 当所有数据都写入之后会调用prefinish回调,之后就会依次执行每次写入成功的回调,等所有回调执行完了,这些可写流的操作就结束了调用finish方法。
objectMode
我们看到我么写入的是String类型的,但是输出的都是Buffer类型。
我么可以给流设置模式:Readable({ objectMode: true })
;
- 在非objectMode时,data只能是String, Buffer, Null, Undefined。 同时,消耗时获得的数据一定是Buffer类型。
- 在objectMode时,data可以是任意类型,null仍然有其特殊含义。 同时,消耗时获得的数据与push进来的一样。实际就是同一个引用。
每次调用push(data)时,如果是objectMode,便直接调用state.buffer.push(data)。 这里,state = readable._readableState。
如果是非objectMode,会将String类型转成Buffer,再调用state.buffer.push(chunk)。 这里,chunk即转换后的Buffer对象。 默认会以utf8的编码形式进行转换。
可写流objectMode也一样。
Duplex
Duplex等同于继承了Readable,和Writable。
一个Duplex实例duplex,拥有Readable和Writable原型上的所有方法, 而且内部同时包含了_readableState和_writableState。 因此,实现了duplex._read便可以将它当作可读流来用, 实现了duplex._write便可以将它当作可写流来用。
Transform
Transform继承自Duplex,但是将内部的两个缓存给关联起来了。 简单来说,就是调用write(data)后,经过_transform的处理,下游可读取到处理后的数据。browserify内部就是使用的这种流,不过使用的是封装好的库through2
。
var Stream = require('stream');
var transform = Stream.Transform({
transform(buf, _, next) {
next(null, buf.toString().toUpperCase())
}
})
transform.pipe(process.stdout);
transform.write('a')
transform.write('b')
transform.end('c');
write方法接收到数据后,引起_transform方法的调用, 在数据处理完时,需要调用next方法。 next会调用push方法,从而将转换后的数据放入可读缓存。 下游便能读取到。
var Stream = require('stream')
var transform = createTransform();
transform.on('finish', function () {
console.log('finish')
})
transform.on('end', function () {
console.log('end')
})
transform.on('data', function (data) {
console.log('data:' + data);
})
transform.write('a')
transform.write('b')
transform.end('c')
// 此外,Transform还有一个_flush方法。 当prefinish事件发生时,便会调用它,表示上游已经没有数据要写入了,即“写端”已经结束。
function createTransform() {
var input = []
return Stream.Transform({
objectMode: true,
transform: function (buf, _, next) {
console.log('transform', buf.toString())
input.push(buf)
next()
},
flush: function (next) {
console.log('flush')
var buf
while (buf = input.pop()) {
this.push(buf)
}
setTimeout(() => {
this.push('extra')
next()
}, 10)
},
})
}
// NOTE _transform() => end() => flush() => finish => end
Transform与Duplex比较
Duplex可同时当作Readable(可读端)和Writable(可写端)来用, Transform在此基础上,将可读端与可写端的底层打通, 写入的数据会被当作_read调用时获取数据的源,只是数据还经历了_transform的处理。
实际上,Transform的实现便是实现了_read与_write的Duplex, 但它还要求实现_transform来做数据转换。
效果上,对于duplex.write('a')后,duplex.read()并不能读到这个'a', 但transform.write('a')后,transform.read()却能读到_transform('a')。
pipe
可读流提供了一个pipe方法,用于连接另一个可写流。 即pipe方法用于连通上游和下游,使上游的数据能流到指定的下游:readable.pipe(writable)。 上游必须是可读的,下游必须是可写的。
有两种方法将一个可读流与一个可写流连接起来。
var Stream = require('stream');
var readable = createReadable();
var writable = createWritable();
// readable.on('data', function (data) {
// writable.write(data)
// })
// readable.on('end', function (data) {
// writable.end()
// })
// writable.on('finish', function(data) {
// console.log('done');
// })
// 可以直接食用pipe
readable.pipe(writable).on('finish', () => {
console.log('done');
});
function createReadable() {
var source = ['y', 't', 'j'];
return Stream.Readable({
read() {
process.nextTick(this.push.bind(this), source.shift() || null)
}
});
}
function createWritable() {
return Stream.Writable({
write(data, _, next) {
console.log(data);
next()
}
});
}
可见,pipe方法自动处理了data, end, write等事件和方法, 使得关联变得更简单。 pipe就是注释方法的实现,但是实际上还做了截流的操作,感兴趣的可以自己了解下。
Pipleline
刚刚我们了解到的形式是:
readable.pipe(writable).on('finish', () => {
console.log('done');
});
我们知道头必须是可读,尾巴必须是可写,那么如果我想形成一条管道a.pipe(b).pipe(c).pipe(d)
那么b和c就必须是即可读又可写的流了,故需要使用transform。
var Stream = require('stream');
scr().pipe(toUpperCase()).pipe(reverse()).pipe(process.stdout);
function reverse() {
var input = []
return Stream.Transform({
objectMode: true,
transform(buf, _, next) {
input.push(buf);
next();
},
flush(next) {
var buf;
while(buf = input.pop()) {
this.push(buf);
}
next();
}
})
}
function toUpperCase() {
return Stream.Transform({
transform(buf, _, next) {
next(null, buf.toString().toUpperCase())
}
})
}
function scr() {
var source = ['aaa', 'bbb', 'ccc'];
return Stream.Readable({
read() {
this.push(source.shift() || null);
}
})
}
我们用scr()
创建了一个可读流,那么就可以直接作为头,然后每次接收到数据都大写,然后进行了一个操作就是把每次流入的书记大写,之后我们进入了一个transform流,每次读到数据我们都进行写入操作,并把数据放在一个数组input里,然后当所有写入结束后触发flush,我们将每次写入的数据进行一个反序写入下一个流中,最后就到了process.stdout
标准输出流,最后结果:
CCC
BBB
AAA
那么其实pipe(toUpperCase()).pipe(reverse())
就是一条pipeline,我们如果进行一个封装让他变成一条真实的pipeline?
stream-combiner2
stream-combiner2可用来将多个Duplex
(包括Transform
)组合成一个pipeline
, 返回一个Duplex
给外界使用。
这是构造固定管道的一个非常方便的工具。
var combine = require('stream-combiner2')
function createPipeline() {
return combine.obj(
toUpperCase(),
reverse()
)
}
scr().pipe(createPipeline()).pipe(process.stdout);
这样一条pipeline就形成了,我们还可以直接调用
var pipeline = createPipeline();
pipeline.write('xx');
直接给pipeline传入数据。
through2
到这里我们该了解的流就结束了,browserify其实就是采用了a.pipe(b).pipe(c).pipe(d)
这样的方式进行了一系列操作,但是browserify使用的是封装好的through2
:
var through = require('through2')
var transform = through(_transform, _flush)
// objectMode
var transform = through.obj(_transform, _flush)
// 等价于
var Transform = require('stream').Transform
var transform = Transform({
transform: _transform,
flush: _flush,
})
// objectMode
var transform = Transform({
objectMode: true,
transform: _transform,
flush: _flush,
})
他的效果和Stream.Transform一样的,只是更简便一些了,还做了一些额外操作。
labeled-stream-splicer
browserify创建pipeline并没有使用stream-combiner2,而是使用labeled-stream-splicer,他们有什么区别呢?
现在我pipeline已经创建好了,但是如果这个时候我这个pipeline暴露给甲使用了,但是甲需要在toUpperCase和reverse中间加一个操作每次转换成大写之后打一个log。 那么这个时候甲是无从下手的。 于是会有labeled-stream-splicer的出现,可以对已经生成的pipeline进行扩充。
var splicer = require('labeled-stream-splicer')
function createPipeline() {
return splicer.obj([
'toUpperCase', [toUpperCase()],
'reverse', [reverse()],
])
}
function createLog() {
}
var pipeline = createPipeline();
pipeline.get('toUpperCase').push(createLog());
可以调用get方法获取到某一个流的位置,并且在想要改变的地方加上你想加入的流操作。browserify就是采用的这个方案进行扩展。
- 上面browserify的流程中:执行了
self.pipeline.write(row);
和
self.pipeline.write({
transform: globalTr,
global: true,
options: {}
});
然而browserify的pipeline是由一个函数Browserify.prototype._createPipeline
构建的:
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', []
]);
每次当我们write一次数据,其实数据就进行了这一系列的操作,最后得到打包结果。
参考文献:
browserify使用手册 -- 原版
browserify使用手册 -- 中文
browserify - github
解析browserify工作原理
对Node.js中 stream模块的学习积累和理解