1. React脚手架解析

https://www.bilibili.com/video/BV1cv411C74F?p=2

2. loader

loader本质是一个函数,其接收三个参数,第一参数content是入口文件的代码内容

2.1 配置loader解析规则

旧:

  1. const { resolve } = require('path')
  2. module.exports = {
  3. module: {
  4. rules: [
  5. {
  6. test: /\.js$/,
  7. // 在这里写很长一串
  8. loader: resolve(__dirname, 'loaders', 'loader1')
  9. }
  10. ]
  11. },
  12. mode:"development"
  13. }

新:

  1. const { resolve } = require('path')
  2. module.exports = {
  3. module: {
  4. rules: [
  5. {
  6. test: /\.js$/,
  7. // 在这里写loader文件名,后续配置loader解析规则
  8. loader: 'loader1'
  9. }
  10. ]
  11. },
  12. // ********配置loader解析规则********
  13. resolveLoader: {
  14. // 先去node_modules文件中找loader,找不到再去自定义文件夹中
  15. modules: [
  16. 'node_modules',
  17. resolve(__dirname, 'loaders')
  18. ]
  19. },
  20. // *********************************
  21. mode: "development"
  22. }

2.2 loader执行顺序

自定义loader

  1. //loader3.js
  2. module.exports = function (content,map,meta){
  3. console.log(333);
  4. return content
  5. }
  6. module.exports.pitch = () => {
  7. console.log("pitch 333");
  8. }
  9. //loader2.js
  10. module.exports = function (content,map,meta){
  11. console.log(222);
  12. return content
  13. }
  14. module.exports.pitch = () => {
  15. console.log("pitch 222");
  16. }
  17. //loader1.js
  18. module.exports = function (content,map,meta){
  19. console.log(111);
  20. return content
  21. }
  22. module.exports.pitch = () => {
  23. console.log("pitch 111");
  24. }

webpack.config.js

  1. const { resolve } = require('path')
  2. module.exports = {
  3. module: {
  4. rules: [
  5. {
  6. test: /\.js$/,
  7. // 将多个loader放入use数组里面
  8. use:[
  9. 'loader3',
  10. 'loader2',
  11. 'loader1'
  12. ]
  13. }
  14. ]
  15. },
  16. resolveLoader: {
  17. modules: [
  18. 'node_modules',
  19. resolve(__dirname, 'loaders')
  20. ]
  21. },
  22. mode: "development"
  23. }

loader执行结果:
会先按照use数组从前到后解析loader并依次调用了loader的pitch方法
然后真正从后到前执行loader

  1. pitch 333
  2. pitch 222
  3. pitch 111
  4. 111
  5. 222
  6. 333

image.png

2.3 同步和异步loader

同步loader:

  1. // 同步写法1:
  2. module.exports = function (content,map,meta){
  3. console.log(111);
  4. return content
  5. }
  6. // 同步写法2:
  7. module.exports = function (content,map,meta){
  8. console.log(111);
  9. this.callback(null,content,map,meta)
  10. }
  11. module.exports.pitch = () => {
  12. console.log("pitch 111");
  13. }

异步loader:

  1. module.exports = function (content,map,meta){
  2. console.log(222);
  3. const callback = this.async()
  4. setTimeout(() => {
  5. callback(null,content)
  6. }, 1000);
  7. }
  8. module.exports.pitch = () => {
  9. console.log("pitch 222");
  10. }

2.4 获取和校验loader的options

webpack.config.js

  1. //.....
  2. use:[
  3. {
  4. loader:'loader3',
  5. options:{
  6. name:"hxy"
  7. }
  8. }
  9. ]
  10. //.....

loader3.js

  1. // 引入工具,来获取loader的options
  2. const { getOptions } = require('loader-utils')
  3. // 引入工具,来校验options是否合法
  4. const { validate } = require('schema-utils')
  5. // 引入校验规则
  6. const schema = require('./schema.json')
  7. module.exports = function (content, map, meta) {
  8. // 获取loader的options
  9. const options = getOptions(this)
  10. console.log(options);
  11. // 校验options是否合法,第一个参数是校验规则,第二个是被校验loader的options
  12. // 校验失败会报错
  13. validate(schema, options, {
  14. name: 'loader3'
  15. })
  16. return content
  17. }
  18. module.exports.pitch = () => {
  19. console.log("pitch 333");
  20. }

schema.json 配置校验规则

  1. {
  2. //options的类型
  3. "type": "object",
  4. // 配置options下的属性
  5. "properties": {
  6. // 定义一个options中的name属性
  7. "name": {
  8. // 属性name的类型
  9. "type": "string",
  10. "description": "名称~"
  11. }
  12. },
  13. // options中追加额外的属性
  14. "additionalProperties": true
  15. }

2.5 自定义babel-loader

webpack.config.js

  1. //...
  2. module: {
  3. rules: [
  4. {
  5. test: /\.js$/,
  6. loader:'babelLoader',
  7. options:{
  8. presets:[
  9. '@babel/preset-env'
  10. ]
  11. }
  12. }
  13. ]
  14. }
  15. //...

babelLoader.js

  1. const { getOptions } = require('loader-utils')
  2. const { validate } = require('schema-utils')
  3. // 引入babel核心库
  4. const babel = require('@babel/core')
  5. // 使用工具util
  6. const util = require('util')
  7. // 引入babelLoader的options的校验规则
  8. const babelSchema = require("./babelSchema.json")
  9. // babel.transform是用来编译代码的方法,它是一个普通异步方法
  10. // util.promisify将普通异步方法转化成基于promise的异步方法
  11. const transform = util.promisify(babel.transform)
  12. module.exports = function (content, map, meta) {
  13. // 获取babelLoader的options配置
  14. const options = getOptions(this)
  15. // 校验babelLoader的options的配置
  16. validate(babelSchema, options, {
  17. name: "babelLoader"
  18. })
  19. // 创建异步
  20. const callback = this.async()
  21. // 使用loader
  22. transform(content, options)
  23. .then(({ code, map }) => {
  24. callback(null, code, map, meta)
  25. })
  26. .catch((e) => callback(e))
  27. }

babelSchema.json

  1. {
  2. "type": "object",
  3. "properties": {
  4. "presets": {
  5. "type": "array"
  6. }
  7. },
  8. "additionalProperties": true
  9. }

3. plugin

3.1 tapable的使用

tapable 这个小型 library 是 webpack 的一个核心工具,但也可用于其他地方,以提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable')

3.1.1 同步hooks

同步hook依次执行

  1. SyncHook ```javascript const { SyncHook } = require(‘tapable’)

class Lesson { constructor() { // 初始化hooks容器 this.hooks = { go: new SyncHook([‘address’]) } }

tap() { // 往hooks容器中注册事件/添加回调函数 // 被触发时会依次调用下面被注册的回调函数 this.hooks.go.tap(‘class0318’, (address) => { console.log(‘class0318’, address); }) this.hooks.go.tap(‘class0410’, (address) => { console.log(‘class0410’, address); }) }

start() { // 触发hooks this.hooks.go.call(‘c318’) } }

const l = new Lesson(); l.tap() l.start() / 输出: class0318 c318 class0410 c318 /

  1. 2. SyncBailHook
  2. ```javascript
  3. const { SyncBailHook } = require('tapable')
  4. class Lesson {
  5. constructor() {
  6. // 初始化hooks容器
  7. this.hooks = {
  8. go: new SyncBailHook(['address'])
  9. }
  10. }
  11. tap() {
  12. // 往hooks容器中注册事件/添加回调函数
  13. this.hooks.go.tap('class0318', (address) => {
  14. console.log('class0318', address);
  15. // 当使用SyncBailHook时,如果回调函数中有返回值,就不会再执行后面的回调函数
  16. return 111
  17. })
  18. this.hooks.go.tap('class0410', (address) => {
  19. console.log('class0410', address);
  20. })
  21. }
  22. start() {
  23. // 触发hooks
  24. this.hooks.go.call('c318')
  25. }
  26. }
  27. const l = new Lesson();
  28. l.tap()
  29. l.start()
  30. /*
  31. 输出:
  32. class0318 c318
  33. */

3.1.2 异步hooks

  1. const { AsyncParallelHook } = require('tapable')
  2. class Lesson {
  3. constructor() {
  4. // 初始化hooks容器
  5. this.hooks = {
  6. // 异步hooks
  7. // AsyncParallelHook:异步并行
  8. leave: new AsyncParallelHook(['name', 'age']),
  9. }
  10. }
  11. tap() {
  12. // 往hooks容器中注册事件/添加回调函数
  13. this.hooks.leave.tapAsync('class0510', (name, age, cb) => {
  14. setTimeout(() => {
  15. console.log('class0510', name, age);
  16. cb()
  17. }, 2000);
  18. })
  19. this.hooks.leave.tapPromise('class0510', (name, age, cb) => {
  20. // 返回一个promise对象
  21. return new Promise((resolve) => {
  22. setTimeout(() => {
  23. console.log('class0610', name, age);
  24. resolve()
  25. }, 1000);
  26. })
  27. })
  28. }
  29. start() {
  30. // 触发hooks
  31. this.hooks.leave.callAsync('hxy', 19, function () {
  32. // 代表所有leave容器中的函数触发完了,才触发
  33. console.log("end~~~")
  34. })
  35. }
  36. }
  37. const l = new Lesson();
  38. l.tap()
  39. l.start()
  40. /*
  41. 输出:
  42. 第一秒输出:class0610 hxy 19
  43. 第二秒输出:class0510 hxy 19
  44. end~~~
  45. */
  1. const { AsyncSeriesHook } = require('tapable')
  2. class Lesson {
  3. constructor() {
  4. // 初始化hooks容器
  5. this.hooks = {
  6. // 异步hooks
  7. // AsyncSeriesHook:异步串行
  8. leave: new AsyncSeriesHook(['name', 'age'])
  9. }
  10. }
  11. tap() {
  12. // 往hooks容器中注册事件/添加回调函数
  13. this.hooks.leave.tapAsync('class0510', (name, age, cb) => {
  14. setTimeout(() => {
  15. console.log('class0510', name, age);
  16. cb()
  17. }, 2000);
  18. })
  19. this.hooks.leave.tapPromise('class0510', (name, age, cb) => {
  20. // 返回一个promise对象
  21. return new Promise((resolve) => {
  22. setTimeout(() => {
  23. console.log('class0610', name, age);
  24. resolve()
  25. }, 1000);
  26. })
  27. })
  28. }
  29. start() {
  30. // 触发hooks
  31. this.hooks.leave.callAsync('hxy', 19, function () {
  32. // 代表所有leave容器中的函数触发完了,才触发
  33. console.log("end~~~")
  34. })
  35. }
  36. }
  37. const l = new Lesson();
  38. l.tap()
  39. l.start()
  40. /*
  41. 输出:
  42. 第二秒输出:class0510 hxy 19
  43. end~~~
  44. 第三秒输出:class0610 hxy 19
  45. */

3.2 compiler的hooks的使用

compiler钩子
plugins/Plugin1.js

  1. class Plugin1 {
  2. apply(compiler) {
  3. compiler.hooks.emit.tap('Plugin1', (compilation) => {
  4. console.log('emit.tap 触发了')
  5. })
  6. compiler.hooks.emit.tapAsync('Plugin1', (compilation, cb) => {
  7. setTimeout(() => {
  8. console.log('emit.tapAsync 触发了')
  9. cb()
  10. }, 1000);
  11. })
  12. compiler.hooks.emit.tapPromise('Plugin1', (compilation) => {
  13. return new Promise((resolve) => {
  14. setTimeout(() => {
  15. console.log('emit.tapPromise 触发了')
  16. resolve()
  17. }, 1000);
  18. })
  19. })
  20. compiler.hooks.afterEmit.tap('Plugin1', (compilation) => {
  21. console.log('afterEmit.tap 触发了')
  22. })
  23. compiler.hooks.done.tap('Plugin1', (stats) => {
  24. console.log('done.tap 触发了')
  25. })
  26. }
  27. }
  28. module.exports = Plugin1

webpack.config.js

  1. const Plugin1 = require('./plugins/Plugin1')
  2. module.exports = {
  3. plugins:[
  4. new Plugin1()
  5. ]
  6. }

3.3 compilation的使用

compilation钩子

const fs = require('fs')
const { resolve } = require('path')
const webpack = require('webpack')
// 引入webpack里面的资源格式化工具,来格式化资源以便于添加到dist目录中
const { RawSource } = webpack.sources

class Plugin2 {
  apply(compiler) {
    // 初始化compilation钩子,thisCompilation周期是最早拿到compilation的地方
    compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
      // debugger
      // console.log(compilation);
      // 添加资源
      compilation.hooks.additionalAssets.tapAsync('Plugin2', (cb) => {
        // 构造资源添加到dist目录
        const content = 'hello plugin2'
           // 往要输出的资源中添加一个a.txt文件
        compilation.assets['a.txt'] = new RawSource(content)

        // 也可以将已有的文件资源添加到dist目录中
        const data = fs.readFileSync(resolve(__dirname, 'b.txt'))
        compilation.assets['b.txt'] = new RawSource(data)
        /* 
          或者这种写法:
          compilation.emitAsset("b.txt",new RawSource(data))
        */

        cb()
      })
    })
  }
}
module.exports = Plugin2

3.4 实战—自定义CopyWebpackPlugin

3.4.1 plugin代码编写

CopyWebpackPlugin.js 将from中的资源复制到to中,输出出去

const path = require('path')
// 获取校验options的方法
const { validate } = require("schema-utils")
// 获取校验options的标准
const schema = require("./schema.json")
// 用于匹配某一个文件夹下的文件,根据规则忽略一些文件
const globby = require('globby')
// 获取资源格式化的方法
const webpack = require("webpack")
const { RawSource } = webpack.sources

const { readFileSync } = require('fs')

class CopyWebpackPlugin {
  constructor(options = {}) {
    // 验证options是否符合规范

    validate(schema, options, {
      name: "CopyWebpackPlugin"
    })

    this.options = options
  }
  apply(compiler) {
    // 初始化compilation
    compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', async (compilation) => {
      // 添加资源的hooks
      compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
        // 我们下面代码要做的是将from中的资源复制到to中,输出出去

        // 获取options里面的配置
        const { from, to = '.', ignore } = this.options

        // 0. 得到from的绝对路径,因为globby最好用绝对路径处理
        // context就是webpack配置
        // 运行node指令的目录
        const context = compiler.options.context // process.cwd():运行时的路径
        // 将输入路径变成绝对路径
        const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from)
        // 1. 从from下面获取所有文件的绝对路径并且过滤掉ignore的文件
        // globby(要处理的文件夹路径,options),返回值为promise对象
        let paths = await globby(absoluteFrom, { ignore })

        console.log(paths); // 所有要加载的文件路径数组['D:/code/hxy/webpack5/高阶进阶/plugin/public/reset.css']

        // 2. 读取paths中所有资源
        const files = paths.map((absolutepath) => {
          // 读取文件
          const data = readFileSync(absolutepath)
          // 生成该文件的文件名(basename方法得到路径的最后一个文件名称)
          const relativePath = path.basename(absolutepath)
          /* 
            和to属性结合
            没有to --> reset.css
            有to --> css/reset.css
          */
          const filename = path.join(to,relativePath)

          return {
            // 文件数据
            data,
            // 文件名称
            filename
          }
        })

        // 3. 生成webpack格式的资源
        const assets = files.map((file) => {
          const source = new RawSource(file.data)
          const filename = file.filename
          return {
            source,
            filename
          }
        })

        // 4. 添加compilation中,输出出去
        assets.forEach((asset) => {
          compilation.emitAsset(asset.filename, asset.source)
        })

        // 最后调用callback函数结束
        cb()
      })
    })
  }
}
module.exports = CopyWebpackPlugin

webpack.config.js 应用插件

// 引入自定义plugin
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')

module.exports = {
  plugins:[
    new CopyWebpackPlugin({
      // 从哪里复制
      from:'public',
      // 输出到哪里去,不填默认为'.'
      to:'css',
      // 忽略文件
      ignore:['**/index.html'],
    }),
  ]
}

schema.json options的校验规则

{
  "type":"object",
  "properties":{
    "from":{
      "type":"string"
    },
    "to":{
      "type":"string"
    },
    "ignore":{
      "type":"array"
    }
  },
  "additionalProperties":false
}

public文件夹
image.png

3.4.2 plugin使用结果

设置了to属性
image.png
未设置to属性
image.png

5. references

webpack-API: https://www.webpackjs.com/api/