<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    從零實(shí)現(xiàn)一個(gè)迷你 Webpack

    共 15619字,需瀏覽 32分鐘

     ·

    2022-11-29 18:21

    本文為來自 字節(jié)跳動(dòng)-國(guó)際化電商-S項(xiàng)目 的文章,已授權(quán) ELab 發(fā)布。

    webpack 是當(dāng)前使用較多的一個(gè)打包工具,將眾多代碼組織到一起使得在瀏覽器下可以正常運(yùn)行,下面以打包為目的,實(shí)現(xiàn)一個(gè)簡(jiǎn)易版 webpack,支持單入口文件的打包,不涉及插件、分包等。

    前置知識(shí)

    舉個(gè)??,先來看看下面這個(gè) demo,例子很簡(jiǎn)單,一個(gè) index.js,里面引用了一個(gè)文件 a.js,a.js 內(nèi)部引入了 b.js,通過 webpack 最簡(jiǎn)單的配置,將 index.js 文件作為入口進(jìn)行打包。

    來看看打包后的內(nèi)容是怎樣的

    // index.js
    require('./a.js');
    console.log('entry load');

    // a.js
    require("./b.js");
    const a = 1;
    console.log("a load");
    module.exports = a;

    // b.js
    console.log("b load");
    const b = 1;
    module.exports = b;

    可以看到打包產(chǎn)物是一個(gè)立即執(zhí)行函數(shù),函數(shù)初始先定義了多個(gè) module,每個(gè) module 是實(shí)際代碼中被 require 的文件內(nèi)容,同時(shí)由于瀏覽器不支持 require 方法,webpack 內(nèi)部自行實(shí)現(xiàn)了一個(gè) __webpack__require__,并將代碼中的 require 全部替換為該函數(shù)(從打包結(jié)果可看出)。

    webpack__require 定義之后,便開始執(zhí)行入口文件,同時(shí)可以看出,webpack 的打包過程便是通過入口文件,將直接依賴和間接依賴以 module 的形式組織到一起,并通過自行實(shí)現(xiàn)的 require 實(shí)現(xiàn)模塊的同步加載。

    了解了打包產(chǎn)物后,便可以開始實(shí)現(xiàn)簡(jiǎn)易版的 webpack ,最終打包產(chǎn)物與 webpack 保持一致。

    初始化參數(shù)

    根據(jù) Node 接口 | webpack 中文文檔[1] 可以知道,webpack node api 對(duì)外暴露出了 webpack 方法,通過調(diào)用 webpack 方法傳入配置,返回 compiler 對(duì)象,compiler 對(duì)象包含 run 方法可執(zhí)行編譯,即

    const webpack = require('webpack'); // 引用 webpack

    const compiler = webpack(options); // 傳入配置生成 compiler 對(duì)象

    compiler.run((err, stats) => {  // 執(zhí)行編譯, 傳入回調(diào)

    });

    因此,首先需要實(shí)現(xiàn)一個(gè) webpack 方法,同時(shí)該方法支持傳入 webpack 配置,返回 compiler 實(shí)例,webpack 官方支持了以 cli 的形式運(yùn)行 webpack 命令和指定參數(shù)、配置文件,這一部分暫時(shí)簡(jiǎn)單實(shí)現(xiàn),我們暴露出一個(gè)方法,方法接收用戶的配置。

    // mini-webpack/core/index.js

    function webpack() {
      // 創(chuàng)建compiler對(duì)象
      const compiler = new Compiler(options);
    }

    module.exports = webpack;

    如上,實(shí)現(xiàn)了一個(gè) webpack 方法,可傳入一個(gè) options 參數(shù),包括用戶指定的打包入口 entry、output 等。

    webpack({
        entry: './index.js',
        output: {
            path: path.resolve(__dirname, "dist"),
            filename: "[name].js",
        },
        module: {
            rules: []
        }
    })

    編譯

    上面已經(jīng)實(shí)現(xiàn)了 webpack 配置的傳入,compiler 的創(chuàng)建,接下來還需要實(shí)現(xiàn) Compiler 類,該類內(nèi)部暴露一個(gè) run 方法,用于執(zhí)行編譯。

    首先需要明確編譯過程需要做的事情。

    1. 讀取入口文件,將入口文件交給匹配的 loader 處理,返回處理后的代碼
    1. 開始編譯 loader 處理完的代碼
    1. 若代碼中依賴了其他文件,則對(duì) require 函數(shù)替換為 webpack 自行實(shí)現(xiàn)的 __webpack__require__, 保存該文件的處理結(jié)果,同時(shí)讓其他文件回到第 1 步進(jìn)行處理,不斷循環(huán)。
    1. 編譯結(jié)束后,每個(gè)文件都有其對(duì)應(yīng)的處理結(jié)果,將這些文件的編譯結(jié)果從初始的入口文件開始組織到一起。

    入口文件 loader 處理

    讀取入口文件,將入口文件交給 匹配的 loader 處理

    // mini-webpack compiler.js

    const fs = require('fs');
    class Compiler {
      constructor(options) {
        this.options = options || {};
        
        // 保存編譯過程編譯的 module
        this.modules = new Set();
      }

      run(callback) {
        const entryChunk = this.build(path.join(process.cwd(), this.options.entry));
      }

      build(modulePath) {
        let originCode = fs.readFileSync(modulePath);
        originCode = this.dealWidthLoader(modulePath, originCode.toString());
        return this.dealDependencies(originCode, modulePath);
      }

      // 將源碼交給匹配的 loader 處理
      dealWidthLoader(modulePath, originCode) {
        [...this.options.module.rules].reverse().forEach(item => {
            if (item.test(modulePath)) {
                const loaders = [...item.use].reverse();
                loaders.forEach(loader => originCode = loader(originCode))
            }
        })
        return originCode
      }
    }

    module.exports = Compiler;

    入口文件處理

    這里需要開始處理入口文件的依賴,將其 require 轉(zhuǎn)換成 自定義的 __webpack_require__,同時(shí)將其依賴收集起來,后續(xù)需要不斷遞歸處理其直接依賴和間接依賴,這里用到了 babel 進(jìn)行處理。

     // 調(diào)用 webpack 處理依賴的代碼
      dealDependencies(code, modulePath) {
        
        const fullPath = path.relative(process.cwd(), modulePath);
        // 創(chuàng)建模塊對(duì)象
        const module = {
          id: fullPath,
          dependencies: [] // 該模塊所依賴模塊絕對(duì)路徑地址
        };

        // 處理 require 語句,同時(shí)記錄依賴了哪些文件
        const ast = parser.parse(code, {
          sourceType: "module",
          ast: true,
        });
        // 深度優(yōu)先 遍歷語法Tree
        traverse(ast, {

          CallExpression: (nodePath) => {
            const node = nodePath.node;
            if (node.callee.name === "require") {
          
              // 獲得依賴的路徑
              const requirePath = node.arguments[0].value;
                
              const moduleDirName = path.dirname(modulePath);
              const fullPath = path.relative(path.join(moduleDirName, requirePath), requirePath);                    

              // 替換 require 語句為 webpack 自定義的 require 方法
              node.callee = t.identifier("__webpack_require__");
              // 將依賴的路徑修改成以當(dāng)前路行為基準(zhǔn)
              node.arguments = [t.stringLiteral(fullPath)];

              const exitModule = [...this.modules].find(item => item.id === fullPath)
              // 該文件可能已經(jīng)被處理過,這里判斷一下
              if (!exitModule) {
                // 記錄下當(dāng)前處理的文件所依賴的文件(后續(xù)需逐一處理)
                module.dependencies.push(fullPath);
              }
            }
          },
        });
        // 根據(jù)新的 ast 生成代碼
        const { code: compilerCode } = generator(ast);
        // 保存處理后的代碼
        module._source = compilerCode;
        // 返回當(dāng)前模塊對(duì)象
        return module;
      }

    依賴處理

    到這里為止便處理完了入口文件,但是在處理文件過程,還收集了入口文件依賴的其他文件未處理,因此,在 dealDependencies 尾部,加入以下代碼

     // 調(diào)用 webpack 處理依賴的代碼
      dealDependencies(code, modulePath) {
        ...
        ...
        ...
        // 為當(dāng)前模塊掛載新的生成的代碼
        module._source = compilerCode;
        
        // 遞歸處理其依賴
        module.dependencies.forEach((dependency) => {
          const depModule = this.build(dependency);
          
          // 同時(shí)保存下編譯過的依賴
          this.modules.add(depModule);
        });
        
        ...
        ...
        ...
        
        // 返回當(dāng)前模塊對(duì)象
        return module;
      }

    Chunk

    在上面的步驟中,已經(jīng)處理了入口文件、依賴文件,但目前它們還是分散開來,在 webpack 中,是支持多個(gè)入口,每個(gè)入口是一個(gè) chunk,這個(gè) chunk 將包含入口文件及其依賴的 module

    // mini-webpack compiler.js

    const fs = require('fs');
    class Compiler {
      constructor(options) {
        this.options = options || {};
        
        // 保存編譯過程編譯的 module
        this.modules = new Set();
      }

      run(callback) {
        const entryModule = this.build(path.join(process.cwd(), this.options.entry));
        const entryChunk = this.buildChunk("entry", entryModule);
      }
      
      build(modulePath) {
      }

      // 將源碼交給匹配的 loader 處理
      dealWidthLoader(modulePath, originCode) {
      }

      // 調(diào)用 webpack 處理依賴的代碼
      dealDependencies(code, modulePath) {    
      }

      buildChunk(entryName, entryModule) {
        return {
          name: entryName,
          // 入口文件編譯結(jié)果
          entryModule: entryModule,
          // 所有直接依賴和間接依賴編譯結(jié)果
          modules: this.modules,
        };
      }
    }

    module.exports = Compiler;

    文件生成

    至此我們已經(jīng)將入口文件和其所依賴的所有文件編譯完成,現(xiàn)在需要將編譯后的代碼生成對(duì)應(yīng)的文件。

    根據(jù)最上面利用官方 webpack 打包出來的產(chǎn)物,保留其基本結(jié)構(gòu),將構(gòu)造的 chunk 內(nèi)部的 entryModule 的 source 以及 modules 的 souce 替換進(jìn)去,并根據(jù)初始配置的 output 生成對(duì)應(yīng)文件。

    // mini-webpack compiler.js

    const fs = require('fs');
    class Compiler {
      constructor(options) {
        this.options = options || {};
        
        // 保存編譯過程編譯的 module,下面會(huì)講解到
        this.modules = new Set();
      }

      run(callback) {
        const entryModule = this.build(path.join(process.cwd(), this.options.entry));
        const entryChunk = this.buildChunk("entry", entryModule);
        this.generateFile(entryChunk);
      }
      
      build(modulePath) {
      }

      // 將源碼交給匹配的 loader 處理
      dealWidthLoader(modulePath, originCode) {
      }

      // 調(diào)用 webpack 處理依賴的代碼
      dealDependencies(code, modulePath) {    
      }

      buildChunk(entryName, entryModule) {
      }
      
      generateFile(entryChunk) {
      
        // 獲取打包后的代碼
        const code = this.getCode(entryChunk);
        if (!fs.existsSync(this.options.output.path)) {
          fs.mkdirSync(this.options.output.path);
        }
        
        // 寫入文件
        fs.writeFileSync(
          path.join(
            this.options.output.path,
            this.options.output.filename.replace("[name]", entryChunk.name)
          ),
          code
        );
      }

      getCode(entryChunk) {
        return `
          (() => {
          // webpackBootstrap
          var __webpack_modules__ = {
            ${entryChunk.modules.map(module => `
                "${module.id}": (module, __unused_webpack_exports, __webpack_require__) => {
                ${module._source}
              }
            `).join(',')}
          };

          var __webpack_module_cache__ = {};

          function __webpack_require__(moduleId) {
            // Check if module is in cache
            var cachedModule = __webpack_module_cache__[moduleId];
            if (cachedModule !== undefined) {
              return cachedModule.exports;
            }
            // Create a new module (and put it into the cache)
            var module = (__webpack_module_cache__[moduleId] = {
              exports: {},
            });

            // Execute the module function
            __webpack_modules__[moduleId](
              module,
              module.exports,
              __webpack_require__
            );

            // Return the exports of the module
            return module.exports;
          }

          var __webpack_exports__ = {};
          // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
          (() => {
           ${entryChunk.entryModule._source};
          })();
        })()
        `;
      }
    }

    module.exports = Compiler;

    試試在瀏覽器下跑一下生成的代碼

    符合預(yù)期,至此便完成了一個(gè)極簡(jiǎn)的 webpack,針對(duì)單入口文件進(jìn)行打包。當(dāng)然真正的 webpack 遠(yuǎn)非如此簡(jiǎn)單,這里僅僅只是實(shí)現(xiàn)其一個(gè)打包思路。

    ?? 謝謝支持

    以上便是本次分享的全部?jī)?nèi)容,希望對(duì)你有所幫助^_^

    喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

    歡迎關(guān)注公眾號(hào) ELab團(tuán)隊(duì) 收貨大廠一手好文章~

    • 字節(jié)跳動(dòng)校/社招內(nèi)推碼: WWCM1TA
    • 投遞鏈接: https://job.toutiao.com/s/rj1fwQW

    可憑內(nèi)推碼投遞 字節(jié)跳動(dòng)-國(guó)際化電商-S項(xiàng)目 團(tuán)隊(duì) 相關(guān)崗位哦~

    參考資料

    [1]

    Node 接口 | webpack 中文文檔: https://webpack.docschina.org/api/node/#webpack

    - END -

    瀏覽 38
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)
    評(píng)論
    圖片
    表情
    推薦
    點(diǎn)贊
    評(píng)論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報(bào)

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    成人欧美一区二区三区男男 | 男人天堂1024 | 图片区偷拍区小说区 | 欧美老熟妇乱大交XXXXX动漫 | 天天综合色 |