{T}

微前端业务全应用代码分析

上一篇的应用实践是从依赖调用方角度展开的,学习了如何通过代码分析工具拦截业务代码中的“脏调用”,以及如何通过 CI 自动化实现代码告警、分析报告推送等。本篇我们从依赖提供方角度出发,学习如何实现微前端业务全应用层面的代码分析,帮助开发者了解基础项目导出的 API 在全部子应用项目中真实的调用情况,从而更好的评估上下线影响,更好的管控框架重构进度。

本文档的示例项目在 all-projects-analysis,建议大家 clone 到本地,对照代码来学习。

全应用代码分析报告

全应用代码分析报告对于评估 API 上/下 线影响,把控基础库升级进度等有着非常重要的意义。

实现原理

想要获取全局层面的 API 调用信息,需要分析所有依赖调用方的项目代码,分析工具配置文件中 scanSource 配置项是数组结构,是支持多扫描源配置的。

typescript
......
scanSource:[
    {
        name: 'ProjectA',
        path: ['code/ProjectA/src'],
        packageFile: 'code/ProjectA/package.json',
        format: (str) => {  // 分析报告中展示路径的fotmat处理,排除多项目对于展示结果的影响
            return str.replace('code/ProjectA/','');
        }
    },
    {
        name: 'ProjectB',
        path: ['code/ProjectB/src'],
        packageFile: 'code/ProjectB/package.json',
        format: (str) => {  // 分析报告中展示路径的fotmat处理,排除多项目对于展示结果的影响
            return str.replace('code/ProjectB/','');
        }
    },
    {
        name: 'ProjectC',
        path: ['code/ProjectC/src'],
        packageFile: 'code/ProjectC/package.json',
        format: (str) => {  // 分析报告中展示路径的fotmat处理,排除多项目对于展示结果的影响
            return str.replace('code/ProjectC/','');
        }
    }
]
......

scanSource 配置中的 path 属性是确定路径,也就是说分析工具需要所有待扫描的项目代码在一些指定路径下,但不同项目来自不同的 GitLab repo,所以我们需要主动给分析工具创造一个多项目的工作环境。

我们的方案是创建一个独立仓库(如:all-projects-analysis),在分析前先把所有需要分析的项目代码通过脚本下载到该仓库指定目录下,然后根据这个目录来生成 scanSource 配置,最后再执行代码分析。

方案优点:全应用代码分析不会影响到各个子应用项目,而且可以在独立的项目中配置 GitLab CI,用于自动化分析,推送全应用分析报告等。

相关代码

repo.js:配置多个项目的项目名、repo 信息、分支信息。

typescript
// 子应用repo信息
exports.repoInfos = [
    {
        name: 'ProjectA',
        repo: 'https://github.com/liangxin199045/project-app1.git',
        branch: 'main'
    },
    {
        name: 'ProjectB',
        repo: 'https://github.com/liangxin199045/project-app2.git',
        branch: 'main'
    },
    {
        name: 'ProjectC',
        repo: 'https://github.com/liangxin199045/project-app3.git',
        branch: 'main'
    },
    {
        name: 'ProjectD',
        repo: 'https://github.com/liangxin199045/project-app4.git',
        branch: 'main'
    }
]
// 代码下载目录
exports.downloadDir = 'codes';

download.js:拉取子应用项目代码的可执行脚本。

typescript
const { repoInfos, downloadDir } = require('./repo.js');            // Repo信息
const { execSync } = require('child_process');                      // 子进程操作
const fs = require('fs');                                           // 文件处理
const path = require('path');                                       // 路径处理
const chalk = require('chalk');                                     // 美化输出
const ora = require('ora');                                         // 美化命令行
const download = require('download-git-repo');                      // 代码下载器


// 删除指定目录及目录下所有文件
function rmDir(dirPath) {
    try{
        if( fs.existsSync(dirPath) ) {                                      // 判断给定的路径是否存在
            const files = fs.readdirSync(dirPath);                          // 返回文件和子目录的数组
            files.forEach(function(file){
                var curPath = path.join(dirPath, file);

                if(fs.statSync(curPath).isDirectory()) {                    // 如果是文件夹,则继续
                    rmDir(curPath);
                } else {
                    fs.unlinkSync(curPath);                                 // 如果是文件,则删除
                }

            });
            fs.rmdirSync(dirPath);                                          // 清除文件夹
        }
    }catch(e){
        throw e;
    }
}

function downloadItem(project) {
    return new Promise((resolve, reject) => {
        const spinner = ora(chalk.blue('download start: ' + project.name)).start();
        download(`direct:${project.repo}#${project.branch}`, `${downloadDir}/${project.name}`, {clone: true}, function (err) {
            if(err){
                console.log(err);
                spinner.fail(chalk.red(project.name+' download fail'));
                reject(err);
            }else{
                spinner.succeed(chalk.green(project.name + ' download success'));
                resolve();
            }
        })
    });
}

async function downloadAll(repoInfos){
    if(repoInfos.length >0){
        const codePath =path.join(process.cwd(),downloadDir);
        // 代码下载目录存在则先删除
        rmDir(codePath);
        // 下载所有子应用代码仓库
        await Promise.allSettled(repoInfos.map((item) => {
            return downloadItem(item);
        }));
        // 判定下载情况
        if(fs.existsSync(codePath)){
            let modules = fs.readdirSync(downloadDir);
            // console.log(modules);
            if(modules.length>0){
                console.log(chalk.green('=== download finish ==='));
            }else{
                console.log(chalk.red('error : 待分析代码目录不存在文件'));        // 输出错误信息
                process.exit(1);
            }
        }else{
            console.log(chalk.red('\nerror : 待分析代码目录为空'));        // 输出错误信息
            process.exit(1);
        }
    }else{
        console.log(chalk.red('error : repoInfos为空'));        // 输出错误信息
        process.exit(1);
    }
}

downloadAll(repoInfos);
  1. all-projects-analysis 项目中执行 npm install 安装依赖后,执行 npm run download

PS:download 可能会遇到无权限等原因导致拉取失败,那是因为 download-git-repo 这个工具包是以 child_process 子进程的形式来执行 git clone 操作,所以需要相应的 GitLab 账户有所有子应用仓库的拉取权限。

建议将所有的子应用仓库放在同一个 Group 下,账户只要有这个 Group 的权限即可,另外在 CI 拉取时推荐配置一个有全部仓库拉取权限的公共账户。

analysis.config.js:多扫描源场景的配置文件,会根据工作目录生成 scanSource 配置,因为是全项目分析,所以配置文件中关闭了代码评分与告警配置项。

typescript
const { repoInfos, downloadDir } = require('./repo.js');            // Repo信息
const path = require('path');                                       // 路径处理
const fs = require('fs');                                           // 文件处理

let scanSource = [];
// httpRepo处理
function httpRepoDeal (name) {
    let tempIndex = '';
    let httpRepo = '';
    let branch = '';
    repoInfos.forEach((item)=>{
        if(item.name == name){
            tempIndex = item.repo.indexOf('.git');
            httpRepo = item.repo.substring(0, tempIndex);
            branch = item.branch;
        }
    })
    return httpRepo + '/blob/' + branch + '/';
}
// 模块代码存放目录
const codePath = path.join(process.cwd(), downloadDir);
// scanSource动态处理
if(fs.existsSync(codePath)){
    const modules = fs.readdirSync(downloadDir);
    if(modules.length>0){
        scanSource = modules.map((item)=>{
            return {
                name: item,
                path: [downloadDir + '/' + item + '/src'],
                packageFile: downloadDir + '/' + item + '/package.json',
                format: (str) => {
                    return str.replace(downloadDir + '/' + item + '/','');
                },
                httpRepo: httpRepoDeal(item)
            }
        })
    }
}
// console.log(scanSource);

module.exports ={
    scanSource: scanSource,                                           // 必须,待扫描源码的配置信息
    analysisTarget: 'framework',                                      // 必须,要分析的目标依赖名
    analysisPlugins: [],                                              // 可选,自定义分析插件,默认为空数组
    blackList: ['window.FB.login', 'app.localStorage.set'],           // 可选,需要标记的黑名单api,默认为空数组
    browserApis: ['window','document','history','location'],          // 可选,要分析的BrowserApi,默认为空数组
    reportDir: 'docs',                                                // 可选,生成代码分析报告的目录,默认为report
    reportTitle: '全项目依赖(framework)分析报告',                       // 可选,代码分析报告标题,默认为'代码依赖分析报告'
    isScanVue: true,                                                  // 可选,是否要扫描分析vue中的ts代码,默认为false
    scorePlugin: null,                                                // 可选,评分插件: Function|'default'|null, default表示运行默认插件,null表示不评分
    alarmThreshold: null                                              // 可选,开启代码告警及阈值分数(0-100),默认为null即关闭告警逻辑 (CLI模式生效)
}
  1. 然后在项目 all-projects-analysis 中执行 npm run analysis 即可。

自动化配置

.gitlab-ci.yml 配置与单项目类似,这里就不再赘述了,可以参考 all-projects-analysis 中的 .gitlab-ci.yml 文件:

bash
# 任务指定镜像
image: node 14

# 流水线有2个阶段,先执行analysis阶段的任务,然后执行depoly阶段的任务
stages:
  - analysis
  - depoly

# 在每个任务开始之前需要执行的命令
before_script:
  - npm install

# 执行代码分析
work:                       # job name
  stage: analysis           # 归属于analysis阶段
  only:
    - master                # master分支发生变化时触发Pipeline
  script:
    - npm run download      # 下载代码脚本
    - npm run analysis      # 代码分析脚本
  artifacts:
    paths:                  # 缓存文件夹,可以在CI流水线任务 UI 界面中下载
      - docs                # 代码分析报告生成目录,与analysis.config.js配置保持一致

# 部署GitLab pages
pages:                           # job name
  stage: depoly                  # 归属于deploy阶段
  only:
    - master                     # master分支发生变化时触发Pipeline
  script:
    - mkdir -p public            # 执行脚本创建public目录
    - mv docs/* public           # 执行脚本将docs目录下的代码分析报告相关静态文件复制到public目录
    - bash ./notification.sh     # 推送代码分析报告消息
  dependencies:
    - work                       # 依赖work job
  artifacts:
    paths:
      - public                   # 声明GitLab Pages静态资源目录

同样,CI 定时任务会在代码分析结束后推送分析报告,这里以 all-projects-analysis 项目的 分析报告 为例:

全应用代码分析平台

CLI 模式配合 GitLab CI 这种方案的优点是简单便捷,但存在一些缺陷:

  1. 每次分析都是在特定时间针对特定代码版本进行的一次性分析,分析结果之间彼此孤立。
  2. GitLab Pages 没有持久存储能力,新的分析报告部署后,旧的会被覆盖,无法追溯历史。
  3. 代码分析配置文件放在代码仓库内,修改分析配置需要修改仓库文件,很不灵活。
  4. 无法针对特定时间段内的分析结果二次分析,所以无法追踪 API 调用趋势变化。
  5. 分析工具默认的分析报告模板无法满足使用者丰富的可视化交互需求,无法按需检索。

那我们怎么解决这些问题呢?

方案设计

搭建一个代码分析平台,后端服务以 API模式 集成代码分析工具的分析能力,然后在前端 Admin 创建针对全项目的定时分析任务,分析任务会从 GitLab server 拉取相关项目的代码并进行分析,在分析完成后将分析结果入库存储,在前端 Admin 可以按各种检索条件查看项目的分析结果,也可以查看选定时间段内某些 API 的调用趋势变化。

下面是代码分析平台的技术方案:

平台示例

查看 Project A 项目中 app.localStorage.get 这个 API 真实的调用及分布情况:

查看各个子应用项目的代码评分及分析详情:

查看 app.localStorage.set 这个 API 在全应用项目中的调用趋势变化:

分析意义

  1. 帮助基础架构团队优化 API 导出,减少冗余设计,评估 API 上/下线的影响,评估高频 API 的变更影响。
  2. 对子应用代码进行评分,督促其优化调用方式,杜绝“脏调用”,减少非建议调用,推进新 API 的普及进度。
  3. 基础框架重构是一个长期线性的过程,开发者可以定期跟踪新 / 旧 API 在各个子应用中的调用趋势变化,从而更好的把控重构进度。

小结

这一小节我们学习了如何实现微前端业务全应用代码分析,需要大家掌握以下知识点:

  1. 分析工具支持多扫描源分析,前提是所有待扫描的项目代码必须在指定路径下,但不同项目来自不同的 gitlab repo,所以需要给分析工具创造一个多项目的工作环境。
  2. 通过 CLI 模式生成全应用分析报告需要几个步骤,首先通过脚步拉取全部子应用的项目代码到指定工作目录,然后根据工作目录生成 analysis.config.js 配置文件,最后执行代码分析脚步。
  3. CLI + gitLab CI 这种分析方案简单便捷,但存在缺陷,无法持久存储分析数据,我们可以搭建一个代码分析平台来处理更多维度的分析数据,实现更精细的数据消费。