手撸Webpack自定义Loader

前言

我们知道 webpack 只能处理 JavaScript 和 Json 文件,面对 CSS、图片等资源是无能为力的,它需要通过 loader 将这些资源转换为可处理的模块。

loader 的本质是一个解析资源的函数模块,该函数对接受到的内容进行转换,然后返回 webpack 可处理的资源。

loader的执行顺序

loader 可根据执行顺序区分为:

  • pre: 前置 loader
  • normal: 普通 loader
  • inline: 内联 loader
  • post: 后置 loader

通过配置 enforce,限定 loader 类型

bash 复制代码
{
  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

配置

java 复制代码
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

bash 复制代码
npm install babel-loader @babel/core @babel/preset-env -D

在 webpack.config.js 中配置,这里需要将 Babel 预设@babel/preset-env通过 options 传递给babel-loader

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

这里配置的 options,在loader中提供了一个工具包loader-utils,通过里面的getOptions方法获取。

ini 复制代码
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

在规则中配置

bash 复制代码
rules: [
  {
    test: /.ts$/,
    use: 'ts-loader',
  }
]

vue-loader

如果你引入了 vue 库,并使用单文件组件的写法,vue 官方提供了这个loader处理 .vue 的单文件组件。

更多细节参考:vue-loader.vuejs.org/zh/

自定义 Loader

现在,我们动手撸一个自定义的 loader。

需求是:解析/src/utils包下的工具函数,根据函数的注释生成md文档

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

安装依赖

kotlin 复制代码
npm init 
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/core @babel/preset-env -D

webpack基本配置

javascript 复制代码
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,写入基本的暴露函数。

javascript 复制代码
module.exports = function (content) {}

我们在 webpack 配置中,将loader配置上。

我们只需要解析/src/utils包下的工具函数,因此设定 include 为 /src/utils。

同时配置 options ,用于配置些其他参数。比如这里,我们配置了文档的输出路径。

javascript 复制代码
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',// 可选参数,指定输出文件路径
            }
          }],
        },
      ]
    }
  };
};

接下来,我们要做的是解析代码里的注释,得到一个对象形式的注释。这里具体实现细节就不多说明了

vbnet 复制代码
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参数

ini 复制代码
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;
};

最终,我们将注释对象输出到目标文件里。

javascript 复制代码
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

dart 复制代码
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的细节可参考官方网站:webpack.js.org/contribute/...

相关推荐
前端Hardy9 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie39 分钟前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生1 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js
程序员爱技术5 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js