<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手教你擼一個簡易的 webpack

          共 16580字,需瀏覽 34分鐘

           ·

          2021-03-10 12:34

          作者:muwoo
          來源:https://juejin.cn/post/6844903617971879949

          背景

          隨著前端復(fù)雜度的不斷提升,誕生出很多打包工具,比如最先的grunt,gulp。到后來的webpackParcel。但是目前很多腳手架工具,比如vue-cli已經(jīng)幫我們集成了一些構(gòu)建工具的使用。有的時候我們可能并不知道其內(nèi)部的實現(xiàn)原理。其實了解這些工具的工作方式可以幫助我們更好理解和使用這些工具,也方便我們在項目開發(fā)中應(yīng)用。

          一些知識點

          在我們開始造輪子前,我們需要對一些知識點做一些儲備工作。

          模塊化知識

          首先是模塊的相關(guān)知識,主要的是 es6 modulescommonJS模塊化的規(guī)范。更詳細(xì)的介紹可以參考這里 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理淺析?,F(xiàn)在我們只需要了解:

          1. es6 modules 是一個編譯時就會確定模塊依賴關(guān)系的方式。
          2. CommonJS的模塊規(guī)范中,Node 在對 JS 文件進行編譯的過程中,會對文件中的內(nèi)容進行頭尾包裝 ,在頭部添加(function (export, require, modules, __filename, __dirname){\n 在尾部添加了\n};。這樣我們在單個JS文件內(nèi)部可以使用這些參數(shù)。

          AST 基礎(chǔ)知識

          什么是抽象語法樹?

          在計算機科學(xué)中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結(jié)構(gòu)的樹狀表現(xiàn)形式,這里特指編程語言的源代碼。樹上的每個節(jié)點都表示源代碼中的一種結(jié)構(gòu)。之所以說語法是「抽象」的,是因為這里的語法并不會表示出真實語法中出現(xiàn)的每個細(xì)節(jié)。

          image

          大家可以通過Esprima 這個網(wǎng)站來將代碼轉(zhuǎn)化成 ast。首先一段代碼轉(zhuǎn)化成的抽象語法樹是一個對象,該對象會有一個頂級的type屬性Program,第二個屬性是body是一個數(shù)組。body數(shù)組中存放的每一項都是一個對象,里面包含了所有的對于該語句的描述信息:

          type:描述該語句的類型 --變量聲明語句
          kind:變量聲明的關(guān)鍵字 -- var
          declaration: 聲明的內(nèi)容數(shù)組,里面的每一項也是一個對象
           type: 描述該語句的類型 
           id: 描述變量名稱的對象
            type:定義
            name: 是變量的名字
              init: 初始化變量值得對象
            type: 類型
            value: 值 "is tree" 不帶引號
            row: "\"is tree"\" 帶引號
          復(fù)制代碼

          進入正題

          webpack 簡易打包

          有了上面這些基礎(chǔ)的知識,我們先來看一下一個簡單的webpack打包的過程,首先我們定義3個文件:

          // 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
          復(fù)制代碼

          方式很簡單,定義了一個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});
              }
            };
            // 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__) {
              // ...
            })
          });
          復(fù)制代碼

          看起來很亂?沒關(guān)系,我們來屢一下。一眼看過去我們看到的是這樣的形式:

          (function(modules) {
            // ...
          })({
           // ...
          })
          復(fù)制代碼

          這樣好理解了吧,就是一個自執(zhí)行函數(shù),傳入了一個modules對象,modules 對象是什么樣的格式呢?上面的代碼已經(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__) {
              // ...
            })
          }
          復(fù)制代碼

          是這樣的一個 路徑 \--> 函數(shù) 這樣的 key,value 鍵值對。而函數(shù)內(nèi)部是我們定義的文件轉(zhuǎn)移成 ES5 之后的代碼:

          "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?");
          復(fù)制代碼

          到這里基本上結(jié)構(gòu)是分析完了,接著我們看看他的執(zhí)行,自執(zhí)行函數(shù)一開始執(zhí)行的代碼是:

          __webpack_require__(__webpack_require__.s = "./src/index.js");
          復(fù)制代碼

          調(diào)用了__webpack_require_函數(shù),并傳入了一個moduleId參數(shù)是"./src/index.js"。再看看函數(shù)內(nèi)部的主要實現(xiàn):

          // 定義 module 格式   
          var module = installedModules[moduleId] = {
                i: moduleId, // moduleId
                l: false, // 是否已經(jīng)緩存
                exports: {} // 導(dǎo)出對象,提供掛載

          };

          modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
          復(fù)制代碼

          這里調(diào)用了我們modules中的函數(shù),并傳入了__webpack_require__函數(shù)作為函數(shù)內(nèi)部的調(diào)用。module.exports參數(shù)作為函數(shù)內(nèi)部的導(dǎo)出。因為index.js里面引用了test.js,所以又會通過__webpack_require__來執(zhí)行對test.js的加載:

          var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");
          復(fù)制代碼

          test.js內(nèi)又使用了message.js所以,test.js內(nèi)部又會執(zhí)行對message.js的加載。message.js執(zhí)行完成之后,因為沒有依賴項,所以直接返回了結(jié)果:

          var b = 'world'
          __webpack_exports__["default"] = (b)
          復(fù)制代碼

          執(zhí)行完成之后,再一級一級返回到根文件index.js。最終完成整個文件依賴的處理。 整個過程中,我們像是通過一個依賴關(guān)系樹的形式,不斷地向數(shù)的內(nèi)部進入,等返回結(jié)果,又開始回溯到根。

          開發(fā)一個簡單的 tinypack

          通過上面的這些調(diào)研,我們先考慮一下一個基礎(chǔ)的打包編譯工具可以做什么?

          1. 轉(zhuǎn)換ES6語法成ES5
          2. 處理模塊加載依賴
          3. 生成一個可以在瀏覽器加載執(zhí)行的 js 文件

          第一個問題,轉(zhuǎn)換語法,其實我們可以通過babel來做。核心步驟也就是:

          • 通過babylon生成AST

          • 通過babel-core將AST重新生成源碼

            /**

            */ function getAst (filename) { const content = fs.readFileSync(filename, 'utf-8')

            return babylon.parse(content, { sourceType: 'module', }); }

            /**

            */ function getTranslateCode(ast) { const {code} = transformFromAst(ast, null, { presets: ['env'] }); return code }

            • 編譯
            • @param ast
            • @returns {*}
            • 獲取文件,解析成ast語法
            • @param filename // 入口文件
            • @returns {*}

          接著我們需要處理模塊依賴的關(guān)系,那就需要得到一個依賴關(guān)系視圖。好在babel-traverse提供了一個可以遍歷AST視圖并做處理的功能,通過 ImportDeclaration 可以得到依賴屬性:

          function getDependence (ast) {
            let dependencies = []
            traverse(ast, {
              ImportDeclaration: ({node}) => {
                dependencies.push(node.source.value);
              },
            })
            return dependencies
          }

          /**
           * 生成完整的文件依賴關(guān)系映射
           * @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),
            };
          }
          復(fù)制代碼

          到目前為止,我們也只是得到根文件的依賴關(guān)系和編譯后的代碼,比如我們的index.js依賴了test.js但是我們并不知道test.js還需要依賴message.js,他們的源碼也是沒有編譯過。所以此時我們還需要做深度遍歷,得到完成的深度依賴關(guān)系:

          /**
           * 獲取深度隊列依賴關(guān)系
           * @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
          }
          復(fù)制代碼

          那么進行到這一步我們已經(jīng)完成了所有文件的編譯解析。最后一步,就是需要我們按照webpack的思想對源碼進行一些包裝。第一步,先是要生成一個modules對象:

          function bundle(queue) {
            let modules = ''
            queue.forEach(function (mod) {
              modules += `'${mod.fileName}'function (require, module, exports) { ${mod.code} },`
            })
            // ...
          }
          復(fù)制代碼

          得到 modules 對象后,接下來便是對整體文件的外部包裝,注冊require,module.exports

          (function(modules) {
                function require(fileName) {
                    // ...
                }
               require('${config.entry}');
           })({${modules}})
          復(fù)制代碼

          而函數(shù)內(nèi)部,也只是循環(huán)執(zhí)行每個依賴文件的 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;
          }
          復(fù)制代碼

          到這里基本上也就介紹完了,我們來打包試一下:

          (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;
            },
          })
          復(fù)制代碼

          再測試一下:

          image

          恩,基本上已經(jīng)完成一個簡易的 tinypack

          參考文章

          • 抽象語法樹 Abstract syntax tree
          • 一看就懂的JS抽象語法樹


          瀏覽 23
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久操视频网站 | 精品国产a∨一区天美传媒 | 色播成人网站 | 亚洲免费视频观看 | 久久久无码电影 |