详解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 select下拉框操作常用方法
Nov 09 Javascript
按钮JS复制文本框和表格的代码
Apr 01 Javascript
JavaScript根据数据生成百分比图和柱状图的实例代码
Jul 14 Javascript
JS实现的打字机效果完整实例
Jun 20 Javascript
javascript完美实现给定日期返回上月日期的方法
Jun 15 Javascript
基于react后端渲染模板引擎noox发布使用
Jan 11 Javascript
vue实现Excel文件的上传与下载功能的两种方式
Jun 28 Javascript
微信小程序获取用户信息及手机号(后端TP5.0)
Sep 12 Javascript
微信小程序收藏功能的实现代码
Jun 19 Javascript
基于JavaScript实现猜数字游戏代码实例
Jul 30 Javascript
基于vue中的scoped坑点解说
Sep 04 Javascript
json.stringify()与json.parse()的区别以及用处
Jan 25 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
mysql中存储过程、函数的一些问题
2007/02/14 PHP
php调用MySQL存储过程的方法集合(推荐)
2013/07/03 PHP
PHP采用自定义函数实现遍历目录下所有文件的方法
2014/08/19 PHP
Yii2搭建后台并实现rbac权限控制完整实例教程
2016/04/28 PHP
动态为事件添加js代码示例
2009/02/15 Javascript
学习ExtJS TextField常用方法
2009/10/07 Javascript
JS 事件绑定函数代码
2010/04/28 Javascript
简介JavaScript中的push()方法的使用
2015/06/09 Javascript
Bootstrap自定义文件上传下载样式
2016/05/26 Javascript
wap手机端解决返回上一页的js实例
2016/12/08 Javascript
jQuery绑定事件方法及区别(bind,click,on,live,one)
2017/08/14 jQuery
JS设计模式之数据访问对象模式的实例讲解
2017/09/30 Javascript
node命令行工具之实现项目工程自动初始化的标准流程
2019/08/12 Javascript
JavaScript回调函数callback用法解析
2020/01/14 Javascript
Python闭包执行时值的传递方式实例分析
2018/06/04 Python
在Django model中设置多个字段联合唯一约束的实例
2019/07/17 Python
numpy.meshgrid()理解(小结)
2019/08/01 Python
PYTHON绘制雷达图代码实例
2019/10/15 Python
TensorFlow查看输入节点和输出节点名称方式
2020/01/04 Python
tensorflow没有output结点,存储成pb文件的例子
2020/01/04 Python
Python 文件数据读写的具体实现
2020/01/24 Python
Python tornado上传文件的功能
2020/03/26 Python
纯css3实现思维导图样式示例
2018/11/01 HTML / CSS
英国著名国际平价时尚男装品牌:Topman
2016/08/27 全球购物
设计师家具购买和委托在线市场:Viyet
2016/11/16 全球购物
公积金单位接收函
2014/01/11 职场文书
勤俭节约演讲稿
2014/05/08 职场文书
2015年度员工自我评价范文
2015/03/11 职场文书
休学证明范本
2015/06/19 职场文书
2019年中学生的思想品德评语集锦
2019/12/19 职场文书
python如何利用traceback获取详细的异常信息
2021/06/05 Python
低版本Druid连接池+MySQL驱动8.0导致线程阻塞、性能受限
2021/07/01 MySQL
mysql中int(3)和int(10)的数值范围是否相同
2021/10/16 MySQL
css3新特性的应用示例分析
2022/03/16 HTML / CSS
漫画「古见同学有交流障碍症」第25卷封面公开
2022/03/21 日漫
Android移动应用开发指南之六种布局详解
2022/09/23 Java/Android