xiaochengzi6 / Blog

个人博客
GNU Lesser General Public License v2.1
0 stars 0 forks source link

Webpack 的 Plugins 机制和 Loader 机制 #45

Open xiaochengzi6 opened 2 years ago

xiaochengzi6 commented 2 years ago

(一)Plugins

一个最基础的 插件可能是这样的形式

class MyPlugins {
    constructor() {}
    apply(compiler){
        ....
    }
}

webpack 在运行的生命周期中会广播很多事件 插件就会监听这些事件 在恰当的时机调用回调函数

class MaPlugins {

  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('这是一个示例插件!');
        console.log(
          '这里表示了资源的单次构建的 `compilation` 对象:',
          compilation
        );
        })
  }
}

exports.MaPlugins = MaPlugins

在使用的时候可以像 webapck 那样配置

const {MaPlugins} = require('./src/MyPlugins')
const path = require('path');
module.exports = {
    mode: "production",
    entry: './src/index.js',
    output: {
        filename: 'vue.js',
        path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
        new MaPlugins()
    ]
};

这里定义了一个 apply方法 接受一个 compiler 的对象在 hook 上去监听了 emit 的事件 使用异步的方式 tapAsync

在编写 Plugins 的时候最重要的就是 compiler 和 compilertion 这两个对象

1、Compiler 对象包含 Webpack 环境的所有配置 是webpack 的主要引擎 用来注册和调用事件

2、Compilation 包含了当前模块资源、编译生成资源、变化的文件,当 webpack 运行时 每一次的文件的变动 都会创建一次 Compilation

这两者明显的区别的在于 Compiler 它在webapck 整个生命周期的过程都始终存在 后者 每一次的资源变动都会重新编译进而重新创建一个 compilation

这就是一次在 compiler 的 emit 钩子上绑定的 当emit 事件触发 回调函数调用,使用的是 同步的方式 接收两个参数 一个是插件名另一个是 回调函数

compiler.hook.emit.tap(name, ()=>{})

类似的 compilation.hook.optimize.tap(name, ()=>{})

(二)loader

webpack 处理的是 js 的文件资源 如果想要处理其他的 比如 css 或者 图片就要使用到 loader 它主要的作用就是将 webpack 不能处理的资源进而转换成能处理的

const path = require('path')
module.exports = {
    entry: './src/index.js',
    output: {
        filename: '[name].js'
        path: path.resolve(__dirname, 'dist')
    }
    modules: {
        relus: [
            {
                test: /\.css$/i
                use: ['style.loader', 'css.loader']
            },
            {/*...*/},
        ]
    }
}

loader 比较特殊 它支持链式调用 从右往左执行 loader 的链式调用 这样也决定了 只要不是最后一个调用的 loader 它就不需要返回 js 格式的数据 可以返回任意类型的数据。

可以简单的将 loader 看作是一个导出的函数

module.exports = function (source){
    ....
    return source
}

如果使用 一个 loader 来处理文件只能传入一个参数 一个用于包含资源文件内容的字符串 return 返回一个被处理过单一值 也可以使用 this.callback(err, values...) 返回任意数量值

loader 最终会 返回一个或 两个值 一个是js 代码的字符串 或者是 buffer 第二个是 scourceMap

// index.js
var a = require('./style.css') 
function a () {
  console.log(1)
}
a()

// loader.js
module.exports = function (source){
    console.log(source)
    return source
}

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    mode: "production",
    entry: './src/index.js',
    output: {
        filename: 'vue.js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader',{
                    loader:  path.resolve('./src/loader.js')
                }]
            }
        ]
    },
    plugins: [new HtmlWebpackPlugin({ template: './index.html' })]
};

// output
//*{
//  background-color: red;
//}

参考

1、webpack 官网

2、如何写 loader

xiaochengzi6 commented 1 year ago

loader

loader 是对非 js 文件的处理 一般可分为

  1. css 文件
  2. 资源文件(png, svg, jpg, jpeg, gif, ico)
  3. 字体文件(woff, woff2, eot, ttf, otf)
  4. js/ts (js)

针对 css 文件需要去查看是否使用到了 less 或 sass

module.exports = {
  module: {
    rules: [
      {
        test: /\.less$/i,
        use: [
          'style-loader',
          'css-loader',
          'less-loader',
        ],
      },
    ],
  },
};

loader 其实就是其中的 less-loadercss-loaderstyle-loader 使用 use 属性来使用, test属性来匹配文件

执行时机是后序遍历,类似于[].pop(),每一个 loader 处理完就会输出,下一个 loader 接收到后继续处理完输出

在一个 loader 在使用的时候,loader 函数只接受一个参数,一个包含内容的字符串

同步 loader 可以 return 一个代表已转换模块(transformed module)的单一值。在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么抛给(thrown in)同步 loader 。 loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个可选值是 SourceMap,它是个 JavaScript 对象。

在链式调用多个 loader 时

最后的 loader 最早调用,将会传入原始资源(raw resource)内容。 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。 中间的 loader 执行时,会传入前一个 loader 的结果。

loader 的形式大致就可以等同于这样

function loader_1 (source) {
  // ....
  return source  
}

loader 有两种执行方式 同步异步

在整个 loader 生命周期中分为两个执行阶段 picthnormal

loader 会先执行 pitch,然后获取资源再执行 normal loader。如果 pitch 有返回值时,就不会走之后的 loader,并将返回值返回给之前的 loader。这就是为什么 pitch 有 熔断 的作用!

function loader(source) {

}

loader.pitch = function (res) {
  // 先执行 pitch 阶段
}

// 获取资源后才会触发 normal 阶段

loader.normal = function (res) {
  // 再执行 normal 阶段
}

如果是链式调用就 先前序编列 以此触发 pitch 拿到资源后会后续遍历 依次触发 normal

  1. 同步方式

可以使用 return 或 this.callback() 两种形式返回结果

this.callback(error, content, sourceMap, meta)

  1. 第一个参数必须是 Error 或者 null
  2. 第二个参数是一个 string 或者 Buffer。
  3. 可选的:第三个参数必须是一个可以被 this module 解析的 source map。
  4. 可选的:第四个参数,会被 webpack 忽略,可以是任何东西(例如一些元数据)

如果希望在 loader 之间共享公共的 AST,可以将抽象语法树 AST(例如 ESTree)作为第四个参数(meta)传递,以加快构建时间。


function loader (source, map, meta) {

// 注意:当调用 callback() 时,始终返回 undefined this.callback(null, source, map, meta) return }

> 参考:https://webpack.docschina.org/api/loaders#thiscallback

2. 异步方式

告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。
> loader-runner 是 loader 调用函数
~~~js
function loader (source) {

    // this.async()  返回 this.callback() 函数
    const callback = this.async()

    setTimeout(() => {
        callback(null, source) 
    }, 1000)
}

module.exports = loader