Vue CLI 浏览器兼容性详解:browserslist、Polyfill 策略与现代模式(Modern Mode) Vue CLI 浏览器兼容性详解browserslist、Polyfill 策略与现代模式Modern Mode【免费下载链接】vue-cli️ webpack-based tooling for Vue.js Development项目地址: https://gitcode.com/gh_mirrors/vu/vue-cli本文围绕 Vue CLIwebpack-based tooling for Vue.js Development的浏览器兼容性机制展开browserslist如何驱动 JavaScript 语法转译与 CSS 前缀、useBuiltIns: usage下依赖包 polyfill 的三种处理策略以及现代模式双产物构建的完整实现原理。读完后你将能够针对老浏览器兼容、第三方依赖 polyfill 缺失、打包体积优化这三类典型场景做出正确配置并理解 Vue CLI 在构建期是如何通过环境变量与 Webpack 插件编排实现 ES Modules 差分加载的。browserslist一切兼容性决策的源头一个标准的 Vue CLI 项目中package.json的browserslist字段或独立的.browserslistrc文件声明了项目目标浏览器的范围。这个值被两个关键消费方读取babel/preset-env据此决定需要转译哪些 JavaScript 语言特性Autoprefixer据此决定需要添加哪些 CSS 浏览器厂商前缀。从源码结构看这套机制在构建链路中贯穿多处。构建命令入口 build/index.js 在启动时会调用 targets.js 解析项目browserslist配置并预计算allProjectTargetsSupportModule——判断所有目标浏览器是否都原生支持 ES Modules这个结果直接决定是否要走双产物构建。同理babel-preset-app/index.js 通过babel/helper-compilation-targets的getTargets()读取browserslist作为babel/preset-env的编译目标。说明browserslist的语法如 0.25%、not dead、last 2 versions等查询式表达由 browserslist 项目本身定义这里不再展开配置时只需保证所写范围覆盖你真正需要支持的浏览器即可。现代模式下的目标收敛getModuleTargets有一个容易被忽略的细节开启现代模式后现代包并不是以chrome 51之类的低版本为目标做转译而是把目标提升到支持script typemodule的最低浏览器版本与用户browserslist的交集。实现位于 babel-preset-app/index.js 的getModuleTargets先用{ esmodules: true }查得各浏览器原生支持 ES Modules 的最低版本数据来自babel-compat-data的native-modules.json再与用户目标取交集——若用户指定的版本高于该最低版本则沿用用户的否则采用最低支持版本。当环境变量VUE_CLI_MODERN_BUILD被设置时Babel 就按这套收敛后的目标做编译// packages/vue/babel-preset-app/index.js节选 } else if (process.env.VUE_CLI_MODERN_BUILD) { // targeting browsers that at least support script typemodule targets getModuleTargets(targets) }这就是为什么现代包中可以保留箭头函数、async/await等原生特性而遗留包必须全部转成 ES5——两者用的是同一份browserslist只是现代包在此基础上额外收紧了目标。Polyfill 策略useBuiltIns 与依赖包的三种困境默认 Vue CLI 项目使用 vue/babel-preset-app它把useBuiltIns: usage默认传给babel/preset-env见 index.js 中的选项解构useBuiltIns usage。usage模式会按源代码中实际出现的语言特性自动注入所需 polyfill从而把最终包里 polyfill 的数量压到最小。但这个按使用注入的前提是 Babel 能看到那份代码。因此当某个依赖包自己依赖了某些 API 的 polyfill 时默认配置下 Babel 检测不到——依赖包默认不会被 Babel 转译除非配置了transpileDependencies。针对三种典型依赖官方文档给出了对应解法方案一依赖本身基于目标环境不支持的 ES 版本撰写把该依赖加入vue.config.js的transpileDependencies选项。这样该依赖会同时被开启语法转换和基于使用情况的 polyfill 检测Babel 就能看见它用到的Promise、Map等特性并补上对应 polyfill。方案二依赖交付 ES5 代码且明确列出所需 polyfill使用vue/babel-preset-app的polyfills选项预包含所需 polyfill// babel.config.js module.exports { presets: [ [vue/app, { polyfills: [ es.promise, es.symbol ] }] ] }这里推荐用这种方式、而不是在业务源码里直接importpolyfill 的原因源码里写得很清楚index.js 的getPolyfills会用core-js-compat的数据配合babel/helper-compilation-targets的isRequired()逐一过滤用户列出的 polyfill——如果某条目标浏览器原生就支持该特性对应的 polyfill 会被自动排除。也就是说通过配置声明的 polyfill 是有条件的硬编码 import 的则永远是全量。另外注意一个事实默认列表并不只有es.promise。源码中的 defaultPolyfills 为const defaultPolyfills [ // promise polyfill alone doesnt work in IE, // needs this as well. see: #1642 es.array.iterator, // this is required for webpack code splitting, vuex etc. es.promise, // this is needed for object rest spread support in templates es.object.assign, // #2012 es.promise replaces native Promise in FF and causes missing finally es.promise.finally ]注释解释了每一条的原因es.promise是 webpack 代码分割、Vuex 等机制的基础es.array.iterator是 IE 下 Promise polyfill 正常工作的配套es.object.assign服务于模板中对象展开语法编译出的Object.assign调用。这些默认 polyfill 在buildTarget app且useBuiltIns usage时生效并经getPolyfills按browserslist过滤后由 polyfillsPlugin.js 以 side-effect import 的形式注入到入口文件入口清单来自环境变量VUE_CLI_ENTRY_FILES由 Service.js 在解析入口后设置。方案三依赖是 ES5 代码但悄悄用了 ES6 特性如 Vuetify改用useBuiltIns: entry并在入口文件添加import core-js/stable import regenerator-runtime/runtime这会依据browserslist目标导入所有需要的 polyfill彻底甩掉猜依赖用了什么的问题代价是包中会含有一部分用不到的 polyfill体积增大。构建库 / Web Component 时的 polyfill 策略当使用 Vue CLI 构建库或 Web Component 时推荐给vue/babel-preset-app传useBuiltIns: false关闭自动 polyfill 注入确保产物不含多余 polyfill——polyfill 应当由最终消费你的库的应用负责。这一点与源码一致useBuiltIns: false时 babel-preset-app/index.js 不会启用polyfillsPluginbabel/preset-env的corejs选项也会置为false。对wc/wc-async构建目标Babel 目标还会被额外收敛getWCTargets 把目标限制在至少支持 ES2015 class的浏览器集合Chrome 46、Firefox 45、Safari 10、Edge 13、iOS 10、Electron 0.36与用户目标的交集上。现代模式Modern Mode一份代码两个产物问题背景有了 Babel 可以使用全部 ES2015 新特性但也意味着要为旧浏览器交付转译 polyfill 后的包。这类包通常比原生 ES2015 代码更冗长解析和运行也更慢。而绝大多数现代浏览器已原生支持 ES2015——仅仅为了兼容老浏览器却让现代浏览器也加载笨重的转译代码是一种浪费。Vue CLI 的现代模式就是为此设计的。构建命令与产物文档中给出的经典命令是vue-cli-service build --modern需要说明的是就当前仓库而言该行为已经演化为默认开启build 命令 的默认选项里module: true且帮助中只提供--no-modulebuild app without generatingscript type\module\chunks for modern browsers。也就是说在当前版本中普通vue-cli-service build即产出现代包 遗留包两份产物文件名分别形如app.hash.js与app-legacy.hash.js、chunk-vendors.hash.js与chunk-vendors-legacy.hash.js可由 modernMode.spec.js 的断言验证--no-module则退化为只产出单一 ES5 兼容包测试 modernMode.spec.js 验证产物中无typemodule、无-legacy.js文件。双产物构建的执行流程见 build/index.js是主进程先以VUE_CLI_MODERN_MODEtrue、VUE_CLI_MODERN_BUILD未设置的状态执行legacy 构建随后用execa派生一个子进程仅额外设置VUE_CLI_MODERN_BUILDtrue执行modern 构建两次构建共用同一份vue.config.js靠环境变量区分。还有一个重要的自动优化若 targets.js 判定browserslist中所有目标浏览器都支持 ES Modules则打印提示因此不会构建两套差分加载产物只构建单份包needsDifferentialLoading直接置为false对应测试 should only build one bundle if all targets support ES module。HTML 注入module / nomodule / modulepreload现代模式没有特殊部署要求的关键在于生成的 HTML 自动采用了标准的差分加载技巧实现全部封装在 ModernModePlugin.js 中遗留构建阶段isModuleBuild: falseapplyLegacy通过 html-webpack-plugin 的alterAssetTagGroups钩子把本次构建的 script 标签列表写到临时文件legacy-assets-htmlName.json现代构建阶段isModuleBuild: trueapplyModule读取该临时文件然后做三件事把现代包 script 标签加上typemodule第 49-53 行把link relpreload asscript改写为link relmodulepreload第 55-63 行给从遗留构建拿来的标签补上nomodule属性后 push 进 HTML第 65-74 行。最终效果现代浏览器加载script typemodule的现代包并用link relmodulepreload预加载不支持 ES Modules 的旧浏览器忽略 module 标签、只加载script nomodule的遗留包。modernMode.spec.js 精确断言了生成的 HTML 形态例如script deferdefer typemodule src/js/app.hash.js/script script deferdefer src/js/app-legacy.hash.js nomodule/script性能收益方面文档给出参考数据对一个 Hello World 应用现代包已经小了 16%在生产环境中现代包通常能带来显著更快的解析与运算速度改善加载性能。Safari 10 的 nomodule 修复Safari 10 有一个著名缺陷它能解析script nomodule却不完全支持 ES Modules修复方式是一小段探测脚本。Vue CLI 通过 SafariNomoduleFixPlugin.js 自动处理但并非无条件注入插件读取projectModuleTargetsbrowserslist 与 ES Modules 支持版本的交集仅当交集后的safari或ios最低版本低于 11 时才需要修复第 9-14 行需要修复时默认把修复脚本作为独立文件safari-nomodule-fix.js输出并插入第一个真实 script 标签之前测试用例 should inject nomodule-fix script when Safari 10 support is required 验证了当browserslist中加入safari 10时dist/js/safari-nomodule-fix.js会出现而默认 targets 下should not inject ...产物中既无内联脚本也无该文件。这正对应文档所说针对 Safari 10 的 nomodule 修复会被自动注入——准确说是按需自动注入。CORS 与 crossorigin 注意事项script typemodule始终在 CORS 模式下加载因此部署服务器必须返回有效的 CORS 头例如Access-Control-Allow-Origin: *。如果需要携带凭据cookie获取脚本把vue.config.js的crossorigin选项设为use-credentials。对应实现是 app.js 中按options.crossorigin注册CorsPlugin// packages/vue/cli-service/lib/config/app.js节选 if (options.crossorigin ! null || options.integrity) { webpackConfig .plugin(cors) .use(require(../webpack/CorsPlugin), [{ crossorigin: options.crossorigin, integrity: options.integrity, publicPath: options.publicPath }]) }测试 modernMode.spec.js 验证了设置crossorigin: use-credentials后现代包 script 标签会带上crossoriginuse-credentials属性。在配置中区分现代 / 遗留构建有时需要只对某一种构建修改 webpack 配置例如只给现代包加 SourceMap。Vue CLI 通过两个环境变量传递当前构建身份VUE_CLI_MODERN_MODE本次构建启用了现代模式即差分加载VUE_CLI_MODERN_BUILD为 true 时当前配置服务于现代包构建否则为遗留包构建。重要这两个变量只有在chainWebpack()/configureWebpack()函数被求值时才能读取到不能直接写在vue.config.js的模块顶层作用域也因此PostCSS 配置文件里同样可以使用它们。注意部分插件如html-webpack-plugin、preload-plugin在两种模式的配置中并不都存在。若要在遗留配置中 tap 这些插件的选项请先用上面的环境变量确认当前处于哪种模式并检查插件确实存在于当前配置中再操作否则会因插件不存在而抛错。小结browserslist是 Vue CLI 兼容性体系的中枢语法转译、polyfill 取舍、CSS 前缀、乃至是否需要双产物构建都由它驱动依赖包 polyfill 缺失时按依赖形态选择transpileDependencies、vue/babel-preset-app的polyfills选项会被 targets 自动过滤且默认已含es.promise等四项或useBuiltIns: entry全量导入构建库 / Web Component 时改用useBuiltIns: false把 polyfill 责任交还给消费方当前版本中差分加载默认生效build命令通过VUE_CLI_MODERN_MODE/VUE_CLI_MODERN_BUILD双进程编排现代包与遗留包HTML 自动注入module/nomodule/modulepreload标签与按需的 Safari 10 修复若browserslist目标全部支持 ES Modules 则自动退化为单包构建部署唯一硬性要求是服务器返回正确 CORS 头需要凭据时用crossorigin: use-credentials。以上行为均可在仓库中复核modernMode.spec.js 覆盖双产物命名、HTML 标签形态、Safari 修复注入与--no-modulebabel-preset.spec.js 验证 polyfill 注入到入口文件的机制。【免费下载链接】vue-cli️ webpack-based tooling for Vue.js Development项目地址: https://gitcode.com/gh_mirrors/vu/vue-cli创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考