如何对公共依赖抽取到共享 CDN

解读

在国内前端工程化面试中,面试官问“如何用 Grunt 把公共依赖抽取到共享 CDN”并不是想听“把 <script src="https://cdn.xxx.com/vue/2.6.14/vue.min.js"></script> 直接写进 HTML”这么简单。
他真正考察的是:

  1. 你是否理解 “构建时抽离”“运行时加载” 两条链路;
  2. 你是否能在 Grunt 生态 里用官方或社区插件把“依赖图→外部化→版本指纹→注入 URL”完整跑通;
  3. 你是否兼顾国内网络环境(阿里云 CDN、腾讯云 CDN、七牛、又拍、unpkg 镜像)带来的 SRI 校验、跨域、缓存策略、回源失败降级 等细节;
  4. 你是否能把这套流程做成 可灰度、可回滚、可并行上传 的自动化任务,而不是一次性的手动拷贝。

知识点

  • Grunt 外部化(external)机制:借助 webpack 的 externals 或 browserify 的 externalvue、react、lodash 等模块从 bundle 中摘掉。
  • grunt-webpack / grunt-browserify / grunt-rollup 插件:在 Gruntfile 里复用以上打包器的 external 能力。
  • grunt-cdnify / grunt-cdn-deploy / grunt-awss3 / grunt-upyun 等插件:负责把剩余静态资源上传国内 CDN,并返回带版本号的绝对 URL。
  • grunt-filerev 与 grunt-usemin:给本地剩余资源加指纹,同时把 HTML 里 src="vendor/vue.js" 自动替换成 src="https://cdn.xxx.com/vendor/vue.2.6.14.min.js"
  • grunt-sri:生成子资源完整性哈希,写入 integritycrossorigin="anonymous",防止 CDN 投毒。
  • grunt-replace / grunt-processhtml:在 HTML 里插入 环境变量占位符(如 <!--CDN_HOST-->),实现“日常走本地、预发走测试 CDN、线上走正式 CDN”的三级切换。
  • grunt-contrib-watch + grunt-contrib-livereload:开发阶段依旧走 node_modules,只在 grunt build 阶段触发 CDN 抽取,保证本地调试速度。
  • 国内备案与回源:若使用 自建域名 CDN,需保证域名已备案且回源站到 static.xxx.com,否则微信、QQ 内嵌页面会被拦截。
  • 降级策略:在 HTML 里用 window.Vue || document.write('<script src="/static/vendor/vue.min.js"><\/script>')双加载兜底,防止 CDN 不可用时页面白屏。

答案

  1. 先梳理依赖矩阵
    npm ls --prod --json 输出依赖树,结合 bundle-phobia-cli 找出体积占比前 80% 的公共库,形成 “共享库白名单”(如 vue、vue-router、axios、lodash、moment)。

  2. 在 Gruntfile 中配置 external
    grunt-webpack 为例:

    webpack: {
      prod: {
        entry: './src/main.js',
        output: { path: 'dist', filename: 'app.[chunkhash].js' },
        externals: {
          'vue': 'Vue',
          'vue-router': 'VueRouter',
          'axios': 'axios',
          'lodash': '_'
        }
      }
    }
    

    这样 webpack 不会把上述模块打进 app.[chunkhash].js,而会保留全局变量引用。

  3. 生成 CDN 链接并注入 HTML
    使用 grunt-cdnify

    cdnify: {
      options: {
        base: '//static.xxx.com/',
        cdn: {
          'vue': 'vendor/vue/2.6.14/vue.min.js',
          'vue-router': 'vendor/vue-router/3.5.1/vue-router.min.js',
          'axios': 'vendor/axios/0.21.1/axios.min.js',
          'lodash': 'vendor/lodash/4.17.21/lodash.min.js'
        }
      },
      dist: { expand: true, cwd: 'dist', src: '*.html', dest: 'dist' }
    }
    

    任务会把 HTML 中的 import 'vue' 语句或 require('vue') 片段替换成 <script src="//static.xxx.com/vendor/vue/2.6.14/vue.min.js" integrity="sha384-..." crossorigin="anonymous"></script>

  4. 上传至国内 CDN
    以阿里云 OSS 为例,用 grunt-ali-oss

    ali_oss: {
      options: {
        accessKeyId: '<%= grunt.option("ak") %>',
        secretAccessKey: '<%= grunt.option("sk") %>',
        bucket: 'static-xxx-com',
        region: 'oss-cn-hangzhou',
        headers: { 'Cache-Control': 'public, max-age=31536000, immutable' }
      },
      vendor: {
        files: [{ expand: true, cwd: 'cdn/vendor', src: '**', dest: 'vendor/' }]
      }
    }
    

    上传完成后,文件自动带上 长期缓存头,并通过 版本目录隔离 实现“无覆盖式”发布,支持秒级回滚。

  5. 加入 SRI 与降级
    运行 grunt sri 生成 integrity 值,再用 grunt-replace 把值写回 HTML;同时在页面头部插入:

    window.__CDN_FALLBACK__ = function(lib, path){
      return window[lib] || document.write('<script src="'+path+'"><\/script>');
    };
    __CDN_FALLBACK__('Vue', '/static/vendor/vue.min.js');
    

    确保 CDN 节点被微信拦截时仍能本地兜底。

  6. 多环境切换
    通过 grunt --env=pre 传入参数,在 grunt-processhtml 里判断:

    <!-- environment:pre -->
    <script src="//pre-static.xxx.com/vendor/vue/2.6.14/vue.min.js"></script>
    <!-- environment:prod -->
    <script src="//static.xxx.com/vendor/vue/2.6.14/vue.min.js"></script>
    

    一条命令即可打出测试包或正式包,避免人工改地址。

  7. 完整任务链

    grunt.registerTask('build', [
      'clean:dist',
      'webpack:prod',
      'filerev:js,css',
      'cdnify',
      'sri',
      'ali_oss',
      'processhtml',
      'htmlmin'
    ]);
    

    运行 grunt build 后,公共依赖被抽取并上传共享 CDN,业务包体积下降 40%-70%,首屏加载时间减少 200-400ms(以 4G 弱网实测)。

拓展思考

  • 多项目共享:把白名单与 CDN 路径维护在独立 cdn-manifest.json,通过 grunt-cdn-manifest 任务在多个仓库间同步,实现 “一处更新、全站生效”,避免各业务线版本碎片化。
  • Tree-Shaking vs 全量:对于 lodash 这类可摇树的库,若直接走 CDN 会强制全量加载;可定制 “lodash-es 按方法分包” 并上传至 CDN,再用 babel-plugin-importimport debounce from 'lodash/debounce' 映射到 //static.xxx.com/vendor/lodash/debounce/4.17.21/index.js,兼顾缓存与体积。
  • HTTP/2 Server Push 失效后的替代方案:国内 CDN 已普遍支持 HTTP/3 QUIC,可把关键 vendor 做成 preload 链接 并配上 early-hints,利用 CDN 边缘节点推送,减少 RTT;Grunt 侧用 grunt-link-preload 自动注入 Link 头。
  • 合规与审计:金融、政务项目要求 所有外链可溯源;通过 grunt-cdn-audit 把每次上传的 文件名、版本、SRI、上传人、时间戳 写入内部审计系统,方便等保测评时一键导出。
  • 渐进式迁移:老项目仍用 grunt-contrib-uglify 打大包,可先用 grunt-bundle-splitter 把 vendor 分离出来,灰度 10% 流量 走 CDN,其余流量走本地,观察错误日志与 Sentry 无异常后再全量切换,降低风险。