從 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} 參數缺失`) } } } }