编写webpack的loader和plugin

2019 / 09 / 26

Webpack中的loader们

webpack的核心工作机制是收集依赖,然后根据文件类型和配置的相关的解析工具进行代码解析,然后打包输出,从而可以让各种不同的资源根据设定者的喜好去完成各自的使命。其中工作在第一线用来解析文件的工具们就是loader,webpack中的loader非常重要。

比如用babel-loader可以解析各种ecma的新语法以及提案中的语法,再加上polyfill,使得前端开发可以无需顾忌兼容性问题放心的使用各种语法和功能。

style-loader, css-loader, scss-loader, less-loader, scss-loader,postcss-loader让我们可以非常灵活的处理不同的样式甚至可以很合理的去同时。

file-loader,url-loader可以让开发者使用小型图片的使用可以直接以base64的方式被打包到代码中,就不需要加载了,可以大幅度提升性能和体验。

webpack官方推荐的loader非常多,具体可以参考这里

编写loader

loader的实现非常简单,本质上来说只是把一段字符串转化为另一段字符串。所以只需要借助各种工具进行字符串处理就好了。

// project.info

This project's author is {{author}}!
Current app version is {{version}}.

webpack配置

{
test: /\.info$/,
use: [{
loader: path.resolve('./config/info-loader.js'),
options: { author: 'xiao han', version: '2.0.0' }
}],
},

loader实现

Webpack的plugin是什么

插件的出现的作用本身是用来处理loader做不了的事情,就是说他是用来干杂活的,或者说是一些辅助性的任务。纵观webpack打包一个项目,首先他需要从一个入口文件,开始一个一个文件的引入,然后通过babel编译成各种其他的语法,最后生成到目的文件夹中。

这个过程的主要参与者还是entry,output和各种loader,plugin在这个核心过程中的参与度其实非常的少,而除了编译文件,如果要上线的话webpack还需要把压缩混淆代码,也有时需要分离css,或者把html压缩一下,或者打包的时候拆分文件,防止打包体积过大,这些就需要plugin去做了,总而言之,plugin看起来就像一个打杂的,但事实上plugin非常的强大,甚至可以取缔loader。

plugin的设计是基于事件和钩子的。这也是webpack非常核心的一个设计模式,所以webpack官方会告诉你实现plugin比实现loader要更为高级,因为他能触及到更底层的东西。

编写一个plugin

编写plugin需要遵循webpack设定的一个格式,非常简单。

  1. 构造函数或者class
  2. 有一个apply方法
  3. 调用compiler和compilation实现各种功能。

比如这样子的,具体参考编写一个插件

class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap('Hello world', () => {
console.log('hello world')
})
}
}

module.exports = HelloWorldPlugin

class的声明方式,是因为需要实例化,比如打包的时候可能同时需要多个互相没有关系的同一个class实例化的plugin对象共同工作,虽然这种情况很少见。

apply方法是webpack写给compiler的,当然class上可以挂载更多的其他方法然后自己调用,只是编译器只关心apply方法。

tapable, compiler和compilation是webpack非常核心的理念,tapable是比compiler和complation还要核心的东西,webpack的很多对象都继承自Tapable对象。

Tapable, compiler和comilation

tap直译过来是触及,tapable可触及的,这个东西比较抽象,可以看看这个学习一下tapable是个什么样的东西。总的来说tapable是一个类似于观察者模式的任务队列,每个Tapable对象可以注册一系列的任务,这些任务分为三种,同步tap,异步tapAsync和tapPromise,异步的就是不会阻塞后续的执行,promise会阻塞。

class TestPlugin {
apply(compiler) {
compiler.hooks.done.tapPromise('TestPluginDone', () => {
return new Promise(resolve => {
console.log('this task will block for 5 seconds')
setTimeout(resolve, 5000)
})
})
}
}

module.exports = TestPlugin

compiler作为一个编译器,有一系列的动作,比如开始编译,执行完成,执行中等等,这些动作都会留出一个hooks的属性,上边用来添加各种tapable事件,即可触及的事件。通过给这些事件添加tapable事件从而影响整个编译过程。

比如compiler有如下的不同行为。具体可以参考 compiler钩子

class TestPlugin {
apply(compiler) {
compiler.hooks.beforeRun.tap('TestPluginBeforeBuild', () => {
console.log('before build')
})

compiler.hooks.beforeCompile.tap('TestPluginBeforeCompile', () => {
console.log('before compile')
})

compiler.hooks.done.tap('TestPluginAfterBuild', () => {
console.log('after build')
})
}
}

module.exports = TestPlugin

tapable事件覆盖了compiler的不同阶段的行为,通过tap触发这些不同阶段的行为。

通过对这些时间的操作基本上就可以在webpack里为所欲为了。

对于compiler而言,其实整个编译过程可以理解为不停的创建compilation的过程,每个compilation也都是Tapable对象。具体可以参考webpack compilation钩子

一个很简单的html-webpack-plugin和clean-plugin

写plugin最重要的就是要会fs和path API,这些都是node环境下的,这两个API都非常简单。

html plugin用来拷贝html和favico等到输出目录下,其实除此之外还要对html和icon以及根据script等做一系列处理,这里不做处理。

compilation.assets就是输出的文件,可以根据这个改变输出的内容,html-webpack-plugin就是这个原理。

const fs = require('fs')
const path = require('path')


class HtmlPlugin {
constructor(props) {
this.template = props.template
this.favicon = props.favicon
}

apply(compiler) {
compiler.hooks.afterCompile.tap('HtmlPlugin', (compilation) => {
const html = fs.readFileSync(this.template)
const favicon = fs.readFileSync(this.favicon)
const htmlFileName = path.basename(this.template)
const faviconName = path.basename(this.favicon)

compilation.assets[htmlFileName] = {
source: () => html,
size: () => html.length,
}

compilation.assets[faviconName] = {
source: () => favicon,
size: () => favicon.length,
}
})
}
}

module.exports = HtmlPlugin

clean plugin用来在编译前删除输出目录里的文件。

const fs = require('fs')
const path = require('path')

class CleanPlugin {
apply(compiler) {
compiler.hooks.beforeRun.tap('CleanPlugin', (compiler) => {
const distDir = compiler.options.output.path
const filelist = fs.readdirSync(distDir)
filelist.forEach(file => {
const filePath = path.join(distDir, file)
fs.unlinkSync(filePath)
})
})
}
}

module.exports = CleanPlugin

注意,不同阶段的hooks的tapable性质不同,webpack本身提供了很多种的tapable,不同的阶段的hooks类型不同,有的hooks是Async的就不能接受异步的tap,还有不同阶段的hooks的参数也不同,这是因为位于不同状态的时候webpack做的事情也不一样,比如done阶段的参数是stat,开始阶段参数可能会是compiler,编译阶段参数可能是compilation。

后话

这些是要玩转webpack需要入门的一些概念,后边有空我会学习resolver, parser, factory, dependency处理等能力的时候继续写点文章,然后说点后话。

对于工程化而言,敞开了聊是一个非常大的话题,各种引用机制,打包机制,处理机制,项目规范化。但是如果说的简单点,顺着一篇配置相关的介绍就能鲁出来一个像模像样的项目。而另一方面其实项目配置好了,基本上很久也不会出现什么大的调整,所以很少会有机会做工程化方面的实践,学习的性价比对于初级或者中级的开发而言,也并不是一件性价比比较高的事情。但对于项目管理者或者技术负责人而言,很多时候想要优化项目或者更改方案都不得不从工程化方面下手。同时造一些工程化方面的轮子也会有很重大的意义,可以提升开发效率或者优化项目架构,这种提升很大程度上是封装个方法或者写个工具无法相比较的。所以工程化可能是用create-react-app分分钟就能完成的事情,也可能是颠覆项目架构,推进项目构建效率,作出建设性贡献的事情,因人而异。

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟