解析browserify工作原理
原文 http://segmentfault.com/a/1190000004128257
歡迎到個人博客去看看: 戳著里
0. browserify是什么?
-
browserify 是目前比較流行的模塊打包工具之一(另外一個 webpack )
-
基于流式(stream)思想設計
-
可以通過command line,也可以通過API來使用
-
僅處理javascript
-
模塊化的逆過程,但是推動著模塊化的更好發展
-
內置了一些 node core module
-
node模塊可以在瀏覽器端使用,是 同構應用 的有力武器
1. 從demo說起
存在兩個js: square.js、foo.js
// square.js
module.exports = function (a) {
return a*a;
}// foo.js
var sq = require('./square');
console.log(sq(2)); 接著通過API進行打包:
var browserify = require('../node-browserify');
var fs = require('fs');
var b = browserify(['./foo.js']);
b.require('./square.js', {
expose: 'square'
})
b.bundle().pipe(fs.createWriteStream('./bundle.js')); 得到一個可以在瀏覽器端使用的bundle.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){
'use strict';
var sq = require('./square');
console.log(sq(2));
},{"./square":2}],2:[function(require,module,exports){
"use strict";
module.exports = function (a) {
return a * a;
};
},{}]},{},[1]); 接下來分析browserify是如何做到的。
2. 總體設計
3. 具體分析
3.1 輸入(input)
browserfiy的輸入內容整體上分為兩類:file、transform function。file就是需要打包的文件,transform function用于對輸入的file內容進行處理。例如:我們需要對coffeeScript文件進行打包,那么打包之前就需要將coffeeScript文件轉換成javascript,然后才能進行打包。
那么輸入的接口都有哪些呢:
-
b = new Browserify(file, opts): 在實例化的時候,將相關內容輸入進去
-
b.require(file, opts): 指定可以在瀏覽器端require的文件
-
b.add(file, opts):實例化時未指定參數的情況下,可以使用該接口
-
b.external/exclude/ignore: 一些file的特殊配置
-
b.transform(tr, opts): 指定transform function,用于對module進行轉換
3.2 處理
b.pipeline是browserify里面的核心對象。通過這個對象,可以對module進行一系列的處理,這個對象具有如下特點:
-
整合了很多transform stream后,生成一個整體transform stream
-
帶有label,通過label訪問內部具體的transform stream
-
write進去的chunk分兩種情況:帶有file的對象,帶有transform function的對象
偽代碼:
// 創建pipeline
Browserify.prototype._createPipeline = function (opts) {
...
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', []
]);
...
return pipeline;
}
// pipeline write進去的chunk
Browserify.prototype.transform = function (tr, opts) {
...
var rec = {
transform: tr,
options: opts,
global: opts.global
};
// 帶有transform function的對象
this.pipeline.write(transform);
...
}
Browserify.prototype.require = function (file, opts) {
...
var rec = {
source: buf.toString('utf8'),
entry: defined(opts.entry, false),
file: filename,
id: id
};
// 帶有file的對象
this.pipeline.write(rec);
} 接下來針對其中關鍵的deps、pack進行分析。
3.3 模塊依賴的解析
面對的問題:
-
在模塊合并前,首先要做的工作就是找出入口文件(entry file)的依賴以及依賴的依賴,例如在charpter1的demo中,入口文件foo.js僅依賴square.js
-
考慮要對源代碼進行轉換
browserify通過 module-deps 來解決上述問題,通過下面的代碼可以看到解析的結果:
var browserify = require('../node-browserify');
var fs = require('fs');
var b = browserify(['./foo.js'], {
debug: true,
basedir: './'
});
b.require('./square.js', {
expose: 'square'
})
b.on('dep', function (row) {
console.log(row);
})
b.bundle().pipe(fs.createWriteStream('./bundle.js')); 輸出的JSON結果為:
{
"entry":true,
"expose":false,
"basedir":"./",
"file":"/Users/lizhenhua/Documents/france/z-react/foo.js",
"id":1,
"order":0,
"source":"'use strict';\n\nvar sq = require('./square');\nconsole.log(sq(2));\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZvby5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLElBQUksRUFBRSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUMsQ0FBQztBQUM3QixPQUFPLENBQUMsR0FBRyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDIiwiZmlsZSI6ImZvby5qcyIsInNvdXJjZXNDb250ZW50IjpbInZhciBzcSA9IHJlcXVpcmUoJy4vc3F1YXJlJyk7XG5jb25zb2xlLmxvZyhzcSgyKSk7XG4iXX0=",
"deps":{
"./square":2
},
"index":1,
"indexDeps":{
"./square":2
}
}
{
"id":2,
"source":"\"use strict\";\n\nmodule.exports = function (a) {\n return a * a;\n};\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNxdWFyZS5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLE1BQU0sQ0FBQyxPQUFPLEdBQUcsVUFBVSxDQUFDLEVBQUU7QUFDNUIsU0FBTyxDQUFDLEdBQUMsQ0FBQyxDQUFDO0NBQ1osQ0FBQSIsImZpbGUiOiJzcXVhcmUuanMiLCJzb3VyY2VzQ29udGVudCI6WyJtb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uIChhKSB7XG4gIHJldHVybiBhKmE7XG59XG4iXX0=",
"deps":{},
"file":"/Users/lizhenhua/Documents/france/z-react/square.js",
"index":2,
"indexDeps":{}
} 另外,在node端、browser端可能用到不同的代碼,例如請求發送(參看 superagent ),node端使用http/https模塊,而瀏覽器端則使用XMLHttpRequest對象。為了解決該問題,browserify通過在package.json中添加 browser 字段來制定瀏覽器使用的模塊。
{
"name": "mypkg",
"version": "1.2.3",
"main": "main.js",
"browser": "browser.js"
} 在解析的時候,則需要分別使用 resolve 、 browser-resolve 來進行解析。
3.4 模塊的打包
利用 browser-pack ,將上述的json數據打包合并成一個文件,browser-pack具體做了如下工作:
-
定義瀏覽器端可用的require關鍵詞
到 browser-pack 中可以看到如下寫好的代碼片段:
(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;
}) 2.將json source合并在一起
可以對charpter1的結果進行解析,除了上述的newRequire外,還包含如下結構內容:
(newRequire...)(source, cache, entry);
// source 代碼
source = {
1:[
function(require,module,exports){
'use strict';
var sq = require('./square');
console.log(sq(2));
},
{"./square":"square"}
],
"square":[
function(require,module,exports){
"use strict";
module.exports = function (a) {
return a * a;
};
},
{}
]
};
// cache
cache = {}
// entry
entry = [1]; 上述代碼片段中:
-
source: 是一個map結構,key存在兩種情況(默認為內部數字、顯示 expose 出來的key)
-
source元素分為兩部分:源代碼(包含的function(require, module, exports) {...})、代碼中存在的依賴
-
cache是緩存信息,避免再次讀取souce
-
entry: 是打包代碼入口文件的key
4. browserify的使用
在ES6沒有全面支持以前,browserify還會存在很長一段時間,還會有很強的生命力。相比較webpack,個人更喜歡browserify,webpack給人的感覺是一直在配置、使用plugin,并且代碼結構很復雜,里面涉及大量事件,源碼不容易閱讀。而使用browserify給人感覺是在開發,使用起來也較為靈活,同時browserify的stream設計思路給人更多啟發,可以向其他方向(例如css合并)遷移。