详解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 相关文章推荐
javascript 写类方式之七
Jul 05 Javascript
IE中radio 或checkbox的checked属性初始状态下不能选中显示问题
Jul 25 Javascript
10个实用的脚本代码工具
May 04 Javascript
JavaScript控制各种浏览器全屏模式的方法、属性和事件介绍
Apr 03 Javascript
node.js中的socket.io入门实例
Apr 26 Javascript
JS实现向表格中动态添加行的方法
Mar 30 Javascript
JavaScript中var关键字的使用详解
Aug 14 Javascript
JS前向后瞻正则表达式定义与用法示例
Dec 27 Javascript
详解基于vue-cli优化的webpack配置
Nov 06 Javascript
JS中call和apply函数用法实例分析
Jun 20 Javascript
vue单页面实现当前页面刷新或跳转时提示保存
Nov 02 Javascript
JavaScript实现新年倒计时效果
Nov 17 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清除字符串中所有无用标签的方法
2014/12/01 PHP
php微信公众号开发之微信企业付款给个人
2018/10/04 PHP
用函数式编程技术编写优美的 JavaScript_ibm
2008/05/16 Javascript
JS 密码强度验证(兼容IE,火狐,谷歌)
2010/03/15 Javascript
JavaScript不刷新实现浏览器的前进后退功能
2014/11/05 Javascript
js定时器实现倒计时效果
2017/11/05 Javascript
JavaScript实现动态添加、移除元素或属性的方法分析
2019/01/03 Javascript
vuex 动态注册方法 registerModule的实现
2019/07/03 Javascript
vue router总结 $router和$route及router与 router与route区别
2019/07/05 Javascript
微信小程序实现页面分享onShareAppMessage
2019/08/12 Javascript
taro小程序添加骨架屏的实现代码
2019/11/15 Javascript
微信小程序返回上一级页面的实现代码
2020/06/19 Javascript
vue中实现拖动调整左右两侧div的宽度的示例代码
2020/07/22 Javascript
[01:10:24]DOTA2-DPC中国联赛 正赛 VG vs Aster BO3 第一场 2月28日
2021/03/11 DOTA
python3.3实现乘法表示例
2014/02/07 Python
python对字典进行排序实例
2014/09/25 Python
Python魔术方法详解
2015/02/14 Python
Django框架中数据的连锁查询和限制返回数据的方法
2015/07/17 Python
Python 绘图库 Matplotlib 入门教程
2018/04/19 Python
python日期时间转为字符串或者格式化输出的实例
2018/05/29 Python
Python 串口读写的实现方法
2019/06/12 Python
Python timer定时器两种常用方法解析
2020/01/20 Python
离线状态下在jupyter notebook中使用plotly实例
2020/04/24 Python
解决Python3.7.0 SSL低版本导致Pip无法使用问题
2020/09/03 Python
农田水利实习自我鉴定
2013/09/19 职场文书
物业电工岗位职责
2013/11/20 职场文书
电子商务专员岗位职责
2013/12/11 职场文书
经典禁毒标语
2014/06/16 职场文书
班委竞选稿范文
2015/11/21 职场文书
2016入党积极分子考察评语
2015/12/01 职场文书
基层医务人员三严三实心得体会
2016/01/05 职场文书
Go语言 go程释放操作(退出/销毁)
2021/04/30 Golang
Python 线程池模块之多线程操作代码
2021/05/20 Python
python中Pyqt5使用Qlabel标签播放视频
2022/04/22 Python
Centos7 Shell编程之正则表达式、文本处理工具详解
2022/08/05 Servers
OpenFeign实现远程调用
2022/08/14 Java/Android