webpack loader和plugin编写

1 基础回顾

首先咱们先回顾一下webpack常见配置,由于后面会用到,因此简单介绍一下。css

1.1 webpack常见配置

// 入口文件
  entry: {
    app: './src/js/index.js',
  },
  // 输出文件
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/'     //确保文件资源可以在 http://localhost:3000 下正确访问
  },
  // 开发者工具 source-map
  devtool: 'inline-source-map',
  // 建立开发者服务器
  devServer: {
    contentBase: './dist',
    hot: true                // 热更新
  },
  plugins: [
    // 删除dist目录
    new CleanWebpackPlugin(['dist']),
    // 从新穿件html文件
    new HtmlWebpackPlugin({
      title: 'Output Management'
    }),
    // 以便更容易查看要修补(patch)的依赖
    new webpack.NamedModulesPlugin(),
    // 热更新模块
    new webpack.HotModuleReplacementPlugin()
  ],
  // 环境
  mode: "development",
  // loader配置
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /\.(png|svg|jpg|gif)$/,
        use: [
          'file-loader'
        ]
      }
    ]
  }
复制代码

这里面咱们重点关注 module和plugins属性,由于今天的重点是编写loader和plugin,须要配置这两个属性。html

1.2 打包原理

  • 识别入口文件
  • 经过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack作的就是分析代码。转换代码,编译代码,输出代码
  • 最终造成打包后的代码

这些都是webpack的一些基础知识,对于理解webpack的工做机制颇有帮助。vue

2 loader

OK今天第一个主角登场node

2.1 什么是loader?

loader是文件加载器,可以加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一块儿打包到指定的文件中webpack

  • 处理一个文件可使用多个loader,loader的执行顺序是和自己的顺序是相反的,即最后一个loader最早执行,第一个loader最后执行。
  • 第一个执行的loader接收源文件内容做为参数,其余loader接收前一个执行的loader的返回值做为参数。最后执行的loader会返回此模块的JavaScript源码

2.2 手写一个loader

需求:es6

  1. 处理.txt文件
  2. 对字符串作反转操做
  3. 首字母大写

例如:abcdefg转换后为Gfedcbaweb

OK,咱们开始npm

1)首先建立两个loader(这里以本地loader为例)json

为何要建立两个laoder?理由后面会介绍小程序

image

reverse-loader.js

module.exports = function (src) {
  if (src) {
    console.log('--- reverse-loader input:', src)
    src = src.split('').reverse().join('')
    console.log('--- reverse-loader output:', src)
  }
  return src;
}
复制代码

uppercase-loader.js

module.exports = function (src) {
  if (src) {
    console.log('--- uppercase-loader input:', src)
    src = src.charAt(0).toUpperCase() + src.slice(1)
    console.log('--- uppercase-loader output:', src)
  }
  // 这里为何要这么写?由于直接返回转换后的字符串会报语法错误,
  // 这么写import后转换成可使用的字符串
  return `module.exports = '${src}'`
}
复制代码

看,loader结构是否是很简单,接收一个参数,而且return一个内容就ok了。

而后建立一个txt文件

image

2)mytest.txt

abcdefg
复制代码

3)如今开始配置webpack

module.exports = {
  entry: {
    index: './src/js/index.js'
  },
  plugins: [...],
  optimization: {...},
  output: {...},
  module: {
    rules: [
      ...,
      {
        test: /\.txt$/,
        use: [
          './loader/uppercase-loader.js',
          './loader/reverse-loader.js'
        ]
      }
    ]
  }
}
复制代码

这样就配置完成了

4)咱们在入口文件中导入这个脚本

为何这里须要导入呢,咱们不是配置了webapck处理全部的.txt文件么?

由于webpack会作过滤,若是不引用该文件的话,webpack是不会对该文件进行打包处理的,那么你的loader也不会执行

import _ from 'lodash';
import txt from '../txt/mytest.txt'
import '../css/style.css'
function component() {
  var element = document.createElement('div');
  var button = document.createElement('button');
  var br = document.createElement('br');

  button.innerHTML = 'Click me and look at the console!';
  element.innerHTML = _.join('【' + txt + '】');
  element.className = 'hello'
  element.appendChild(br);
  element.appendChild(button);

  // Note that because a network request is involved, some indication
  // of loading would need to be shown in a production-level site/app.
  button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
    var print = module.default;

    print();
  });

  return element;
}
document.body.appendChild(component());
复制代码

package.json配置

{
  ...,
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack-dev-server --open --config webpack.dev.js",
    "server": "node server.js"
  },
  ...
}

复制代码

而后执行命令

npm run build
复制代码

image

这样咱们的loader就写完了。

如今回答为何要写两个loader?

看到执行的顺序没,咱们的配置的是这样的

use: [
  './loader/uppercase-loader.js',
  './loader/reverse-loader.js'
]
复制代码

正如前文所说,处理一个文件可使用多个loader,loader的执行顺序是和自己的顺序是相反的

咱们也能够本身写loader解析自定义模板,像vue-loader是很是复杂的,它内部会写大量的对.vue文件的解析,而后会生成对应的html、js和css。

咱们这里只是讲述了一个最基础的用法,若是有更多的须要,能够查看 《loader官方文档》

3 plugin

3.1 什么是plugin?

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

plugin和loader的区别是什么?

对于loader,它就是一个转换器,将A文件进行编译造成B文件,这里操做的是文件,好比将A.scss或A.less转变为B.css,单纯的文件转换过程

plugin是一个扩展器,它丰富了wepack自己,针对是loader结束后,webpack打包的整个过程,它并不直接操做文件,而是基于事件机制工做,会监听webpack打包过程当中的某些节点,执行普遍的任务。

3.2 一个最简的插件

/plugins/MyPlugin.js(本地插件)

class MyPlugin {
  // 构造方法
  constructor (options) {
    console.log('MyPlugin constructor:', options)
  }
  // 应用函数
  apply (compiler) {
    // 绑定钩子事件
    compiler.plugin('compilation', compilation => {
      console.log('MyPlugin')
    ))
  }
}

module.exports = MyPlugin
复制代码

webpack配置

const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
  entry: {
    index: './src/js/index.js'
  },
  plugins: [
    ...,
    new MyPlugin({param: 'xxx'})
  ],
  ...
};
复制代码

这就是一个最简单的插件(虽然咱们什么都没干)

  • webpack 启动后,在读取配置的过程当中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 得到其实例。
  • 在初始化 compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  • 插件实例在获取到 compiler 对象后,就能够经过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。
  • 而且能够经过 compiler 对象去操做 webpack。

看到这里可能会问compiler是啥,compilation又是啥?

  • Compiler 对象包含了 Webpack 环境全部的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局惟一的,能够简单地把它理解为 Webpack 实例;

  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被建立。Compilation 对象也提供了不少事件回调供插件作扩展。经过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:

Compiler 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是表明了一次新的编译。

3.3 事件流

  • webpack 经过 Tapable 来组织这条复杂的生产线。
  • webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
  • webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 很是类似。

绑定事件

compiler.plugin('event-name', params => {
  ...	  
});
复制代码

触发事件

compiler.apply('event-name',params)
复制代码

3.4 须要注意的点

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,因此在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每一个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时须要调用回调函数通知 webpack,才会进入下一处理流程。例如:
compiler.plugin('emit',function(compilation, callback) {
  ...
    
  // 处理完毕后执行 callback 以通知 Webpack 
  // 若是不执行 callback,运行流程将会一直卡在这不往下执行 
  callback();
});
复制代码

关于complier和compilation,webpack定义了大量的钩子事件。开发者能够根据本身的须要在任何地方进行自定义处理。

《compiler钩子文档》

《compilation钩子文档》

3.5 手写一个plugin

场景:

小程序mpvue项目,经过webpack编译,生成子包(咱们做为分包引入到主程序中),而后考入主包当中。生成子包后,里面的公共静态资源wxss引用地址须要加入分包的前缀:/subPages/enjoy_given。

在未编写插件前,生成的资源是这样的,这个路径若是做为分包引入主包,是无法正常访问资源的。

image

因此需求来了:

修改dist/static/css/pages目录下,全部页面的样式文件(wxss文件)引入公共资源的路径。

由于全部页面的样式都会引用通用样式vender.wxss

那么就须要把@import "/static/css/vendor.wxss"; 改成:@import "/subPages/enjoy_given/static/css/vendor.wxss";
复制代码

OK 开始!

1)建立插件文件 CssPathTransfor.js

image

CssPathTransfor.js

class CssPathTransfor {
  apply (compiler) {
    compiler.plugin('emit', (compilation, callback) => {
      console.log('--CssPathTransfor emit')
      // 遍历全部资源文件
      for (var filePathName in compilation.assets) {
        // 查看对应的文件是否符合指定目录下的文件
        if (/static\/css\/pages/i.test(filePathName)) {
          // 引入路径正则
          const reg = /\/static\/css\/vendor\.wxss/i
          // 须要替换的最终字符串
          const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss'
          // 获取文件内容
          let content = compilation.assets[filePathName].source() || ''
          
          content = content.replace(reg, finalStr)
          // 重写指定输出模块内容
          compilation.assets[filePathName] = {
            source () {
              return content;
            },
            size () {
              return content.length;
            }
          }
        }
      }
      callback()
    })
  }
}
module.exports = CssPathTransfor
复制代码

看着挺多,实际就是遍历compilation.assets模块。对符合要求的文件进行正则替换。

2)修改webpack配置

var baseWebpackConfig = require('./webpack.base.conf')
var CssPathTransfor = require('../plugins/CssPathTransfor.js')

var webpackConfig = merge(baseWebpackConfig, {
  module: {...},
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {...},
  plugins: [
    ...,
    // 配置插件
    new CssPathTransfor(),
  ]
})
复制代码

插件编写完成后,执行编译命令

image

搞定~

若是有更多的需求能够参考《如何写一个插件》

相关文章
相关标签/搜索