深入浅出webpack -- loader和plugin原理及区别

一、loader原理

1、概念

loader就像一个翻译员,能将源文件翻译后输出新的结果,并且一个文件可以链式的经过几个翻译员。

以.scss文件为例子:

  • 先将.scss文件内容交给sass-loader翻译为css
  • 在将翻译后的css交给css-loader,找出css中依赖的资源,压缩css
  • 再将css-loader输出的内容交给style-loader,转化为通过脚本加载的JavaScript代码
const path = require('path');
var HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
// console.log(path.resolve('webpack.config.js'))
module.exports = {
    mode:'development',
    entry: './app.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js'
    },
    module: {
        rules: [
         ...
          {
              test: /\.scss$/,
              use: [
                
                  {
                    loader: 'style-loader'
                  },
                  {
                    loader: 'css-loader'
                  },
                  {
                    loader: 'style-loader'
                  }
              ]
              
          }
        ]
    },
  
}

webpack是运行在Node.js上面的,一个Loader其实就是一个模块,需要导出一个函数。

2、自己来实现一个Loader:

a、简单实现

新建一个test.wy文件,内容如下

c(89)

根目录下建一个文件夹  

index.js的内容

module.exports = function(source) {
    return source.replace('c', 'console.log');
}

webpack.common.js中增加配置

     {
          test: /\.wy$/,
          loader: './wy-loader'
        }

index.js中引入  import './test.wy'


npm run build 

内容会被转化为: 

给loader设置属性:

{
   test: /\.wy$/,
    loader: './wy-loader',
    options: {
       name: '麦乐'
     }
}

wy-loader/index.js

b、使用一个插件,获取配置的属性:

const loaderUtils = require('loader-utils')
module.exports = function(source) {
  const options = loaderUtils.getOptions(this)
  console.log(source, options)
  return source;
}

 上面的loader只是返回了原内容转换后的内容,在某些情况下还需要返回其它的内容。

以babel-loader转换es6为例子,需要输出转化后的es5和代码对应的Source Map,这种情况需要这样写:

module.exports = function(source) {
  this.callback(null, source, sourceMaps)
  return;
}

这样就告诉webpack内容在callback中不在返回值中。 

c、异步loader

如果处理结果是异步拿到的,可以这样来写loader:

module.exports = function(source) {
  var callback = this.async()
  someAsyncOperation(source, function(err, result, sourceMaps, ast) {
    callback(err, result, sourceMaps, ast)
  })  
}

以二进制的格式输入给 loader 

webpack传递给Loader的数据格式是utf-8的字符串,有的时候需要处理二进制文件,例如file-loader, 这是就需要webpack为Loader传入二进制的数据。

const loaderUtils = require('loader-utils')
module.exports = function(source) {
  console.log(source)
  return source;
}
module.exports.raw = true // 将source变成buffer类型

d、缓存加速

在某些情况下有写转换非常耗时,如果每次构建都执行重复的操作,构建会变得很缓慢,webpack会缓存所有Loader处理的结果,在需要处理的文件和其依赖的文件 没有发生变化时,不会重新调用Loader去执行转换。如果让webpack不缓存处理的结果,可以这样:

module.exports = function(source) {
  // 关闭缓存功能
  this.cacheable(false)
  return source;
}

e、加载本地loader

自己开发好的Loader,需要测试下能不能正常运行,配置到webpack中,才可以正确的使用Loader。例如上面的style-loader, 引入的时候是访问的node_modules中的。这样就需要把编写好的loader发布到npm才可以正常测试,这就会很麻烦。解决问题的办法有两种:

  • npm link
  • ResolveLoader

假如本地的 Loader 在项目目录中的 ./wy-loader中,则需要如下配置:

module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules','./wy-loader'],
  }
}

f、其它 Loader API

除了以上提到的在 Loader 中能调用的 Webpack API 外,还存在以下常用 API:

  • this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src

  • this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1

  • this.resourcePath:当前处理文件的路径,例如 /src/main.js

  • this.resourceQuery:当前处理文件的 querystring。

  • this.target:等于 Webpack 配置中的 Target

  • this.loadModule:当 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。

  • this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))

  • this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)

  • this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)

  • this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()

  • this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {...})

其它没有提到的 API 可以去 Webpack 官网 查看。

二、plugin原理

1、概念

在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

2、自己实现plugin

Webpack 通过 Plugin 机制让其更加灵活,以适应各种应用场景。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

一个简单的plugin

class BasicPlugin{
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options){
  }

  // Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
  apply(compiler){
    compiler.plugin('compilation',function(compilation) {
    })
  }
}

// 导出 Plugin
module.exports = BasicPlugin;

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下: 

/**
* 广播出事件
* event-name 为事件名称,注意不要和现有的事件重名
* params 为附带的参数
*/
compiler.apply('event-name',params);

/**
* 监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
* 同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

使用

const BasicPlugin = require('./BasicPlugin.js');
module.export = {
  plugins:[
    new BasicPlugin(options),
  ]
}

 Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

插件可以用来修改输出文件、增加输出文件、甚至可以提升 Webpack 性能、等等,总之插件通过调用 Webpack 提供的 API 能完成很多事情。

a、读取输出资源、代码块、模块及其依赖

在根目录下建一个plugin文件夹,创建一个index.js文件:

class Plugin {
  apply(compiler) {
    compiler.plugin('emit', function (compilation, callback) {
      console.log(compilation, 'compilation')
      // compilation.fileDependencies 存放所有依赖的文件路径,是一个数组
      compilation.fileDependencies.forEach(function (filepath) {
        console.log(filepath, 'filepath')
      });
      // compilation.assets 存放当前所有即将输出的资源
      // 调用一个输出资源的 source() 方法能获取到输出资源的内容
      
      // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
      // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
      // 该 Chunk 就会生成 .js 和 .css 两个文件

      // let source = compilation.assets[filename].source();

      console.log(compilation.chunks, 'chunks')
    
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        console.log(chunk)
      })
      // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
      callback();
    })
  }
}
module.exports = Plugin;

如果有多个入口文件, 就有多个chunk。

在webpack中:

const Plugin = require('./plugin/index')
    
plugins: [
  new Plugin(),
  ...
],

b、监听文件变化

在开发插件时经常需要知道是哪个文件发生变化导致了新的 Compilation,为此可以使用如下代码:

 compiler.plugin('watch-run', (compiler, callback) => {
      // console.log(compiler, 'compiler')
      // 获取发生变化的文件列表
      const changedFiles = compiler.watchFileSystem.watcher.mtimes;
      console.log(changedFiles, 'changedFiles') // { '/Users/artadmire/Downloads/test-热更新/app.js': 1602574380455 }
      callback();
    });

修改app.js文件,保存就会看到打印:

、、

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码:

compiler.plugin('after-compile', (compilation, callback) => {
  // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
console.log(compilation.fileDependencies)
    // compilation.fileDependencies.push(filePath);
    callback();
});

原来配置文件中对index.html做了处理,这里需要把这段代码注释掉,再启动服务器;

    plugins: [
      new Plugin(),
        // 处理html文件 打包压缩
        // new HtmlWebpackPlugin({
        //   filename:'index.html',
        //   template : './index.html',
        //   inject: true, // 是否自动引入 默认true true/false
        //   minify:{ // 压缩
        //     removeComments:true,   //删除注释
        //     collapseWhitespace: true      //删除空格,压缩
        //   },
        // }),

        new webpack.HotModuleReplacementPlugin(),
      ],

打印结果中是没有index.html文件的。 

 

添加html,compilation.fileDependencies是set结构,需要用add来添加数据:

 compiler.plugin('after-compile', (compilation, callback) => {
      // 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
      console.log(compilation.fileDependencies)
      compilation.fileDependencies.add(path.resolve('index.html'));
      console.log(compilation.fileDependencies)
      callback();
    });

再看打印结构index.html已经再依赖文件列表中了: 

c、修改输出资源

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

    compiler.plugin('emit', function (compilation, callback) {
      // console.log(compilation, 'compilation')
      // compilation.fileDependencies 存放所有依赖的文件路径,是一个数组
      compilation.fileDependencies.forEach(function (filepath) {
        // console.log(filepath, 'filepath')
      });
      // compilation.assets 存放当前所有即将输出的资源
      // 调用一个输出资源的 source() 方法能获取到输出资源的内容
      
      // Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
      // 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
      // 该 Chunk 就会生成 .js 和 .css 两个文件

      console.log(compilation.assets, 'compilation.assets')
      // let source = compilation.assets[filename].source();
      // console.log(compilation.chunks, 'chunks')
    
      // compilation.chunks 存放所有代码块,是一个数组
      compilation.chunks.forEach(function (chunk) {
        // console.log(chunk)
      })
   
      
      // 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
      // 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
      callback();
    })

下面compilation.assets的打印结果,这里是输出文件的相关内容:

 

读取文件内容和大小: 

    const filename = 'a.bundle.js'
      // 文件的内容 字符串
      let source = compilation.assets[filename].source();
      // 文件的大小
      let size = compilation.assets[filename].size();
      console.log(size, 'size') // 465628 size
    

设置文件内容和大小:

    compilation.assets[fileName] = {
        // 返回文件内容
        source: () => {
          // fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
          return "fileContent";
          },
        // 返回文件大小
          size: () => {
          return Buffer.byteLength("fileContent", 'utf8');
        }
      };

 

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值