手把手教你实现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

相关推荐
啦啦右一几秒前
前端 | MYTED单篇TED词汇学习功能优化
前端·学习
测试界萧萧2 分钟前
外包干了4年,技术退步太明显了。。。。。
自动化测试·软件测试·功能测试·程序人生·面试·职场和发展
半开半落5 分钟前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
百事老饼干12 分钟前
Java[面试题]-真实面试
java·开发语言·面试
理想不理想v32 分钟前
vue经典前端面试题
前端·javascript·vue.js
不收藏找不到我34 分钟前
浏览器交互事件汇总
前端·交互
YBN娜1 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=1 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck1 小时前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js