欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Webpack完整打包流程深入分析

 更新時間:2022年12月10日 09:51:13   作者:gogo2027  
webpack是一種前端資源構建工具,一個靜態(tài)模塊打包器(module bundler),下面這篇文章主要給大家介紹了關于Webpack完整打包流程的相關資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下

前言

webpack 在前端工程領域起到了中流砥柱的作用,理解它的內部實現機制會對你的工程建設提供很大的幫助(不論是定制功能還是優(yōu)化打包)。

下面我們基于 webpack5 源碼結構,對整個打包流程進行簡單梳理并進行實現,便與思考和理解每個階段所做的事情,為今后擴展和定制工程化能力打下基礎。

一、準備工作

在流程分析過程中我們會簡單實現 webpack 的一些功能,部分功能的實現會借助第三方工具:

  • tapable 提供 Hooks 機制來接入插件進行工作;
  • babel 相關依賴可用于將源代碼解析為 AST,進行模塊依賴收集和代碼改寫。
// 創(chuàng)建倉庫
mkdir webpack-demo && cd webpack-demo && npm init -y

// 安裝 babel 相關依賴
npm install @babel/parser @babel/traverse @babel/types @babel/generator -D

// 安裝 tapable(注冊/觸發(fā)事件流)和 fs-extra 文件操作依賴
npm install tapable fs-extra -D

接下來我們在 src 目錄下新建兩個入口文件和一個公共模塊文件:

mkdir src && cd src && touch entry1.js && touch entry2.js && touch module.js

并分別為文件添加一些內容:

// src/entry1.js
const module = require('./module');
const start = () => 'start';
start();
console.log('entry1 module: ', module);

// src/entry2.js
const module = require('./module');
const end = () => 'end';
end();
console.log('entry2 module: ', module);

// src/module.js
const name = 'cegz';
module.exports = {
  name,
};

有了打包入口,我們再來創(chuàng)建一個 webpack.config.js 配置文件做一些基礎配置:

// ./webpack.config.js
const path = require('path');
const CustomWebpackPlugin = require('./plugins/custom-webpack-plugin.js');

module.exports = {
  entry: {
    entry1: path.resolve(__dirname, './src/entry1.js'),
    entry2: path.resolve(__dirname, './src/entry2.js'),
  },
  context: process.cwd(),
  output: {
    path: path.resolve(__dirname, './build'),
    filename: '[name].js',
  },
  plugins: [new CustomWebpackPlugin()],
  resolve: {
    extensions: ['.js', '.ts'],
  },
  module: {
    rules: [
      {
        test: /\.js/,
        use: [
          path.resolve(__dirname, './loaders/transformArrowFnLoader.js'), // 轉換箭頭函數
        ],
      },
    ],
  },
};

以上配置,指定了兩個入口文件,以及一個 output.build 輸出目錄,同時還指定了一個 plugin 和一個 loader。

接下來我們編寫 webpack 的核心入口文件,來實現打包邏輯。這里我們創(chuàng)建 webpack 核心實現所需的文件:

// cd webpack-demo
mkdir lib && cd lib
touch webpack.js // webpack 入口文件
touch compiler.js // webpack 核心編譯器
touch compilation.js // webpack 核心編譯對象
touch utils.js // 工具函數

這里我們創(chuàng)建了兩個比較相似的文件:compilercompilation,在這里做下簡要說明:

  • compiler:webpack 的編譯器,它提供的 run 方法可用于創(chuàng)建 compilation 編譯對象來處理代碼構建工作;
  • compilation:由 compiler.run 創(chuàng)建生成,打包編譯的工作都由它來完成,并將打包產物移交給 compiler 做輸出寫入操作。

對于入口文件 lib/webpack.js,你會看到大致如下結構:

// lib/webpack.js
function webpack(options) {
  ...
}

module.exports = webpack;

對于執(zhí)行入口文件的測試用例,代碼如下:

// 測試用例 webpack-demo/build.js
const webpack = require('./lib/webpack');
const config = require('./webpack.config');

const compiler = webpack(config);

// 調用run方法進行打包
compiler.run((err, stats) => {
  if (err) {
    console.log(err, 'err');
  }
  // console.log('構建完成!', stats.toJSON());
});

接下來,我們從 lib/webpack.js 入口文件,按照以下步驟開始分析打包流程。

1、初始化階段 - webpack

  • 合并配置項
  • 創(chuàng)建 compiler
  • 注冊插件

2、編譯階段 - build

  • 讀取入口文件
  • 從入口文件開始進行編譯
  • 調用 loader 對源代碼進行轉換
  • 借助 babel 解析為 AST 收集依賴模塊
  • 遞歸對依賴模塊進行編譯操作

3、生成階段 - seal

  • 創(chuàng)建 chunk 對象
  • 生成 assets 對象

4、寫入階段 - emit

二、初始化階段

初始化階段的邏輯集中在調用 webpack(config) 時候,下面我們來看看 webpack() 函數體內做了哪些事項。

2.1、讀取與合并配置信息

通常,在我們的工程的根目錄下,會有一個 webpack.config.js 作為 webpack 的配置來源;

除此之外,還有一種是通過 webpak bin cli 命令進行打包時,命令行上攜帶的參數也會作為 webpack 的配置。

在配置文件中包含了我們要讓 webpack 打包處理的入口模塊、輸出位置、以及各種 loader、plugin 等;

在命令行上也同樣可以指定相關的配置,且權重高于配置文件。(下面將模擬 webpack cli 參數合并處理)

所以,我們在 webpack 入口文件這里將先做一件事情:合并配置文件與命令行的配置。

// lib/webpack.js
function webpack(options) {
  // 1、合并配置項
  const mergeOptions = _mergeOptions(options);
  ...
}

function _mergeOptions(options) {
  const shellOptions = process.argv.slice(2).reduce((option, argv) => {
    // argv -> --mode=production
    const [key, value] = argv.split('=');
    if (key && value) {
      const parseKey = key.slice(2);
      option[parseKey] = value;
    }
    return option;
  }, {});
  return { ...options, ...shellOptions };
}

module.exports = webpack;

2.2、創(chuàng)建編譯器(compiler)對象

好的程序結構離不開一個實例對象,webpack 同樣也不甘示弱,其編譯運轉是由一個叫做 compiler 的實例對象來驅動運轉。

compiler 實例對象上會記錄我們傳入的配置參數,以及一些串聯插件進行工作的 hooks API。

同時,還提供了 run 方法啟動打包構建,emitAssets 對打包產物進行輸出磁盤寫入。這部分內容后面介紹。

// lib/webpack.js
const Compiler = require('./compiler');

function webpack(options) {
  // 1、合并配置項
  const mergeOptions = _mergeOptions(options);
  // 2、創(chuàng)建 compiler
  const compiler = new Compiler(mergeOptions);
  ...
  return compiler;
}

module.exports = webpack;

Compiler 構造函數基礎結構如下:

// core/compiler.js
const fs = require('fs');
const path = require('path');
const { SyncHook } = require('tapable'); // 串聯 compiler 打包流程的訂閱與通知鉤子
const Compilation = require('./compilation'); // 編譯構造函數

class Compiler {
  constructor(options) {
    this.options = options;
    this.context = this.options.context || process.cwd().replace(/\\/g, '/');
    this.hooks = {
      // 開始編譯時的鉤子
      run: new SyncHook(),
      // 模塊解析完成,在向磁盤寫入輸出文件時執(zhí)行
      emit: new SyncHook(),
      // 在輸出文件寫入完成后執(zhí)行
      done: new SyncHook(),
    };
  }

  run(callback) {
    ...
  }

  emitAssets(compilation, callback) {
    ...
  }
}

module.exports = Compiler;

當需要進行編譯時,調用 compiler.run 方法即可:

compiler.run((err, stats) => { ... });

2.3、插件注冊

有 compiler 實例對象后,就可以注冊配置文件中的一個個插件,在合適的時機來干預打包構建。

插件需要接收 compiler 對象作為參數,以此來對打包過程及產物產生 side effect

插件的格式可以是函數或對象,如果為對象,需要自定義提供一個 apply 方法。常見的插件結構如下:

class WebpackPlugin {
  apply(compiler) {
    ...
  }
}

注冊插件邏輯如下:

// lib/webpack.js
function webpack(options) {
  // 1、合并配置項
  const mergeOptions = _mergeOptions(options);
  // 2、創(chuàng)建 compiler
  const compiler = new Compiler(mergeOptions);
  // 3、注冊插件,讓插件去影響打包結果
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler); // 當插件為函數時
      } else {
        plugin.apply(compiler); // 如果插件是一個對象,需要提供 apply 方法。
      }
    }
  }
  return compiler;
}

到這里,webpack 的初始工作已經完成,接下來是調用 compiler.run() 進入編譯構建階段。

三、編譯階段

編譯工作的起點是在 compiler.run,它會:

  • 發(fā)起構建通知,觸發(fā) hooks.run 通知相關插件;
  • 創(chuàng)建 compilation 編譯對象;
  • 讀取 entry 入口文件;
  • 編譯 entry 入口文件;

3.1、創(chuàng)建 compilation 編譯對象

模塊的打包(build)和 代碼生成(seal)都是由 compilation 來實現。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 觸發(fā) run hook
    this.hooks.run.call();
    // 創(chuàng)建 compilation 編譯對象
    const compilation = new Compilation(this);
    ...
  }
}

compilation 實例上記錄了構建過程中的 entries、module、chunks、assets 等編譯信息,同時提供 buildseal 方法進行代碼構建和代碼生成。

// lib/compilation.js
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');
const { tryExtensions, getSourceCode } = require('./utils');

class Compilation {
  constructor(compiler) {
    this.compiler = compiler;
    this.context = compiler.context;
    this.options = compiler.options;
    // 記錄當前 module code
    this.moduleCode = null;
    // 保存所有依賴模塊對象
    this.modules = new Set();
    // 保存所有入口模塊對象
    this.entries = new Map();
    // 所有的代碼塊對象
    this.chunks = new Set();
    // 存放本次產出的文件對象(與 chunks 一一對應)
    this.assets = {};
  }
  build() {}
  seal() {}
}

有了 compilation 對象后,通過執(zhí)行 compilation.build 開始模塊構建。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 觸發(fā) run hook
    this.hooks.run.call();
    // 創(chuàng)建 compilation 編譯對象
    const compilation = new Compilation(this);
    // 編譯模塊
    compilation.build();
  }
}

3.2、讀取 entry 入口文件

構建模塊首先從 entry 入口模塊開始,此時首要工作是根據配置文件拿到入口模塊信息。

entry 配置的方式多樣化,如:可以不傳(有默認值)、可以傳入 string,也可以傳入對象指定多個入口。

所以讀取入口文件需要考慮并兼容這幾種靈活配置方式。

// lib/compilation.js
class Compilation {
  ...
  build() {
    // 1、讀取配置入口
    const entry = this.getEntry();
    ...
  }

  getEntry() {
    let entry = Object.create(null);
    const { entry: optionsEntry } = this.options;
    if (!optionsEntry) {
      entry['main'] = 'src/index.js'; // 默認找尋 src 目錄進行打包
    } else if (typeof optionsEntry === 'string') {
      entry['main'] = optionsEntry;
    } else {
      entry = optionsEntry; // 視為對象,比如多入口配置
    }
    // 相對于項目啟動根目錄計算出相對路徑
    Object.keys(entry).forEach((key) => {
      entry[key] = './' + path.posix.relative(this.context, entry[key]);
    });
    return entry;
  }
}

3.3、編譯 entry 入口文件

拿到入口文件后,依次對每個入口進行構建。

// lib/compilation.js
class Compilation {
  ...
  build() {
    // 1、讀取配置入口
    const entry = this.getEntry();
    // 2、構建入口模塊
    Object.keys(entry).forEach((entryName) => {
      const entryPath = entry[entryName];
      const entryData = this.buildModule(entryName, entryPath);
      this.entries.set(entryName, entryData);
    });
  }
}

構建階段執(zhí)行如下操作:

  • 通過 fs 模塊讀取 entry 入口文件內容;
  • 調用 loader 來轉換(更改)文件內容;
  • 為模塊創(chuàng)建 module 對象,通過 AST 解析源代碼收集依賴模塊,并改寫依賴模塊的路徑;
  • 如果存在依賴模塊,遞歸進行上述三步操作;

讀取文件內容:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 讀取文件原始代碼
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    ...
  }
}

調用 loader 轉換源代碼:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 讀取文件原始代碼
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    // 2. 調用 loader 進行處理
    this.runLoaders(modulePath);
    ...
  }
}

loader 本身是一個 JS 函數,接收模塊文件的源代碼作為參數,經過加工改造后返回新的代碼。

// lib/compilation.js
class Compilation {
  ...
  runLoaders(modulePath) {
    const matchLoaders = [];
    // 1、找到與模塊相匹配的 loader
    const rules = this.options.module.rules;
    rules.forEach((loader) => {
      const testRule = loader.test;
      if (testRule.test(modulePath)) {
        // 如:{ test:/\.js$/g, use:['babel-loader'] }, { test:/\.js$/, loader:'babel-loader' }
        loader.loader ? matchLoaders.push(loader.loader) : matchLoaders.push(...loader.use);
      }
    });
    // 2. 倒序執(zhí)行 loader
    for (let i = matchLoaders.length - 1; i >= 0; i--) {
      const loaderFn = require(matchLoaders[i]);
      // 調用 loader 處理源代碼
      this.moduleCode = loaderFn(this.moduleCode);
    }
  }
}

執(zhí)行 webpack 模塊編譯邏輯:

// lib/compilation.js
class Compilation {
  ...
  buildModule(moduleName, modulePath) {
    // 1. 讀取文件原始代碼
    const originSourceCode = fs.readFileSync(modulePath, 'utf-8');
    this.moduleCode = originSourceCode;
    // 2. 調用 loader 進行處理
    this.runLoaders(modulePath);
    // 3. 調用 webpack 進行模塊編譯 為模塊創(chuàng)建 module 對象
    const module = this.handleWebpackCompiler(moduleName, modulePath);
    return module; // 返回模塊
  }
}
  • 創(chuàng)建 module 對象;
  • 對 module code 解析為 AST 語法樹;
  • 遍歷 AST 去識別 require 模塊語法,將模塊收集在 module.dependencies 之中,并改寫 require 語法為 __webpack_require__;
  • 將修改后的 AST 轉換為源代碼;
  • 若存在依賴模塊,深度遞歸構建依賴模塊。
// lib/compilation.js
class Compilation {
  ...
  handleWebpackCompiler(moduleName, modulePath) {
    // 1、創(chuàng)建 module
    const moduleId = './' + path.posix.relative(this.context, modulePath);
    const module = {
      id: moduleId, // 將當前模塊相對于項目啟動根目錄計算出相對路徑 作為模塊ID
      dependencies: new Set(), // 存儲該模塊所依賴的子模塊
      entryPoint: [moduleName], // 該模塊所屬的入口文件
    };

    // 2、對模塊內容解析為 AST,收集依賴模塊,并改寫模塊導入語法為 __webpack_require__
    const ast = parser.parse(this.moduleCode, {
      sourceType: 'module',
    });

    // 遍歷 ast,識別 require 語法
    traverse(ast, {
      CallExpression: (nodePath) => {
        const node = nodePath.node;
        if (node.callee.name === 'require') {
          const requirePath = node.arguments[0].value;
          // 尋找模塊絕對路徑
          const moduleDirName = path.posix.dirname(modulePath);
          const absolutePath = tryExtensions(
            path.posix.join(moduleDirName, requirePath),
            this.options.resolve.extensions,
            requirePath,
            moduleDirName
          );
          // 創(chuàng)建 moduleId
          const moduleId = './' + path.posix.relative(this.context, absolutePath);
          // 將 require 變成 __webpack_require__ 語句
          node.callee = t.identifier('__webpack_require__');
          // 修改模塊路徑(參考 this.context 的相對路徑)
          node.arguments = [t.stringLiteral(moduleId)];

          if (!Array.from(this.modules).find(module => module.id === moduleId)) {
            // 在模塊的依賴集合中記錄子依賴
            module.dependencies.add(moduleId);
          } else {
            // 已經存在模塊集合中。雖然不添加進入模塊編譯 但是仍要在這個模塊上記錄被依賴的入口模塊
            this.modules.forEach((module) => {
              if (module.id === moduleId) {
                module.entryPoint.push(moduleName);
              }
            });
          }
        }
      },
    });

    // 3、將 ast 生成新代碼
    const { code } = generator(ast);
    module._source = code;

    // 4、深度遞歸構建依賴模塊
    module.dependencies.forEach((dependency) => {
      const depModule = this.buildModule(moduleName, dependency);
      // 將編譯后的任何依賴模塊對象加入到 modules 對象中去
      this.modules.add(depModule);
    });

    return module;
  }
}

通常我們 require 一個模塊文件時習慣不去指定文件后綴,默認會查找 .js 文件。

這跟我們在配置文件中指定的 resolve.extensions 配置有關,在 tryExtensions 方法中會嘗試為每個未填寫后綴的 Path 應用 resolve.extensions

// lib/utils.js
const fs = require('fs');

function tryExtensions(
  modulePath,  extensions,  originModulePath,  moduleContext
) {
  // 優(yōu)先嘗試不需要擴展名選項(用戶如果已經傳入了后綴,那就使用用戶填入的,無需再應用 extensions)
  extensions.unshift('');
  for (let extension of extensions) {
    if (fs.existsSync(modulePath + extension)) {
      return modulePath + extension;
    }
  }
  // 未匹配對應文件
  throw new Error(
    `No module, Error: Can't resolve ${originModulePath} in  ${moduleContext}`
  );
}

module.exports = {
  tryExtensions,
  ...
}

至此,「編譯階段」到此結束,接下來是「生成階段」 seal

四、生成階段

在「編譯階段」會將一個個文件構建成 module 存儲在 this.modules 之中。

在「生成階段」,會根據 entry 創(chuàng)建對應 chunk 并從 this.modules 中查找被 entry 所依賴的 module 集合。

最后,結合 runtime webpack 模塊機制運行代碼,經過拼接生成最終的 assets 產物。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 觸發(fā) run hook
    this.hooks.run.call();
    // 創(chuàng)建 compilation 編譯對象
    const compilation = new Compilation(this);
    // 編譯模塊
    compilation.build();
    // 生成產物
    compilation.seal();
    ...
  }
}

entry + module --> chunk --> assets 過程如下:

// lib/compilation.js
class Compilation {
  ...
  seal() {
    // 1、根據 entry 創(chuàng)建 chunk
    this.entries.forEach((entryData, entryName) => {
      // 根據當前入口文件和模塊的相互依賴關系,組裝成為一個個包含當前入口所有依賴模塊的 chunk
      this.createChunk(entryName, entryData);
    });
    // 2、根據 chunk 創(chuàng)建 assets
    this.createAssets();
  }

  // 根據入口文件和依賴模塊組裝chunks
  createChunk(entryName, entryData) {
    const chunk = {
      // 每一個入口文件作為一個 chunk
      name: entryName,
      // entry build 后的數據信息
      entryModule: entryData,
      // entry 的所依賴模塊
      modules: Array.from(this.modules).filter((i) =>
        i.entryPoint.includes(entryName)
      ),
    };
    // add chunk
    this.chunks.add(chunk);
  }

  createAssets() {
    const output = this.options.output;
    // 根據 chunks 生成 assets
    this.chunks.forEach((chunk) => {
      const parseFileName = output.filename.replace('[name]', chunk.name);
      // 為每一個 chunk 文件代碼拼接 runtime 運行時語法
      this.assets[parseFileName] = getSourceCode(chunk);
    });
  }
}

getSourceCode 是將 entrymodules 組合而成的 chunk,接入到 runtime 代碼模板之中。

// lib/utils.js
function getSourceCode(chunk) {
  const { entryModule, modules } = chunk;
  return `  (() => {    var __webpack_modules__ = {      ${modules        .map((module) => {          return `          '${module.id}': (module) => {            ${module._source}
      }        `;        })        .join(',')}
    };    var __webpack_module_cache__ = {};    function __webpack_require__(moduleId) {      var cachedModule = __webpack_module_cache__[moduleId];      if (cachedModule !== undefined) {        return cachedModule.exports;      }      var module = (__webpack_module_cache__[moduleId] = {        exports: {},      });      __webpack_modules__[moduleId](module, module.exports, __webpack_require__);      return module.exports;    }    (() => {      ${entryModule._source}
    })();  })();  `;
}

到這里,「生成階段」處理完成,這也意味著 compilation 編譯工作的完成,接下來我們回到 compiler 進行最后的「產物輸出」。

五、寫入階段

「寫入階段」比較容易理解,assets 上已經擁有了最終打包后的代碼內容,最后要做的就是將代碼內容寫入到本地磁盤之中。

// lib/compiler.js
class Compiler {
  ...
  run(callback) {
    // 觸發(fā) run hook
    this.hooks.run.call();
    // 創(chuàng)建 compilation 編譯對象
    const compilation = new Compilation(this);
    // 編譯模塊
    compilation.build();
    // 生成產物
    compilation.seal();
    // 輸出產物
    this.emitAssets(compilation, callback);
  }

  emitAssets(compilation, callback) {
    const { entries, modules, chunks, assets } = compilation;
    const output = this.options.output;

    // 調用 Plugin emit 鉤子
    this.hooks.emit.call();

    // 若 output.path 不存在,進行創(chuàng)建
    if (!fs.existsSync(output.path)) {
      fs.mkdirSync(output.path);
    }

    // 將 assets 中的內容寫入文件系統(tǒng)中
    Object.keys(assets).forEach((fileName) => {
      const filePath = path.join(output.path, fileName);
      fs.writeFileSync(filePath, assets[fileName]);
    });

    // 結束之后觸發(fā)鉤子
    this.hooks.done.call();

    callback(null, {
      toJSON: () => {
        return {
          entries,
          modules,
          chunks,
          assets,
        };
      },
    });
  }
}

至此,webpack 的打包流程就以完成。

接下來我們完善配置文件中未實現的 loaderplugin,然后調用測試用例,測試一下上面的實現。

六、編寫 loader

webpack.config.js 中我們?yōu)?.js 文件類型配置了一個自定義 loader 來轉換文件內容:

// webpack.config.js
module: {
  rules: [
    {
      test: /\.js/,
      use: [
        path.resolve(__dirname, './loaders/transformArrowFnLoader.js'),
      ],
    },
  ],
},

loader 本身是一個函數,接收文件模塊內容作為參數,經過改造處理返回新的文件內容。

下面我們在 loaders/transformArrowFnLoader.js 中,對文件中使用到的箭頭函數,轉換為普通函數,來理解 webpack loader 的作用。

// loaders/transformArrowFnLoader.js
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const t = require('@babel/types');

function transformArrowLoader(sourceCode) {
  const ast = parser.parse(sourceCode, {
    sourceType: 'module'
  });
  traverse(ast, {
    ArrowFunctionExpression(path, state) {
      const node = path.node;
      const body = path.get('body');
      const bodyNode = body.node;
      if (bodyNode.type !== 'BlockStatement') {
        const statements = [];
        statements.push(t.returnStatement(bodyNode));
        node.body = t.blockStatement(statements);
      }
      node.type = "FunctionExpression";
    }
  });
  const { code } = generator(ast);

  return code;
}

module.exports = transformArrowLoader;

最終,箭頭函數經過處理后變成如下結構:

const start = () => 'start';
    ||
    ||
const start = function () {
  return 'start';
};

七、編寫插件

從上面介紹我們了解到,每個插件都需要提供一個 apply 方法,此方法接收 compiler 作為參數。

通過 compiler 可以去訂閱 webpack 工作期間不同階段的 hooks,以此來影響打包結果或者做一些定制操作。

下面我們編寫自定義插件,綁定兩個不同時機的 compiler.hooks 來擴展 webpack 打包功能:

  • hooks.emit.tap 綁定一個函數,在 webpack 編譯資源完成,輸出寫入磁盤前執(zhí)行(可以做清除 output.path 目錄操作);
  • hooks.done.tap 綁定一個函數,在 webpack 寫入磁盤完成之后執(zhí)行(可以做一些靜態(tài)資源 copy 操作)。
// plugins/custom-webpack-plugins
const fs = require('fs-extra');
const path = require('path');

class CustomWebpackPlugin {
  apply(compiler) {
    const outputPath = compiler.options.output.path;
    const hooks = compiler.hooks;

    // 清除 build 目錄
    hooks.emit.tap('custom-webpack-plugin', (compilation) => {
      fs.removeSync(outputPath);
    });

    // copy 靜態(tài)資源
    const otherFilesPath = path.resolve(__dirname, '../src/otherfiles');
    hooks.done.tap('custom-webpack-plugin', (compilation) => {
      fs.copySync(otherFilesPath, path.resolve(outputPath, 'otherfiles'));
    });
  }
}

module.exports = CustomWebpackPlugin;

現在,我們通過 node build.js 運行文件,最終會在 webpack-demo 下生成 build 目錄以及入口打包資源。

文末

到此這篇關于Webpack完整打包流程的文章就介紹到這了,更多相關Webpack打包流程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論