从 Android 和 iOS 2端 App 被驳回的一些信息来看,驳回缘由通常划分为下面几类:javascript
常见审核失败的缘由不少,很大比重一个就是代码或者文本里面存在一些敏感词,因此本文的侧重点在于关键词扫描。像上架设置的截图和当前设备不匹配、提供的帐号没法使用功能 😂 这种状况打一顿就行了,非主流行为不在本文范围内java
每一个公司通常来讲都不止一条业务线,因此每一个业务线的 App 状况和内容也不同,因此敏感词也是千差万别。敏感词收集这个事情,应该由业务线主要负责 App 的开发者来收集,根据平时的上架状况,苹果的驳回的邮件来整理。算法
公司自研工具 cli(iOS SDK、iOS App、Android SDK、Android App、RN、Node、React 依赖分析、构建、打包、测试、热修复、埋点、构建),各个端都是经过「模版」来提供能力。包含若干子项目,每一个子项目就是所谓的 “模版”,每一个模版其实就是一个 Node 工程,一个 npm 模块,主要负责如下功能:特定项目类型的目录结构、自定义命令供开发、构建等使用、模版持续更新及 patch 等。npm
因此能够在打包构建(各个端将项目提交到打包系统,打包系统根据项目语言、平台调度打包机)的时候,拿到源代码进行扫描。基于这个现状,因此方案是「扫描是基于源代码出发的扫描的」。json
按照 iOS 端 pod install
这个过程,cocoapods 为咱们预留了钩子:PreInstallHook.rb
、PostInstallHook.rb
,容许咱们在不一样的阶段为工程作一些自定义的操做,因此咱们的 iOS 模版设计也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:prebuild
、build
、postbuild
。定位好了问题,要作的就是在 prebuild 里面进行关键词扫描的编码工做。api
肯定了何时作什么事情,接下来就要讨论怎么作才合适。数组
字符串匹配算法 KMP 是一开始想到的内容,针对某个 App 进行时机测试,发现50多个敏感词的状况下,代码扫描耗时60秒钟,以为很是不理想,看 KMP 算法没有啥问题,因此换个思路走下去。async
由于模版本质上 Node 项目,因此 Node 下的 glob 模块正好提供根据正则匹配到合适的文件,也能够匹配文件里面的字符串。而后继续作实验,数据以下:9个铭感词语、代码文件5967个,耗时3.5秒工具
scaner.yml
文件。sh|pch|json|xcconfig|mm|cpp|h|m
error: - checkSwitch warning: - loan - online - ischeck searchPath: ../fixtures fileType: - h - m - cpp - mm - js warningkeywordsScan: true errorKeywordsScan: true
其实这些问题都是业界标准的作法,确定须要预留这样的能力,因此自定义规则的格式能够查看上面 yml 文件的各个字段所肯定。明确了作什么事,以及作事情的标准,那就能够很快的开展并落地实现。post
'use strict' const { Error, logger } = require('@company/BFF-utils') const fs = require('fs-extra') const glob = require('glob') const YAML = require('yamljs') module.exports = class PreBuildCommand { constructor(ctx) { this.ctx = ctx this.projectPath = '' this.fileNum = 0 this.isExist = false this.errorFiles = [] this.warningFiles = [] this.keywordsObject = {} this.errorReg = null this.warningReg = null this.warningkeywordsScan = false this.errorKeywordsScan = false this.scanFileTypes = '' } async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') { return new Promise((resolve, reject) => { glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => { if (err) reject(err) resolve(files) }) }) } async scanConfigurationReader(keywordsPath) { return new Promise((resolve, reject) => { fs.readFile(keywordsPath, 'UTF-8', (err, data) => { if (!err) { let keywords = YAML.parse(data) resolve(keywords) } else { reject(err) } }) }) } async run() { const { argv } = this.ctx const buildParam = { scheme: argv.opts.scheme, cert: argv.opts.cert, env: argv.opts.env } // 处理包关键词扫描(敏感词汇 + 私有 api) this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {} this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false if (Array.isArray(this.keywordsObject.fileType)) { this.scanFileTypes = this.keywordsObject.fileType.join('|') } if (Array.isArray(this.keywordsObject.error)) { this.errorReg = this.keywordsObject.error.join('|') } if (Array.isArray(this.keywordsObject.warning)) { this.warningReg = this.keywordsObject.warning.join('|') } // 从指定目录下获取全部文件 this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes) if (this.errorReg && this.errorKeywordsScan) { await Promise.all( files.map(async file => { try { const content = await fs.readFile(file, 'utf-8') const result = await content.match(new RegExp(`(${this.errorReg})`, 'g')) if (result) { if (result.length > 0) { this.isExist = true this.fileNum++ this.errorFiles.push( `编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result && (result.length || 0)}` ) } } } catch (error) { throw error } }) ) } if (this.errorFiles.length > 0) { throw new Error( `从你的项目中扫描到了 error 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${ this.errorReg }」\n存在问题的文件有 ${JSON.stringify(this.errorFiles, null, 2)}` ) } // warning if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) { await Promise.all( files.map(async file => { try { const content = await fs.readFile(file, 'utf-8') const result = await content.match(new RegExp(`(${this.warningReg})`, 'g')) if (result) { if (result.length > 0) { this.isExist = true this.fileNum++ this.warningFiles.push( `编号: ${this.fileNum}, 所在文件: ${file}, 出现次数: ${result && (result.length || 0)}` ) } } } catch (error) { throw error } }) ) if (this.warningFiles.length > 0) { logger.info( `从你的项目中扫描到了 warning 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${ this.warningReg }」。有问题的文件有${JSON.stringify(this.warningFiles, null, 2)}` ) } } for (const key in buildParam) { if (!buildParam[key]) { throw new Error(`build: ${key} 参数缺失`) } } } }