天天看點

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

debug(調試)webpack:

因為plugin和loader的編寫需要nodejs環境,需要追蹤一些參數,這時候調試就顯得很重要了,但調試webpack不像在浏覽器中debug那麼輕松,需要一些配置:

首先在

package.json

中加入如下代碼:

改指令表示調試webpack并停在第一行

"scripts": {
    "start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
  }
           

調試分為兩種:

  1. 浏覽器中調試
  2. vscode中調試

浏覽器中debug:

指令行直接執行

npm run start

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

出現上述調試,說明

啟動成功

,

打開浏覽器

,

打開開發者工具

,可以看到控制台多了一個選項

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架
webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

到這裡說明調試成功了

vscode中調試(我更喜歡的方式):

更改

package.json

如下:

"scripts": {
    "start": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack.js"
  },
           

點選

vscode

中如下按鍵建立

launch.json

檔案:

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架
webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

更改

launch.json

檔案如下:

端口好要對上

{
  // 使用 IntelliSense 了解相關屬性。
  // 懸停以檢視現有屬性的描述。
  // 欲了解更多資訊,請通路: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "啟動程式",
      "skipFiles": ["<node_internals>/**"],
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start"],
      "port": 5858
    }
  ]
}
           

此時按

F5

或者點選此處即可開始

debug

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

效果如下:

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

同理,如果你是

vue

或者

react

的腳手架建立出來的項目,隻需要把

script

中調試的

js

檔案換成

cli

中的

核心起始js檔案

即可,以

vuecli

為例,

package.json

script

換成如下:

"scripts": {
        "debug": "node --inspect-brk=9229 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve"
  }
           

loader:

loader

本質上是一個函數

在一個

loader.js

檔案中編寫如下代碼:

module.exports = function (content, map, meta) {
   console.log(content);
	return content;
}
           

webpack.config.js

配置如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'babelLoader',
      },
    ]
  },
}
           

那麼當執行

webpack

的時候,所有js檔案都會經過我們編寫的

loader

,

content

中的内容即是源碼,我們可以通過一些正則,方法把源碼轉換後打包,這就是

loader

的原理了

loader的分類:

同步:

寫法一:

//同步loader
module.exports = function (content, map, meta) {
  return content;
}
           

寫法二:

module.exports = function (content, map, meta) {
  this.callback(null, content, map, meta);
}

           

異步:

// 異步loader
module.exports = function (content, map, meta) {

  const callback = this.async();

  setTimeout(() => {
    callback(null, content);
  }, 1000)
}
           

一般推薦的寫法是異步

loader

,因為不會對打包造成阻塞,性能較好

loader中一些工具方法:

  1. getOptions

  2. validate

loader.js

代碼如下:

const { getOptions } = require('loader-utils');
const { , } = require('schema-utils');

const schema = require('./schema');

module.exports = function (content, map, meta) {
  // 擷取options
  const options = getOptions(this);


  // 校驗options是否合法
  validate(schema, options, {
    name: 'loader'  //填一個loader的名字,控制台會提示哪個loader的配置校驗失敗
  })

  return content;
}
           

webpack.config.js

配置如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: './loader.js',
        options: {
          name:"zhangsan"
        }
      },
    ]
  },
}
           

./schema.json

如下:

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "名稱~"
    }
  },
  "additionalProperties": false   //是否允許options中有額外的屬性
}
           

./schema.json

是從來配置

option

傳入的規則,不符合規則,則會報錯,

getOptions

是用來取到傳入的

loader

option

配置,

validate

則是配合

./schema.json

中的規則來校驗拿到的

options

配置是否符合預期

實際應用:

使用@babel/core編譯js檔案:

編寫

loader.js

如下:

const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');

const babelSchema = require('./babelSchema.json');

// babel.transform用來編譯代碼的方法
// 是一個普通異步方法
// util.promisify将普通異步方法轉化成基于promise的異步方法
const transform = util.promisify(babel.transform);

module.exports = function (content, map, meta) {
  // 擷取loader的options配置
  const options = getOptions(this) || {};
  // 校驗babel的options的配置
  validate(babelSchema, options, {
    name: 'Babel Loader'
  });

  // 建立異步
  const callback = this.async();

  // 使用babel編譯代碼
  transform(content, options)
    .then(({code, map}) => callback(null, code, map, meta))
    .catch((e) => callback(e))
}
           

plugin:

plugin要比loader強大很多,不僅能影響源碼,還能影響檔案

webpack.config.js

編寫如下:

/const Plugin1 = require('./plugins/Plugin')

module.exports = {
  plugins: [
     new Plugin1()
  ]
}
           

Plugin.js

編寫如下:

class Plugin1 {
  apply(complier) {
    complier.hooks.emit.tap('Plugin1', (compilation) => {
      console.log('emit.tap 111');
    })
  }
}
module.exports = Plugin1;
           

apply

是一種固定的寫法,可以參考官網,

apply

中我們可以注冊自己的鈎子,那麼在

webpack

執行的時候,執行到

emit

階段,就會觸發我們的鈎子,對檔案進行操作

實際應用:

編寫一個插件,增加一個a.txt檔案,檢視文檔可知,可在

additionalAssets

這個鈎子中增加資源

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

plugin.js

代碼如下:

class Plugin2 {

  apply(compiler) {
    // 初始化compilation鈎子
    compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
      compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
        const content = 'hello plugin2';
        // 往要輸出資源中,添加一個a.txt
        compilation.assets['a.txt'] = {
          // 檔案大小
          size() {
            return content.length;
          },
          // 檔案内容
          source() {
            return content;
          }
        }
        cb();
      })
    })
  }
}
module.exports = Plugin2;
           

webpack.config.js

中使用:

// const Plugin2 = require('./plugins/Plugin2')

module.exports = {
  plugins: [
     new Plugin2()
  ]
}
           

那麼打包後,dist檔案中就會新增一個a.txt的資源檔案了

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

這種添加資源的方式挺麻煩的内容和大小都要自己填寫,有沒有自動計算的方式呢?

利用

webpack

提供的一些工具函數,可以更加友善的生成和操作檔案

改寫

plugin.js

如下:

const fs = require('fs');
const util = require('util');
const path = require('path');
const webpack = require('webpack');
const { RawSource } = webpack.sources;
// 将fs。readFile方法變成基于promise風格的異步方法
const readFile = util.promisify(fs.readFile);
class Plugin2 {
  apply(compiler) {
    compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
      compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
					//讀取b.txt檔案,讀取後是個buffer 
         const data = await readFile(path.resolve(__dirname, 'b.txt'));
         compilation.assets['a.txt'] = new RawSource(data);
        cb();
      })
    })
  }
}
module.exports = Plugin2;
           

以上介紹了一些常用的工具方法,更多方法還得參考官網

實際應用:

編寫一個

copy plugin

:用于複制資源,複制public中的資源到dist目錄下

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

plugin.js

代碼如下:

const path = require('path');
const fs = require('fs');
const {promisify} = require('util')
const { validate } = require('schema-utils');
//可以比對檔案清單,過濾一些檔案
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources

class CopyWebpackPlugin {
  constructor(options = {}) {
    // 驗證options是否符合規範
    validate(schema, options, {
      name: 'CopyWebpackPlugin'
    })
    this.options = options;
  }

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
        // 将from中的資源複制到to中,輸出出去
        const { from, ignore } = this.options;
        const to = this.options.to ? this.options.to : '.';
        
        // context就是webpack配置
        // 運作指令的目錄
        const context = compiler.options.context; // process.cwd()
        // 将輸入路徑變成絕對路徑
        const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);

        // 1. 過濾掉ignore的檔案
        // globby(要處理的檔案夾,options)
        const paths = await globby(absoluteFrom, { ignore });

        console.log(paths); // 所有要加載的檔案路徑數組

        // 2. 讀取paths中所有資源
        const files = await Promise.all(
          paths.map(async (absolutePath) => {
            // 讀取檔案
            const data = await readFile(absolutePath);
            // basename得到最後的檔案名稱
            const relativePath = path.basename(absolutePath);
            // 和to屬性結合
            // 沒有to --> reset.css
            // 有to --> css/reset.css
            const filename = path.join(to, relativePath);

            return {
              // 檔案資料
              data,
              // 檔案名稱
              filename
            }
          })
        )

        // 3. 生成webpack格式的資源
        const assets = files.map((file) => {
          const source = new RawSource(file.data);
          return {
            source,
            filename: file.filename
          }
        })
        
        // 4. 添加compilation中,輸出出去
        assets.forEach((asset) => {
          compilation.emitAsset(asset.filename, asset.source);
        })

        cb();
      })
    })
  }
}
module.exports = CopyWebpackPlugin;
           

schema.json

驗證規則如下:

{
  "type": "object",
  "properties": {
    "from": {
      "type": "string"
    },
    "to": {
      "type": "string"
    },
    "ignore": {
      "type": "array"
    }
  },
  "additionalProperties": false
}
           

webpack.config.js

代碼如下:

const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')
module.exports = {
  plugins: [
    new CopyWebpackPlugin({
      from: 'public',
      to: 'css',
      ignore: ['**/index.html']
    })
  ]
}
           

首先我的

public

目錄如下:

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

執行

npx webpack

指令輸出如下:

webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架
webpack中編寫自定義loader和plugin,及如何調試(debug)webpack,vuecli等腳手架

插件是成功運作的

plugin和loader編寫的思路大緻就是這樣了,更加深入的使用,無非就是熟讀文檔和參與到更多的實踐中去

繼續閱讀