Skip to content

Latest commit

 

History

History
333 lines (253 loc) · 11 KB

browserify.md

File metadata and controls

333 lines (253 loc) · 11 KB

browserify

browserify是一个典型的基于stream的Node应用,其主仓库代码不到1000行, 其余逻辑都通过transform和plugin机制灵活添加,达到了架构上的“小而美”。 这里就剖析一下browserify的设计。

需求

朴素点说,就是要将Node实现的CommonJS规范搬到客户端来。

Node将文件内容当作函数体,构造出一个函数:

function (exports, require, module, __filename, __dirname) {
// 函数体是文件内容
}

传入module, exports, require等参数进行调用,可得到exports作为外面require的返回值。

仿此,在浏览器端,针对每一个模块,也需要构造出一个这样的函数。在此它地方require这个模块时,执行这个函数,然后返回module.exports

看一个实例:

main.js

var math = require('./math')

console.log(
  math.abs(-1)
)

math.js

exports.abs = function (v) {
  return v < 0 ? -v : v
}

经过browserify打包成的JS文件:

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

{
  1:[
    function(require,module,exports){
      var math = require('./math')

      console.log(
        math.abs(-1)
      )


    },
    {"./math":2}
  ],
  2:[
    function(require,module,exports){
      exports.abs = function (v) {
        return v < 0 ? -v : v
      }


    },
    {}
  ]
},
{},
[1]
);

整个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 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;
})

传给它的第一个参数即所有模块的定义,第二个参数为模块缓存,第三个参数为入口模块。

可以看到,main.jsmath.js都对应了一个函数,执行这个函数,便是加载了这个模块到模块系统中。

在上面的outer函数中,对于入口模块会立即例执行require,即立即加载。

因此,输入是一系列入口模块,输出是一个如上的字符串。

pipeline设计

模块机制是比较独立的东西,可以抽象出来。 这便是browser-pack的功能: 给定若干模块对象,生成前面描述的JS文件。

给定入口模块后,需要解析依赖从而创建整张依赖关系图,并生成对应的模块对象。 这便是module-deps的功能。 譬如前面的例子,给了一个入口模块main.js,分析出它有依赖math.js, 从而创建了两个模块对象,输入给browser-pack

另外,从前面的打包结果可以看到第一个参数是一个map,其键值为数字,作为模块的ID。 事实上,在生成模块对象时,是用文件路径作为ID的。 但为了不暴露路径信息,将其映射成了数字。当然,这个行为是可配置的。

当然,还有一些其它的功能。 整体看来,实际上就是将一个初始的模块对象{ file: moduleFile }, 变换成丰富的内容{ file: moduleFile, id: id, source: source, deps: {} }

所有这一系列变换,是通过一个pipeline完成的:

    var pipeline = splicer.obj([
        'record', [ this._recorder() ],
        'deps', [ this._mdeps ],
        'json', [ this._json() ],
        'unbom', [ this._unbom() ],
        'unshebang', [ this._unshebang() ],
        'syntax', [ this._syntax() ],
        'sort', [ depsSort(dopts) ],
        'dedupe', [ this._dedupe() ],
        'label', [ this._label(opts) ],
        'emit-deps', [ this._emitDeps() ],
        'debug', [ this._debug(opts) ],
        'pack', [ this._bpack ],
        'wrap', []
    ]);

这个pipeline可通过b.pipeline去访问,它就是一个Duplex对象。 写入的是初始入口模块对象({ file: moduleFile }), 读出的是前面描述的JS文件格式。

这里,b即通过browserify(entries, opts)得到的Browserify实例。

splicer是模块labeled-stream-splicer暴露的接口。 splicer.obj创建了一个Duplex对象(objectMode), 其本质是将一系列Duplex对象(包括Transform对象)通过pipe连接起来, 并且可像数组那样使用splice, push, pop去操作内部连接的Duplex对象。

b.pipeline中各阶段的功能描述具体见这里

recordpack阶段,流动的都是模块对象。 这里一般称一个模块对象为rowrow.file即文件路径,row.source即文件内容。

deps阶段的Transform对象是this._mdeps, 即module-deps生成的Transform, 其作用便是从一些入口row, 通过语法解析require,检测出所有用到的row, 生成row.deps字段,并输出这些row

插件机制

了解browserify的插件机制,便能理解为什么要使用labeled-stream-splicer去创建b.pipeline了。

先为browserify提供一个打包时间统计的功能:

log.js

var through = require('through2')

module.exports = function (b) {
  b.on('reset', reset)
  reset()
  
  function reset () {
    var time = null
    var bytes = 0
    b.pipeline.get('record').on('end', function () {
      time = Date.now()
    })
    
    b.pipeline.get('wrap').push(through(write, end))
    function write (buf, enc, next) {
      bytes += buf.length
      this.push(buf)
      next()
    }
    function end () {
      var delta = Date.now() - time
      b.emit('time', delta)
      b.emit('bytes', bytes)
      b.emit('log', bytes + ' bytes written ('
        + (delta / 1000).toFixed(2) + ' seconds)'
      )
      this.push(null)
    }
  }
}

b.pipeline.get('record')会获取前面的this._recorder()生成的Transform对象, 监听end事件,记录入口模块全部写入的时刻,作为打包的开始时刻。

然后通过b.pipeline.get('wrap')拿到pipeline中最后一个Transform对象(实则为PassThrough), 然后通过push方法将through生成的Transform添加到这个pipeline中。 由于pack对应的browser-pack,所以wrap阶段结束时,打包也已经结束。

build.js

var browserify = require('browserify')
var fs = require('fs')

browserify('src/main.js', { basedir: __dirname })
  .on('log', console.log.bind(console))
  .plugin('./log')
  .bundle()
  .pipe(fs.createWriteStream(__dirname + '/bundle.js'))

b.plugin实际上就是执行log.js提供的函数, 从而按上面描述的方式修改了b.pipeline

⌘ node example/browserify/build.js
669 bytes written (0.03 seconds)

可见,browserify的插件机制, 为用户提供了修改b.pipeline的基础, 而这个基础,是由labeled-stream-splicer实现的。

反过来看,利用labeled-stream-splicer这样的工具, 可以构造一个易于修改的pipeline, 从而实现一套灵活的插件机制。

Transform机制

除了修改b.pipeline外,很多时候需要对文件内容进行修改。 譬如envify便可用来替换代码中的process.env.NODE_ENV === "development"表达式。

browserify是通过它的Transform机制来实现的。

这里举一个简单的例子,在所有文件后加一行注释:

comment.js

var through = require('through2')

module.exports = function (file) {
  return through(function (buf, enc, next) {
    next(null, buf)
  }, function (next) {
    this.push('/* AWESOME ' + file + '*/')
    next()
  })
}
⌘ node example/browserify/build-transform.js

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

console.log(
  math.abs(-1)
)

/* AWESOME /Users/zoubin/usr/src/zoub.in/stream-handbook/example/browserify/src/main.js*/
},{"./math":2}],2:[function(require,module,exports){
exports.abs = function (v) {
  return v < 0 ? -v : v
}

/* AWESOME /Users/zoubin/usr/src/zoub.in/stream-handbook/example/browserify/src/math.js*/
},{}]},{},[1]);

这套机制是在module-deps中支持的,在读取文件内容后, 会根据指定的Transform创建一个pipeline对象(Duplex), 然后将文件内容写入,并接收pipeline的输出作为row.source