详解webpack loader和plugin编写


Posted in Javascript onOctober 12, 2018

1 基础回顾

首先我们先回顾一下webpack常见配置,因为后面会用到,所以简单介绍一下。

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,需要配置这两个属性。

1.2 打包原理

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

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

2 loader

OK今天第一个主角登场

2.1 什么是loader?

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

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

2.2 手写一个loader

需求:

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

例如:abcdefg转换后为Gfedcba

OK,我们开始

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

为什么要创建两个laoder?理由后面会介绍

详解webpack loader和plugin编写

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文件

详解webpack loader和plugin编写

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

详解webpack loader和plugin编写

这样我们的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。

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

详解webpack loader和plugin编写

所以需求来了:

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

因为所有页面的样式都会引用通用样式vender.wxss

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

OK 开始!

1)创建插件文件 CssPathTransfor.js

详解webpack loader和plugin编写

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(),
 ]
})

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

详解webpack loader和plugin编写

搞定~

如果有更多的需求可以参考《如何写一个插件》

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js动态加载以及确定加载完成的代码
Jul 31 Javascript
js关闭浏览器窗口及检查浏览器关闭事件
Sep 03 Javascript
js获取当前路径的简单示例代码
Jan 08 Javascript
javacript使用break内层跳出外层循环分析
Jan 12 Javascript
javascript瀑布流式图片懒加载实例
Jun 28 Javascript
js生成随机数(指定范围)的实例代码
Jul 10 Javascript
JavaScript常用代码书写规范的超全面总结
Sep 11 Javascript
Node.js 数据加密传输浅析
Nov 16 Javascript
微信小程序 slider 详解及实例代码
Jan 10 Javascript
JS判断指定dom元素是否在屏幕内的方法实例
Jan 23 Javascript
Vue计算属性的学习笔记
Mar 22 Javascript
微信小程序实现tab左右切换效果
Nov 15 Javascript
深入理解Angularjs 脏值检测
Oct 12 #Javascript
vue中render函数的使用详解
Oct 12 #Javascript
详解Vue的常用指令v-if, v-for, v-show,v-else, v-bind, v-on
Oct 12 #Javascript
Vue插值、表达式、分隔符、指令知识小结
Oct 12 #Javascript
Vue中 v-if 和v-else-if页面加载出现闪现的问题及解决方法
Oct 12 #Javascript
vue使用v-if v-show页面闪烁,div闪现的解决方法
Oct 12 #Javascript
开发用到的js封装方法(20种)
Oct 12 #Javascript
You might like
php中常用的预定义变量小结
2012/05/09 PHP
php中 $$str 中 "$$" 的详解
2015/07/06 PHP
php多进程并发编程防止出现僵尸进程的方法分析
2020/02/28 PHP
JavaScript 学习笔记(十六) js事件
2010/02/01 Javascript
写给想学习Javascript的朋友一点学习经验小结
2010/11/23 Javascript
JavaScript加强之自定义callback示例
2013/09/21 Javascript
如何判断元素是否为HTMLElement元素
2013/12/06 Javascript
分享十五款 jQuery 社交网络分享插件
2015/05/16 Javascript
Javascript 实现简单计算器实例代码
2016/10/23 Javascript
JS实现重新加载当前页面或者父页面的几种方法
2016/11/30 Javascript
JavaScript用JSONP跨域请求数据实例详解
2017/01/06 Javascript
vue-resource拦截器设置头信息的实例
2017/10/27 Javascript
vue 导航锚点_点击平滑滚动,导航栏对应变化详解
2020/08/10 Javascript
Python编程中对super函数的正确理解和用法解析
2016/07/02 Python
python 中random模块的常用方法总结
2017/07/08 Python
Python实现的人工神经网络算法示例【基于反向传播算法】
2017/11/11 Python
Python实现OpenCV的安装与使用示例
2018/03/30 Python
Python tkinter label 更新方法
2018/10/11 Python
pycharm打开命令行或Terminal的方法
2019/01/16 Python
python如何获取当前文件夹下所有文件名详解
2019/01/25 Python
Python datetime和unix时间戳之间相互转换的讲解
2019/04/01 Python
关于python3中setup.py小概念解析
2019/08/22 Python
一文轻松掌握python语言命名规范规则
2020/06/18 Python
python实现简单的tcp 文件下载
2020/09/16 Python
python Gabor滤波器讲解
2020/10/26 Python
详解Python Celery和RabbitMQ实战教程
2021/01/20 Python
机械设计及其自动化求职推荐信
2014/02/17 职场文书
体育课课后反思
2014/04/24 职场文书
贯彻落实“八项规定”思想汇报
2014/09/13 职场文书
授权委托书范本(单位)
2014/09/28 职场文书
2015年教师节贺卡寄语
2015/03/24 职场文书
介绍信怎么写
2015/05/05 职场文书
海上钢琴师观后感
2015/06/03 职场文书
获奖感言一句话
2015/07/31 职场文书
2016年度先进班组事迹材料
2016/03/01 职场文书
详解Vue中$props、$attrs和$listeners的使用方法
2022/02/18 Vue.js