本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2018年09月06日 统计字数: 10429字 阅读时间: 21分钟阅读 本文链接: https://soulteary.com/2018/09/06/relatively-modern-control-of-front-end-code-quality.html ----- # 相对现代化的把控前端代码质量 最近几天聊天,常常聊到 `持续集成` 辅助把控 `代码质量` ,以前端团队为例,我们来简单聊聊。 本篇很可能是你在网上能找到的使用容器应用最新版本 `SonarQube` 相对详细的一篇,或者是唯一一篇,所以如果遇到问题,欢迎和我进行讨论沟通。 ## 如何把控质量 个人认为把控质量的核心是**形成标准**、**避免错误引入**,从而提高应用的**健壮性**,降低**维护成本**和不应存在的**复杂度**,这里在不讨论 `测试` 的情况下,通常的做法无非: 1. 在代码提交前或后进行 Code Review,定期走读代码。 2. 使用 `eslint`、`jshint`、`jslint` 等工具在开发环境通过 `Git Hook` 触发,进行风格和简单的质量评估,避免 `Bug`、`漏洞`、`坏味道` 的代码提交到平台。 3. 配合 `pipeline` 使用,定义一些过程脚本,在代码提交后,或者分支合并前去调用以上工具,避免质量不佳的代码合并到开发主干,被“传承”下去。 4. 使用 `istanbul` 之类的工具简单检查覆盖率,避免有一定复杂度的业务代码潜在风险过高。 5. 人员培训,技术氛围建设。 ## 策略的差异 一个良好的开发氛围,第一条是必不可少的,不论是否使用辅助审查系统,使用审查机制,可以把很多不合理的设计和明显使用错误的代码在萌芽期杀死。 如果辅助手段使用本地运行验证,在配套前端开发技术设施不完善的情况下,则可能出现为了省事卸载 `Hook` 跳过审查的事件;或者需要额外维护规则更新机制的成本,将团队代码规则保存到仓库或者私有包仓库的维护成本,甚至是保存仓库的建设成本。如果云端执行资源不足的话,本地运行则会是一个很好的补充手段。 如果使用云端审查,需要配套设施都相对完善:私有包仓库、CVS 系统、甚至容器仓库、CI Runner 执行环境...(后续或许可以聊聊如何建设这些) 云端审查可以将规则和标准统一,配合 `Docker` 容器技术可以轻松进行应用资源扩容,远超本地运行效率,并且可以进行扫描存档,客观的反馈代码质量和功能加入、人员培训前后的变化。 而技术团队的氛围建设、人员的技术培训是一件长期持久的事情,打造“有人愿意教”、“有人愿意学”的机会可遇不可求。 相比较之下,如果你的公司已经将 `GitLab` 升级到了 `9.x` 以上,并且有资源去做 `CI/CD` 的事情,那么直接在 `Pipeline` 过程中添加代码审查是一个相对靠谱的事情。 ## 使用 SonarQube 进行代码分析 接下来我会演示如何使用 `SonarQube` 最新版本辅助进行代码分支把控质量。 我将最新版本的 `SonarQube` 和 `scanner-runner` 封装成了镜像,避免直接在 `GitLab Runner` 宿主环境直接执行,污染执行环境。 然后在 `CI` 阶段定义一个新的 `Stage`,这里假定叫 `sonar` 好了。 ```yml sonar: image: docker.lab.com/sonar-scanner-runner:3.2.0.1227 script: - cd .. && BASE_DIR=$(pwd) && rm -rf /data && ln -s $BASE_DIR/${CI_PROJECT_NAME} /data - ls /data - echo ${CI_PROJECT_NAME}-${CI_PROJECT_ID} - sonar ${CI_PROJECT_NAME}-${CI_PROJECT_ID} ``` 这里使用 `Publish` 模式将扫描结果回传 `SonarQube Web Server`,形成持续的报告。 如果你需要将扫描阶段定义为部署前的一个“质量把控关口”,那么需要再简单写一个脚本,在扫描器执行完毕后,抓取当前项目的状态,是否超过 `设定的阈值`,从而中断后续的流程,脚本参考我上一篇 [如果不用 Node.js 写业务](https://soulteary.com/2018/08/31/play-with-node.html) 中的使用 `Node.js` 作为持续集成的粘合剂,获取扫描器返回的接口地址中的 `status` 状态值即可,比较简单,就略了。 ![提交触发构建](https://attachment.soulteary.com/2018/09/06/trigger-ci.png) 当你提交代码之后,`GitLab Runner` 调用镜像中的扫描器脚本(这里使用的`Runner` 为 `docker` 类型的 `Runner`)进行扫描,如果成功,大概是这个样子。 ![CI执行扫描](https://attachment.soulteary.com/2018/09/06/sonar-logs.png) 执行器日志会返回这个任务的具体执行结果,上文有提到该如何处理。 ```json { task: { id: "AWWtMWX1Frzdillyw-1a", type: "REPORT", componentId: "AWWtLY29Frzdillyw-1T", componentKey: "leetcode-547", componentName: "leetcode-547", componentQualifier: "TRK", analysisId: "AWWtMWsbbu6HJjdFR-kA", status: "SUCCESS", submittedAt: "2018-09-06T04:43:40+0000", startedAt: "2018-09-06T04:43:41+0000", executedAt: "2018-09-06T04:43:42+0000", executionTimeMs: 1260, logs: false, hasScannerContext: true, organization: "default-organization" } } ``` 访问 Web 平台,可以看到我们刚刚提交触发的测试报告已经生成,如果有缺陷的话,会展示因为什么,又该如何修正。 ![扫描结果](https://attachment.soulteary.com/2018/09/06/report-example.png) 相关功能还是比较多的,参考本文自行搭建后,可以自行了解。 ![默认规则列表](https://attachment.soulteary.com/2018/09/06/sonar-rule.png) 如果上面的规则看不太清楚,可以尝试浏览官方规则列表的在线版本: - [JavaScript 的规则列表](https://rules.sonarsource.com/javascript/) 当然,如果你愿意的话,可以参考下面的链接进行自定义配置,让扫描报告中包含覆盖率,以及使用自定义的 `eslint` 规则进行补充或者替换默认的规则: - [SonarJS 的配置](https://docs.sonarqube.org/display/PLUG/SonarJS) 当然,还有一个定制化更强的套路,使用专门的 `eslint` 插件,目前插件只能跑在老平台上,感兴趣的你如果对 Java 很熟悉,可以参考官方开源社区的示例和开源的三方 `eslint` 插件进行升级改造,这里就不展开了。 - [SonarJS provides the ability to import ESLint issues](https://jira.sonarsource.com/browse/MMF-1231) 官方提供了示例、也有三方插件,你甚至可以定义如果包含了哪些词汇、或者文件超出指定大小等就拒绝这次的审查通过,避免代码合并到主干分支造成更大的损失。 说了这么多,那么如何快速搭建一个相对高可用的 `SonarQube` 呢。 ## 构建你的平台镜像 例子中可以看到,我个人使用私有域名,并签署了私有证书,如果你的公司团队状况类似,可以直接参考,不然的话,去掉相关代码即可。(不去掉也无所谓) 下面是我的镜像,会自动将私有证书进行授信,一般来说未来版本升级,只要修改这个版本号即可使用(7.1、7.2、7.3 验证通过)。 ```bash FROM openjdk:8-alpine ENV JAVA_CERTS_DIR "$JAVA_HOME/jre/lib/security/" WORKDIR $JAVA_CERTS_DIR ADD ssl/*.crt $JAVA_CERTS_DIR # https://stackoverflow.com/questions/41497871/importing-self-signed-cert-into-dockers-jre-cacert-is-not-recognized-by-the-ser RUN ls *.crt | xargs -I {} keytool -importcert -storepass changeit -noprompt -trustcacerts -alias {} -file {} -keystore "$JAVA_CERTS_DIR/cacerts" # https://github.com/SonarSource/docker-sonarqube/blob/4d76dfe9fb4c5d41ba1852cd5072dbff15ca5a20/7.1-alpine/Dockerfile ENV SONAR_VERSION=7.3 \ SONARQUBE_HOME=/opt/sonarqube \ # Database configuration # Defaults to using H2 SONARQUBE_JDBC_USERNAME=sonar \ SONARQUBE_JDBC_PASSWORD=sonar \ SONARQUBE_JDBC_URL= # Http port EXPOSE 9000 RUN addgroup -S sonarqube && adduser -S -G sonarqube sonarqube RUN set -x \ && apk add --no-cache gnupg unzip \ && apk add --no-cache libressl wget \ && apk add --no-cache su-exec \ && apk add --no-cache bash \ # pub 2048R/D26468DE 2015-05-25 # Key fingerprint = F118 2E81 C792 9289 21DB CAB4 CFCA 4A29 D264 68DE # uid sonarsource_deployer (Sonarsource Deployer) # sub 2048R/06855C1D 2015-05-25 && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys F1182E81C792928921DBCAB4CFCA4A29D26468DE \ && mkdir /opt \ && cd /opt \ && wget -O sonarqube.zip --no-verbose https://sonarsource.bintray.com/Distribution/sonarqube/sonarqube-$SONAR_VERSION.zip \ && wget -O sonarqube.zip.asc --no-verbose https://sonarsource.bintray.com/Distribution/sonarqube/sonarqube-$SONAR_VERSION.zip.asc \ && gpg --batch --verify sonarqube.zip.asc sonarqube.zip \ && unzip sonarqube.zip \ && mv sonarqube-$SONAR_VERSION sonarqube \ && chown -R sonarqube:sonarqube sonarqube \ && rm sonarqube.zip* \ && rm -rf $SONARQUBE_HOME/bin/* VOLUME "$SONARQUBE_HOME/data" WORKDIR $SONARQUBE_HOME COPY run.sh $SONARQUBE_HOME/bin/ ENTRYPOINT ["./bin/run.sh"] ``` 配合的执行脚本: ```bash #!/bin/sh # https://raw.githubusercontent.com/SonarSource/docker-sonarqube/4d76dfe9fb4c5d41ba1852cd5072dbff15ca5a20/7.1-alpine/run.sh set -e if [ "${1:0:1}" != '-' ]; then exec "$@" fi chown -R sonarqube:sonarqube $SONARQUBE_HOME exec su-exec sonarqube \ java -jar lib/sonar-application-$SONAR_VERSION.jar \ -Dsonar.log.console=true \ -Dsonar.jdbc.username="$SONARQUBE_JDBC_USERNAME" \ -Dsonar.jdbc.password="$SONARQUBE_JDBC_PASSWORD" \ -Dsonar.jdbc.url="$SONARQUBE_JDBC_URL" \ -Dsonar.web.javaAdditionalOpts="$SONARQUBE_WEB_JVM_OPTS -Djava.security.egd=file:/dev/./urandom" \ "$@" ``` 最后是编排工具使用的配置: ```yml # https://github.com/SonarSource/docker-sonarqube/blob/master/recipes.md version: '3' services: sonarqube: image: docker.lab.com/sonar.lab.com:7.3 expose: - 9000 - 9001 - 9092 environment: SONARQUBE_JDBC_USERNAME: sonar SONARQUBE_JDBC_PASSWORD: sonar SONARQUBE_JDBC_URL: 'jdbc:postgresql://db:5432/sonar' JAVA_OPTS: '-Duser.timezone=Asia/Shanghai' depends_on: - db volumes: - ./data/conf:/opt/sonarqube/conf - ./data/data:/opt/sonarqube/data - ./data/extensions:/opt/sonarqube/extensions - ./data/bundled-plugins:/opt/sonarqube/lib/bundled-plugins networks: - traefik labels: - "traefik.enable=true" - "traefik.port=9000" - "traefik.frontend.rule=Host:sonar.lab.com" - "traefik.frontend.entryPoints=http,https" db: image: postgres:10.4-alpine expose: - 5432 environment: - POSTGRES_USER=sonar - POSTGRES_PASSWORD=sonar volumes: - ./data/postgresql:/var/lib/postgresql # This needs explicit mapping due to https://github.com/docker-library/postgres/blob/4e48e3228a30763913ece952c611e5e9b95c8759/Dockerfile.template#L52 - ./data/postgresql_data:/var/lib/postgresql/data networks: - traefik networks: traefik: external: true ``` 这里依旧是使用 `Traefik` 的服务发现进行自动注册,扩容我已经讲过好多次了,翻翻前面的文章吧:D。 这里为了能够达到用户和 `GitLab` 互通等,我还做了一个简单的默认配置,保存在 `data/conf/sonar.properties`,并使用编排工具映射到容器内,你也可以选择直接把配置打到容器里,或者走配置中心进行远程获取(最佳方案)。 ```conf # https://github.com/SonarSource/sonarqube/blob/master/sonar-application/src/main/assembly/conf/sonar.properties sonar.telemetry.enable=false sonar.updatecenter.activate=false sonar.exclusions=bower_components/**/*, node_modules/**/* sonar.core.serverBaseURL=https://sonar.lab.com # GitLab Authentication sonar.auth.gitlab.enabled=true sonar.auth.gitlab.url=https://gitlab.lab.com sonar.auth.gitlab.applicationId=YOUR sonar.auth.gitlab.secret=YOUR sonar.auth.gitlab.allowUsersToSignUp=true sonar.auth.gitlab.ignore_certificate=true # GitLab Reporting sonar.gitlab.url=https://gitlab.lab.com sonar.gitlab.ignore_certificate=true sonar.gitlab.user_token=YOUR ``` 如果你有想定制修改的地方,可以参考官方文档,限于篇幅和时间,这里也先略过,后面还有好多内容。 ## 构建你的扫描器镜像 扫描器我这里分为了两部分,一个镜像是给用户使用的,也就是跨平台的 `扫描工具`,另外一个是专属于 `GitLab Runner`的扫描器。 两者差异主要是是否要进行文件挂载、以及是否包含默认执行入口,为了方便维护,我将跨平台的镜像设计为基础镜像,使用脚本基于基础镜像进行修改。 下面是基础镜像: ```bash FROM java:openjdk-8u45-jre MAINTAINER soulteary ENV NODE_VERSION 10.8.0 ENV SONAR_RUNNER_VERSION 3.2.0.1227 ENV SONAR_RUNNER_DIR sonar-scanner ENV SONAR_RUNNER_HOME /opt/${SONAR_RUNNER_DIR} ENV SONAR_RUNNER_PACKAGE sonar-scanner-cli-${SONAR_RUNNER_VERSION}-linux.zip ENV HOME ${SONAR_RUNNER_HOME} WORKDIR /opt RUN wget https://sonarsource.bintray.com/Distribution/sonar-scanner-cli/${SONAR_RUNNER_PACKAGE} && \ unzip ${SONAR_RUNNER_PACKAGE} && \ mv ${SONAR_RUNNER_DIR}-${SONAR_RUNNER_VERSION}-linux ${SONAR_RUNNER_DIR} && \ rm ${SONAR_RUNNER_PACKAGE} RUN groupadd -r sonar && \ useradd -r -s /usr/sbin/nologin -d ${SONAR_RUNNER_HOME} -c "Sonar service user" -g sonar sonar && \ chown -R sonar:sonar ${SONAR_RUNNER_HOME} && \ mkdir -p /data && \ chown -R sonar:sonar /data RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash && \ export NVM_DIR="$HOME/.nvm" && \. "$NVM_DIR/nvm.sh" && \ nvm install ${NODE_VERSION} && nvm alias default ${NODE_VERSION} ENV JAVA_CERTS_DIR "$JAVA_HOME/jre/lib/security" ADD /ssl/*.crt /tmp/ ADD run.sh ${SONAR_RUNNER_HOME}/bin/ # https://stackoverflow.com/questions/41497871/importing-self-signed-cert-into-dockers-jre-cacert-is-not-recognized-by-the-ser RUN ls /tmp/*.crt | xargs -I {} keytool -importcert -storepass changeit -noprompt -trustcacerts -alias {} -file {} -keystore "$SONAR_RUNNER_HOME$JAVA_CERTS_DIR/cacerts" && \ cp /tmp/*.crt /usr/share/ca-certificates && update-ca-certificates && \ rm -rf /tmp/*.crt && \ ln ${SONAR_RUNNER_HOME}/bin/run.sh /bin/sonar # https://stackoverflow.com/questions/28740785/error-in-sonarqube-when-launching-svn-blame RUN echo "sonar.host.url=https://sonar.lab.com" >> ${SONAR_RUNNER_HOME}/conf/sonar-scanner.properties && \ echo "sonar.projectBaseDir=/data" >> ${SONAR_RUNNER_HOME}/conf/sonar-scanner.properties && \ echo "sonar.sources=/data" >> ${SONAR_RUNNER_HOME}/conf/sonar-scanner.properties && \ echo "sonar.scm.disabled=true" >> ${SONAR_RUNNER_HOME}/conf/sonar-scanner.properties USER sonar VOLUME /data ENTRYPOINT ["/bin/sonar"] ``` 可以看到,基础镜像包含可配置化的 `Node` 版本,以及可配置的扫描器,当然,也支持私有证书的授信。 在本地执行的话,到项目代码根目录,需要输入下面的命令运行即可,结果等同使用 CI Runner 执行。 ```bash docker run --rm -it -v `pwd`:/data docker.lab.com/sonar-scanner:3.2.0.1227 YOUR_PROJECT_ID ``` 交付给 CI 使用的镜像大致相同,前文提到的脚本内容如下: ```bash #!/usr/bin/env bash cat ../normal/Dockerfile | \ sed 's/"\/bin\/sonar"/"\/bin\/bash"/g' | \ sed 's/ENTRYPOINT/CMD/g' | \ sed 's/VOLUME \/data//g' | \ sed 's/USER sonar//g' | tee Dockerfile ``` 在执行完毕你会获得另外一个去除了部分内容的 `Dockerfile`。 如何使用,在上一小节定义 `GitLab Runner` 有提过,定义 `image` 字段后,添加一行执行脚本 `sonar YOUR_PROJECT_ID` 即可。 ## 关于 CI Runner 至于文章中一直提到的 CI Runner,如果公司为你准备好了,直接使用就是了。 如果还没有,参考下面命令,根据自己需求进行调整,注册一个 docker 类型的执行器即可。 ```bash gitlab-runner register -n \ --url https://gitlab.lab.com/ \ --registration-token $YOUR_TOKEN \ --executor docker \ --description "docker runner" \ --docker-image "docker.lab.com/docker-with-certs:stable" \ --docker-privileged --tls-ca-file=/lab.com.crt ``` ## 其他 最后,我特别喜欢来自 [sonarlint](https://www.sonarlint.org/) 的一句话,也送给你: > Fix issues before they exist 今天下午还有事情,行文仓促,难免有误,如果你发现问题,欢迎向我反馈。 --EOF