<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

          共 13283字,需瀏覽 27分鐘

           ·

          2021-09-15 22:14

          一、前言

          最近正好在學(xué)習(xí) Webpack,覺得 Webpack 這種通過構(gòu)建模塊依賴圖來打包項目文件的思想很有意思,于是參考了網(wǎng)上的一些文章實現(xiàn)了一個簡陋版本的 mini-webpack,通過入口文件將依賴的模塊打包在一起,生成一份最終運(yùn)行的代碼。想了解 Webpack 的構(gòu)建原理還需要補(bǔ)充一些相關(guān)的背景知識,下面一起來看看。

          二、背景知識

          1. 抽象語法樹(AST)

          什么是抽象語法樹?

          平時我們編寫程序的時候,會經(jīng)常在代碼中根據(jù)需要 import 一些模塊,那 Webpack 在構(gòu)建項目、分析依賴的時候是如何得知我們代碼中是否有 import 文件,import 的是什么文件的呢?Webpack 并不是人,無法像我們一樣一看到代碼語句就明白其含義,所以我們需要將編寫的代碼轉(zhuǎn)換成 Webpack 認(rèn)識的格式讓他它進(jìn)行處理,這份轉(zhuǎn)換后生成的東西就是抽象語法樹。下面這張圖能很好地說明什么是抽象語法樹:

          可以看到,抽象語法樹是源代碼的抽象語法結(jié)構(gòu)樹狀表現(xiàn)形式,我們每條編寫的代碼語句都可以被解析成一個個的節(jié)點,將一整個代碼文件解析后就會生成一顆節(jié)點樹,作為程序代碼的抽象表示。通過抽象語法樹,我們可以做以下事情:

          • IDE 的錯誤提示、代碼格式化、代碼高亮、代碼自動補(bǔ)全等

          • JSLint、JSHint、ESLint 對代碼錯誤或風(fēng)格的檢查等

          • Webpack、rollup 進(jìn)行代碼打包等

          • Babel 轉(zhuǎn)換 ES6 到 ES5 語法

          • 注入代碼統(tǒng)計單元測試覆蓋率

          想看看你的代碼會生成怎樣的抽象語法樹嗎?這里有一個工具 AST Explorer 能夠在線預(yù)覽你的代碼生成的抽象語法樹,感興趣的不妨上去試一試。

          2. Babel

          Babel 是一個工具鏈,主要用于將采用 ECMAScript 2015+ 語法編寫的代碼轉(zhuǎn)換為向后兼容的 JavaScript 語法,以便能夠運(yùn)行在當(dāng)前和舊版本的瀏覽器或其他環(huán)境中。通過 Babel 我們可以做以下事情:

          • 語法轉(zhuǎn)換

          • 通過 Polyfill 方式在目標(biāo)環(huán)境中添加缺失的特性(通過第三方 Polyfill 模塊,例如 core-js,實現(xiàn))

          • 源碼轉(zhuǎn)換 (codemods)

          一般來說項目使用 Webpack 來打包文件都會配置 babel-loader 將 ES6 的代碼轉(zhuǎn)換成 ES5 的格式以兼容瀏覽器,這個過程就需要將我們的代碼轉(zhuǎn)換成抽象語法樹后再進(jìn)行轉(zhuǎn)換處理,轉(zhuǎn)換完成后再將抽象語法樹還原成代碼。

          // Babel 輸入:ES2015 箭頭函數(shù)
          [123].map((n) => n + 1);

          // Babel 輸出:ES5 語法實現(xiàn)的同等功能
          [123].map(function(n{
            return n + 1;
          });

          3. Webpack 打包原理

          Webpack 的構(gòu)建過程一般會分為以下幾步:

          • 讀取 Webpack 基礎(chǔ)配置
              // 讀取 webpack.config.js 配置文件:
              const path = require"path"
              module.exports = {
                  entry:"./src/index.js"
                  mode:"development"
                  output:{
                    path:path.resolve(__dirname,"./dist"),
                    filename:"bundle.js"
                  }
              }
          • 入口文件分析

            • 分析依賴模塊

            • 分析內(nèi)容

            • 編譯內(nèi)容

          • 依賴模塊分析

            • 分析依賴模塊是否有其他模塊

            • 分析內(nèi)容

            • 編譯內(nèi)容

          • 生成打包文件

              // 基礎(chǔ)結(jié)構(gòu)為一個IIFE自執(zhí)行函數(shù)
              // 接收一個對象參數(shù),key 為入口文件的目錄,value為一個執(zhí)行入口文件里面代碼的函數(shù)
              (function (modules{
                // installedModules 用來存放緩存
                const installedModules = {};
                // __webpack_require__用來轉(zhuǎn)化入口文件里面的代碼
                function __webpack_require__(moduleIid{ ... }
                // IIFE將 modules 中的 key 傳遞給 __webpack_require__ 函數(shù)并返回。
                return __webpack_require__(__webpack_require__.s = './src/index.js');
              }({
                './src/index.js': (function (module, exports{
                  eval('console.log(\'test webpack entry\')');
                }),
              }));


          三、具體實現(xiàn)

          1. 安裝相關(guān)依賴

          我們需要用到以下幾個包:

          • @babel/parser:用于將輸入代碼解析成抽象語法樹(AST)

          • @babel/traverse:用于對輸入的抽象語法樹(AST)進(jìn)行遍歷

          • @babel/core:babel 的核心模塊,進(jìn)行代碼的轉(zhuǎn)換

          • @babel/preset-env:可根據(jù)配置的目標(biāo)瀏覽器或者運(yùn)行環(huán)境來自動將 ES2015 + 的代碼轉(zhuǎn)換為 es5

          使用 npm 命令安裝一下:

          npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D

          2. 讀取基本配置

          要讀取 Webpack 的基本配置,首先我們得有一個全局的配置文件:

          // mini-webpack.config.js
          const path = require('path');

          module.exports ={
              entry"./src/index.js",
              mode"development",
              output: {
                path: path.resolve(__dirname,"./dist"),
                filename"bundle.js"
              }
          }

          然后我們新建一個類,用于實現(xiàn)分析編譯等函數(shù),并在構(gòu)造函數(shù)中初始化配置信息:

          const options = require('./mini-webpack.config');

          class MiniWebpack{
              constructor(options){
                  this.options = options;
              }
              // ...
          }

          3. 代碼轉(zhuǎn)換,獲取模塊信息

          我們使用 fs 讀取文件內(nèi)容,使用 parser 將模塊代碼轉(zhuǎn)換成抽象語法樹,再使用 traverse 遍歷抽象語法樹,針對其中的 ImportDeclaration 節(jié)點保存模塊的依賴信息,最終使用 babel.transformFromAst 方法將抽象語法樹還原成 ES5 風(fēng)格的代碼。

          parse = filename => {
              // 讀取文件
              const fileBuffer = fs.readFileSync(filename, 'utf-8');
              // 轉(zhuǎn)換成抽象語法樹
              const ast = parser.parse(fileBuffer, { sourceType'module' });

              const dependencies = {};
              // 遍歷抽象語法樹
              traverse(ast, {
                  // 處理ImportDeclaration節(jié)點
                  ImportDeclaration({node}){
                      const dirname = path.dirname(filename);
                      const newDirname = './' + path.join(dirname, node.source.value).replace('\\''/');
                      dependencies[node.source.value] = newDirname;
                  }
              })
              // 將抽象語法樹轉(zhuǎn)換成代碼
              const { code } = babel.transformFromAst(ast, null, {
                  presets:['@babel/preset-env']
              });
              
              return {
                  filename,
                  dependencies,
                  code
              }
          }

          4. 分析依賴關(guān)系

          從入口文件開始,循環(huán)解析每個文件與其依賴文件的信息,最終生成以文件名為 key,以包含依賴關(guān)系與編譯后模塊代碼的對象為 value 的依賴圖譜對象并返回。

          analyse = entry => {
              // 解析入口文件
              const entryModule = this.parse(entry);
              const graphArray = [entryModule];
              // 循環(huán)解析模塊,保存信息
              for(let i=0;i<graphArray.length;++i){
                  const { dependencies } = graphArray[i];
                  Object.keys(dependencies).forEach(filename => {
                      graphArray.push(this.parse(dependencies[filename]));
                  })
              }

              const graph = {};
              // 生成依賴圖譜對象
              graphArray.forEach(({filename, dependencies, code})=>{
                  graph[filename] = {
                      dependencies,
                      code
                  };
              })

              return graph;
          }

          5. 生成打包代碼

          生成依賴圖譜對象,作為參數(shù)傳入一個自執(zhí)行函數(shù)當(dāng)中。可以看到,自執(zhí)行函數(shù)中有個 require 函數(shù),它的作用是通過調(diào)用 eval 執(zhí)行模塊代碼來獲取模塊內(nèi)部 export 出來的值。最終我們返回打包的代碼。

          generate = (graph, entry) => {
              return `
              (function(graph){
                  function require(filename){
                      function localRequire(relativePath){
                          return require(graph[filename].dependencies[relativePath]);
                      }
                      const exports = {};
                      (function(require, exports, code){
                          eval(code);
                      })(localRequire, exports, graph[filename].code)

                      return exports;
                  }
                  
                  require('${entry}');
              })(${graph})
              `

          }

          6. 輸出最終文件

          通過獲取 this.options 中的 output 信息,將打包代碼輸出到對應(yīng)文件中。

          fileOutput = (output, code) => {
              const { path: dirPath, filename } = output;
              const outputPath = path.join(dirPath, filename);

              // 如果沒有文件夾的話,生成文件夾
              if(!fs.existsSync(dirPath)){
                  fs.mkdirSync(dirPath)
              }
              // 寫入文件中
              fs.writeFileSync(outputPath, code, 'utf-8');
          }

          7. 模擬 run 函數(shù)

          我們將上面的流程集成到一個 run 函數(shù)中,通過調(diào)用該函數(shù)來將整個構(gòu)建打包流程跑通。

          run = () => {
              const { entry, output } = this.options;
              const graph = this.analyse(entry);
              // stringify依賴圖譜對象,防止在模板字符串中調(diào)用toString()返回[object Object]
              const graphStr = JSON.stringify(graph);
              const code = this.generate(graphStr, entry);
              this.fileOutput(output, code);
          }

          8.mini-webpack 大功告成

          通過上面的流程,我們的 mini-webpack 已經(jīng)完成了。我們將文件保存為 main.js,新建一個 MiniWebpack 對象并執(zhí)行它的 run 函數(shù):

          // main.js
          const options = require('./mini-webpack.config');

          class MiniWebpack{
              constructor(options){
                  // ...
              }

              parse = filename => {
                  // ...
              }

              analyse = entry => {
                  // ...
              }

              generate = (graph, entry) => {
                  // ...
              }

              fileOutput = (output, code) => {
                  // ...
              }

              run = () => {
                  // ...
              }
          }

          const miniWebpack = new MiniWebpack(options);
          miniWebpack.run();

          四、實際演示

          我們來實際試驗一下,看看這個 mini-webpack 能不能正常運(yùn)行。

          1. 新建測試文件

          首先在根目錄下創(chuàng)建 src 文件夾,新建 a.jsb.jsindex.js 三個文件

          三個文件內(nèi)容如下:

          • a.js
          export default 1;
          • b.js
          export default function(){
              console.log('I am b');
          }
          • index.js
          import a from './a.js';
          import b from './b.js';

          console.log(a);
          console.log(b);

          2. 填入配置文件

          配置好入口文件、輸出文件等信息:

          const path = require('path');

          module.exports ={
              entry"./src/index.js",
              mode"development",
              output: {
                path: path.resolve(__dirname,"./dist"),
                filename"bundle.js"
              }
          }

          3. 完善 package.json

          我們在 package.json 的 scripts 中新增一個 build 命令,內(nèi)容為執(zhí)行 main.js:

          {
            "name""mini-webpack",
            "version""1.0.0",
            "description""",
            "main""index.js",
            "scripts": {
              "test""echo \"Error: no test specified\" && exit 1",
              "build""node main.js"
            },
            "author""",
            "license""ISC",
            "devDependencies": {
              "@babel/core""^7.15.4",
              "@babel/parser""^7.15.4",
              "@babel/preset-env""^7.15.4",
              "@babel/traverse""^7.15.4"
            }
          }

          4. 效果演示

          我們執(zhí)行 npm run build 命令,可以看到在根目錄下生成了 dist 文件夾,里面有個 bundle.js 文件,內(nèi)容正是我們輸出的打包代碼:

          執(zhí)行下 bundle.js 文件,看看會有什么輸出:

          可以看到,bundle.js 的輸出正是 index.js 文件中兩個 console.log 輸出的值,說明我們的代碼轉(zhuǎn)換沒有問題,到這里試驗算是成功了。

          五、項目 Git 地址

          項目代碼在此:mini-webpack

          六、參考文章

          1. 實現(xiàn)一個簡單的 Webpack

          2. Babel 中文文檔

          3. 【你應(yīng)該了解的】抽象語法樹 AST

          4. webpack 構(gòu)建原理和實現(xiàn)簡單 webpack



          往期推薦


          大廠面試過程復(fù)盤(微信/阿里/頭條,附答案篇)
          面試題:說說事件循環(huán)機(jī)制(滿分答案來了)
          專心工作只想搞錢的前端女程序員的2020

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...

          點個在看支持我吧
          瀏覽 69
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  成人免费看片 无需下载 | 免费观看三区视频 | 久久精品夜色国产亚洲AV | 黑人精品欧美一区二区蜜桃 | 特级西西人体大胆无码 |