动态

详情 返回 返回

手擼Webpack自定義Loader - 动态 详情

前言

我們知道 webpack 只能處理 JavaScript 和 Json 文件,面對 CSS、圖片等資源是無能為力的,它需要通過 loader 將這些資源轉換為可處理的模塊。

loader 的本質是一個解析資源的函數模塊,該函數對接受到的內容進行轉換,然後返回 webpack 可處理的資源。

loader的執行順序

loader 可根據執行順序區分為:

  • pre: 前置 loader
  • normal: 普通 loader
  • inline: 內聯 loader
  • post: 後置 loader

通過配置 enforce,限定 loader 類型

{
  enforce: "[pre|normal(缺省)|inline|post]",
  test: /.js$/,
  loader: "xxx-loader",
}

loader執行順序:pre > normal > inline > post,同級的 loader 根據配置順序自上而下(從右到左)執行

常見的 Loader

在手寫自定義 loader 前,先來回顧一下 webpack 中常見的 loader。

樣式處理loader

webpack 處理樣式資源,提供了兩個 loader:style-loader、css-loader。

css-loader 用於將 css 資源轉化成 webpack 可處理的模塊。而 style-loader 將模塊導出的內容作為樣式並添加到 DOM 中。下面是兩個 loader 的用法:

安裝依賴

npm install css-loader style-loader -D

配置

module.exports = {
    // ...
    module: {
        rules: [{
            test: /.css$/,
            use: ['style-loader', 'css-loader'],
        }]
    },
};

module.rules代表模塊的處理規則。test 選項接收一個匹配需要處理資源的正則表達式。use 選項接收一個處理資源的 loader 數組。

babel-loader

babel-loader 是一個基於 Babel 實現的,用於加載 ES2015+ 代碼並將其轉換為 ES5。

在安裝依賴時,同時需要安裝 Babel 的核心包@babel/core以及 Babel 官方的預設@babel/preset-env

npm install babel-loader @babel/core @babel/preset-env -D

在 webpack.config.js 中配置,這裏需要將 Babel 預設@babel/preset-env通過 options 傳遞給babel-loader


module: {
    rules: [{
        test: /.js$/,
        include: path.resolve(__dirname, 'src'),
        use: ['babel-loader'],
        options: {
            presets: ['@babel/preset-env'],
        }
    }]
},

這裏配置的 options,在loader中提供了一個工具包loader-utils,通過裏面的getOptions方法獲取。

const loaderUtils = require('loader-utils');
module.exports = function (content, map, meta) {
    const options = loaderUtils.getOptions(this);
    // ...
}

ts-loader

隨着 JavaScript 的超集 TypeScript 的發展,越來越的項目引用了 TypeScript 作為開發語言和規範。而 TypeScript 同樣是需要編譯轉換成 JavaScript 代碼運行,ts-loader 就是用於編譯轉換 TypeScript 的工具。

安裝依賴

npm install ts-loader -D

在規則中配置

rules: [
  {
    test: /.ts$/,
    use: 'ts-loader',
  }
]

vue-loader

如果你引入了 vue 庫,並使用單文件組件的寫法,vue 官方提供了這個loader處理 .vue 的單文件組件。

更多細節參考:https://vue-loader.vuejs.org/zh/

自定義 Loader

現在,我們動手擼一個自定義的 loader。

需求是:解析/src/utils包下的工具函數,根據函數的註釋生成md文檔

首先,搭建好 webpack 的基本環境

安裝依賴

npm init 
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-env -D

webpack基本配置

const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");

const title = 'webpack-template'

module.exports = env => {
  const isProd = env['production']
  return {
    mode: isProd ? 'production' : 'development',
    entry: {
      app: './src/index.js'
    },
    output: {
      filename: `static/js/[name].[contenthash].js`,
      clean: true
    },
    module: {
      rules: [
        {
          test: /.js$/,
          include: path.resolve(__dirname, 'src'),
          use: ['babel-loader']
        }
      ]
    },
    plugins: [
      new HtmlWebpackPlugin({
        title,
        template: path.resolve(__dirname, 'index.html')
      }),
    ],
    devServer: {
      port: 88,
      open: true
    }
  };
};

接着,我們在根目錄下創建一個 loader 目錄,用於存放我們開發的 loader。同時創建 js 文件utils2md-loader,寫入基本的暴露函數。

module.exports = function (content) {}

我們在 webpack 配置中,將loader配置上。

我們只需要解析/src/utils包下的工具函數,因此設定 include 為 /src/utils。

同時配置 options ,用於配置些其他參數。比如這裏,我們配置了文檔的輸出路徑。

const path = require('path');

module.exports = env => {
  return {
    module: {
      rules: [
        {
          test: /.js$/,
          include: path.resolve(__dirname, 'src', 'utils'),// 我們只需要
          use: [{
            loader: './loader/utils2md-loader',
            options: {
              outputFile: 'utils.md',// 可選參數,指定輸出文件路徑
            }
          }],
        },
      ]
    }
  };
};

接下來,我們要做的是解析代碼裏的註釋,得到一個對象形式的註釋。這裏具體實現細節就不多説明了

const commentRegex = //*[\s\S]*?*//g

function parseComment(content){
  // 使用正則表達式提取註釋
  const comments = content.match(commentRegex)
  return comments.map(comment => {
    const commentMap = new Map()
    const lines = comment.split('\r\n').slice(1, -1)
    let key = ''
    for (const commentItem of lines) {
      // 去除行首行尾的無效字符( * )
      const line = commentItem.match(/^\s**\s(.*)/)[1];
      // @字符開頭,存下key
      if (line.charAt(0) === '@') {
        const lineMap = line.split(' ')
        key = lineMap[0].slice(1)
        const value = lineMap.slice(1, lineMap.length).join(' ')
        commentMap.set(key, commentMap.get(key) ? [commentMap.get(key), value].join(',') : value)
      } else {
        commentMap.set(key, commentMap.get(key).concat(line))
      }
    }
    return Object.fromEntries(commentMap)
  })
}

接着,我們通過 webpack 提供的 loader-utils 工具包下的 getOptions 獲取我們在配置中配置的options參數

const loaderUtils = require('loader-utils');
const defaultOutputPath = 'utils.md'

module.exports = function (content) {
    const commentList = parseComment(content)
    const title = path.basename(this.resourcePath)
    // 獲取輸出文檔的路徑
    const options = loaderUtils.getOptions(this);
    const outputPath = options.outputFile || defaultOutputPath;
};

最終,我們將註釋對象輸出到目標文件裏。

function output(commentList, path, title) {
  return new Promise((resolve, reject) => {
    if (!commentList || !commentList.length) {
      reject('comment is not defined')
    }
    const beginTime = Date.now()
    const ws = fs.createWriteStream(path, { flags: 'a' })
    ws.on('finish', () => {
      console.log(`寫入完成,耗時:${Date.now() - beginTime} ms`);
    });
    ws.on('error', (err) => {
      ws.destroy();
      reject(`寫入錯誤:${err}`)
    });
    ws.write(`# ${title}\r\n`)
    for (const [index, comment] of commentList.entries()) {
      for (const key in comment) {
        ws.write(`##### ${key}\r\n`)
        ws.write(`${comment[key]}\r\n`)
      }
      if (index < commentList.length - 1) {
        ws.write('---\r\n')
      }
    }
    ws.end();
    resolve()
  })
}

這裏可以看到,輸出函數中採用了異步的流式寫入,因此返回的是一個Promise。而在 loader 函數中,需要採用異步 loader 的方式處理。

異步 loader 的處理方式就是,調用 loader 提供的async()方法得到 callback 回調函數,再由callback 函數返回文件處理結果。它包含四個參數:

err: Error | null

content: string | Buffer

sourceMap?: SourceMap

meta?: any

const callback = this.async()
output(commentList, outputPath, title).then(() => {
  callback(null, content.replace(commentRegex, ''));
}).catch(err => {
  callback(err, content.replace(commentRegex, ''));
})

最後,我們執行一下打包命令,查看生成的md文檔。到這,就算是完成了一次 loader 的開發

Github:webpack-template/loader

更多開發loader的細節可參考官方網站:https://webpack.js.org/contribute/writing-a-loader/

user avatar tianmiaogongzuoshi_5ca47d59bef41 头像 toopoo 头像 dingtongya 头像 Leesz 头像 linlinma 头像 yinzhixiaxue 头像 nihaojob 头像 freeman_tian 头像 front_yue 头像 dirackeeko 头像 aqiongbei 头像 zourongle 头像
点赞 274 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.