问:如何在复杂大型项目中大胆删除不需要的.vue文件? 答: 自定义 babel plugin

263 阅读4分钟

背景:笔者参与了一个迭代6年之久的 vue2 项目,里面的老旧代码比较多(屎山上玩耍), 随着深度参与发现几个问题

  1. 在组内把部分业务模块用微前端拆分出去重构之后,如何敢大胆的删除这些老旧不需要的代码是一个问题?因为历史悠久,此项目dev模式启动一次需要3分钟,热更新一次30s,prod 模式构建一次需要8分钟。(后续有一篇文章<还没写> 介绍我是如何利用 webpack 熔断机制 在dev模式完成40s启动,1s-2s内完成热更新。)
  2. 因为一些历史原因,有的功能相似的组件文件有2份,(直接复制一份老的文件,在上面改吧改吧直接使用。当时为了快速上线实现功能) 如何敢大胆的删除这个已经废弃的组件。如何知道项目中还有没有对这个文件进行引用
  3. 针对问题2 部分同学说我直接搜索 组件文件路径然后看有没有好了。其实吧,也不是不可以。就是场景比较复杂, 因为我们要考虑的事情要一点。
  4. 组件可能直接被 import xx from './pathA/pathB/index.vue' 这种方式引入,也可能使用 dynamic import 引入其实就是 import('./pathB/index.vue'), 也可能在项目中存在多个 alis 配置,发现引入方式多种,通过 vscode查找要多种方式一起查找,比较费心费力。
  5. 如何解决: 当然是编写脚本,自定义查找被引用的.vue文件

我有一个文件,希望查找到该文件所有引用的.vue 其他文件 (可能通过 import xxx, 也可能通过 import('./xx') 这两种方式引入)

页面的根.vue 文件为 client/pages/_lang/xxx/yyy/index.vue(此文件是一个大模块的入口文件),我希望查找到该组件引用的 所有的 .vue 文件(因为我决定在老项目删除该模块),在 getURLByPath 函数里输入改路径,输出结果包含所有的引用组件

效果:

image.png

难点解析:

  1. 如何让AST工具识别.vue 文件,我们现在手上只有vue文件,他不是一个js文件,没办法用babel来进行ast?
  2. .vue 文件被编译成为js文件之后,我该如何解析该js文件,如何找到原.vue 文件里面所有 import 的资源?哪些import 节点需要我再次解析呢?

难点分解:

  1. 我们知道 babel 只能对js进行ast,我们用@vue/component-compiler首先把.vue 编译成为一个单个的js文件,在对其进行ast
  2. 我们使用自定义babel插件记录结果,并且在发现的 import节点和 import() 节点进行递归编译

核心代码: 核心就是对单个入口.vue 文件进行编译,然后使用 babel 对这个js进行再次编译,同时在自定义 babel plugin 找到几个节点(ImportDeclaration 节点、同时需要找到 import() 节点)

image.png

脚本:


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'))
)