持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
虽然是五月的尾巴,但是竟然也算6月更文呢!就很棒哟~😄
前言
大家好,我是小阵 🔥,一路奔波不停的码字业务员
身为一个前端小菜鸟,总是有一个飞高飞远的梦想,因此,每点小成长,我都想要让它变得更有意义,为了自己,也为了更多值得的人
如果喜欢我的文章,可以关注 ➕ 点赞,与我一同成长吧~😋
加我微信:zzz886885,邀你进群,一起学习交流,摸鱼学习两不误🌟
开开心心学技术大法~~
来了来了,他真的来了~
正文
前面讲了webpack-loader的编写方法
和手写webpack-core
中用到的babel相关库
这次基于之前的基础,我们手写一个webpack-loader
来实现以下功能
- 去除项目中指定的console,可以配置去除log、info、error等
- 可以在不同console前加上指定前缀,包括手动配置的字符串或者自动生成的文件名和文件位置信息
首先我们搭建一个基础的loader
先在根目录的webpack.config.js
中引用我们本地的loader
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'console-ast-loader',
options: {
// 我们预期能配置哪些console需要被过滤
needRemovedConsoleArray: ['log', 'warn'],
// 我们也希望可以手动配置不同的console前缀
consolePrefix: {
error: '我是error前缀:',
info: {
// 或者通过boolean值来配置显示文件名和console出现的位置信息
filename: true,
location: true
},
count:'我是count前缀:'
}
}
}
]
},
]
},
mode: 'development',
resolveLoader: {
modules: [path.resolve(__dirname, 'loaders')]
}
}
然后在/src/loaders/
目录下新建一个console-ast-loader.js
写好loader的基本框架
// loader就是一个最基本的function,注意不要是箭头函数
module.exports = function (content, map, ast) {
// 通过this.getOptions获取传入的options参数
const { needRemovedConsoleArray, consolePrefix } = this.getOptions()
// callback的第一个参数是错误,如果有就传,没有的话就是null,第二参数content就是test匹配文件的源代码的utf-8的编码内容
this.callback(null, content, map, ast);
}
在之前的看完就会的webapck loader编写教程一文中我们用到了loader-utils
的getOptions
方法来获取传入的options
参数,但是在webpack5中,直接内置了类似的方法,我们只需要调用this.getOptions
即可。
具体实现
首先我们分析下要实现上面说的功能要怎样实现
- 首先我要能识别到代码中的console,包括info、log、warn等
- 我要能把识别到的console给干掉,当然也不能影响其他代码
- 我还要能识别到console中的参数,比如console.log('zzz')中的zzz
- 识别之后还要将用户定义的字符串塞入进去
- 如果用户配置了显示文件名和文件行列信息的话,我们还要拿到这些信息并且塞到原来的console中
对第一点,我们能想到的方法应该只有两种,一种是正则,另一种则是ast。但是正则只是想一下就比较头疼,因为console参数中还可以嵌套很多其他的内容,要写很多的匹配规则。
所以这里我们用ast语法树来解决这个问题,简单而高效。
根据源代码生成ast语法树
首先我们要根据源代码生成ast语法树
const { parse } = require('@babel/parser')
const ast = parse(content, {
sourceType: 'module'
})
借助@babel/parser
的parse
方法很简单就生成了ast
通过ast语法树找到console节点
接着需要遍历ast拿到console节点
。
首先我们要知道console对应到ast上的节点类型是什么,我们可以在astexplorer上在线查看ast上查看
可以看到我们要找的console对应在ast语法树上的节点是CallExpression
。
接着我们通过@babel/traverse
这个库遍历拿到console的节点
const traverse = require("@babel/traverse");
// 这里的ast就是前面生成的ast语法树
traverse.default(ast, {
CallExpression: function (path) {
// 这里的path就是我们找到的节点
console.log('path',path)
}
})
移除找到的console节点
已经找到了ast节点,那怎样移除掉这个节点呢?
// 可以通过path.remove()来移除掉ast的节点
// 注意,"节点"两个字描述不太清晰,事实上前面通过CallExpression传入的path是ast的path信息,可以通过path追溯到父path或兄弟path,而真正的节点node信息被包含在path中
// 当我们移除了path,当然也就一并移除了node节点
astPath.remove();
到这里,就已经完成了第二步,剩下的几部可以一起来看,无非是我们要找到我们所需要的信息,真正的拼接字符串其实不算太难。当然,我们这里是ast语法树的节点拼接。
获取拼接的前缀信息
那怎样获取我们需要的信息呢?
通过打断点我们,我们看到我们能拿到console的properties
信息,能分别出是log
还是其他。还能拿到console的start位置的line和colum信息
,当然也能拿到console的参数信息value
还少一个文件信息,我们可以通过this.resource
拿到加载的资源信息,然后通过path
拿到文件名称
const path = require('path')
// 注意,这里的this是loader的this,而不是babel中任意方法体的this
const resourceFileName = path.basename(this.resource);
想要的信息都有了,我们还要知道一个知识点,怎样在ast中添加一个ast节点,然后,添加完节点之后,又要怎样才可以将ast语法树转换成我们正常的文件的utf-8的编码?
先来添加ast节点
在已有ast语法树上添加新的ast节点
// 通过@babel/types暴露的方法可以让我们自己定义我们想要的ast节点
// 比如下面的types.stringLiteral,其中stringLiteral表示的就是字符串。更多节点类型可以参考babel的官网
const types = require('@babel/types')
// path是之前我们在traverse中拿到的path
path.node.arguments.unshift(types.stringLiteral(prefix))
再来看转化成utf-8的源代码
将更改过的ast输出到源代码
// @babel/generator是一个专门将ast转换成源代码的方法,还有更多的options可以参考官网
const generate = require('@babel/generator')
// 我们只需要拿到转化后的源代码即可。不传options,默认转化成utf-8
const { code } = generate.default(ast)
两个关键的点搞完之后,之后就是正常的coding
阶段啦
traverse.default(ast, {
CallExpression: function (path, state) {
// 拿到console的property,log、info、warn、table、count等
const consoleName = path.container.expression.callee && path.container.expression.callee.property && path.container.expression.callee.property.name
// 拿到console定义时的位置信息
const consoleLocation = path.container.expression.loc && path.container.expression.loc.start
// 如果在需要移除的console中,则移除
if (needRemovedConsoleArray.includes(consoleName)) {
return path.remove()
}
for (const consoleType in consolePrefix) {
// 如果跟options传入的信息匹配则塞入节点
if (Object.hasOwnProperty.call(consolePrefix, consoleType) && consoleName === consoleType) {
const prefix = consolePrefix[consoleType];
// 根据用户传入的配置做string和object的区分
if (typeof prefix === 'string') {
path.node.arguments.unshift(types.stringLiteral(prefix))
}
if (prefix instanceof Object) {
path.node.arguments.unshift(types.stringLiteral(`${resourceFileName} \nline:${consoleLocation.line};column:${consoleLocation.column} \n`))
}
}
}
}
})
之后再将改变后的ast通过前面说到的generator
生成源代码之后通过loader
的callback
抛出去即可。
const { code } = generate.default(ast)
this.callback(null, code)
享受成果
ok,前面的搞完了,接下来享受甜丝丝的成果吧!
在/src/
目录下新建index.js
const a = 'test1'
console.log('a',a)
console.log('我是log')
console.info('我是info')
console.warn('我是warn')
console.error('我是error')
console.count('我是count')
console.trace('我是trace')
console.dir('我是dir')
console.dirxml('我是dirxml')
console.assert('我是assert')
console.table('我是table')
console.time('我是time')
console.timeEnd('我是timeEnd')
console.group('我是group')
console.groupEnd('我是groupEnd')
console.profile('我是profile');
console.profileEnd('我是profileEnd');
在根目录通过npx webpack
来生成build后的main.js
文件,新建一个index.html
文件将main.js
文件引入,或者直接引入一个html-webpack-plugin
帮你做好这一切
接着浏览器打开index.html
文件看效果
哎哟不错哟,又学会了一项技能呢!鼓掌👏🏻哈哈哈
插件功能不难,重在中间的过程,看完的小伙伴肯定都有更加闪亮✨的想法啊,大家加油!
结语
往期好文推荐「我不推荐下,大家可能就错过了史上最牛逼vscode插件集合
啦!!!(嘎嘎嘎~)😄」