背景:笔者参与了一个迭代6年之久的 vue2 项目,里面的老旧代码比较多(屎山上玩耍), 随着深度参与发现几个问题
- 在组内把部分业务模块用微前端拆分出去重构之后,如何敢大胆的删除这些老旧不需要的代码是一个问题?因为历史悠久,此项目dev模式启动一次需要3分钟,热更新一次30s,prod 模式构建一次需要8分钟。(
后续有一篇文章<还没写> 介绍我是如何利用 webpack 熔断机制 在dev模式完成40s启动,1s-2s内完成热更新。)- 因为一些历史原因,有的功能相似的组件文件有2份,(直接复制一份老的文件,在上面改吧改吧直接使用。当时为了快速上线实现功能) 如何敢大胆的删除这个已经废弃的组件。如何知道项目中还有没有对这个文件进行引用
- 针对问题2 部分同学说我直接搜索 组件文件路径然后看有没有好了。其实吧,也不是不可以。就是场景比较复杂, 因为我们要考虑的事情要一点。
- 组件可能直接被 import xx from './pathA/pathB/index.vue' 这种方式引入,也可能使用 dynamic import 引入其实就是 import('./pathB/index.vue'), 也可能在项目中存在多个 alis 配置,发现引入方式多种,通过 vscode查找要多种方式一起查找,比较费心费力。
- 如何解决: 当然是编写脚本,自定义查找被引用的.vue文件
我有一个文件,希望查找到该文件所有引用的.vue 其他文件 (可能通过 import xxx, 也可能通过 import('./xx') 这两种方式引入)
页面的根.vue 文件为 client/pages/_lang/xxx/yyy/index.vue(此文件是一个大模块的入口文件),我希望查找到该组件引用的 所有的 .vue 文件(因为我决定在老项目删除该模块),在 getURLByPath 函数里输入改路径,输出结果包含所有的引用组件
效果:
难点解析:
- 如何让AST工具识别.vue 文件,我们现在手上只有vue文件,他不是一个js文件,没办法用babel来进行ast?
- .vue 文件被编译成为js文件之后,我该如何解析该js文件,如何找到原.vue 文件里面所有 import 的资源?哪些import 节点需要我再次解析呢?
难点分解:
- 我们知道 babel 只能对js进行ast,我们用@vue/component-compiler首先把.vue 编译成为一个单个的js文件,在对其进行ast
- 我们使用自定义babel插件记录结果,并且在发现的 import节点和 import() 节点进行递归编译
核心代码: 核心就是对单个入口.vue 文件进行编译,然后使用 babel 对这个js进行再次编译,同时在自定义 babel plugin 找到几个节点(ImportDeclaration 节点、同时需要找到 import() 节点)
脚本:
const fs = require('fs')
const path = require('path')
const babel = require('@babel/core')
const { assemble, createDefaultCompiler } = require('@vue/component-compiler')
// 编译 vue 文件
const compileVueFile = (content, filename = 'test.vue') => {
const compiler = createDefaultCompiler()
const descriptor = compiler.compileToDescriptor(filename, content)
const result = assemble(compiler, filename, descriptor, {})
return result
}
// 组装路径
const getFilePath = (initPath, importFilePath) => {
const dirname = path.dirname(initPath)
return path.resolve(dirname, importFilePath)
}
// 自定义的babel plugin
// 需要找到 ImportDeclaration 节点,同时需要找到 import() 节点
const recordImportByBabelPlugin = function({ types }, options) {
return {
visitor: {
// 找到 import 的 资源
ImportDeclaration(astPath) {
const importFilePath = astPath.node.source.value // astPath.get('source').toString()
// 有 / 代表不是 node_modules 里面的
const hasSlash = importFilePath.includes('/')
if (hasSlash) {
let flag = importFilePath.startsWith('~') || importFilePath.startsWith('@')
const newImportFilePath = flag
? 'client' + importFilePath.slice(1)
: importFilePath
let isRelativePath =
importFilePath.startsWith('./') || importFilePath.startsWith('../')
const newFilePath = isRelativePath
? getFilePath(options.file, importFilePath)
: path.resolve(__dirname, newImportFilePath)
const relativePath = path.relative(__dirname, newFilePath)
const isExits = fs.existsSync(newFilePath)
// console.log(newFilePath, 'relativePathrelativePathrelativePath', isExits)
if (isExits && newFilePath.indexOf('.') !== -1) {
getURLByPath([relativePath])
} else {
const extList = ['.vue', '/index.vue', '.js', '/index.js']
extList.forEach((itemExt) => {
const possibleFilePath = newFilePath + itemExt
const possibleFileRelativePath = path.relative(
__dirname,
possibleFilePath
)
const ispossibleExits = fs.existsSync(possibleFileRelativePath)
if (ispossibleExits) {
getURLByPath([possibleFileRelativePath])
}
})
}
}
options.resultList.push(importFilePath)
},
CallExpression(astPath) {
const calleeName = astPath.get('callee').toString()
// import() 资源
if (calleeName === 'import') {
const importFilePath =
astPath.node.arguments[0].value ??
astPath.node.arguments[0].quasis[0].value.raw
// console.log(importFilePath, 'importFilePathimportFilePath')
// 有 / 代表不是 node_modules 里面的
const hasSlash = importFilePath.includes('/')
if (hasSlash) {
let flag = importFilePath.startsWith('~') || importFilePath.startsWith('@')
const newImportFilePath = flag
? path.join('client', importFilePath.slice(1))
: importFilePath
let isRelativePath =
importFilePath.startsWith('./') || importFilePath.startsWith('../')
const newFilePath = isRelativePath
? getFilePath(options.file, importFilePath)
: path.resolve(__dirname, newImportFilePath)
const relativePath = path.relative(__dirname, newFilePath)
const isExits = fs.existsSync(newFilePath)
if (isExits && newFilePath.indexOf('.') !== -1) {
getURLByPath([relativePath])
} else {
const extList = ['.vue', '/index.vue', '.js', '/index.js']
extList.forEach((itemExt) => {
const possibleFilePath = newFilePath + itemExt
const possibleFileRelativePath = path.relative(
__dirname,
possibleFilePath
)
const ispossibleExits = fs.existsSync(possibleFileRelativePath)
if (ispossibleExits) {
getURLByPath([possibleFileRelativePath])
}
})
}
}
options.resultList.push(importFilePath)
}
}
}
}
}
const result = {}
const getURLByPath = (pathList) => {
return pathList.forEach((item) => {
const itemPath = path.posix.join(__dirname, item)
let resultList = []
if (fs.existsSync(itemPath)) {
// console.log(itemPath, 'itemPathitemPathitemPathitemPathitemPathitemPath')
let sourceCode = fs.readFileSync(itemPath).toString()
let compiledCode = null
// 把 .vue 编译 为 js
if (itemPath.endsWith('.vue')) {
compiledCode = compileVueFile(sourceCode).code
}
// 编译 纯 js 文件
if (itemPath.endsWith('.vue') || itemPath.endsWith('.js')) {
babel.transformSync(compiledCode ?? sourceCode, {
presets: [
[
'@babel/preset-env',
{
modules: false
}
],
'@babel/preset-flow'
],
plugins: [[recordImportByBabelPlugin, { resultList, file: itemPath }]]
})
}
} else {
throw new Error(`此文件路径不存在, 请检查路径 在执行${itemPath} `)
}
result[item] = resultList
})
}
getURLByPath(['client/pages/_lang/pathA/pathB/index.vue'])
console.log(
'resultObj -> 输出结果 -> 引用的组件为:',
Object.keys(result).filter((_) => _.endsWith('.vue'))
)