如何在 headless Chrome 与 Firefox 间动态切换

解读

国内前端工程化面试中,“headless 浏览器动态切换” 常被用来考察候选人对 Grunt 插件链路的深度掌握。
核心诉求是:

  1. 不改动 Gruntfile 主体结构,一行命令或一个环境变量即可在 Chrome 与 Firefox 间切换;
  2. 兼容国内 CI(如阿里云效、腾讯云 CODING、GitLab-CI 自建 Runner)的 无桌面 Linux 容器 场景;
  3. 兼顾本地开发调试(Windows + 公司代理)与线上打包(Docker Alpine)的 路径差异与下载加速
  4. 产出统一的测试报告(junit/xml),供国内钉钉、飞书机器人 即时推送

知识点

  • grunt-contrib-qunit / grunt-contrib-jasmine / grunt-karma 等测试运行器插件的 options.browsers 配置项
  • karma-chrome-launcher、karma-firefox-launcherHEADLESS_MODE 环境变量注入
  • puppeteer 与 puppeteer-firefoxPUPPETEER_PRODUCT 变量,以及国内镜像源 CNPM_BIN_URL 加速下载
  • grunt-env + grunt-template 实现“同一 Gruntfile,多份运行时参数”
  • grunt-cli 的 --target=chrome|firefox 自定义参数解析,结合 grunt.option() 动态改写子任务配置
  • 国内 CI 容器缺少 libnss3、libatk1.0-0 等系统依赖 的预装脚本(Dockerfile RUN 指令)
  • 并发任务(grunt-concurrent)与 端口自增(karma-port-shifter)避免 headless 多实例端口冲突
  • 覆盖率上报:istanbul + grunt-istanbul-combine,生成 lcov.info 并推送到国内 SonarQube 私有云

答案

  1. 统一安装

    npm i -D karma karma-chrome-launcher karma-firefox-launcher puppeteer grunt-karma grunt-env
    
  2. 在项目根目录放置 .env.chrome.env.firefox 两份环境模板,内容示例:

    # .env.chrome
    HEADLESS_BROWSER=ChromeHeadless
    PUPPETEER_PRODUCT=chrome
    PUPPETEER_DOWNLOAD_HOST=https://npmmirror.com/mirrors
    
    # .env.firefox
    HEADLESS_BROWSER=FirefoxHeadless
    PUPPETEER_PRODUCT=firefox
    
  3. Gruntfile.js 关键片段

    module.exports = function(grunt) {
      // 解析命令行参数:grunt test --target=firefox
      const target = grunt.option('target') || 'chrome';
      grunt.loadNpmTasks('grunt-env');
      grunt.loadNpmTasks('grunt-karma');
    
      grunt.initConfig({
        env: {
          options: { src: `.env.${target}` } // 动态加载对应环境变量
        },
        karma: {
          options: {
            configFile: 'karma.conf.js',
            browsers: [process.env.HEADLESS_BROWSER] // 变量注入
          },
          ci: { singleRun: true },
          dev: { autoWatch: true }
        }
      });
    
      grunt.registerTask('test', ['env', 'karma:ci']);
    };
    
  4. karma.conf.js 适配国产镜像

    const isCI = !!process.env.CI;
    process.env.PUPPETEER_DOWNLOAD_HOST = process.env.PUPPETEER_DOWNLOAD_HOST || 'https://npmmirror.com/mirrors';
    
    module.exports = function(config) {
      config.set({
        frameworks: ['qunit'],
        files: ['test/**/*.js'],
        reporters: ['progress', 'junit'],
        junitReporter: { outputFile: 'reports/TEST.xml' },
        browsers: [process.env.HEADLESS_BROWSER || 'ChromeHeadless'],
        customLaunchers: {
          ChromeHeadless: {
            base: 'ChromeHeadless',
            flags: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] // 国内容器必备
          },
          FirefoxHeadless: {
            base: 'FirefoxHeadless',
            prefs: { 'network.proxy.type': 0 } // 关闭公司代理干扰
          }
        },
        concurrency: isCI ? 1 : Infinity
      });
    };
    
  5. 运行示例

    # 本地 Chrome
    grunt test --target=chrome
    # CI 容器 Firefox
    grunt test --target=firefox
    

至此,一行命令即可在 headless Chrome 与 Firefox 间动态切换,且全程兼容国内镜像与容器环境。

拓展思考

  • 若项目同时需要 Edge(headless),可引入 karma-edge-launcher,并在 CI 里使用 国内微软源 deb 包 安装 microsoft-edge-stable,通过 HEADLESS_BROWSER=EdgeHeadless 统一变量即可横向扩展。
  • 当测试用例过万、并发过高导致 GitLab Runner OOM 时,可结合 grunt-concurrent 把 karma 拆成多 shard,利用 karma-parallel-reporter 合并结果,实现 “多浏览器 + 多 shard” 矩阵 的秒级反馈。
  • 对于 内网无法下载浏览器 的银行、证券场景,可预先将 chrome-headless-shellfirefox-headless 打包成 公司私有 Nexus 的 tar.gz,在 grunt 任务里先用 grunt-curl 下载解压到 node_modules/puppeteer/.local-chromium,实现 离线构建