原因
已经颓废了好久 由于真实不知道写啥了 忽然我某个同事对我说
宝哥 你看这个页面好多console.log
不只会影响功能 而且或许会被不法分子所运用 我觉得很有道理
所以我萌生了写一个小插件来去除出产环境
的console.log的主意
介绍
咱们笼统的介绍下babel
,之前我有一篇写精度插件的babel
文章,babel一共有三个阶段:第一阶段是将源代码转化为ast语法树
、第二阶段是对ast语法树进行修正,生成咱们想要的语法树
、第三阶段是将ast语法树解析,生成对应的目标代码
。
窥探
咱们的意图是去除console.log
,咱们首先需求经过ast查看语法树的结构。咱们以下面的console为例:
留意 由于咱们要写babel插件 所以咱们挑选
@babel/parser
库生成ast,由于babel内部是运用这个库生成ast的
console.log("我会被铲除");
初见AST
AST是对源码的抽象,字面量、标识符、表达式、语句、模块语法、class语法都有各自的AST。
咱们这儿只说下本文章中所运用的AST。
Program
program 是代表整个程序的节点,它有 body 特点代表程序体,寄存 statement 数组,便是具体履行的语句的集合。
能够看到咱们这儿的body只要一个ExpressionStatement语句,即console.log。
ExpressionStatement
statement 是语句,它是能够独立履行的单位,expression是表达式,它俩唯一的区别是表达式履行完以后有回来值。所以ExpressionStatement表明这个表达式是被当作语句履行的。
ExpressionStatement类型的AST有一个expression特点,代表当时的表达式。
CallExpression
expression 是表达式,CallExpression表明调用表达式,console.log便是一个调用表达式。
CallExpression类型的AST有一个callee特点,指向被调用的函数。这儿console.log便是callee的值。
CallExpression类型的AST有一个arguments特点,指向参数。这儿“我会被铲除”便是arguments的值。
MemberExpression
Member Expression通常是用于拜访目标成员的。他有几种方式:
a.b
a["b"]
new.target
super.b
咱们这儿的console.log便是拜访目标成员log。
- 为什么MemberExpression外层有一个CallExpression呢?
实际上,咱们能够理解为,MemberExpression中的某一子结构具有函数调用,那么整个表达式就成为了一个Call Expression。
MemberExpression有一个特点object表明被拜访的目标。这儿console便是object的值。
MemberExpression有一个特点property表明目标的特点。这儿log便是property的值。
MemberExpression有一个特点computed表明拜访目标是何种方式。computed为true表明[],false表明. 。
Identifier
Identifer 是标识符的意思,变量名、特点名、参数名等各种声明和引用的姓名,都是Identifer。
咱们这儿的console便是一个identifier。
Identifier有一个特点name 表明标识符的姓名
StringLiteral
表明字符串字面量。
咱们这儿的log便是一个字符串字面量
StringLiteral有一个特点value 表明字符串的值
公共特点
每种 AST 都有自己的特点,可是它们也有一些公共的特点:
-
type:AST节点的类型
-
start、end、loc:start和end代表该节点在源码中的开端和完毕下标。而loc特点是一个目标,有line和column特点分别记录开端和完毕的队伍号
-
leadingComments、innerComments、trailingComments:表明开端的注释、中心的注释、完毕的注释,每个 AST 节点中都或许存在注释,而且或许在开端、中心、完毕这三种位置,想拿到某个 AST 的注释就经过这三个特点。
如何写一个babel插件?
babel插件是作用在第二阶段即transform阶段。
transform阶段有@babel/traverse,能够遍历AST,并调用visitor函数修正AST。
咱们能够新建一个js文件,其间导出一个办法,回来一个目标,目标存在一个visitor特点,里边能够编写咱们具体需求修正AST的逻辑。
+ export default () => {
+ return {
+ name: "@parrotjs/babel-plugin-console",
+ visitor,
+ };
+ };
结构visitor办法
path 是记录遍历途径的 api,它记录了父子节点的引用,还有很多增修改查 AST 的 api
+ const visitor = {
+ CallExpression(path, { opts }) {
+ //当traverse遍历到类型为CallExpression的AST时,会进入函数内部,咱们需求在函数内部修正
+ }
+ };
咱们需求遍历一切调用函数表达式 所以运用
CallExpression
。
去除一切console
咱们将一切的console.log去掉
path.get 表明获取某个特点的path
path.matchesPattern 查看某个节点是否契合某种模式
path.remove 删去当时节点
CallExpression(path, { opts }) {
+ //获取callee的path
+ const calleePath = path.get("callee");
+ //查看callee中是否契合“console”这种模式
+ if (calleePath && calleePath.matchesPattern("console", true)) {
+ //假如契合 直接删去节点
+ path.remove();
+ }
},
增加env api
一般去除console.log都是在出产环境履行 所以增加env参数
AST的第二个参数opt中有插件传入的配置
+ const isProduction = process.env.NODE_ENV === "production";
CallExpression(path, { opts }) {
....
+ const { env } = opts;
+ if (env === "production" || isProduction) {
path.remove();
+ }
....
},
增加exclude api
咱们上面去除了一切的console,不管是error、warning、table都会铲除,所以咱们加一个exclude api,传一个数组,能够去除想要去除的console类型
....
+ const isArray = (arg) => Object.prototype.toString.call(arg) === "[object Array]";
- const { env } = opts;
+ const { env,exclude } = opts;
if (env === "production" || isProduction) {
- path.remove();
+ //封装函数进行操作
+ removeConsoleExpression(path, calleePath, exclude);
}
+const removeConsoleExpression=(path, calleePath, exclude)=>{
+ if (isArray(exclude)) {
+ const hasTarget = exclude.some((type) => {
+ return calleePath.matchesPattern("console." + type);
+ });
+ //匹配上直接回来不进行操作
+ if (hasTarget) return;
+ }
+ path.remove();
+}
增加commentWords api
某些时候 咱们期望一些console 不被删去 咱们能够给他添加一些注释 比如
//no remove
console.log("测验1");
console.log("测验2");//reserse
//hhhhh
console.log("测验3")
如上 咱们期望带有no remove前缀注释的console 和带有reserse后缀注释的console保留不被删去
之前咱们提到 babel给咱们提供了leadingComments(前缀注释)和trailingComments(后缀注释)咱们能够运用他们 由AST可知 她和CallExpression同级,所以咱们需求获取他的父节点 然后获取父节点的特点
path.parentPath 获取父path
path.node 获取当时节点
- const { exclude, env } = opts;
+ const { exclude, commentWords, env } = opts;
+ const isFunction = (arg) =>Object.prototype.toString.call(arg) === "[object Function]";
+ // 判别是否有前缀注释
+ const hasLeadingComments = (node) => {
+ const leadingComments = node.leadingComments;
+ return leadingComments && leadingComments.length;
+ };
+ // 判别是否有后缀注释
+ const hasTrailingComments = (node) => {
+ const trailingComments = node.trailingComments;
+ return trailingComments && trailingComments.length;
+ };
+ //判别是否有关键字匹配 默认no remove || reserve 且假如commentWords和默认值是相斥的
+ const isReserveComment = (node, commentWords) => {
+ if (isFunction(commentWords)) {
+ return commentWords(node.value);
+ }
+ return (
+ ["CommentBlock", "CommentLine"].includes(node.type) &&
+ (isArray(commentWords)
+ ? commentWords.includes(node.value)
+ : /(no[t]? removeb)|(reserveb)/.test(node.value))
+ );
+};
- const removeConsoleExpression = (path, calleePath, exclude) => {
+ const removeConsoleExpression = (path, calleePath, exclude,commentWords) => {
+ //获取父path
+ const parentPath = path.parentPath;
+ const parentNode = parentPath.node;
+ //标识是否有前缀注释
+ let leadingReserve = false;
+ //标识是否有后缀注释
+ let trailReserve = false;
+ if (hasLeadingComments(parentNode)) {
+ //traverse
+ parentNode.leadingComments.forEach((comment) => {
+ if (isReserveComment(comment, commentWords)) {
+ leadingReserve = true;
+ }
+ });
+ }
+ if (hasTrailingComments(parentNode)) {
//traverse
+ parentNode.trailingComments.forEach((comment) => {
+ if (isReserveComment(comment, commentWords)) {
+ trailReserve = true;
+ }
+ });
+ }
+ //假如没有前缀节点和后缀节点 直接删去节点
+ if (!leadingReserve && !trailReserve) {
+ path.remove();
+ }
}
细节完善
咱们大致完成了插件 咱们引进项目里边进行测验
console.log("测验1");
//no remove
console.log("测验2");
console.log("测验3");//reserve
console.log("测验4");
//新建.babelrc 引入插件
{
"plugins":[["../dist/index.cjs",{
"env":"production"
}]]
}
理论上应该移除测验1、测验4,可是咱们惊奇的发现 居然一个console没有删去!!经过排查 咱们大致确定了问题所在
由于测验2的前缀注释一起也被AST纳入了测验1的后缀注释中了,而测验3的后缀注释一起也被AST纳入了测验4的前缀注释中了
所以测验1存在后缀注释 测验4存在前缀注释 所以测验1和测验4没有被删去
那么咱们怎么判别呢?
关于后缀注释
咱们能够判别后缀注释是否与当时的调用表达式处于同一行,假如不是同一行,则不将其归纳为后缀注释
if (hasTrailingComments(parentNode)) {
+ const { start:{ line: currentLine } }=parentNode.loc;
//traverse
// @ts-ignore
parentNode.trailingComments.forEach((comment) => {
+ const { start:{ line: currentCommentLine } }=comment.loc;
+ if(currentLine===currentCommentLine){
+ comment.belongCurrentLine=true;
+ }
+ //归于当时行才将其设置为后缀注释
- if (isReserveComment(comment, commentWords))
+ if (isReserveComment(comment, commentWords) && comment.belongCurrentLine) {
trailReserve = true;
}
});
}
咱们修正完进行测验 发现测验1 已经被删去
关于前缀注释
那么关于前缀注释 咱们应该怎么做呢 由于咱们在后缀注释的节点中添加了一个变量belongCurrentLine,表明该注释是否是和节点归于同一行。
那么关于前缀注释,咱们只需求判别是否存在belongCurrentLine,假如存在belongCurrentLine,表明不能将其当作前缀注释。
if (hasLeadingComments(parentNode)) {
//traverse
// @ts-ignore
parentNode.leadingComments.forEach((comment) => {
- if (isReserveComment(comment, commentWords)) {
+ if (isReserveComment(comment, commentWords) && !comment.belongCurrentLine) {
leadingReserve = true;
}
});
}
发布到线上
我现已将代码发布到线上
装置
yarn add @parrotjs/babel-plugin-console
运用
举个比如:新建.babelrc
{
"plugins":[["../dist/index.cjs",{
"env":"production"
}]]
}
github 地址 欢迎star
git地址