likes
comments
collection
share

用一个Babel插件简化重复工作(一),解放双手只能解放🤏一点点用一个babel插件简化重复工作(一),丢弃重复工作,

作者站长头像
站长
· 阅读数 48

工具创作背景

这在期间依然没有放松学习,了解了 AST , Babel 插件等简单的基本原理等。 不要问怎么学的,问就是因为朋友无意的一句话,好奇,这句话就是 “他们小程序打包体积过大,然后他们使用vite 写了个插件把图片等静态资源替换成CDN上的了”。

然后加上之前对 AST有一丁点了解。这要追溯到几年前,公司的小程序项目使用wepy框架开发的,但是这个框架呢是一个个人开发者整的,随着时间的推移,项目的逐渐庞大,显得开发效率上不去,力不从新,对于一些缺陷的处理都是采用一些非正常思维去规避;同时需要开发h5,支付宝小程序等各种,基于这些种种原因,一番调研之后决定使用uniapp 进行开发;这就涉及到框架的升级,当时的处理方式就是专门一个同学开发升级工具,其他同学依然就行迭代,这个工具就是使用 babel 对项目源代码进行转化适配uniapp的语法,最后经过这位同学的不懈努力,日日加班,终于在一段时间之后工具成行,能够进行升级了,效果也还算非常理想,工具自动处理了90%+的转换工作;剩下的10%仍然需要我们进行手动的细微调整,最终升级的还是挺顺利。

在之后的一次前端技术分享会议上,该同学就讲解了AST,babel编译器,哪个时候还是非常感兴趣的,但是因为没有提前了解相关信息,当时听的也只是个大概,看演示呢,也是云里雾里的,但是核心思想还是听明白了,就是 “源代码 转换成AST,对AST进行调整,最后生成目标代码, 基于AST甚至可以跨语言进行代码转化“;你说这么有意思那指定得去玩玩啊;这颗想玩玩的种子一直深埋心底,等待合适的机会。

面临的问题

作为先行者这段时间经过了几个礼拜的陆陆续续的探索实践,整理了一些文档,包括了改动范围,以及常见问题等等,这些文档能有效降低同事频繁的找你看问题处理问题的频率,也算是解放自己的一种方式,不然真的就需要到处看问题,说解决方案等等。

经过一次简短的会议后,接下来就迎来了对项目批量升级的日程计划,工期两周完成升级,并且日常的迭代需求一个不能落下;可谓是时间紧任务重,况且针对需要升级的改动范围也是相当的广,基本上 src 下90%+的文件都需要进行调整适配;

在每个项目这么大范围的改动下,保障几十个项目的顺利升级,就需要思考以下的问题了:

1.如何保证时效性,毕竟迭代周期也有限,不可能停下来专门升级而不进行需求的迭代;

2.如何才能最小改动,毕竟最小改动就意味着工作量的陡降;测试范围的缩小;时间上的节省;

3.几十个项目的升级,平均每个人负责3个左右的项目升级,能不能有什么方法能够批量处理;

总结一下这几个问题的关键点就是,时间,工作量,准确性,大量重复操作;

分析问题

第一个需要解决的问题就是对 src/api 文件的改造,一个项目大概最少都有40多个API文件

原始API 文件的形式如下:

import {baseRequert} from '@/plugin/request'

const newPage = 'temp'

const versionList = (p)=> baseRequest.delete('xxx',{...p})

export const versionList2 = (p)=>{
    return  baseRequest.delete('/v1.0/xxx',{...p})
}
function versionPage(p){
    return systemApi.get('/a/xxx',{...p})
}
export function versionDetail(p){
    return baseRequest.post('/b/xxx',{...p})
}

const test =(id)=>{
    return baseRequest.post(`test/a/${id}`)
}

const testC = (id)=>baseRequest.post(`test/a/${id}`)

const testCb = (id)=>{
    return baseRequest.post(`test/a/${id}`)
}

const origin = ()=>{
    return axias.create({
        baseUrl:"127.0.0.01",
        headers:{
            contentType:'application/json;charset=utf-8'
        }
    }).post('/a/b/c')
}


export default {
    versionList,
    versionPage,
    versionDetail,
    test,
    testCb,
    origin,
    testC
}

这就是我们的API文件;包含了 箭头函数,普通函数,既export 对象,又export某些单一的函数等等场景;

然而新框架对API有了新的规范和约定;需要统一放置在apis 下面;export 默认的函数,函数内 return 所有的 api 函数名;如下:

export default ($axios,{$service})=>{
    
    return {
         versionList(data={}){
             $axios.$post('xxx',data)
         },
    }
}

我们需要处理的就是把原来的文件迁移到新的目录,并按指定的要求进行改造;所以整个改动还是挺大的,看上去改动不大但实际上一点也不小;毕竟一个项目几十个,几十个项目就是几百个,光用 CV替换大法,估计人都得改傻。

解决问题

既然手动费神费力,那我们就写工具自动转化,思来想去,这基于AST的代码转换不就非常符合这场景嘛;

经过了几个小时对AST的初步了解,和一下午demo练习,基本上大致明白该怎么玩的;

说到AST就不能不提这个强有力的在线工具astexplorer ,他能够实时的预览源代码生存的AST对象; 让我们简单的感受一下它的魅力:

用一个Babel插件简化重复工作(一),解放双手只能解放🤏一点点用一个babel插件简化重复工作(一),丢弃重复工作,

左边就是源代码,右边就是AST的tree,工具功能挺丰富 有兴趣的可以去玩玩;

AST描述了每一个语句;包括了类型,位置等等各种信息;

有了这个工具我们就能轻松知道源代码对应的AST是什么样子的了;

我们再对AST树进行修改调整;

最后再把AST生成代码即可达到精准修改源码的目的;

参考示例

既然知道了整个流程接下来我们就直接可以开写了:

第一步获取API目录下的所有文件,对于有嵌套层级的文件进行拍平,让其处于只有一级目录;

import fastGlob from 'fast-glob'
// 获取所有api 文件,借助fastGlob包可以有效简化获取目录文件这类操作
const getApiFiles = ()=> fastGlob.sync(['**/vue/src/api/**/*.js'], { dot: true });

第二步 读取文件内容,替换里面的export 关键字

let originCode = fs.readFileSync(path.resolve(apiFile))
originCode = originCode.toString()

originCode = originCode.replaceAll('export const','const')
originCode = originCode.replaceAll('export function','function')

第三步 编写Babel插件修改AST

// api/file => apis/file
function changeApiFileContent(apiFile){
    const exports = [] // 收集需要export 的函数
    const service = [] 
    const changeContentPlugin=()=>{
        return {
            visitor:{
//删除所有的import,export default 节点               'ImportDeclaration|ExportDefaultDeclaration'(path,state){
                    path.remove()
                },
                // 普通函数 获取函数名称存入 需要导出的数组
                FunctionDeclaration(path,state){
                    const {node} = path
                    const functionName = node.id.name
                    exports.push(functionName)
                },
                // 处理 箭头函数
                VariableDeclaration(path,state){
                    const {node} = path
                    const {declarations} = node
                    const node2 = declarations[0]
                    const functionName = node2.id.name
                    const {type,body} = node2.init
                    if(type ==='ArrowFunctionExpression'){
                        exports.push(functionName)

                        if(body.type==='CallExpression'){
                            const serveName = body.callee.object.name
                            if(!service.includes(serveName)){
                                service.push(serveName)
                            }
                            const method = body.callee.property.name

                                console.log("serveName======>",serveName,method)
                            body.callee.object.name ='$axios'
                            body.callee.property.name = '$'+method

                            body.arguments[0] = changeFunctionArgs(body.arguments[0],serveName)
                        }
                    }

                },
                // 处理return 语句
                ReturnStatement(path,state){
                    const node = path.node
                    const serveName = node.argument.callee.object.name
                    if(!service.includes(serveName)){
                        service.push(serveName)
                    }
                    const method = node.argument.callee.property.name
                    node.argument.callee.object.name ='$axios'
                    node.argument.callee.property.name = '$'+method
                    const body =node.argument
                    body.arguments[0] = changeFunctionArgs(body.arguments[0],serveName)

                },
            }
        }
    }

    let originCode = fs.readFileSync(path.resolve(apiFile))
    originCode = originCode.toString()

    originCode = originCode.replaceAll('export const','const')
    originCode = originCode.replaceAll('export function','function')

    // console.log("originCode>>>>>",originCode)
    const targetSource = core.transform(originCode,{
        plugins:[changeContentPlugin()]
    })
    // console.log("exports",exports)
    // console.log("service",service)
    // console.log("输出代码为:\n")
    // console.log(targetSource.code)
    
    return targetSource
}

第四步 对修改后的AST生成代码

// 构造目标代码
const targetCode = `
export default ($axios,{$service})=>{
    const {${service.join(',')}} = $service
    
    ${targetSource.code}
   
    return {
        ${exports.join(',')}
    }
}

// 生成新的文件
let fileName = getFileName(fPath)
// console.log("------",fileName,path.resolve(fPath))
let content =`// ${fPath} => ${fileName} \r` // 记录一下该文件是从哪个文件生成的;
content += targetCode
// console.log("fPath",fileName,content)
fs.writeFileSync(path.resolve(targetPath+'/'+fileName),content.toString(), 'utf8');
console.log("已创建:",targetPath+'/'+fileName)

特殊场景处理

处理 url 的拼接,url 的类型有这么几种:

  1. 直接字符串:'/v1.0/user/page'

  2. 字符串表达式:'/v1.0/user/detail/'+id

  3. 模版字符串:`/v1.0/user/detail/${id}'

  4. 模版字符串表达式:`/v1.0/user/detail/${id}/'+name


// 处理url参数;
const changeFunctionArgs=(argument,serveName)=>{

    const firstArg = argument
    console.log("firstArg",firstArg.type)

    let right = null

    // StringLiteral : 'a/bb/c'
    if(firstArg.type==='StringLiteral'){
        right =  {
            type:'StringLiteral',
            value: argument.value
        }
    }
    //BinaryExpression:  'test/a/'+id
    if(firstArg.type==='BinaryExpression'){
        right =  {
            type:'BinaryExpression',
            left: firstArg.left,
            operator:firstArg.operator,
            right:firstArg.right
        }
    }
    // TemplateLiteral : `test/a/${id}`
    if(firstArg.type ==='TemplateLiteral'){
        right = {
            type:'TemplateLiteral',
            expressions: firstArg.expressions,
            quasis:firstArg.quasis
        }
    }

    // 构造BinaryExpression serveName + 原始参数(url)
    argument.type = 'BinaryExpression'
    argument.left = {
        type:'Identifier',
        name:serveName
    }// 左节点
    argument.operator ='+' //操作符
    argument.right = right //右节点

    return argument
}

整个流程下来就基本完成了代码的转换文件的生成;

最终生成的文件样子就是这个样子了:

// src/vue/src/api/xxx.js => xxx.js
export default ($axios,{$service})=>{
    const {baseRequest} = $service

    const newPage = 'xxx';
    const versionList = p => $axios.$delete(baseRequest + "xxx", {
        ...p
    });
    const versionList2 = p => {
        return $axios.$delete(baseRequest + "/v1.0/xxx", {
            ...p
        });
    };
    function versionPage(p) {
        return $axios.$get(systemApi + "/a/xxx", {
            ...p
        });
    }
    function versionDetail(p) {
        return $axios.$post(baseRequest + "/b/xxx", {
            ...p
        });
    }
    const test = id => {
        return $axios.$post(baseRequest + `test/a/${id}`);
    };
    const testC = id => $axios.$post(baseRequest + `test/a/${id}`);
    const testCb = id => {
        return $axios.$post(baseRequest + `test/a/${id}`);
    };

    return {
        versionList,versionList2,versionPage,versionDetail,test,testC,testCb
    }
}

结果符合预期,唯一不足的是,代码缩进有点问题。 当然这个是可以处理的,比如配置eslint 的插件进行格式化。或者简单暴力 直接扔进项目中 使用 lint fix 脚步去处理;

轻轻松松处理了代码的转换;

提升了效率,原本一个项目的api目录文件的迁移改动;

最终我们脚本一运行 咔咔咔 两三秒秒钟就处理好了;

最终较完整的参考代码

import core from '@babel/core'
import path from 'path'
import fs from 'fs'
import { getApiFiles } from './demo11.js'

// 处理url参数;
const changeFunctionArgs=(argument,serveName)=>{

    const firstArg = argument
    console.log("firstArg",firstArg.type)

    let right = null

    // StringLiteral : 'a/bb/c'
    if(firstArg.type==='StringLiteral'){
        right =  {
            type:'StringLiteral',
            value: argument.value
        }
    }
    //BinaryExpression:  'test/a/'+id
    if(firstArg.type==='BinaryExpression'){
        right =  {
            type:'BinaryExpression',
            left: firstArg.left,
            operator:firstArg.operator,
            right:firstArg.right
        }
    }
    // TemplateLiteral : `test/a/${id}`
    if(firstArg.type ==='TemplateLiteral'){
        right = {
            type:'TemplateLiteral',
            expressions: firstArg.expressions,
            quasis:firstArg.quasis
        }
    }

    // 构造BinaryExpression serveName + 原始参数(url)
    argument.type = 'BinaryExpression'
    argument.left = {
        type:'Identifier',
        name:serveName
    }// 左节点
    argument.operator ='+' //操作符
    argument.right = right //右节点

    return argument
}



// api/file => apis/files
function changeApiFileContent(apiFile){
    const exports = []
    const service = []
    const changeContentPlugin=()=>{
        return {
            visitor:{

                'ImportDeclaration|ExportDefaultDeclaration'(path,state){
                    path.remove()
                },
                // 普通函数
                FunctionDeclaration(path,state){
                    const {node} = path
                    const functionName = node.id.name
                    exports.push(functionName)
                },
                // 箭头函数
                VariableDeclaration(path,state){
                    const {node} = path
                    const {declarations} = node
                    const node2 = declarations[0]
                    const functionName = node2.id.name
                    const {type,body} = node2.init
                    if(type ==='ArrowFunctionExpression'){
                        exports.push(functionName)

                        if(body.type==='CallExpression'){
                            const serveName = body.callee.object.name
                            if(!service.includes(serveName)){
                                service.push(serveName)
                            }
                            const method = body.callee.property.name

                            console.log("serveName======>",serveName,method)
                            body.callee.object.name ='$axios'
                            body.callee.property.name = '$'+method

                            body.arguments[0] = changeFunctionArgs(body.arguments[0],serveName)
                        }
                    }

                },
                ReturnStatement(path,state){
                    const node = path.node
                    const serveName = node.argument.callee.object.name
                    if(!service.includes(serveName)){
                        service.push(serveName)
                    }
                    const method = node.argument.callee.property.name
                    node.argument.callee.object.name ='$axios'
                    node.argument.callee.property.name = '$'+method
                    const body =node.argument
                    body.arguments[0] = changeFunctionArgs(body.arguments[0],serveName)

                },
            }
        }
    }

    let originCode = fs.readFileSync(path.resolve(apiFile))
    originCode = originCode.toString()

    originCode = originCode.replaceAll('export const','const')
    originCode = originCode.replaceAll('export function','function')

    // console.log("originCode>>>>>",originCode)
    const targetSource = core.transform(originCode,{
        plugins:[changeContentPlugin()]
    })
    // console.log("exports",exports)
    // console.log("service",service)
    // console.log("输出代码为:\n")
    // console.log(targetSource.code)

    const targetCode = `
export default ($axios,{$service})=>{
    const {${service.join(',')}} = $service
    
    ${targetSource.code}
   
    return {
        ${exports.join(',')}
    }
}
`
    return targetCode
}
function checkPath(dirPath) {
   return new Promise(resolve => {
       // 尝试访问路径,如果不存在则创建它
       fs.access(dirPath, fs.constants.F_OK, (err) => {
           if (err) {
               // 路径不存在,创建目录
               fs.mkdir(dirPath, { recursive: true }, (mkdirErr) => {
                   if (mkdirErr) {
                       console.error(`无法创建目录:${dirPath} Error: , ${mkdirErr}`);
                       resolve('无法创建目录')
                   } else {
                       console.log('目录已创建:',dirPath);
                       resolve('目录已创建')
                   }
               });
           } else {
               console.log('目录已存在将进行覆盖:',dirPath);
               resolve('目录已存在')
           }
       });
   })
}

/**
 *
 * @param oldFiles
 * @param targetPath
 * @returns {Promise<void>}
 */
async function createApis(oldFiles,targetPath){
    await checkPath(targetPath)
    // console.log("targetPath",oldFiles,targetPath)
    const getFileName = (filePath)=>{
        const filePaths = filePath.split('/')
        let fileName = ''
        if(filePath.includes('index.js')){
            fileName = filePaths[filePaths.length-2]+'.js'
        }else {
            fileName = filePaths[filePaths.length-1]
        }
        return fileName
    }

    oldFiles.forEach((fPath)=>{
        let fileName = getFileName(fPath)
        // console.log("------",fileName,path.resolve(fPath))
        let content =`// ${fPath} => ${fileName} \r`
        const code = changeApiFileContent(fPath)
        content += code
        // console.log("fPath",fileName,content)
        fs.writeFileSync(path.resolve(targetPath+'/'+fileName),content.toString(), 'utf8');
        console.log("已创建:",targetPath+'/'+fileName)
    })
}

createApis(getApiFiles(),path.resolve('src/vue/src/apis'))

最后的最后

下面请上我们今天的主角:有请小趴菜

用一个Babel插件简化重复工作(一),解放双手只能解放🤏一点点用一个babel插件简化重复工作(一),丢弃重复工作,

转载自:https://juejin.cn/post/7409948990819958799
评论
请登录