debug(调试)webpack:
因为plugin和loader的编写需要nodejs环境,需要追踪一些参数,这时候调试就显得很重要了,但调试webpack不像在浏览器中debug那么轻松,需要一些配置:
首先在package.json
中加入如下代码: 改命令表示调试webpack并停在第一行
"scripts": {
"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
}
调试分为两种:
- 浏览器中调试
- vscode中调试
浏览器中debug:
命令行直接执行npm run start
出现上述调试,说明启动成功
,打开浏览器
,打开开发者工具
,可以看到控制台多了一个选项
到这里说明调试成功了
vscode中调试(我更喜欢的方式):
更改package.json
如下:
"scripts": {
"start": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack.js"
},
点击vscode
中如下按键创建launch.json
文件:
更改launch.json
文件如下:端口好要对上
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "启动程序",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"],
"port": 5858
}
]
}
此时按F5
或者点击此处即可开始debug
效果如下:
同理,如果你是vue
或者react
的脚手架创建出来的项目,只需要把script
中调试的js
文件换成cli
中的核心起始js文件
即可,以vuecli
为例,package.json
中script
换成如下:
"scripts": {
"debug": "node --inspect-brk=9229 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve"
}
loader:
loader
本质上是一个函数
在一个loader.js
文件中编写如下代码:
module.exports = function (content, map, meta) {
console.log(content);
return content;
}
webpack.config.js
配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babelLoader',
},
]
},
}
那么当执行webpack
的时候,所有js文件都会经过我们编写的loader
,content
中的内容即是源码,我们可以通过一些正则,方法把源码转换后打包,这就是loader
的原理了
loader的分类:
同步:
写法一:
//同步loader
module.exports = function (content, map, meta) {
return content;
}
写法二:
module.exports = function (content, map, meta) {
this.callback(null, content, map, meta);
}
异步:
// 异步loader
module.exports = function (content, map, meta) {
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 1000)
}
一般推荐的写法是异步loader
,因为不会对打包造成阻塞,性能较好
loader中一些工具方法:
getOptions
validate
loader.js
代码如下:
const {
getOptions } = require('loader-utils');
const {
, } = require('schema-utils');
const schema = require('./schema');
module.exports = function (content, map, meta) {
// 获取options
const options = getOptions(this);
// 校验options是否合法
validate(schema, options, {
name: 'loader' //填一个loader的名字,控制台会提示哪个loader的配置校验失败
})
return content;
}
webpack.config.js
配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: './loader.js',
options: {
name:"zhangsan"
}
},
]
},
}
./schema.json
如下:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "名称~"
}
},
"additionalProperties": false //是否允许options中有额外的属性
}
./schema.json
是从来配置option
传入的规则,不符合规则,则会报错,getOptions
是用来取到传入的loader
的option
配置,
validate
则是配合./schema.json
中的规则来校验拿到的options
配置是否符合预期
实际应用:
使用@babel/core编译js文件:
编写loader.js
如下:
const {
getOptions } = require('loader-utils');
const {
validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');
const babelSchema = require('./babelSchema.json');
// babel.transform用来编译代码的方法
// 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);
module.exports = function (content, map, meta) {
// 获取loader的options配置
const options = getOptions(this) || {
};
// 校验babel的options的配置
validate(babelSchema, options, {
name: 'Babel Loader'
});
// 创建异步
const callback = this.async();
// 使用babel编译代码
transform(content, options)
.then(({
code, map}) => callback(null, code, map, meta))
.catch((e) => callback(e))
}
plugin:
plugin要比loader强大很多,不仅能影响源码,还能影响文件
webpack.config.js
编写如下:
/const Plugin1 = require('./plugins/Plugin')
module.exports = {
plugins: [
new Plugin1()
]
}
Plugin.js
编写如下:
class Plugin1 {
apply(complier) {
complier.hooks.emit.tap('Plugin1', (compilation) => {
console.log('emit.tap 111');
})
}
}
module.exports = Plugin1;
apply
是一种固定的写法,可以参考官网,apply
中我们可以注册自己的钩子,那么在webpack
执行的时候,执行到emit
阶段,就会触发我们的钩子,对文件进行操作
实际应用:
编写一个插件,增加一个a.txt文件,查看文档可知,可在additionalAssets
这个钩子中增加资源
plugin.js
代码如下:
class Plugin2 {
apply(compiler) {
// 初始化compilation钩子
compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
const content = 'hello plugin2';
// 往要输出资源中,添加一个a.txt
compilation.assets['a.txt'] = {
// 文件大小
size() {
return content.length;
},
// 文件内容
source() {
return content;
}
}
cb();
})
})
}
}
module.exports = Plugin2;
webpack.config.js
中使用:
// const Plugin2 = require('./plugins/Plugin2')
module.exports = {
plugins: [
new Plugin2()
]
}
那么打包后,dist文件中就会新增一个a.txt的资源文件了
这种添加资源的方式挺麻烦的内容和大小都要自己填写,有没有自动计算的方式呢?
利用webpack
提供的一些工具函数,可以更加方便的生成和操作文件
改写plugin.js
如下:
const fs = require('fs');
const util = require('util');
const path = require('path');
const webpack = require('webpack');
const {
RawSource } = webpack.sources;
// 将fs。readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile);
class Plugin2 {
apply(compiler) {
compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
//读取b.txt文件,读取后是个buffer
const data = await readFile(path.resolve(__dirname, 'b.txt'));
compilation.assets['a.txt'] = new RawSource(data);
cb();
})
})
}
}
module.exports = Plugin2;
以上介绍了一些常用的工具方法,更多方法还得参考官网
实际应用:
编写一个copy plugin
:用于复制资源,复制public中的资源到dist目录下
plugin.js
代码如下:
const path = require('path');
const fs = require('fs');
const {
promisify} = require('util')
const {
validate } = require('schema-utils');
//可以匹配文件列表,过滤一些文件
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const readFile = promisify(fs.readFile);
const {
RawSource} = webpack.sources
class CopyWebpackPlugin {
constructor(options = {
}) {
// 验证options是否符合规范
validate(schema, options, {
name: 'CopyWebpackPlugin'
})
this.options = options;
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
// 将from中的资源复制到to中,输出出去
const {
from, ignore } = this.options;
const to = this.options.to ? this.options.to : '.';
// context就是webpack配置
// 运行指令的目录
const context = compiler.options.context; // process.cwd()
// 将输入路径变成绝对路径
const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);
// 1. 过滤掉ignore的文件
// globby(要处理的文件夹,options)
const paths = await globby(absoluteFrom, {
ignore });
console.log(paths); // 所有要加载的文件路径数组
// 2. 读取paths中所有资源
const files = await Promise.all(
paths.map(async (absolutePath) => {
// 读取文件
const data = await readFile(absolutePath);
// basename得到最后的文件名称
const relativePath = path.basename(absolutePath);
// 和to属性结合
// 没有to --> reset.css
// 有to --> css/reset.css
const filename = path.join(to, relativePath);
return {
// 文件数据
data,
// 文件名称
filename
}
})
)
// 3. 生成webpack格式的资源
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename
}
})
// 4. 添加compilation中,输出出去
assets.forEach((asset) => {
compilation.emitAsset(asset.filename, asset.source);
})
cb();
})
})
}
}
module.exports = CopyWebpackPlugin;
schema.json
验证规则如下:
{
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"ignore": {
"type": "array"
}
},
"additionalProperties": false
}
webpack.config.js
代码如下:
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
from: 'public',
to: 'css',
ignore: ['**/index.html']
})
]
}
首先我的public
目录如下:
执行npx webpack
命令输出如下:
插件是成功运行的
plugin和loader编写的思路大致就是这样了,更加深入的使用,无非就是熟读文档和参与到更多的实践中去