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
需求:
例如:abcdefg转换后为Gfedcba
OK,我们开始
1)首先创建两个loader(这里以本地loader为例)
为什么要创建两个laoder?理由后面会介绍
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文件
2)mytest.txt
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"
- },
- ...
- }
然后执行命令
这样我们的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。
在未编写插件前,生成的资源是这样的,这个路径如果作为分包引入主包,是没法正常访问资源的。
所以需求来了:
修改dist/static/css/pages目录下,所有页面的样式文件(wxss文件)引入公共资源的路径。
因为所有页面的样式都会引用通用样式vender.wxss
那么就需要把@import "/static/css/vendor.wxss"; 改为:@import "/subPages/enjoy_given/static/css/vendor.wxss";复制代码
OK 开始!
1)创建插件文件 CssPathTransfor.js
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(),
- ]
- })
插件编写完成后,执行编译命令
搞定~
如果有更多的需求可以参考《如何写一个插件》
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持w3xue。