详解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 相关文章推荐
文字幻灯片
Jun 26 Javascript
JXTree对象,读取外部xml文件数据,生成树的函数
Apr 02 Javascript
文本框回车提交与禁止提交示例
Sep 27 Javascript
JQuery选择器绑定事件及修改内容的方法
Jan 23 Javascript
jQuery控制cookie过期时间的方法
Apr 07 Javascript
Node.js实现数据推送
Apr 14 Javascript
详解jQuery UI库中文本输入自动补全功能的用法
Apr 23 Javascript
原生js实现下拉框功能(支持键盘事件)
Jan 13 Javascript
vuejs如何配置less
Apr 25 Javascript
jQuery.Ajax()的data参数类型详解
Jul 23 jQuery
使用async-validator编写Form组件的方法
Jan 10 Javascript
vue新建项目并配置标准路由过程解析
Dec 09 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合并数组+与array_merge的区别分析
2010/08/01 PHP
Zend Studio去除编辑器的语法警告设置方法
2012/10/24 PHP
php解析json数据实例
2014/08/19 PHP
PHPMAILER实现PHP发邮件功能
2018/04/18 PHP
一个多次搜索+多次传值的解决方案
2007/01/20 Javascript
jQuery下扩展插件和拓展函数的写法(匿名函数使用的典型例子)
2010/10/20 Javascript
js类型检查实现代码
2010/10/29 Javascript
JS 如果改变span标签的是否隐藏属性
2011/10/06 Javascript
javascript 事件处理程序介绍
2012/06/27 Javascript
NodeJS实现阿里大鱼短信通知发送
2016/01/17 NodeJs
分享10个优化代码的CSS和JavaScript工具
2016/05/11 Javascript
AngularJS在IE8的不支持的解决方法
2016/05/13 Javascript
js流动式效果显示当前系统时间
2016/05/16 Javascript
JQuery点击行tr实现checkBox选中的简单实例
2016/05/26 Javascript
深入研究React中setState源码
2017/11/17 Javascript
AngularJs 最新验证手机号码的实例,成功测试通过
2017/11/26 Javascript
Bootstrap-table使用footerFormatter做统计列功能
2018/09/07 Javascript
在vue项目中使用Jquery-contextmenu插件的步骤讲解
2019/01/27 jQuery
vue 自动化路由实现代码
2019/09/03 Javascript
Python标准库与第三方库详解
2014/07/22 Python
Python实现给qq邮箱发送邮件的方法
2015/05/28 Python
Python中http请求方法库汇总
2016/01/06 Python
Python构建网页爬虫原理分析
2017/12/19 Python
特征脸(Eigenface)理论基础之PCA主成分分析法
2018/03/13 Python
python生成密码字典的方法
2018/07/06 Python
Django中使用MySQL5.5的教程
2019/12/18 Python
凯特方迪化妆品官网:Kat Von D Beauty
2016/11/15 全球购物
墨西哥皇宫度假村预订:Palace Resorts
2018/06/16 全球购物
数据库什么时候应该被重组
2012/11/02 面试题
成品仓管员岗位职责
2013/12/11 职场文书
超市营业员岗位职责
2013/12/20 职场文书
教育孩子心得体会
2014/01/01 职场文书
乱丢垃圾袋检讨书
2014/10/08 职场文书
上班迟到检讨书300字
2014/10/18 职场文书
工作时间调整通知
2015/04/24 职场文书
Win11如何启用启动修复 ? Win11执行启动修复的三种方法
2022/04/08 数码科技