手把手教你实现Webpack Loader

前言

对于 loader 大家都不陌生,比较常见的 loader 有 css-loader、style-loader、vue-loader、babel-loader等,这篇文章将带你学习如何实现一个自定义 loader。项目地址在文末,期待大家的一键三连~

一、准备工作

在实现 loader 前,我们先简单的搭建一个 webpack 运行环境。关于如何搭建 webpack 项目可以参考:手把手教你搭建 Webpack 5 + React 项目

1.1 初始化项目

新建一个文件夹 create-loader,在该目录执行以下命令来完成初始化工作。

js 复制代码
     pnpm init

关于 pnpm 的更多知识参考:包管理工具 ------ 更推荐的 pnpm

1.2 安装依赖

安装 webpack 、webpack-cli 以及 webpack-dev-server。前两者打包必备,后者是一个提供热更新的开发服务器,对开发阶段友好。

js 复制代码
 pnpm add webpack webpack-cli webpack-dev-server --save-dev

1.3 创建入口文件

创建 src 目录,并创建 index.js 入口文件。

js 复制代码
   // index.js
    document.write('hello wp')

1.4 创建 Webpack 配置文件 webpack.config.js

虽然 Webpack v4 + 开箱即用,可以无需配置文件就可以打包, Webpack 会默认打包入口文件为 src/index.js 文件,打包产物为 dist/main.js ,并且开启生产环境的压缩和优化。但大多数的项目还是需要一些复杂的配置,配置文件 webpack.config.js 文件还是很有必要的 。 Webpack 在打包时会自动识别这个文件,根据里面的配置来进行打包。

js 复制代码
 const path = require('path')

    module.exports = {
      mode: 'development', // 以什么模式进行打包
      entry: './src/index.js',
      output: {
        path: path.resolve(__dirname, 'dist'), // 打包后的代码放在dist目录下
        filename: 'bundle.js', // 文件名为 bundle.js
      },
      devServer: {
        static: './dist'
      }
    }

💡 如果我们想更改为指定的配置文件 prod.config.js 来打包,可以使用--config标志来修改。

js 复制代码
    "scripts": {
      "build": "webpack --config prod.config.js"
    }

1.5 添加 npm script

我们可以在 package.json 文件中创建快捷方式来启动开发和打包。

js 复制代码
    {
    // ...省略
    "main": "src/index.js", // 修改入口文件
     "scripts": {
        "dev": "webpack serve --open",
        "build": "webpack"
      },
      // ...省略
    }

使用 pnpm run build 可以实现打包:

我们可以看到目录里生成了一个 dist 目录,里面有一个 bundle.js 文件。

1.6 创建 index.html 文件

上面打包的产物只有一个js 文件,我们想要使用浏览器访问里面的内容就要手动创建一个 html 文件,并引入 bundle.js 脚本文件,使用浏览器打开即可。

html 复制代码
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <script src="./bundle.js"></script>
    </body>
    </html>

这样可以通过 pnpm run dev 来启动项目,浏览器里可以看到下面的内容:

以上的步骤简单的完成了一个基于 webpack 打包的最简单例子。

二、Loader 的简单介绍

2.1 loader 是什么?

loader 是一个导出为函数的 JavaScript 模块,用于对模块的源码进行转换。形如:

js 复制代码
    moudle.exports = (source) => {
      // 按照自己的转换要求进行处理
      return source
    }

2.2 为什么需要 loader?

webpack 本身只能识别 js 和 json 文件,对于 css、ts 等其他文件就需要 loader 对齐进行处理,转换为 webpack 能识别的模板。

2.3 如何定义 loader?

官方文档上有很多种 loader,但个人认为可总结为两种 loader,一种是normal loader,另一种是pitch loader。

  • normal loader
js 复制代码
    // normal loader
    module.exports = function normalLoader() {}
  • pitch loader :新增一种pitch loader的概念,类似于dom事件模型中的捕获,先执行pitch loader,然后在执行normal loader,且如果picth loader有返回非undefined的值,则直接中断后续loader的执行
js 复制代码
    // pitch loader
    module.exports.pitch = function pitchLoader() {}

关于 loader 和 pitch loader 后面会单独出一篇文章来进行说明。

2.4 loader 原则

  • 单一: 一个loader 只做一件事。
  • 链式调用:从右往左依次调用。
  • 模块化:保证输出的是模块化。
  • 无状态: 每一个 loader 的运行相对独立,不与其他 loader
  • loader-utils 工具库: 提供了很多工具

三、如何实现一个同步 loader

我们先来编写一个简单的字符串替换 loader,目的是将 wp 被替换为 xiaoqi 。

3.1 创建 loader

在 src 下创建 xq-loader 目录,创建 my-loader.js 文件。

js 复制代码
    // my-loader.js
    module.exports = function (source) {
      // 在这里按照你的需求处理 source
      // 可以通过 this.getOptions 或者 this.query 来获取参数
      const options = this.getOptions()
      console.log(options)
      return source.replace('wp', 'xiaoqi')
    }

    // 或者 使用this.callback
    module.exports = function (source) {
      // 在这里按照你的需求处理 source
      // 可以通过 this.getOptions 或者 this.query 来获取参数
      const options = this.getOptions()
      const result = source.replace('wp', 'xiaoqi')
      this.callback(null, result)
    }

这里编写的是一个简单的同步loader,我们可以通过 return 一个表示已转换模块的单一值。如果情况比较复杂,也可以通过 this.callback(err,values...) 这个回调函数来返回转换后的结果。

对于同步 loader 而言,使用return或者this.callback均可以达到想要的效果。只是说,相对于returnthis.callback可以返回更多的信息。

js 复制代码
    this.callback(
    // 转换异常时,抛出错误
    err: Error || null,
    // 转换后的结果
    content: string | Buffer,
    // 用于把转换后的内容得出原内容的 Source Map,方便调试
    sourceMap?: SourceMap,
    )

3.2 使用 loader

在 webpack.config.js 中配置 loader 有两种方式。一种是 path.resolve ,另一种是 ResolveLoader

  • 使用 path.resolve 指定 loader 的文件路径
js 复制代码
        // webpack.config.js
        {
          // ...省略
          module: {
            rules: [
              {
                test: /\.js$/,
                use: [
                  {
                    loader: path.resolve(__dirname, './src/xq-loader/my-loader.js'),
                    options: {
                      name: 'xiaoqi',
                    },
                  },
                ],
              },
            ],
          }
        }
  • 使用 ResolveLoader
js 复制代码
        {
         module: {
            rules: [
              {
                test: /\.js$/,
                use: ['my-loader'],
              },
            ],
          },
          resolveLoader: {
            // webpack 将会从这些目录中依次搜索 loader,
            modules: ['node_modules', './src/xq-loader'],
          },
        }

运行 npm run dev 可以看到 wp 被替换为 xiaoqi 。

四、如何实现一个异步 loader

在某些耗时久的场景下,比如处理网络请求的结果,我们可以使用异步 loader ,这样不会阻塞整个构建。

我们实现的这个异步 loader 的功能为读取 async.txt 中的内容并返回。 在 src 目录下创建 async.txt 文件,随便写点内容。

4.1 创建 loader

在 src 下创建 xq-loader 目录,创建 my-async-loader.js 文件。

对于异步loader而言,需要通过this.async(),来获取到callback函数。

js 复制代码
    // my-async-loader.js
    const fs = require('fs')
    const path = require('path')

    module.exports = function (source) {
      // 通过 this.async 来返回一个异步函数,第一个参数 Error, 第二个参数是处理的结果。
      const callback = this.async()
      fs.readFile(path.join(__dirname, '../async.txt'), 'utf-8', (err, data) => {
        const html = `module.exports = ${JSON.stringify(data)}`
        callback(null, html)
      })
    }

4.2 添加 loader 配置

在 webpack.config.js 文件中的 module.rules 下添加 my-async-loader

js 复制代码
   // webpack.config.js
    // ...
    {
      test: /\.txt$/,
      use: {
      loader: 'my-async-loader',
      },
    },
    // ...

4.3 引入 txt 文件

在 index.js 入口文件中引入。

js 复制代码
    // index.js
    import txt from './async.txt'
    document.write('hello wp')
    document.write(`</br>异步loader: ${txt}`)

运行 || 打包

pnpm run dev 可以看到 async.txt 文件下的内容被打印出来。

五、实现一个渲染 markdown 的 loader

简易版 mark-loader,借助 markdown-it 库的能力。

首先我们在 src 下创建一个 md 文件。

5.1 创建 loader

在 src 下创建 xq-loader 目录,创建 mark-loader.js 文件。

js 复制代码
    // mark-loader.js
    const MarkdownIt = require('markdown-it')

    module.exports = function (source) {
      const options = this.getOptions()
      const md = new MarkdownIt({
        html: true,
        ...options,
      })

      let html = md.render(source)
      // webpack 无法直接去解析html模板,所以要返回一段 js 代码
      html = `module.exports = ${JSON.stringify(html)}`
      this.callback(null, html)
    }

5.2 添加配置 loader

js 复制代码
    // webpack.config.js
    {
      test: /\.md$/,
      use: [
        {
        loader: 'mark-loader',
        },
      ],
    },

5.3 引入 md 文件

创建一个 div ,将生成的 html 插入其中。

js 复制代码
    import txt from './async.txt'
    import md from './mk.md'

    document.write('hello wp')
    document.write(`</br>异步loader: ${txt}`)

    const div = document.createElement('div')
    div.innerHTML = `${md}`
    document.body.appendChild(div)

运行 || 打包

pnpm run dev 运行,可以看到 md 文件以html 的形式渲染出来。

六、实现一个生成雪碧图的 loader

简易版 sprite-loader,借助 spritesmith 库的能力。

6.1 创建 css 文件

首先我们在 src 下创建一个 css 文件,以 ?__sprite 来标志是需要合成的图片。

6.2 创建 sprite-loader

找到需要合成的图片,组合生成一个数组,再使用 spritesmith 的能力将多张图合并成一张图。

js 复制代码
    const path = require('path')
    const fs = require('fs')
    const Spritesmith = require('spritesmith')

    module.exports = function (source) {
      const callback = this.async()
      // 匹配 url(开头 ?__sprite 结尾的
      const imgs = source.match(/url\((\S*)\?__sprite/g)
      const matchImgs = []
      for (let i = 0; i < imgs.length; i++) {
        // 解析出图片路径
        const img = imgs[i].match(/url\((\S*)\?__sprite/)
        matchImgs.push(path.join('./src', img[1]))
      }
      Spritesmith.run({ src: matchImgs }, (err, result) => {
        // 将合成的图片写入到 src/images/sprite.jpg 中
        fs.writeFileSync(path.join(process.cwd(), 'src/images/sprite.jpg'), result.image)
        // 替换原引入为合成图片的路径
        source = source.replace(/url\((\S*)\?__sprite/g, () => {
          return `url('./images/sprite.jpg'`
        })
        callback(null, source)
      })
    }

6.3 添加 loader 配置

由于 webpack 没法识别 css 文件,这里先用 sprite-loader 合成图片;再调用 css-loader,它会帮我们对 @importurl() 进行处理,就像 js 解析 import/require() 一样;最后调用 style-loader 帮我们将 css 代码以 <style>的形式插入到 DOM 中。

js 复制代码
    // webpack.config.js
    {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'sprite-loader'],
    },

6.4 引入 css 文件

在 index.js 中引入 css 文件,给上述的 div 加上类名 img2。

js 复制代码
    import './index.css'
    import txt from './async.txt'
    import md from './mk.md'
    // ...
    const div = document.createElement('div')
    div.className = 'img2'
    div.innerHTML = `${md}`
    document.body.appendChild(div)

6.5 运行 || 打包

可以看到背景图生效了,并且在 dist 下会生成一个图片文件。

七、小结

整篇文章介绍了几个自定义 loader 的实例,想必大家对如何实现 loader 已经有了一个整体的认知,感兴趣的小伙伴可以自己手动实现一下,会有更大的收获。欢迎大家评论交流,相互学习~

项目地址:手把手教你实现 Loader

相关推荐
y先森21 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy21 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891124 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿1 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡2 小时前
commitlint校验git提交信息
前端
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员3 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐3 小时前
前端图像处理(一)
前端