提取 Vue 文件中 TS 代码
对于 React 技术栈的子应用项目,TypeScript parser 可以解析 TS(JX)、TSX(JSX) 等类型的代码文件,但对于 Vue 技术栈的子应用项目,如果不对 Vue 文件中的 TS 代码进行分析,就无法得到项目最全面、最准确的 API 调用信息,因为 Vue 文件也可以从目标依赖导入 API 并调用。
那么,有什么办法可以分析 Vue 中的 TS 代码呢?以下面 Vue 文件为例:
<template>
<div class="page">
<div class="header">
<div class="title">
{{ $tr('title_sp_set') }}
</div>
<div class="desc">
{{ $tr('b_basic_set_desc') }}
</div>
</div>
<bn-tabs ref="tabs" :value="tab" @tab-click="handleClick">
<bn-tab-pane :label="$tr('m_bic_settings')" name="sp" />
<bn-tab-pane :label="$tr('m_pri_settings')" name="privacy" />
<bn-tab-pane name="chat">
<span id="bic-setings-tab-chat" slot="label">{{ $tr('m_chat_set') }}</span>
</bn-tab-pane>
<bn-tab-pane :label="$tr('m_notif_settings')" name="notification" />
<bn-tab-pane v-if="showSipTab" name="sip">
<span id="bic-setings-tab-sip" slot="label">{{ $tr('m_sip_set') }}</span>
</bn-tab-pane>
</bn-tabs>
<router-view :can-edit-setting="canEditSetting" />
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { app } from 'framework';
const onrnpStorageKey = 'basicSettingsskp';
@Component
export default class Index extends Vue {
private current = 0;
private showskp = false;
private get canEditSetting() {
return this.$acl.data('food_setting_edit_setting');
}
private hideskp() {
this.showskp = false;
app.localStorage.set(onrnpStorageKey, false);
}
private handleClick(tab: string) {
this.$router.push(`/portal/settings/basic/${tab}`);
}
private mounted() {
const show = app.localStorage.get(onrnpStorageKey);
const showskp = show === null ? true : show;
setTimeout(() => {
this.showskp = showskp;
}, 1000);
}
private beforeDestroy() {
const mm = app.localStorage.get(onrnpStorageKey);
}
}
</script>
<style lang="scss" scoped>
.page {
background-color: #fff;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.12);
border-radius: 2px;
margin-top: 16px;
}
.header {
padding: 24px 24px 8px;
}
.title {
font-size: 22px;
color: $trext-primary;
font-weight: $bold;
margin-bottom: 6px;
}
</style>Vue 文件是由三种类型代码组合而成的,那能不能把 <script lang="ts"></script> 标签内的 TS 代码片段提取出来单独进行分析呢?答案是:可以。
本篇主要学习如何提取、解析 Vue 文件中的 TS 代码,属于分析范式中对 step3 和 step4 步骤的完善,需要大家弄清楚分析 Vue 中 TS 片段与直接分析 TS 代码文件两者的差异,在文档最后我们还会讲解文件解析异常,语法不规范等因素是如何影响分析数据准确性的。
@vue/compiler-dom 编译器
我们先把上面的 Vue 示例代码放入 AST explorer,parser 选择 @vue/compiler-dom,观察一下右侧的 AST 结构:代码被拆解成 template、script 和 style 这三部分,并且通过 AST 节点属性可以获取到 script 标签内 TS 代码完整的字符串信息:
(ps:上图中右侧蓝色框中的部分就是 Vue 中的 TS 代码字符串信息)
言外之意,我们可以通过 @vue/compiler-dom 这个 parser 来解析 Vue,获取其中的 TS 代码片段。
Parse Vue 逻辑实现
既然可以获取到 Vue 中的 TS 代码片段,那么自然可以分析这部分代码,我们可以实现一个名叫 parseVue 的方法来解析 Vue 中的 TS,与 parseTS 一样,返回 AST、checker。
具体包含 3 个步骤,提取 TS 片段、写入临时 TS 文件和解析临时 TS 文件,我们分别来说:
1.提取 TS 片段
要提取 TS 代码片段,肯定要先找到 Vue 文件并获取其文件内容,scanFileVue 与 scanFileTs 类似,用于扫描 Vue 文件,getCode 函数用于读取代码文件的字符串内容,这两个函数我们放在 file 模块中实现,因为它们属于文件操作方面的辅助方法。
file 模块中相关方法的实现如下:
// 扫描VUE文件
exports.scanFileVue = function(scanPath) {
const entryFiles = glob.sync(path.join(process.cwd(), `${scanPath}/**/*.vue`));
// console.log(entryFiles);
return entryFiles;
}
// 获取代码文件内容
exports.getCode = function(fileName) {
try{
const code = fs.readFileSync(fileName, 'utf-8');
// console.log(code);
return code;
}catch(e){
throw e;
}
}在 parse 模块中实现 parseVue 函数, 它会引入 file 模块的 getCode 方法,获取 Vue 代码文件的字符串内容,然后调用 @vue/compiler-dom 解析器的 parse 方法,将 Vue 代码解析为包含三部分代码片段的 AST,通过遍历它的 children 数组,找到其中的 script 片段,获取 content 属性,content 属性值就是 script 片段的代码字符串。
另外我们需要获取 TS 代码片段在 Vue 代码文件中的代码行数据作来作为基准行,这是因为我们分析的是提取后的 TS 片段,在统计代码行信息时需要通过基准行来映射它在 Vue 文件中原本的代码行。
parse 模块中 parseVue 函数的实现如下:
const vueCompiler = require('@vue/compiler-dom'); // Vue编译器
const tsCompiler = require('typescript'); // TS编译器
const { getCode } = require(path.join(__dirname, './file')); // 文件操作
// 解析vue文件中的ts script片段,解析获取ast,checker
exports.parseVue = function(fileName) {
// 获取vue代码
const vueCode = getCode(fileName);
// 解析vue代码
const result = vueCompiler.parse(vueCode);
const children = result.children;
// 获取script片段
let tsCode = '';
let baseLine = 0;
children.forEach(element => {
if (element.tag == 'script') {
tsCode = element.children[0].content;
baseLine = element.loc.start.line - 1;
}
});
console.log(tsCode);
console.log(baseLine);
}2.写入临时 TS 文件
因为 TypeScript parser 在创建 Program 时需要传入 TS 文件的路径作为参数 ,所以我们需要把第一步提取出来的 TS 代码字符串流写入一些临时 TS 文件中,在完成代码分析后,将这些临时文件删除即可。
file 模块中相关方法的实现如下:
// 将TS片段写入TS文件
exports.writeTsFile = function(content, fileName) {
try{
fs.writeFileSync(path.join(process.cwd(),`${fileName}.ts`), content, 'utf8');
}catch(e){
throw e;
}
}writeTsFile 函数用于将指定字符串写入特定路径下的 TS 文件中。
在 constant 模块中定义临时目录文件名常量如下所示。其中,VUETEMPTSDIR 表示临时文件的暂存目录配置。
// vue TS 片段提取后写入临时文件所在的暂存目录
exports.VUETEMPTSDIR = 'vue_temp_ts_dir';然后,我们完善一下 parseVue 函数:
const md5 = require('js-md5'); // md5加密
const vueCompiler = require('@vue/compiler-dom'); // Vue编译器
const tsCompiler = require('typescript'); // TS编译器
const { getCode, writeTsFile } = require(path.join(__dirname, './file')); // 文件工具
const { VUETEMPTSDIR } = require(path.join(__dirname, './constant')); // 常量模块
// 解析vue文件中的ts script片段,解析获取ast,checker
exports.parseVue = function(fileName) {
// 获取vue代码
const vueCode = getCode(fileName);
// 解析vue代码
const result = vueCompiler.parse(vueCode);
const children = result.children;
// 获取 TS 片段
let tsCode = '';
let baseLine = 0;
children.forEach(element => {
if (element.tag == 'script') {
tsCode = element.children[0].content;
baseLine = element.loc.start.line - 1;
}
});
// console.log(tsCode);
// console.log(baseLine);
const ts_hash_name = md5(fileName);
// 将ts片段写入临时目录下的ts文件中
writeTsFile(tsCode, `${VUETEMPTSDIR}/${ts_hash_name}`);
const vue_temp_ts_name = path.join(process.cwd(), `${VUETEMPTSDIR}/${ts_hash_name}.ts`);
}这里通过 js-md5 对 Vue 文件的完整路径字符串进行单向加密处理,生成唯一 hash 串后,将这个 hash 串作为 TS 片段写入临时文件时的文件名。这样做一方面可以防止文件出现重名,另一方面可以保证所有从 Vue 文件提取出来的 TS 片段都可以唯一映射到提取它的 Vue 文件。
下图是在分析过程中临时生成的 TS 文件列表示例:
3.解析临时 TS 文件
最后这一步与 parseTS 基本一致,只不过需要多传递一个 baseLine 的基准行信息,原因我们在上面已经解释过了。
最终,parseVue 函数完整的代码实现如下:
const path = require('path'); // 路径操作
const md5 = require('js-md5'); // md5加密
const vueCompiler = require('@vue/compiler-dom'); // Vue编译器
const tsCompiler = require('typescript'); // TS编译器
const { getCode, writeTsFile } = require(path.join(__dirname, './file')); // 文件工具
const { VUETEMPTSDIR } = require(path.join(__dirname, './constant')); // 常量模块
// 解析vue文件中的ts script片段,解析获取ast,checker
exports.parseVue = function(fileName) {
// 获取vue代码
const vueCode = getCode(fileName);
// 解析vue代码
const result = vueCompiler.parse(vueCode);
const children = result.children;
// 获取script片段
let tsCode = '';
let baseLine = 0;
children.forEach(element => {
if (element.tag == 'script') {
tsCode = element.children[0].content;
baseLine = element.loc.start.line - 1;
}
});
// console.log(tsCode);
// console.log(baseLine);
const ts_hash_name = md5(fileName);
// 将ts片段写入临时目录下的ts文件中
writeTsFile(tsCode, `${VUETEMPTSDIR}/${ts_hash_name}`);
const vue_temp_ts_name = path.join(process.cwd(), `${VUETEMPTSDIR}/${ts_hash_name}.ts`);
// 将ts代码转化为AST
const program = tsCompiler.createProgram([vue_temp_ts_name], {})
const ast = program.getSourceFile(vue_temp_ts_name);
const checker = program.getTypeChecker();
// console.log(ast);
return { ast, checker, baseLine };
}
// 解析ts文件代码,获取ast,checker
exports.parseTs = function(fileName) {
// 将ts代码转化为AST
const program = tsCompiler.createProgram([fileName], {})
const ast = program.getSourceFile(fileName);
const checker = program.getTypeChecker();
// console.log(ast);
return { ast, checker };
}有了 parseVue 函数,我们就可以像解析 TS 文件那样解析 Vue 中的 TS 代码,进而对其进行调用分析了。
代码文件解析异常
影响代码分析准确性的因素会有很多,其中最直接的莫过于解析异常了,试想一下如果一个文件在 parse 阶段就出错了,AST 都没法正常获取,那么就没法进行分析了。举个例子:
<template>
<div class="page">
<div class="title">
{{ 'title_settings' }}
</div>
<div v-if="showAntest" class="onboarding">
<div class="icemecontent">
<div class="icemetarget">
<div class="icemetarget-text">
{{ currentAntest.text }}
</div>
</div>
<div class="icemetips">
<div class="icemetips-content">
{{ currentAntest.content }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { app } from 'framework';
const testStorageKey = 'basicSettings';
@Component
export default class Index extends Vue {
private title_settings = 10;
private showAntest = false;
private get currentAntest() {
return {
text: '222',
content: 'iceman'
};
}
private beforeDestroy() {
const mm = app.cookie.get(testStorageKey);
document.documentElement.style.overflow = '';
}
}
</script>
<style lang="scss" scoped>
.page {
background-color: #fff;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.12);
border-radius: 2px;
margin-top: 16px;
}
</style>上面的 Vue 文件在 parse 时会报错:SyntaxError: Element is missing end tag. at createCompilerError,原因是 template 中存在 div 未闭合。大家可能会觉的这种异常文件应该很少,但实际情况是项目在迭代过程中,一些废弃的文件可能会一直残留在项目中,因为它们不参与构建,所以有问题也不会被发现。另外,还有一些同学对于小的变更会直接修改代码文件,不进行本地调试,这也可能导致有问题的代码文件被提交到仓库中。
我们无法保证需要分析的 Vue、TS 文件一定是规范的,所以在分析过程中可以统计解析失败的文件及错误信息,将这些信息输出到代码分析报告中,推动开发者去修复文件,提升分析准确性。
_scanCode 函数在解析代码文件的外层逻辑会添加 try catch 来捕获可能出现的解析错误,在捕获到错误后将错误信息添加到 codeAnalysis 实例的 parseErrorInfos 属性中。相关代码如下,完整代码参考 lib/analysis.js:
// 扫描文件,分析代码
_scanCode(scanSource, type) {
let entrys = this._scanFiles(scanSource, type);
// console.log(entrys);
entrys.forEach((item)=>{
const parseFiles = item.parse;
if(parseFiles.length>0){
parseFiles.forEach((element, eIndex) => {
const showPath = item.name + '&' + item.show[eIndex];
try {
if(type === CODEFILETYPE.VUE){
const { ast, checker, baseLine } = parseVue(element); // 解析vue文件中的ts script片段,将其转化为AST
const importItems = this._findImportItems(ast, showPath, baseLine); // 从import语句中获取导入的需要分析的目标API
// console.log(importItems);
if(Object.keys(importItems).length>0 || this._browserApis.length>0){
this._dealAST(importItems, ast, checker, showPath, item.name, item.httpRepo, baseLine); // 递归分析AST,统计相关信息
}
}else if(type === CODEFILETYPE.TS){
const { ast, checker } = parseTs(element); // 解析ts文件代码,将其转化为AST
const importItems = this._findImportItems(ast, showPath); // 从import语句中获取导入的需要分析的目标API
// console.log(importItems);
if(Object.keys(importItems).length>0 || this._browserApis.length>0){
this._dealAST(importItems, ast, checker, showPath, item.name, item.httpRepo); // 递归分析AST,统计相关信息
}
}
} catch (e) {
const info = {
projectName: item.name,
httpRepo: item.httpRepo + item.show[eIndex],
file: item.show[eIndex],
stack: e.stack
}
this.parseErrorInfos.push(info);
this.addDiagnosisInfo(info);
}
processLog.stdout(chalk.green(`\n${item.name} ${type}分析进度: ${eIndex+1}/${parseFiles.length}`));
});
}
})
}规范代码语法
除了代码文件解析异常,我们还需要注意语法错误对于分析准确性的影响。举个例子:
// 错误语法,或无意义代码
const env } 'iceman';
if(d){将上面的代码放入 TypeScript AST Viewer,发现它可以正常解析成 AST,但这些语句是存在问题的,并没有分析意义,甚至会对正常的分析造成干扰。
所以在实际应用中,建议使用者在代码分析前进行 ESLint 语法检查,减少不必要的错误,提升分析准确性。
配置解析 Vue
配置文件中的 isScanVue 配置项用来控制是否开启 Vue 文件的扫描分析流程,对于有 Vue 文件的项目开启即可,下面是入口函数 analysis 关于分析 Vue 文件的逻辑演示,完整代码参考 lib/analysis.js:
const { CODEFILETYPE } = require(path.join(__dirname, './constant')); // 常量模块
// 代码分析基础类
class CodeAnalysis {
// 构造函数
constructor(options) {
......
// 私有属性
this._scanSource = options.scanSource; // 扫描源配置信息
this._isScanVue = options.isScanVue || false; // 是否扫描Vue配置
......
}
// 入口函数
analysis() {
......
// 扫描分析Vue
if(this._isScanVue){ // 开启配置则进行分析
this._scanCode(this._scanSource, CODEFILETYPE.VUE);
}
// 扫描分析TS
this._scanCode(this._scanSource, CODEFILETYPE.TS);
......
}
}小结
这一小节我们学习了如何提取解析 Vue 文件中的 TS 代码片段,需要大家掌握以下知识点:
- 通过
@vue/compiler-dom这个 parser 可以将 Vue 解析为包含三种代码类型的 AST,通过节点属性可以获取其中的 TS 代码片段字符串信息。 - 解析 TS 需要创建 program,所以需要把提取出来的 TS 代码字符串流写入一些临时 TS 文件中,通过 js-md5 生成的文件名 hash 名可用于映射提取 TS 片段的原 Vue 文件。
- 代码文件解析异常,待分析代码文件中存在语法问题等因素都会影响代码分析的准确性。
学习完本篇程,对于 Vue 文件中的 TS,我们也可以分析其最真实的 API 调用情况了。