如何对公共依赖抽取到共享 CDN
解读
在国内前端工程化面试中,面试官问“如何用 Grunt 把公共依赖抽取到共享 CDN”并不是想听“把 <script src="https://cdn.xxx.com/vue/2.6.14/vue.min.js"></script> 直接写进 HTML”这么简单。
他真正考察的是:
- 你是否理解 “构建时抽离” 与 “运行时加载” 两条链路;
- 你是否能在 Grunt 生态 里用官方或社区插件把“依赖图→外部化→版本指纹→注入 URL”完整跑通;
- 你是否兼顾国内网络环境(阿里云 CDN、腾讯云 CDN、七牛、又拍、unpkg 镜像)带来的 SRI 校验、跨域、缓存策略、回源失败降级 等细节;
- 你是否能把这套流程做成 可灰度、可回滚、可并行上传 的自动化任务,而不是一次性的手动拷贝。
知识点
- Grunt 外部化(external)机制:借助 webpack 的
externals或 browserify 的external把vue、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:生成子资源完整性哈希,写入
integrity与crossorigin="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 不可用时页面白屏。
答案
-
先梳理依赖矩阵
用npm ls --prod --json输出依赖树,结合bundle-phobia-cli找出体积占比前 80% 的公共库,形成 “共享库白名单”(如vue、vue-router、axios、lodash、moment)。 -
在 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,而会保留全局变量引用。 -
生成 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>。 -
上传至国内 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/' }] } }上传完成后,文件自动带上 长期缓存头,并通过 版本目录隔离 实现“无覆盖式”发布,支持秒级回滚。
-
加入 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 节点被微信拦截时仍能本地兜底。
-
多环境切换
通过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>一条命令即可打出测试包或正式包,避免人工改地址。
-
完整任务链
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-import把import 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 无异常后再全量切换,降低风险。