1. 手把手教你寫一個迷你 Webpack

        共 13283字,需瀏覽 27分鐘

         ·

        2021-09-15 22:14

        一、前言

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

        二、背景知識

        1. 抽象語法樹(AST)

        什么是抽象語法樹?

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

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

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

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

        • Webpack、rollup 進行代碼打包等

        • Babel 轉換 ES6 到 ES5 語法

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

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

        2. Babel

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

        • 語法轉換

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

        • 源碼轉換 (codemods)

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

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

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

        3. Webpack 打包原理

        Webpack 的構建過程一般會分為以下幾步:

        • 讀取 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"
                }
            }
        • 入口文件分析

          • 分析依賴模塊

          • 分析內容

          • 編譯內容

        • 依賴模塊分析

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

          • 分析內容

          • 編譯內容

        • 生成打包文件

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


        三、具體實現

        1. 安裝相關依賴

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

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

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

        • @babel/core:babel 的核心模塊,進行代碼的轉換

        • @babel/preset-env:可根據配置的目標瀏覽器或者運行環(huán)境來自動將 ES2015 + 的代碼轉換為 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"
            }
        }

        然后我們新建一個類,用于實現分析編譯等函數,并在構造函數中初始化配置信息:

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

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

        3. 代碼轉換,獲取模塊信息

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

        parse = filename => {
            // 讀取文件
            const fileBuffer = fs.readFileSync(filename, 'utf-8');
            // 轉換成抽象語法樹
            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;
                }
            })
            // 將抽象語法樹轉換成代碼
            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. 生成打包代碼

        生成依賴圖譜對象,作為參數傳入一個自執(zhí)行函數當中??梢钥吹?,自執(zhí)行函數中有個 require 函數,它的作用是通過調用 eval 執(zhí)行模塊代碼來獲取模塊內部 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 函數

        我們將上面的流程集成到一個 run 函數中,通過調用該函數來將整個構建打包流程跑通。

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

        8.mini-webpack 大功告成

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

        // 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.js、b.jsindex.js 三個文件

        三個文件內容如下:

        • 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 命令,內容為執(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 文件,內容正是我們輸出的打包代碼:

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

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

        五、項目 Git 地址

        項目代碼在此:mini-webpack

        六、參考文章

        1. 實現一個簡單的 Webpack

        2. Babel 中文文檔

        3. 【你應該了解的】抽象語法樹 AST

        4. webpack 構建原理和實現簡單 webpack



        往期推薦


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

        最后


        • 歡迎加我微信,拉你進技術群,長期交流學習...

        • 歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...

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

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
          
          

            1. 黄色AA片 | 国产人成免费爽爽爽视频 | 淫色五月 | 黄色视频网站免费观看 | 好叼视频 |