likes
comments
collection
share

从零开始学 Webpack 4.0(九)完

作者站长头像
站长
· 阅读数 24

原文本人写于 2022-05-26 22:26:48

一、前言

通过前面一系列文章的学习,我们对 Webpack 中的 Loader 以及 Plugin 已经不陌生了。我们使用 Loader 对特定的文件或者说模块去进行打包,使用 Plugin 在 Webpack 运行到某个时刻去进行一些操作。

这次我们来简单讲下 Loader 以及 Plugin 的实现机制。

二、如何编写一个 Loader?

基础结构

demo09 下创建 make-loader 作为项目文件夹,也就是 make-loader 目录是项目根路径。

进入 make-loader 目录,运行命令生成 package.json 文件。

npm init -y

安装 webpack 以及 webpack-cli

npm install webpack@4.29.0 webpack-cli@3.2.1 -D

创建 src 目录,在该目录下新增 index.js 进行业务代码编写。

index.js

console.log('hello world')

创建 loaders 目录,在该目录下新增 replaceLoader.js 进行 Loader 的编写。

replaceLoader.js

// 这就是一个最简单的 Loader。
// Loader 简单理解为就是一个函数,但是这里不能写成箭头函数的形式,
// 否则在调用 Loader 时 this 的指向会有问题,我们需要用到 Loader 内 this 的一些变量以及方法。
// 该函数接受参数 source 是需要 Loader 处理的模块内容,Loader 对源代码内容做变更再返回。
module.exports = function (source) {
  return source.replace('world', 'phao')
}

编写 Webpack 打包配置文件 webpack.config.js,使用自己编写的 Loader 打包 js 文件。

webpack.config.js

const path = require('path')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  module:{
    rules: [{
      test: /\.js/,
      // 使用自己的 Loader
      use: [
        path.resolve(__dirname, './loaders/replaceLoader.js')
      ]
    }]
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

package.json 去除默认 scripts 脚本命令 test,新增 build。

package.json

{
  ...
  "scripts": {
    "build": "webpack"
  },
  ...
}

于是,当前项目结构为:

从零开始学 Webpack 4.0(九)完

运行打包命令

npm run build

打包成功后,查看 dist 目录下 main.js 底部代码,可以看到原先 index.js 内写的 console.log('hello world') 给替换成 console.log('hello phao')

从零开始学 Webpack 4.0(九)完

传递参数

在使用 Loader 打包模块时,可以进行传参。

webpack.config.js

...
module.exports = {
  ...
  module:{
    rules: [{
      test: /\.js/,
      use: [
        {
          loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
          // 传参给 Loader
          options: {
            name: 'phao'
          }
        }
      ]
    }]
  },
  ...
}

在编写 Loader 里的函数可以通过 this.query 获取到传过来的参数,但 this.query 有时候可能会遇到一些奇怪的问题,例如传过来的不是个对象而是字符串,官方建议是使用 loader-utils 工具类提供的 getOptions() 去获取携带参数。

安装 loader-utils

npm install loader-utils@1.2.3 -D

replaceLoader.js

const loaderUtils = require('loader-utils')

module.exports = function (source) {
  // return source.replace('world', this.query.name)

  // getOptions() 会自动去分析 this.query
  const options = loaderUtils.getOptions(this)
  return source.replace('world', options.name)
}

运行打包命令,打包成功后查看 dist 目录下 main.js 底部代码,原先 index.js 内写的 console.log('hello world') 正常替换成 console.log('hello phao')

返回值处理

当我们进行 Loader 的编写时,对传进来的代码处理完进行返回,通过 return 只能返回一个参数,这时候可以通过 this.callback() 把一些额外的信息返回出去。

replaceLoader.js

...
module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const result = source.replace('world', options.name)

  // this.callback(
  //   err: Error | null, // 没有错误就填 null
  //   content: string | Buffer, // 返回内容
  //   sourceMap?: SourceMap, // 可以配置一个 SourceMap
  //   meta?: any // 可以放置一些想要额外传递出去的信息
  // )

  // sourceMap 参数的具体意义
  // 例如 css-loader,处理 css 之后,sourceMap 的映射关系会发生变化,
  // 那么 css-loader 就需要调用 sourceMap 重新对代码的关系做处理。
  
  // 没有需要额外传递的信息,这样写等价于 return。
  this.callback(null, result)
}

当我们在编写 Loader 的时候加入了异步代码,需要声明一下这里面的代码有异步操作,不然外面调用时会因为没有及时拿到返回值而报错。

replaceLoader.js

...
module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  // 声明一下,这里面的代码有异步操作
  const callback = this.async()

  setTimeout(() => {
    const result = source.replace('world', options.name)
    callback(null, result)
  }, 2000)
}

运行打包命令,打包被延迟两秒才完成。

从零开始学 Webpack 4.0(九)完

案例实践

现在我们来做这样一件事情,通过调用两个自己编写的 Loader 来打包 index.js 的业务代码做打印内容的替换。

修改 replaceLoader.js

replaceLoader.js

module.exports = function (source) {
  return source.replace('phao', 'phaode')
}

loaders 目录下新建 replaceLoaderAsync.js

replaceLoaderAsync.js

const loaderUtils = require('loader-utils')

module.exports = function (source) {
  const options = loaderUtils.getOptions(this)
  const callback = this.async()

  setTimeout(() => {
    const result = source.replace('world', options.name)
    callback(null, result)
  }, 2000)
}

使用 Loader

webpack.config.js

...
module.exports = {
  ...
  resolveLoader: {
    // 修改引入 Loader 的路径
    // 在引入 Loader 时先去 node_modules 目录下找,没有找到就去 loaders 目录下找。
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [{
      test: /\.js/,
      use: [
        {
          // loader: path.resolve(__dirname, './loaders/replaceLoader.js')
          loader: 'replaceLoader'
        },
        {
          // loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
          loader: 'replaceLoaderAsync',
          options: {
            name: 'phao'
          }
        }
      ]
    }]
  },
  ...
}

进行了这些修改后,我们的 index.js 首先会被 replaceLoaderAsync 把 console.log('hello world') 替换成 console.log('hello phao'),然后被 replaceLoader 替换成 console.log('hello phaode')

运行打包命令,打包成功后查看 dist 目录下 main.js 底部代码,内容为 console.log('hello phaode')

同步与异步

编写 Loader 的函数时,接受的参数是源代码,在源代码基础上返回想要的内容。

返回想要的内容,分为同步以及异步两种形式。

同步直接通过 return 以及 this.callback() 来返回结果。

异步需要先调用 this.async() 进行声明,然后通过 callback() 返回结果。

同步以及异步的 callback() 参数结构一致。

Loader 的使用场景

这里只是提供个思路,实际的实现会复杂一些。

  • 异常捕获
    // 在编写 Loader 的函数时拿到源代码 source 的 js 代码,
    // 通过抽象语法树的一些东西对代码进行一个分析,当代码里出现:
    function(){}
    // 就把它替换成:
    try{
      function(){}
    } catch(e) {
      ...
    }
    
    // 通过 Loader 在 Webpack 里做异常捕获,业务代码不需要做任何改变。
    
  • 网站语言切换
    // 把网页显示内容放到一些占位符里面去写,例如: {{title}} ,
    // loader 通过 node 传入的一些全局变量判断当前打包语言是哪一种,
    // 对源代码进行分析,把占位符替换为想要展示的文本内容。
    
    // 伪代码
    if (Node全局变量 === '中文') {
      source.replace('{{title}}', '中文标题')
    } else {
      source.replace('{{title}}', 'english title')
    }
    
    // 在编写业务代码时就不用考虑需要显示的是哪种语言,只需编写占位符。
    

小结

Loader 能做的东西非常多,当在编写业务代码时发现源代码需要做一些包装或者说替换,就可以考虑通过 Loader 来解决。

三、如何编写一个 Plugin?

案例实践

Plugin 就是插件,在打包过程中某个具体的时刻进行一些操作,例如打包后生成一个 html 文件,或者说打包前把 dist 目录下清空。

Webpack 的源码大多数都是基于 Plugin 的机制来编写的。

对于 Plugin 来说,它的核心机制是事件驱动,或者说是发布订阅这样的设计模式。在这个模式里,代码之间的执行都是通过事件来驱动。

现在我们来编写一个简单的 Plugin,要实现的效果是在整个项目打包结束,即将要把打包结果放入 dist 目录时,希望在 dist 目录下生成一个版权文件,例如叫 copyright.text,里面写一些版权信息。

demo09 下创建 make-plugin 作为项目文件夹,也就是 make-plugin 目录是项目根路径,各文件直接拿上面项目 make-loader 的,接下来做些修改。

从零开始学 Webpack 4.0(九)完

package.json

{
  "name": "make-plugin",
  ...
}

package-lock.json

{
  "name": "make-plugin",
  ...
}

安装依赖

npm ci

创建 plugins 目录,在该目录下新增 copyright-webpack-plugin.js 进行 Plugin 的编写。

copyright-webpack-plugin.js

// 一个最基本的插件
class CopyrightWebpackPlugin {
  constructor() {
    console.log('插件被使用了')
  }

  // 调插件时会执行 apply 这个方法,compiler 可以理解为 Webpack 的一个实例。
  // compiler 里存储着 Webpack 相关的各种各样配置文件,以及打包的过程等一系列内容。
  apply(compiler) {
    console.log('插件被调用了')
  }
}

module.exports = CopyrightWebpackPlugin

修改 webpack.config.js,因为插件本质上是一个类,所以一般使用插件都需要去 new 一个实例。

webpack.config.js(原文件内容清空)

const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')

module.exports = {
  mode: 'development',
  entry: {
    main: './src/index.js'
  },
  plugins: [
    new CopyrightWebpackPlugin()
  ],
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  }
}

运行打包命令

npm run build

观察控制台打印结果

从零开始学 Webpack 4.0(九)完

通过简单的 Plugin 代码结构以及引入使用的方式,我们知道了 Plugin 怎么定义怎么使用。

还可以给 Plugin 传参

webpack.config.js

...
module.exports = {
  ...
  plugins: [
    new CopyrightWebpackPlugin({
      name: 'phao'
    })
  ],
  ...
}

copyright-webpack-plugin.js

class CopyrightWebpackPlugin {
  // options 接收到外部传入参数
  constructor(options) {
    console.log('插件被使用了')
    console.log(options)
  }
  ...
}
...

运行打包命令,观察控制台打印结果。

从零开始学 Webpack 4.0(九)完

传参仅做演示,我们这里不需要,配置还原

webpack.config.js

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

copyright-webpack-plugin.js

class CopyrightWebpackPlugin {
  constructor(options) {
    console.log('插件被使用了')
  }
  ...
}
...

我们这个 Webpack 实例 compiler 在打包过程中某些时刻要去做一些事情,查阅官方文档,hooks(翻成中文就是钩子)里面就定义了一些具体的时刻值。

修改 copyright-webpack-plugin.js,编写 compile 以及 emit 这两个时刻。compile 是同步时刻,在一个新的编译创建之后执行。emit 是异步时刻,在生成资源到 output 目录之前执行。

copyright-webpack-plugin.js

class CopyrightWebpackPlugin {
  // constructor(options) {
  //   console.log('插件被使用了')
  // }
  
  apply(compiler) {
    // tap() 运行同步以及异步时刻
    // tap() 第一个参数传入要使用的插件名,第二个参数传入函数编写逻辑,
    // 第二参数传入函数接收的参数 compilation 存放着这次打包的相关内容。
    compiler.hooks.compile.tap('CopyrightWebpackPlugin', (compilation) => {
      console.log('compile')
    })

    // tapAsync() 运行异步时刻
    // tapAsync() 第一个参数传入要使用的插件名,第二个参数传入函数编写逻辑,
    // 第二参数传入函数接收两个参数,参数一 compilation 存放着这一次打包的相关内容,
    // 参数二 callback 在函数末尾需要调用一下。
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      // compilation.assets 是本次打包生成的内容
      console.log(compilation.assets)

      // 往生成的文件里去新增一个
      compilation.assets['copyright.txt'] = {
        // source 表明文件的内容是什么
        source: function() {
          return 'copyright by phao'
        },
        // 告诉它这个文件大概有多长
        size: function() {
          // 17 个字符长度
          return 17
        }
      }

      callback()
    })
  }
}

module.exports = CopyrightWebpackPlugin

运行打包命令,观察控制台打印结果,实现了我们想要的效果,dist 目录下新增 copyright.text,完成这个简单的插件。

从零开始学 Webpack 4.0(九)完 从零开始学 Webpack 4.0(九)完

借助 node 调试工具进行 debugger

在本项目中我们提及 compilation 存放着这次打包的相关内容,那么想要查看里面具体包含什么,使用 console.log() 进行控制台的打印并不直观,我们可以借助 node 的调试工具去进行 debugger。

新增 package.json 的脚本命令 debug,node node_modules/webpack/bin/webpack.jswebpack 是一样的,只是这样显式地用 node 去执行文件可以传递一些 node 的参数进去,--inspect 表示要开启 node 的调试工具,--inspect-brk 表示在运行 webpack.js 做调试时,在 webpack 命令执行时的第一行打个断点。

package.json

{
  ...
  "scripts": {
    "debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js",
    "build": "webpack"
  },
  ...
}

在 copyright-webpack-plugin.js 打个断点

copyright-webpack-plugin.js

class CopyrightWebpackPlugin {
  apply(compiler) {
    ...
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, callback) => {
      console.log(compilation.assets)

      // 打个断点
      debugger

      compilation.assets['copyright.txt'] = {
        ...
      }

      ...
    })
  }
}

module.exports = CopyrightWebpackPlugin

运行 debug 命令

npm run debug
从零开始学 Webpack 4.0(九)完

打开谷歌浏览器控制台,会多了个图标。

从零开始学 Webpack 4.0(九)完

点击该图标会打开一个调试面板,再点击右上角按钮跳过第一个断点去到我们在 copyright-webpack-plugin.js 里打的断点,鼠标移动到 compilation 即可看到包含内容。

从零开始学 Webpack 4.0(九)完 从零开始学 Webpack 4.0(九)完

如果觉得不够方便可以在右侧上方的 watch 增加对变量 compilation 的监控,查看到里面具体内容。

从零开始学 Webpack 4.0(九)完

这样,就可以通过 node 的调试工具去帮助我们看到打包的 Plugin 里面用到的一些变量是什么样的形式。

实际上,我们在编写 Webpack 的 Plugin 就是基于调试工具去进行编写的。

四、官方文档

学习完以上内容,可以前往 Webpack 4.0 官网 阅读相关中文文档。

建议阅读的内容为(内容比较多,感兴趣的小伙伴可以看看):

  • (1)文档 > API,页面左侧的 loader API。
  • (2)文档 > API,页面左侧的 PLUGINS。

英语比较好的小伙伴可以直接阅读 Webpack 4.0 官网 的英文文档。

五、总结

通过以上的学习,我们简单了解了 Loader 以及 Plugin 的实现机制,并知道了如何结合 node 调试工具去进行 debugger。

本篇文章是本系列的最后一篇,课程最后还涉及一些内容以文章的形式不是很好总结,所以就更新到这了。

Webpack 中涉及的配置项非常多,通过记忆把这门课的所有内容掌握是很困难的。

实际上,我们只需要把 Webpack 一些基础的知识点记住就可以了,例如 Loader 是什么,Plugin 是什么,entry 是什么,这些内容怎么配置等等。像官网 文档 > 概念 下的内容都要掌握,这些都是核心概念,通过掌握这些内容把 Webpack 的脉络捋清,知道哪块的作用是什么,当遇到不清楚的 Webpack 配置项再去官网进行查阅即可。

  • 想解决某方面问题时,例如代码分割之类的,可以到官网 文档 > 指南 下去找。

  • 像一些 devServer 的配置项,看到别人配不知道是什么作用时,可以到官网 文档 > 配置 下去找。

  • 如果想自己编写 Loader 或 Plugin,可以查阅官网 文档 > API 下相关内容。

  • 当在使用 Loader 解决一些问题,不知道这个 Loader 的具体作用时,可以在官网 文档 > LOADER 下查找,找不到的话去 GitHub 进行搜索查看相关文档。Plugin 同理,去 Webpack 官网以及 GitHub 搜索即可。

Webpack 的配置过程实际上不是记忆的过程,而是查阅文档的过程。

经过本系列文章的学习,我们对 Webpack 4.0 的配置有了一定的了解,接下来就是进行 Webpack 5.0 的学习并尝试进行一些项目的打包配置,在实战中去积累经验。

本文最后的代码我会上传到 码云(gitee.com/phao97)上,项目文件夹为 demo09。

如果觉得本篇文章对你有帮助,不妨点个赞或者给相关的 Git 仓库一个 Star,你的鼓励是我持续更新的动力!

转载自:https://juejin.cn/post/7218023140034527291
评论
请登录