对.vue文件实现自动化函数插桩
对
vue
文件编码实现自动化函数插桩 实现自动国际化方案。
第一步初始化项目
下面是一个标准的
babel
转成ats
再到生成code
代码 就不过多解释了,代码如下:
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import template from '@babel/template';
import * as types from '@babel/types';
const code = `
<template>
<div>
<span>{{ a ? '测试' : 'qq'}}</span>
<el-button>按钮</el-button>
<test-component
v-aaa="我是title"
:size="CARD_SIZE_MAP.BIG"
title="默认值"
/>
</div>
</template>
<script>
import TestComponent, { CARD_SIZE_MAP } from '../components/card/index.vue';
const TYPE_MAP = {
a: '哈哈哈',
};
export default {
name: 'APP',
components: {
TestComponent,
},
props: {
BBBB: {
type: [String, Number],
require: true,
default: '哈哈',
},
permission: {
type: Boolean,
require: true,
default: true,
},
},
data() {
return {
myChart: null,
title: '我是哈哈哈',
};
},
methods: {
initResize() {
const a = '哈哈哈';
this.myChart?.resize();
},
},
};
</script>
<style lang="less" scoped>
</style>
`;
const ast = parser.parse(code, {
sourceType: 'unambiguous',
});
console.log(generate(ast).code);
目标呢就是把上面的code
转成下面这个样
<template>
<div>
<span>{{ a ? $tx('测试') : 'qq'; }}</span>
<el-button>{{ $tx('按钮') }}</el-button>
<test-component
v-aaa="$tx('我是title')"
:size="CARD_SIZE_MAP.BIG"
:title="$tx('默认值')"
/>
</div>
</template>
<script>
import TestComponent, { CARD_SIZE_MAP } from '../components/card/index.vue';
const TYPE_MAP = {
a: window.$tx('哈哈哈')
};
export default {
name: 'APP',
components: {
TestComponent
},
props: {
BBBB: {
type: [String, Number],
require: true,
default: this.$tx('哈哈')
},
permission: {
type: Boolean,
require: true,
default: true
}
},
data() {
return {
myChart: null,
title: this.$tx('我是哈哈哈')
};
},
methods: {
initResize() {
const a = this.$tx('哈哈哈');
this.myChart?.resize();
}
}
};
</script>
<style lang="less" scoped>
</style>
第二步 使用@vue/compiler-dom
解析.vue
code
使用
@vue/compiler-dom
的parse
解析vue源码
import { parse } from '@vue/compiler-dom';
const vueParseAst = parse(code);
console.log(vueParseAst,'vueParseAst...');
会得到如下的一个对象:
{
type: 0,
children: [
{
type: 1,
ns: 0,
tag: 'template',
tagType: 0,
props: [],
isSelfClosing: false,
children: [Array],
loc: [Object],
codegenNode: undefined
},
{
type: 1,
ns: 0,
tag: 'script',
tagType: 0,
props: [],
isSelfClosing: false,
children: [Array],
loc: [Object],
codegenNode: undefined
},
{
type: 1,
ns: 0,
tag: 'style',
tagType: 0,
props: [Array],
isSelfClosing: false,
children: [Array],
loc: [Object],
codegenNode: undefined
}
],
helpers: Set(0) {},
components: [],
directives: [],
hoists: [],
imports: [],
cached: 0,
temps: 0,
codegenNode: undefined,
loc: {
start: { column: 1, line: 1, offset: 0 },
end: { column: 1, line: 63, offset: 826 },
source: '\n' +
'<template>\n' +
' <div>\n' +
" <span>{{ a ? '测试' : 'qq'}}</span>\n" +
' <el-button>按钮</el-button>\n' +
'\n' +
' <test-component\n' +
' v-aaa="我是title"\n' +
' :size="CARD_SIZE_MAP.BIG"\n' +
' title="默认值"\n' +
' />\n' +
' </div>\n' +
'</template>\n' +
'\n' +
'<script>\n' +
'\n' +
"import TestComponent, { CARD_SIZE_MAP } from '../components/card/index.vue';\n" +
'\n' +
'\n' +
'const TYPE_MAP = {\n' +
" a: '哈哈哈',\n" +
'};\n' +
'export default {\n' +
" name: 'APP', \n" +
' \n' +
' components: {\n' +
' TestComponent,\n' +
' },\n' +
'\n' +
' props: {\n' +
' BBBB: {\n' +
' type: [String, Number],\n' +
' require: true,\n' +
" default: '哈哈',\n" +
' },\n' +
'\n' +
' permission: {\n' +
' type: Boolean,\n' +
' require: true,\n' +
' default: true,\n' +
' },\n' +
' },\n' +
'\n' +
' data() {\n' +
' return {\n' +
' myChart: null,\n' +
" title: '我是哈哈哈',\n" +
' };\n' +
' },\n' +
'\n' +
' methods: {\n' +
' initResize() {\n' +
" const a = '哈哈哈';\n" +
' this.myChart?.resize();\n' +
' },\n' +
' },\n' +
'};\n' +
'</script>\n' +
'\n' +
'<style lang="less" scoped>\n' +
'\n' +
'</style>\n'
}
}
第三步 解析 vue template
经过
@vue/compiler-dom
的parse
解析后我们可以得到一个template
的Vue AST
的一段编程,children
里面的tag
分别对应template
、style
、script
三部分。
开始解析template
如下:
/**
* 这是里是解析出来NodeTypes的类型对应
* import { NodeTypes } from '@vue/compiler-dom';
* export declare const enum NodeTypes {
ROOT = 0,
ELEMENT = 1,
TEXT = 2,
COMMENT = 3,
SIMPLE_EXPRESSION = 4,
INTERPOLATION = 5,
ATTRIBUTE = 6,
DIRECTIVE = 7,
COMPOUND_EXPRESSION = 8,
IF = 9,
IF_BRANCH = 10,
FOR = 11,
TEXT_CALL = 12,
VNODE_CALL = 13,
JS_CALL_EXPRESSION = 14,
JS_OBJECT_EXPRESSION = 15,
JS_PROPERTY = 16,
JS_ARRAY_EXPRESSION = 17,
JS_FUNCTION_EXPRESSION = 18,
JS_CONDITIONAL_EXPRESSION = 19,
JS_CACHE_EXPRESSION = 20,
JS_BLOCK_STATEMENT = 21,
JS_TEMPLATE_LITERAL = 22,
JS_IF_STATEMENT = 23,
JS_ASSIGNMENT_EXPRESSION = 24,
JS_SEQUENCE_EXPRESSION = 25,
*/
export const NODE_TYPE_MAP = {
ELEMENT: 1, // NodeTypes.ELEMENT
TEXT: 2, // NodeTypes.TEXT
SIMPLE_EXPRESSION: 4, // NodeTypes.SIMPLE_EXPRESSION
ATTRIBUTE: 6, // NodeTypes.ATTRIBUTE
INTERPOLATION: 5, // NodeTypes.INTERPOLATION
DIRECTIVE: 7, // NodeTypes.DIRECTIVE
};
for (const child of parse(code).children) {
if (child.tag === 'template') {
handleVueTemplate(child); // 先重点关注这里
}
}
const handleVueTemplate = (child) => {
if (node.type === NODE_TYPE_MAP.ELEMENT) { // NodeTypes.ELEMENT 代表的是每一层元素
handleNodeProps(node); // props 上有该元素身上绑定的值 例如 `title="默认值"`
for (const child of node.children) {
handleVueTemplate(child); // dom是一层一层的所以需要递归
}
}
if (node.type === NODE_TYPE_MAP.INTERPOLATION) { // NodeTypes.INTERPOLATION 动态插入的 例如 `{{a ? '测试' : 'qq'}}`
handleNodeInterpolation(node);
}
if (node.type === NODE_TYPE_MAP.TEXT) { // NodeTypes.TEXT 文本 就是标签里面的文本
handleNodeText(node);
}
}
下面咱们针对每个方法对不同的
vue
语法进行处理。
解析文本 handleNodeText
先从
handleNodeText
开始也最好理解,就是把我是文本
变成了$tx('我是文本')
, 代码如下:
/**
* 数据结构可以是这个样的:
*
* loc 是索引
* content 是替换的新代码就是携带 $tx 包裹的。
*
* Array<{
* loc: { start: number, end: number},
* content: string,
* }>
*
*/
interface ReplaceCodeProps {
loc: {
start: number;
end: number;
},
content: string;
}
const replaceCodes: Array<ReplaceCodeProps> = []; // 存放需要替换的代码的信息
工具函数:
/*
HTML 中的前后无效空字符串
*/
function invalidSide(val = '') {
const matchLeft = /^([\n\r\t\s]*)/.exec(val);
const matchRight = /([\n\r\t\s]*)$/.exec(val);
return [matchLeft[0].length, matchRight[0].length];
}
function isCN(value) {
return /[\u4E00-\u9FFF]+/g.test(value);
}
const replaceCodes = [];
const handleNodeText = (node) => {
/**
* console.log(node);
* node 的数据日志如下
* 可以看出 loc 里面存放着咱们需要的信息 start.offset 是开始的下标 end.offset 是结尾的下标
* {
type: 2,
content: '按钮',
loc: {
start: { column: 15, line: 5, offset: 70 },
end: { column: 17, line: 5, offset: 72 },
source: '按钮'
}
}
*/
const { loc } = node;
if (!isCN(loc.source)) {
return;
}
const { source = '', start, end } = loc || {};
const { length } = source;
const [left, right] = invalidSide(source);
const value = source.slice(left, length - right);
replaceCodes.push({
loc: {
start: start.offset + left, // 减去左侧的无效字符之后的索引下标
end: end.offset - right, // 减去右侧的无效字符之后的索引下标
},
content: `{{ $tx('${value}') }}`,
});
};
可以看出我们把需要的信息都收集在了
replaceCodes
里面, 里面有原始字符串的下标结束下标,还有需要替换的新代码。
解析动态插入的语法程序 handleNodeInterpolation
把动态插入语法的经过
handleNodeInterpolation
解析输入到replaceCodes
里面。
下面是使用babel
解析js的一套代码,再上一篇文章中有说明,下面把它封成一个函数供咱们使用如下:
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
import template from '@babel/template';
import * as types from '@babel/types';
import { isCN } from './utils';
export function scriptTranslated(code, options = {
callee: 'window.$tx',
}) {
const ast = parser.parse(code, {
sourceType: 'unambiguous',
});
const txTemp = template(`${options.callee}(NODE_TEMPLATE)`);
traverse(ast, {
CallExpression(path) {
const calleeCode = generate(path.node.callee).code;
if (calleeCode.includes('$tx')) {
path.skip();
}
},
StringLiteral(path) {
if (isCN(path.node.value)) {
const tem = txTemp({
NODE_TEMPLATE: path.node,
});
path.replaceWith(tem);
path.skip();
// const txCallExpression = types.callExpression(
// types.identifier('window.$tx'), // 被调用的函数名
// [types.stringLiteral(path.node.value)] // 入参数 这个是需要插桩的语言文本
// )
// path.replaceWith(txCallExpression);
// path.skip();
}
},
});
return generate(ast, {
jsescOption: {
minimal: true, // issues https://github.com/babel/babel/issues/4909#issuecomment-397715926
}
}).code;
}
handleNodeInterpolation
方法如下:
const handleNodeInterpolation = (node) => {
/**
* node 输出的数据日志
* {
type: 5,
content: {
type: 4,
isStatic: false,
constType: 0,
content: "a ? '测试' : 'qq'",
loc: { start: [Object], end: [Object], source: "a ? '测试' : 'qq'" }
},
loc: {
start: { column: 10, line: 4, offset: 28 },
end: { column: 30, line: 4, offset: 48 },
source: "{{ a ? '测试' : 'qq'}}"
}
}
*/
const { loc, content: { content } } = node;
const { start, end } = loc;
const targetCode = scriptTranslated(content, { // 把 "a ? '测试' : 'qq'" 交给babel js去转成了 a ? $tx('测试') : 'qq';
callee: '$tx',
});
replaceCodes.push({ // 记录信息
loc: {
start: start.offset,
end: end.offset,
},
content: `{{ ${targetCode} }}`,
});
};
咱们是用
babel
帮咱们处理了动态插入的 js。
解析绑定再元素上的信息 由handleNodeProps
处理
使用
handleNodeProps
去解析绑定再元素上的vue语法。例如:v-aaa="我是title" 、title="默认值"
title="默认值
这个样的再vue中属于NodeTypes.ATTRIBUTE
属性;v-aaa="我是title"
这个样的再vue中属于NodeTypes.DIRECTIVE
指令;
const handleNodeProps = (node) => {
if (node.type === 1) { // NodeTypes.ELEMENT
handleNodeAttrs(node); // 属性处理
handleNodeDirectives(node); // 指令处理
}
};
handleNodeAttrs
属性处理
处理元素上的属性,
title="默认值"
转成:title="$tx('默认值")
如下:
const handleNodeAttrs = (node) => {
const { props = [] } = node;
// NodeTypes.ATTRIBUTE
const attributes = props.filter((prop) => prop.type === NODE_TYPE_MAP.ATTRIBUTE);
/**
* attribute 日志信息输出如下:
* {
type: 6,
name: 'title',
value: { type: 2, content: '默认值', loc: [Object] },
loc: { start: [Object], end: [Object], source: 'title="默认值"' }
}
*/
for (const attribute of attributes) {
const { name, value, loc } = attribute;
const { start, end } = loc || {};
const { content = '' } = value || {};
if (isCN(content)) {
replaceCodes.push({ // replaceCodes 记录
loc: {
start: start.offset,
end: end.offset,
},
content: `:${name}="$tx('${content}')"`,
});
}
}
};
handleNodeDirectives 处理自定义指令
handleNodeDirectives
处理自定义指令,v-aaa="我是title"
改成v-aaa="$tx('我是title')"
, 如下:
const handleNodeDirectives = (node) => {
const { props } = node;
// NodeTypes.DIRECTIVE
const directives = props.filter((prop) => prop.type === NODE_TYPE_MAP.DIRECTIVE);
/**
* directive 日志信息输出如下:
* {
type: 7,
name: 'aaa',
exp: {
type: 4,
content: '我是title',
isStatic: false,
constType: 0,
loc: [Object]
},
arg: undefined,
modifiers: [],
loc: { start: [Object], end: [Object], source: 'v-aaa="我是title"' }
}
*/
for (const directive of directives) {
const { exp, name } = directive;
if (!exp || name === 'for') {
continue;
}
const { type: expType, loc: { source, start, end } } = exp;
if (expType === NODE_TYPE_MAP.SIMPLE_EXPRESSION) { // v-aaa、v-title
if (isCN(source)) {
replaceCodes.push({ // 记录
loc: {
start: start.offset,
end: end.offset,
},
content: `$tx('${source}')`,
});
}
}
}
};
到这里咱们的
vue template
算是解析完了 都放在了replaceCodes
里面。
第四步 解析 vue script
对
vue script
进行处理,如下:
for (const child of parse(code).children) {
if (child.tag === 'template') {
handleVueTemplate(child);
}
if (child.tag === 'script') {
handleVueScript(child);
}
}
const handleVueScript = (node) => {
if (node.children.length === 0) {
return;
}
const { loc, content } = node.children[0];
const { start, end } = loc || {};
// 查找 export default 开始索引
const exportDefaultStartIndex = content.search(/export\s*default/);
if(exportDefaultStartIndex !== -1) {
const exportDefaultbeforeCode = content.slice(0, exportDefaultStartIndex);
// scriptTranslated babel 对js转换 再上面
const targetExportDefaultBeforeCode = scriptTranslated(exportDefaultbeforeCode, {
callee: 'window.$tx', // 对export default之外的 用 window.$tx
});
const targetExportDefaultAfterCode = scriptTranslated(content.slice(exportDefaultStartIndex), {
callee: 'this.$tx', // 对export default之内的 用 this.$tx
});
replaceCodes.push({ //收集
loc: {
start: start.offset,
end: end.offset,
},
content: `\n${targetExportDefaultBeforeCode}\n${targetExportDefaultAfterCode}\n`,
});
}
};
需要注意一点就是要区分
export default
之前, 以及之内是做区分的。
第五步 替换
把收集到的
replaceCodes
数据对原数据进行替换,代码如下:
const handleReplaceCode = () => {
const resultsCode = [];
let cursor = 0;
// 排序
const sortReplaceCodes = replaceCodes.sort((pre, next) => pre.loc.start - next.loc.start);
sortReplaceCodes.forEach(({ loc, content }) => {
const { start, end } = loc;
resultsCode.push(code.slice(cursor, start));
resultsCode.push(content); // 分段截取替换
cursor = end;
});
resultsCode.push(code.slice(cursor)); // 最后一段
return resultsCode.join('');
};
const afterReplaceCode = handleReplaceCode();
console.log(afterReplaceCode, 'parse.code....');
console.log(afterReplaceCode, 'parse.code....')
;输出日志:
<template>
<div>
<span>{{ a ? $tx('测试') : 'qq'; }}</span>
<el-button>{{ $tx('按钮') }}</el-button>
<test-component
v-aaa="$tx('我是title')"
:size="CARD_SIZE_MAP.BIG"
:title="$tx('默认值')"
/>
</div>
</template>
<script>
import TestComponent, { CARD_SIZE_MAP } from '../components/card/index.vue';
const TYPE_MAP = {
a: window.$tx('哈哈哈')
};
export default {
name: 'APP',
components: {
TestComponent
},
props: {
BBBB: {
type: [String, Number],
require: true,
default: this.$tx('哈哈')
},
permission: {
type: Boolean,
require: true,
default: true
}
},
data() {
return {
myChart: null,
title: this.$tx('我是哈哈哈')
};
},
methods: {
initResize() {
const a = this.$tx('哈哈哈');
this.myChart?.resize();
}
}
};
</script>
<style lang="less" scoped>
</style>
parse.code
总结
babel 使用后的感受就是还挺有意思,不错不错好极了!!!