<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

          共 13177字,需瀏覽 27分鐘

           ·

          2021-09-16 15:48

          一、前言

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

          二、背景知識

          1. 抽象語法樹(AST)

          什么是抽象語法樹?

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

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

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

          • JSLint、JSHint、ESLint 對代碼錯誤或風格的檢查等

          • Webpack、rollup 進行代碼打包等

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

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

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

          2. Babel

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

          • 語法轉(zhuǎn)換

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

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

          一般來說項目使用 Webpack 來打包文件都會配置 babel-loader 將 ES6 的代碼轉(zhuǎn)換成 ES5 的格式以兼容瀏覽器,這個過程就需要將我們的代碼轉(zhuǎ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 基礎配置
              // 讀取 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)容

          • 生成打包文件

              // 基礎結(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. 安裝相關依賴

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

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

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

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

          • @babel/preset-env:可根據(jù)配置的目標瀏覽器或者運行環(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 風格的代碼。

          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. 分析依賴關系

          從入口文件開始,循環(huán)解析每個文件與其依賴文件的信息,最終生成以文件名為 key,以包含依賴關系與編譯后模塊代碼的對象為 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ù)當中。可以看到,自執(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 信息,將打包代碼輸出到對應文件中。

          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 能不能正常運行。

          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. 【你應該了解的】抽象語法樹 AST

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



          緊追技術前沿,深挖專業(yè)領域
          掃碼關注我們吧!



          瀏覽 83
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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无码 翔田千里无码av | 国产精品内射久久久久欢欢 | 91国产网站 | 日韩素人 的搜索结果 - 91n |