背景
成都創(chuàng)新互聯(lián)自2013年創(chuàng)立以來(lái),先為涇源等服務(wù)建站,涇源等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為涇源企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。
隨著前端復(fù)雜度的不斷提升,誕生出很多打包工具,比如最先的grunt,gulp。到后來(lái)的webpack和Parcel。但是目前很多腳手架工具,比如vue-cli已經(jīng)幫我們集成了一些構(gòu)建工具的使用。有的時(shí)候我們可能并不知道其內(nèi)部的實(shí)現(xiàn)原理。其實(shí)了解這些工具的工作方式可以幫助我們更好理解和使用這些工具,也方便我們?cè)陧?xiàng)目開(kāi)發(fā)中應(yīng)用。
一些知識(shí)點(diǎn)
在我們開(kāi)始造輪子前,我們需要對(duì)一些知識(shí)點(diǎn)做一些儲(chǔ)備工作。
模塊化知識(shí)
es6 modules 是一個(gè)編譯時(shí)就會(huì)確定模塊依賴關(guān)系的方式。
CommonJS的模塊規(guī)范中,Node 在對(duì) JS 文件進(jìn)行編譯的過(guò)程中,會(huì)對(duì)文件中的內(nèi)容進(jìn)行頭尾包裝 ,在頭部添加(function (export, require, modules, filename,dirname){\n 在尾部添加了\n};。這樣我們?cè)趩蝹€(gè)JS文件內(nèi)部可以使用這些參數(shù)。
AST 基礎(chǔ)知識(shí)
什么是抽象語(yǔ)法樹(shù)?
在計(jì)算機(jī)科學(xué)中,抽象語(yǔ)法樹(shù)(abstract syntax tree 或者縮寫(xiě)為 AST),或者語(yǔ)法樹(shù)(syntax tree),是源代碼的抽象語(yǔ)法結(jié)構(gòu)的樹(shù)狀表現(xiàn)形式,這里特指編程語(yǔ)言的源代碼。樹(shù)上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。之所以說(shuō)語(yǔ)法是「抽象」的,是因?yàn)檫@里的語(yǔ)法并不會(huì)表示出真實(shí)語(yǔ)法中出現(xiàn)的每個(gè)細(xì)節(jié)。
手把手教你擼一個(gè)簡(jiǎn)易的 webpack
大家可以通過(guò)Esprima 這個(gè)網(wǎng)站來(lái)將代碼轉(zhuǎn)化成 ast。首先一段代碼轉(zhuǎn)化成的抽象語(yǔ)法樹(shù)是一個(gè)對(duì)象,該對(duì)象會(huì)有一個(gè)頂級(jí)的type屬性Program,第二個(gè)屬性是body是一個(gè)數(shù)組。body數(shù)組中存放的每一項(xiàng)都是一個(gè)對(duì)象,里面包含了所有的對(duì)于該語(yǔ)句的描述信息:
type:描述該語(yǔ)句的類型 --變量聲明語(yǔ)句
kind:變量聲明的關(guān)鍵字 -- var
declaration: 聲明的內(nèi)容數(shù)組,里面的每一項(xiàng)也是一個(gè)對(duì)象
type: 描述該語(yǔ)句的類型
id: 描述變量名稱的對(duì)象
type:定義
name: 是變量的名字
init: 初始化變量值得對(duì)象
type: 類型
value: 值 "is tree" 不帶引號(hào)
row: "\"is tree"\" 帶引號(hào)
進(jìn)入正題
webpack 簡(jiǎn)易打包
有了上面這些基礎(chǔ)的知識(shí),我們先來(lái)看一下一個(gè)簡(jiǎn)單的webpack打包的過(guò)程,首先我們定義3個(gè)文件:
// index.js
import a from './test'
console.log(a)
// test.js
import b from './message'
const a = 'hello' + b
export default a
// message.js
const b = 'world'
export default b
方式很簡(jiǎn)單,定義了一個(gè)index.js引用test.js;test.js內(nèi)部引用message.js??匆幌麓虬蟮拇a:
(function (modules) {
var installedModules = {};
function webpack_require(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports,webpack_require);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (webpack_modules)
webpack_require.m = modules;
// expose the module cache
webpack_require.c = installedModules;
// define getter function for harmony exports
webpack_require.d = function (exports, name, getter) {
if (!webpack_require.o(exports, name)) {
Object.defineProperty(exports, name, {enumerable: true, get: getter});
}
};
// defineesModule on exports
__webpack_require.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, 'esModule', {value: true});
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
webpack_require.t = function (value, mode) {
/**/
if (mode & 1) value =webpack_require(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.esModule) return value;
var ns = Object.create(null);
webpack_require.r(ns);
Object.defineProperty(ns, 'default', {enumerable: true, value: value});
if (mode & 2 && typeof value != 'string') for (var key in value)webpack_require.d(ns, key, function (key) {
return value[key];
}.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
webpack_require.n = function (module) {
var getter = module && module.esModule ?
function getDefault() {
return module['default'];
} :
function getModuleExports() {
return module;
};
__webpack_require.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
webpack_require.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
//webpack_public_path
webpack_require.p = "";
// Load entry module and return exports
return webpack_require(webpack_require.s = "./src/index.js");
})({
"./src/index.js": (function (module, webpack_exports,webpack_require) {
"use strict";
eval("webpack_require.r(webpack_exports);\n/ harmony import / var _testWEBPACK_IMPORTED_MODULE_0 =webpack_require(/! ./test / \"./src/test.js\");\n\n\nconsole.log(_testWEBPACK_IMPORTED_MODULE_0[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/message.js": (function (module,webpack_exports, webpack_require) {
// ...
}),
"./src/test.js": (function (module,webpack_exports, __webpack_require__) {
// ...
})
});
看起來(lái)很亂?沒(méi)關(guān)系,我們來(lái)屢一下。一眼看過(guò)去我們看到的是這樣的形式:
(function(modules) {
// ...
})({
// ...
})
這樣好理解了吧,就是一個(gè)自執(zhí)行函數(shù),傳入了一個(gè)modules對(duì)象,modules 對(duì)象是什么樣的格式呢?上面的代碼已經(jīng)給了我們答案:
{
"./src/index.js": (function (module, webpack_exports,webpack_require) {
// ...
}),
"./src/message.js": (function (module, webpack_exports,webpack_require) {
// ...
}),
"./src/test.js": (function (module, webpack_exports,webpack_require) {
// ...
})
}
是這樣的一個(gè) 路徑 --> 函數(shù) 這樣的 key,value 鍵值對(duì)。而函數(shù)內(nèi)部是我們定義的文件轉(zhuǎn)移成 ES5 之后的代碼:
"use strict";
eval("webpack_require.r(webpack_exports);\n/ harmony import / var _testWEBPACK_IMPORTED_MODULE_0 =webpack_require(/! ./test / \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");
到這里基本上結(jié)構(gòu)是分析完了,接著我們看看他的執(zhí)行,自執(zhí)行函數(shù)一開(kāi)始執(zhí)行的代碼是:
webpack_require(webpack_require.s = "./src/index.js");
調(diào)用了__webpack_require_函數(shù),并傳入了一個(gè)moduleId參數(shù)是"./src/index.js"。再看看函數(shù)內(nèi)部的主要實(shí)現(xiàn):
// 定義 module 格式
var module = installedModules[moduleId] = {
i: moduleId, // moduleId
l: false, // 是否已經(jīng)緩存
exports: {} // 導(dǎo)出對(duì)象,提供掛載
};
modules[moduleId].call(module.exports, module, module.exports, webpack_require);
這里調(diào)用了我們modules中的函數(shù),并傳入了webpack_require函數(shù)作為函數(shù)內(nèi)部的調(diào)用。module.exports參數(shù)作為函數(shù)內(nèi)部的導(dǎo)出。因?yàn)閕ndex.js里面引用了test.js,所以又會(huì)通過(guò)__webpack_require__來(lái)執(zhí)行對(duì)test.js的加載:
var _testWEBPACK_IMPORTED_MODULE_0 =webpack_require("./src/test.js");
test.js內(nèi)又使用了message.js所以,test.js內(nèi)部又會(huì)執(zhí)行對(duì)message.js的加載。message.js執(zhí)行完成之后,因?yàn)闆](méi)有依賴項(xiàng),所以直接返回了結(jié)果:
var b = 'world'
__webpack_exports__["default"] = (b)
執(zhí)行完成之后,再一級(jí)一級(jí)返回到根文件index.js。最終完成整個(gè)文件依賴的處理。 整個(gè)過(guò)程中,我們像是通過(guò)一個(gè)依賴關(guān)系樹(shù)的形式,不斷地向數(shù)的內(nèi)部進(jìn)入,等返回結(jié)果,又開(kāi)始回溯到根。
開(kāi)發(fā)一個(gè)簡(jiǎn)單的 tinypack
通過(guò)上面的這些調(diào)研,我們先考慮一下一個(gè)基礎(chǔ)的打包編譯工具可以做什么?
轉(zhuǎn)換ES6語(yǔ)法成ES5
處理模塊加載依賴
生成一個(gè)可以在瀏覽器加載執(zhí)行的 js 文件
第一個(gè)問(wèn)題,轉(zhuǎn)換語(yǔ)法,其實(shí)我們可以通過(guò)babel來(lái)做。核心步驟也就是:
通過(guò)babylon生成AST
通過(guò)babel-core將AST重新生成源碼
/**
function getDependence (ast) {
let dependencies = []
traverse(ast, {
ImportDeclaration: ({node}) => {
dependencies.push(node.source.value);
},
})
return dependencies
}
/**
/**
function bundle(queue) {
let modules = ''
queue.forEach(function (mod) {
modules += '${mod.fileName}': function (require, module, exports) { ${mod.code} },
})
// ...
}
得到 modules 對(duì)象后,接下來(lái)便是對(duì)整體文件的外部包裝,注冊(cè)require,module.exports:
(function(modules) {
function require(fileName) {
// ...
}
require('${config.entry}');
})({${modules}})
而函數(shù)內(nèi)部,也只是循環(huán)執(zhí)行每個(gè)依賴文件的 JS 代碼而已,完成代碼:
function bundle(queue) {
let modules = ''
queue.forEach(function (mod) {
modules += '${mod.fileName}': function (require, module, exports) { ${mod.code} },
})
const result =
;
(function(modules) {
function require(fileName) {
const fn = modules[fileName];
const module = { exports : {} };
fn(require, module, module.exports);
return module.exports;
}
require('${config.entry}');
})({${modules}})
return result;
}
到這里基本上也就介紹完了,我們來(lái)打包試一下:
(function (modules) {
function require(fileName) {
const fn = modules[fileName];
const module = {exports: {}};
fn(require, module, module.exports);
return module.exports;
}
require('./src/index.js');
})({
'./src/index.js': function (require, module, exports) {
"use strict";
var _test = require("./test");
var _test2 = _interopRequireDefault(_test);
function _interopRequireDefault(obj) {
return obj && obj.esModule ? obj : {default: obj};
}
console.log(_test2.default);
}, './test': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var _message = require("./message");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.esModule ? obj : {default: obj};
}
var a = 'hello' + _message2.default;
exports.default = a;
}, './message': function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var b = 'world';
exports.default = b;
},
})
再測(cè)試一下:
手把手教你擼一個(gè)簡(jiǎn)易的 webpack
恩,基本上已經(jīng)完成一個(gè)簡(jiǎn)易的 tinypack。