隨着前端複雜度的不斷提高,誕生出不少打包工具,好比最早的grunt
,gulp
。到後來的webpack
和 Parcel
。可是目前不少腳手架工具,好比vue-cli
已經幫咱們集成了一些構建工具的使用。有的時候咱們可能並不知道其內部的實現原理。其實瞭解這些工具的工做方式能夠幫助咱們更好理解和使用這些工具,也方便咱們在項目開發中應用。html
在咱們開始造輪子前,咱們須要對一些知識點作一些儲備工做。前端
首先是模塊的相關知識,主要的是 es6 modules
和 commonJS
模塊化的規範。更詳細的介紹能夠參考這裏 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理淺析。如今咱們只須要了解:vue
es6 modules
是一個編譯時就會肯定模塊依賴關係的方式。CommonJS
的模塊規範中,Node 在對 JS 文件進行編譯的過程當中,會對文件中的內容進行頭尾包裝,在頭部添加(function (export, require, modules, __filename, __dirname){\n
在尾部添加了\n};
。這樣咱們在單個JS文件內部可使用這些參數。什麼是抽象語法樹?node
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫爲 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。樹上的每一個節點都表示源代碼中的一種結構。之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。
你們能夠經過Esprima 這個網站來將代碼轉化成 ast
。首先一段代碼轉化成的抽象語法樹是一個對象,該對象會有一個頂級的type
屬性Program
,第二個屬性是body
是一個數組。body數組中存放的每一項都是一個對象,裏面包含了全部的對於該語句的描述信息:webpack
type:描述該語句的類型 --變量聲明語句 kind:變量聲明的關鍵字 -- var declaration: 聲明的內容數組,裏面的每一項也是一個對象 type: 描述該語句的類型 id: 描述變量名稱的對象 type:定義 name: 是變量的名字 init: 初始化變量值得對象 type: 類型 value: 值 "is tree" 不帶引號 row: "\"is tree"\" 帶引號
有了上面這些基礎的知識,咱們先來看一下一個簡單的webpack
打包的過程,首先咱們定義3個文件:git
// 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
方式很簡單,定義了一個index.js
引用test.js
;test.js
內部引用message.js
。看一下打包後的代碼:es6
(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}); } }; // define __esModule 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 _test__WEBPACK_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?"); }), "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) { // ... }), "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) { // ... }) });
看起來很亂?不要緊,咱們來屢一下。一眼看過去咱們看到的是這樣的形式:github
(function(modules) { // ... })({ // ... })
這樣好理解了吧,就是一個自執行函數,傳入了一個modules
對象,modules 對象是什麼樣的格式呢?上面的代碼已經給了咱們答案:web
{ "./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__) { // ... }) }
是這樣的一個 路徑 --> 函數
這樣的 key,value 鍵值對。而函數內部是咱們定義的文件轉移成 ES5 以後的代碼:vue-cli
"use strict"; eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_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?");
到這裏基本上結構是分析完了,接着咱們看看他的執行,自執行函數一開始執行的代碼是:
__webpack_require__(__webpack_require__.s = "./src/index.js");
調用了__webpack_require_
函數,並傳入了一個moduleId
參數是"./src/index.js"
。再看看函數內部的主要實現:
// 定義 module 格式 var module = installedModules[moduleId] = { i: moduleId, // moduleId l: false, // 是否已經緩存 exports: {} // 導出對象,提供掛載 }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
這裏調用了咱們modules
中的函數,並傳入了 __webpack_require__
函數做爲函數內部的調用。module.exports
參數做爲函數內部的導出。由於index.js
裏面引用了test.js
,因此又會經過 __webpack_require__
來執行對test.js
的加載:
var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");
test.js
內又使用了message.js
因此,test.js
內部又會執行對message.js
的加載。message.js
執行完成以後,由於沒有依賴項,因此直接返回告終果:
var b = 'world' __webpack_exports__["default"] = (b)
執行完成以後,再一級一級返回到根文件index.js
。最終完成整個文件依賴的處理。
整個過程當中,咱們像是經過一個依賴關係樹的形式,不斷地向數的內部進入,等返回結果,又開始回溯到根。
經過上面的這些調研,咱們先考慮一下一個基礎的打包編譯工具能夠作什麼?
第一個問題,轉換語法,其實咱們能夠經過babel
來作。核心步驟也就是:
babylon
生成ASTbabel-core
將AST從新生成源碼/** * 獲取文件,解析成ast語法 * @param filename // 入口文件 * @returns {*} */ function getAst (filename) { const content = fs.readFileSync(filename, 'utf-8') return babylon.parse(content, { sourceType: 'module', }); } /** * 編譯 * @param ast * @returns {*} */ function getTranslateCode(ast) { const {code} = transformFromAst(ast, null, { presets: ['env'] }); return code }
接着咱們須要處理模塊依賴的關係,那就須要獲得一個依賴關係視圖。好在babel-traverse
提供了一個能夠遍歷AST
視圖並作處理的功能,經過 ImportDeclaration
能夠獲得依賴屬性:
function getDependence (ast) { let dependencies = [] traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); }, }) return dependencies } /** * 生成完整的文件依賴關係映射 * @param fileName * @param entry * @returns {{fileName: *, dependence, code: *}} */ function parse(fileName, entry) { let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName let dirName = entry ? '' : path.dirname(config.entry) let absolutePath = path.join(dirName, filePath) const ast = getAst(absolutePath) return { fileName, dependence: getDependence(ast), code: getTranslateCode(ast), }; }
到目前爲止,咱們也只是獲得根文件的依賴關係和編譯後的代碼,好比咱們的index.js
依賴了test.js
可是咱們並不知道test.js
還須要依賴message.js
,他們的源碼也是沒有編譯過。因此此時咱們還須要作深度遍歷,獲得完成的深度依賴關係:
/** * 獲取深度隊列依賴關係 * @param main * @returns {*[]} */ function getQueue(main) { let queue = [main] for (let asset of queue) { asset.dependence.forEach(function (dep) { let child = parse(dep) queue.push(child) }) } return queue }
那麼進行到這一步咱們已經完成了全部文件的編譯解析。最後一步,就是須要咱們按照webpack
的思想對源碼進行一些包裝。第一步,先是要生成一個modules
對象:
function bundle(queue) { let modules = '' queue.forEach(function (mod) { modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },` }) // ... }
獲得 modules
對象後,接下來即是對總體文件的外部包裝,註冊require
,module.exports
:
(function(modules) { function require(fileName) { // ... } require('${config.entry}'); })({${modules}})
而函數內部,也只是循環執行每一個依賴文件的 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}}) `; // We simply return the result, hurray! :) return result; }
到這裏基本上也就介紹完了,接下來就是輸出編譯好的文件了,這裏咱們爲了能夠全局使用tinypack
包,咱們還須要爲其添加到全局命令(這裏直接參考個人源碼吧,再也不贅述了)。咱們來測試一下:
npm i tinypack_demo@1.0.7 -g cd examples tinypack
看一下輸出的文件:
(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; }, })
再測試一下:
恩,基本上已經完成一個建議的 tinypack
。
tinypack 全部的源碼已經上傳 github