browserify源码解析1——打包后文件解析

阅读 163
收藏 8
2018-08-16
原文链接:www.jianshu.com

browserify就是一个js打包工具,使用方法见:browserify-github

要学习源码 我们首先要知道这个打包工具打包出了什么。

  • 安装:
npm i browserify -g
  • main.js:
var foo = require('./foo.js');
var bar = require('./lib/bar.js');
// npm 安装的包
var gamma = require('gamma');
var w = require('./w');

var elem = document.getElementById('result');
var x = foo(100) + bar('baz');
w.c();
elem.textContent = gamma(x);
  • foo.js:
var w = require('./w');
w.c();

module.exports = function (n) { return n * 111 }
  • 打包:browserify main.js > bundle.js
  • 我们打包好了bundle.js就直接丢在script了,故我们需要了解为什么打包成了这个样子就能正常执行了。
  • 打包结果:
(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
})()({
    1: [function (require, module, exports) {
        var w = require('./w');
        w.c();

        module.exports = function (n) {
            return n * 111
        }
    }, {
        "./w": 5
    }],
    2: [function (require, module, exports) {
        module.exports = function (n) {
            return 111
        }
    }, {}],
    3: [function (require, module, exports) {
        var foo = require('./foo.js');
        var bar = require('./lib/bar.js');
        var gamma = require('gamma');
        var w = require('./w');

        var elem = document.getElementById('result');
        var x = foo(100) + bar('baz');
        w.c();
        elem.textContent = gamma(x);
    }, {
        "./foo.js": 1,
        "./lib/bar.js": 2,
        "./w": 5,
        "gamma": 4
    }],
    4: [function (require, module, exports) {
        // transliterated from the python snippet here:
        // http://en.wikipedia.org/wiki/Lanczos_approximation
        // gamma包里的逻辑

        module.exports = function gamma(z) {
        };
    }, {}],
    5: [function (require, module, exports) {
        module.exports = {
            c() {
                console.log(1)
            }
        }
    }, {}]
}, {}, [3]);

我们知道这端代码完全没有可读性。。。 我们需要一步一步简化它,理解它。

  1. 去除自执行匿名函数结构:
// 整体调用结构是这样的
(function(){
  let r = function(e, n, t){};
  return r
})()({
  // 1 2 3 4 5
},{},[3])

第一个()执行后其实就是返回了r函数,故等价于

r({
  // 1 2 3 4 5
},{},[3])
// 我们可以将最后括号里的三个参数 按照r函数的参数命名

所以整体代码就可以改写成:

let e = {
    1: [function (require, module, exports) {
        var w = require('./w');
        w.c();

        module.exports = function (n) {
            return n * 111
        }
    }, {
        "./w": 5
    }],
    2: [function (require, module, exports) {
        module.exports = function (n) {
            return 111
        }
    }, {}],
    3: [function (require, module, exports) {
        var foo = require('./foo.js');
        var bar = require('./lib/bar.js');
        var gamma = require('gamma');
        var w = require('./w');

        var elem = document.getElementById('result');
        var x = foo(100) + bar('baz');
        w.c();
        elem.textContent = gamma(x);
    }, {
        "./foo.js": 1,
        "./lib/bar.js": 2,
        "./w": 5,
        "gamma": 4
    }],
    4: [function (require, module, exports) {
        // transliterated from the python snippet here:
        // http://en.wikipedia.org/wiki/Lanczos_approximation
        // gamma包里的逻辑

        module.exports = function gamma(z) {
        };
    }, {}],
    5: [function (require, module, exports) {
        module.exports = {
            c() {
                console.log(1)
            }
        }
    }, {}]
};

let n = {}, t = [3];
r(e,n,t);

这样发现有木有简单很多了,接下来就是了解r函数内部执行:

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
    }

o函数只是一个函数声明,整这些没有用的,简化下:

function r(e, n, t) {
        for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) o(t[i]);
        return o
    }

require是进行环境判断,return o可以忽视调,其实最终r函数就是一个遍历执行:

function r(e, n, t) {
        for (i = 0; i < t.length; i++) o(t[i]);
 }
// 然后我们把上面命名好的t ---- [3]代入,最终就变成了
o(3);

然后这个时候大家会有疑问t是一个什么数组,其实t是一个入口文件下标的数组,我们可以看到我们命名的变量e就是各个模块的代码,里面下标为的3的代码就是我们执行的main.js。所以这个时候就是把入口文件代入执行了。

分析到这里最后就是解析o函数:

// 我们知道执行的是o(3),所以这里的i就是3 f就是undefined
function o(i, f) {
    // 然后n为我们命名的第二参数---空对象,所以第一个if为true
    if (!n[i]) { 
        // e为我们命名的第一个参数,e[3]就是我们入口文件的代码块,如果没有找到就抛错MODULE_NOT_FOUND。找到了这个if就为false
        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
} 

最终就变成了

// i = 3
function o(i, f) {
  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
} 

这里难以理解的点估计就是call其实call就是普通的函数执行,只是把this替换成call函数的第一个参数,其他参数跟着代入而已,所以可以理解为执行了e[3][0]函数而已,e[3][0]其实就是我们的入口文件函数了:

let myMainFunc = function (require, module, exports) {
  var foo = require('./foo.js');
  var bar = require('./lib/bar.js');
  var gamma = require('gamma');
  var w = require('./w');

  var elem = document.getElementById('result');
  var x = foo(100) + bar('baz');
  w.c();
  elem.textContent = gamma(x);
}

// 那么 我们为r作为参数的函数命名下
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)

let r = function (r) {
  var n = e[i][1][r];
  return o(n || r)
}

// 虽然call里有很多参数,但是其实我们函数只接收3个参数而已,故最终变成了:
myMainFunc(r, p, p.exports)

到这里不知道大家理解没有,其实包装了这么大一串的目的就是为了给我们的入口函数包装一段function(require, module, exports){}而已,并且使这个require方法在浏览器环境里能确确实实的执行。然现在就让我们来理解下这个require函数,就是我们传入的r函数:

// i = 3
let r = function (r) {
  var n = e[i][1][r];
  return o(n || r)
}

// e[3][0]就是我们的入口函数
// 那么 e[3][1]是什么? 我们回到我们命名的e变量看下
// e[3][1] = {
  "./foo.js": 1,
  "./lib/bar.js": 2,
  "./w": 5,
  "gamma": 4
}
// 其实这就是我们入口函数依赖的那些包的路径所对应在e变量里的下标

var w = require("./foo.js");为例,那么

var n = e[3][1]["./foo.js"];
// n w为1
return o(1)

返回了一个o(1)是不是很熟悉,之前入口文件是执行的o(3)。这里又递归执行了依赖的包foo.js,并把执行结果返回出来给o(3)继续执行。其实到了这里我们就应该明白整个打包后的文件是怎么执行的了:

  • 把我们入口文件本身,连同所有依赖的包用function(require, module, exports){}包装放在一个对象里。
  • 找到入口文件的下标用o函数执行
  • 入口文件每执行到require时,其实就是找到require文件的下标并用o函数执行返回exports的结果。

其实我们require的文件也可能require了其他文件,所以o函数是一直递归执行返回结果的,e对象的每一个属性值数组下标0就是该模块本身,下标1就是依赖了哪些其他包。

了解了打包后文件是如何执行的,那么接下来我们就需要了解一下是如何进行这个打包过程的。

评论