跳到正文
hello world
文章封面

信创麒麟浏览器下vite兼容性处理过程

做大屏项目时,本地开发往往很顺利。Chrome 上正常,接口正常,图表也正常。到了现场,换成麒麟系统上的浏览器,同一份代码就可能出现白屏、图表不更新、日期弹层错位,或者布局在加载完成后突然撑开。

这次排查的入口很小,就是一行对象合并代码:

const lastOption = { ...option, ...newOption }

把它手动改成 Object.assign 后,现场浏览器能正常运行。但继续往下查,才发现问题不只是这一行:项目已经配置了兼容插件,可最终产物里还是保留了对象展开。

本文记录这次配置排查,也整理一些麒麟环境下做 Vue3 大屏适配时有用的检查方法。配置示例是本次排查形成的方案,仍需在实际现场验收;后面的补充建议也不代表这些问题都在这次现场出现过。

对象合并

原来的图表更新方法大致如下:

const updateChart = (newOption) => {
    if (chart) {
        chart.clear()

        const lastOption = { ...option, ...newOption }

        chart.dispose()
        chart = echarts.init(chartRef.value)
        chart.setOption(lastOption, {
            notMerge: true,
            replaceMerge: ['series', 'geo', 'visualMap'],
        })
    }
}

打包后的对象合并仍然类似:

const t = { ...b, ...e }

压缩只是把变量名变短了,对象展开仍然存在。改成下面的写法后,现场能运行:

const lastOption = Object.assign({}, option, newOption)

对于这里的普通配置对象,两种写法都能完成浅合并,同名属性由 newOption 覆盖。它们不是递归合并,嵌套对象也不会逐层合并。在涉及 getter、setter 等特殊对象行为时,两种写法还存在语义差异。

这一步很有价值,说明问题与这段代码有关。但它还不能代替完整诊断。最终仍要结合控制台错误、浏览器内核和实际加载的 JS,确认是解析失败,还是运行时发生了其他异常。

尤其要注意:Firefox 从 55 开始就支持对象展开。假如现场真的是标准 Firefox 60,单凭 { ...b, ...e } 不能解释为什么运行失败。此时应该核实浏览器身份、文件缓存和完整错误,而不是继续盲目降低版本目标

cssTarget

项目原来有这一项:

build: {
    cssTarget: 'firefox60',
    minify: 'terser',
}

这两项看起来都与构建有关,但作用不同。

配置 作用 不能替代的工作
build.target 设定 JavaScript 语法转换目标 自动补齐所有运行时 API
build.cssTarget 限制部分 CSS 转换、压缩结果 转换 JavaScript 或修复所有布局
build.minify 选择是否压缩,以及压缩工具 定义完整的浏览器支持范围

cssTarget: 'firefox60' 不会把 JavaScript 的对象展开转掉,Terser 也不是完整的语法降级工具。压缩后的代码很短,不代表旧浏览器就能解析。

一开始,我尝试增加:

build: {
    target: 'es2015',
    cssTarget: 'firefox60',
}

ES2015 也叫 ES6。把它设为构建目标,是要求编译器转换高于这个目标的可转换语法,不是要求开发者以后只能写 ES6。

按一般构建流程,对象展开应该被转换。可这个项目打包后仍然保留了原语法,于是需要继续查看插件配置。

legacy

完整配置中还有:

legacy({
    targets: ['Firefox >= 60'],
    modernPolyfills: true,
})

这里才是这次排查的关键。

@vitejs/plugin-legacy 默认会生成现代产物和旧浏览器产物。targets 用于旧浏览器产物,现代产物有自己的目标设置。在相关插件版本中,插件会覆盖 build.target;构建日志也可能提示 plugin-legacy overrode 'build.target'。

因此,即使在 build 里写了 target: 'es2015',也不能忽略插件最终采用的目标。检查 dist 时还要确认看的是哪套产物、页面实际加载的是哪套产物。现代文件里有新语法,本身不代表旧浏览器文件没有完成转换。

另外,下面这项也容易被误解:

modernPolyfills: true

它补充现代产物需要的语言 API,不会把浏览器无法解析的对象展开变成旧语法。浏览器连文件都解析不了,文件里的 polyfill 自然也执行不了。

这次采用的配置:先统一走兼容产物

为了减少排查时两套产物带来的干扰,这次选择先只输出旧浏览器产物。目标暂定为 Firefox 52 和 Chrome 58,这两个版本的选择用于验证对象展开转换,不代表已经确认现场就是这些内核。

完整配置如下,保留项目原有的 SVG、组件自动导入、SCSS 和代理设置:

import { resolve } from 'path'
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import legacy from '@vitejs/plugin-legacy'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig(({ mode }) => {
    const env = loadEnv(mode, process.cwd())

    return {
        plugins: [
            vue(),

            legacy({
                targets: ['Firefox >= 52', 'Chrome >= 58'],
                renderLegacyChunks: true,
                renderModernChunks: false,
                polyfills: true,
            }),

            createSvgIconsPlugin({
                iconDirs: [
                    resolve(process.cwd(), 'src/assets/icons'),
                ],
                symbolId: 'icon-[name]',
            }),

            Components({
                resolvers: [
                    AntDesignVueResolver({
                        importStyle: false,
                    }),
                ],
            }),
        ],

        resolve: {
            alias: {
                '@': resolve(__dirname, 'src'),
            },
        },

        css: {
            preprocessorOptions: {
                scss: {
                    additionalData:
                        '@import "@/assets/style/_variable.scss";',
                },
            },
        },

        build: {
            // JS 目标由 legacy 插件管理
            cssTarget: ['firefox52', 'chrome58'],
            outDir: 'dist',
            emptyOutDir: true,
            sourcemap: false,

            // 排查期间关闭压缩;验证通过后恢复为 'terser'
            minify: false,

            terserOptions: {
                compress: {
                    keep_infinity: true,
                    drop_console: true,
                    drop_debugger: true,
                },
            },

            rollupOptions: {
                output: {
                    chunkFileNames: 'static/js/[hash:8].js',
                    entryFileNames: 'static/js/[hash:8].js',
                    assetFileNames: 'assets/[hash:8][extname]',
                },
            },

            chunkSizeWarningLimit: 1500,
        },

        server: {
            port: 9527,
            open: false,
            hmr: true,
            proxy: {
                [env.VITE_APP_BASE_API]: {
                    target: env.VITE_HOST_URL,
                    changeOrigin: true,
                    rewrite: path =>
                        path.replace(
                            RegExp(`^${env.VITE_APP_BASE_API}`),
                            '',
                        ),
                },
            },
        },
    }
})

这个方案的取舍很明确:所有浏览器都加载兼容产物,可能增加体积和运行开销。对于先恢复现场功能、再整理兼容范围的阶段,它比较容易检查。项目长期面向多种浏览器时,可以再恢复双产物,根据真实环境验收加载分支。

修改前先记录依赖版本:

npm ls vite @vitejs/plugin-legacy @vitejs/plugin-vue

如果当前插件不支持某个选项,需要按它与 Vite 的版本兼容关系调整。不要为了一个配置项直接把所有依赖升级到最新。

麒麟适配,先弄清楚现场用的到底是什么浏览器

“麒麟浏览器不兼容”这个描述太宽了。操作系统是麒麟,并不能推导出浏览器是 Firefox;浏览器产品版本,也不等于 Chromium 或 Gecko 内核版本。

例如,现场可能用的是 Firefox,也可能是 360 安全浏览器信创版。只拿“企业版 13.x”这样的产品号去设置 firefox60,很容易把目标设错。

到现场后,我会先记录浏览器名称、产品版本、系统版本、CPU 架构、分辨率、缩放比例,再在控制台取一份:

navigator.userAgent

UA 有参考价值,但定制浏览器可能修改它。还需要结合“关于浏览器”页面和简单能力测试。下面的测试只针对当前争议的对象展开,不代表浏览器通过了整个项目的兼容验收:

try {
    var result = new Function(
        'return ({ ...{ a: 1 }, ...{ b: 2 } })'
    )()
    console.log('对象展开测试通过', result)
} catch (error) {
    console.error('测试失败', error)
}

如果报的是语法错误,可以继续查解析能力;如果报的是 CSP 禁止动态执行,就不能把它当成语法不支持。此时可用单独的测试文件验证。

在 Ubuntu 上安装相同产品名的浏览器,也未必能复现麒麟现场。构建版本、系统字体、图形驱动和缩放比例都可能不同。远程进入实际终端,或者准备尽量一致的测试环境,往往比反复猜配置更有效。

页面错位时,别只查 JavaScript

页面能运行以后,剩下的兼容问题经常出在布局。大屏中有地图、Canvas、日期选择器和绝对定位按钮,任何一个容器尺寸变化,都可能把其他区域带偏。

图表加载后撑开 Grid

之前遇到过一种现象:加载前布局稳定,折线图渲染后就抖动;去掉图表组件,页面又正常。

这类问题要先看网格项的最小尺寸。Grid 的子项不一定会按我们预想的那样收缩,图表内容可能参与最小尺寸计算,把父布局撑开。

可以先把容器写明确:

.chart-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    grid-template-rows: repeat(2, minmax(0, 1fr));
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
}

.chart-cell {
    min-width: 0;
    min-height: 0;
}

.chart-container {
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
}

对应 Tailwind,常用的就是 min-w-0、min-h-0,以及 grid-rows-[minmax(0,1fr)_minmax(0,1fr)]。但 height: 100% 还要求父级高度有依据,仅给子级加满高不一定有效。

Flex 的 gap,要按实际内核检查

下面这种按钮排布在新浏览器中很常见:

<div class="flex items-center gap-2">
    <!-- 图标、文字、按钮 -->
</div>

旧内核可能支持 Grid 的间距,却不支持 Flex 的 gap。因此,不能只用 CSS.supports('gap', '8px') 就确认 Flex 间距可用。

对于简单的一行工具按钮,可以在这块区域直接使用相邻元素的 margin:

.toolbar {
    display: flex;
    align-items: center;
}

.toolbar > * + * {
    margin-left: 8px;
}

使用这种写法时删除原来的 gap,否则新浏览器中可能叠加间距。换行布局不能直接照搬这个方案,要另外处理每行首项和纵向间距。

clamp()、较新的颜色语法和视口单位也要按内核检查。需要回退值时,先写基础声明,再写增强声明;同时检查最终 CSS,避免压缩后只剩现场无法识别的值。cssTarget 不是通用 CSS polyfill。

日期弹层错位,先看挂载位置和缩放

日期选择器本身看着正常,打开面板后位置偏了,通常比普通布局更难查。

我的排查顺序是:先把浏览器缩放设为 100%,检查页面是否有 transform: scale(...),再确认弹层挂在 body 还是组件所在容器。接着看父级有没有 overflow: hidden,最后检查宽度、行高和字体。

如果当前 Ant Design Vue 版本支持 getPopupContainer,可以把弹层挂到合适的定位容器中尝试。但挂到父级后,也可能被裁剪或遇到层叠上下文问题,不能把它当成固定答案。要同时检查 DOM、定位参照物和组件版本。

不要一看到错位就堆 z-index。弹层层级低和弹层坐标错,是两个不同问题。

ECharts 要等容器尺寸稳定,再初始化和 resize

ECharts 初始化时需要容器有宽高。容器处于隐藏状态、布局尚未完成,或者父级刚切换全屏,都可能导致它拿到错误尺寸。

Vue 中可以在 DOM 更新后处理:

await nextTick()

if (chart) {
    chart.resize()
}

nextTick 不会等待 CSS 动画结束。如果容器带过渡动画,还需要在尺寸真正稳定时调用 resize()。

不能只监听窗口 resize。侧栏展开、标签页切换和 Grid 改变,也可能改变图表尺寸,却不触发窗口变化。有 ResizeObserver 时可监听容器;旧环境缺少它时,可以在已知的布局变化节点主动调用 resize()。

另外,原更新方法每次都 dispose() 再 init(),会重建图表实例,也可能增加闪烁。兼容问题排查期间先保持业务行为,确认原因;之后再单独评估能否复用实例,用 setOption 更新,不要把两类修改混在一起验证。

发布后仍然报错,先确认浏览器加载了新文件

本地 dist 已经转换了,现场却还显示旧代码,这时候应该去 Network 面板核对 JS 请求地址、响应内容和状态码。

哈希文件名有助于缓存管理,但入口 HTML 如果被缓存,仍可能引用旧 JS。只上传静态资源、没更新 index.html,也会出现同样的问题。

部署时应完整发布同一批构建产物。更稳妥的是先让新资源就位,再切换入口;旧的哈希资源保留一段时间,避免已经打开的页面在后续懒加载时请求到不存在的文件。

下面是可供参考的 Nginx 缓存策略,需要放到现有站点中按实际路径调整:

location = /index.html {
    add_header Cache-Control "no-cache" always;
}

location /static/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location /assets/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location / {
    try_files $uri $uri/ /index.html;
}

长期缓存适用于内容变化就换文件名的哈希资源;public 中复制出来的固定名称文件,应单独安排缓存。还有 CDN、Service Worker 时,也要检查它们的缓存策略。

强制刷新能帮助定位缓存问题,但不应该成为用户每次升级后的操作步骤。

按下面的顺序验证:

  1. 构建日志是否出现目标覆盖或插件警告。
  2. 新 dist 中的对象合并是否已经转换。
  3. 现场是否加载了这批产物及对应入口 HTML。
  4. 首屏、路由懒加载、接口请求、图表更新是否正常。
  5. 日期弹层、地图切换、全屏和缩放后的布局是否稳定。
  6. 恢复压缩后,再验证一次关键流程。

还要给兼容范围划一条实际边界。Vue3 官方说明要求浏览器原生支持 ES2016;核心响应式机制使用 Proxy,不能靠常规 polyfill 把任意老浏览器变成可用环境。构建目标降低,并不意味着框架、组件库和所有第三方依赖都支持同样低的版本。

评论

填写昵称与邮箱即可评论,无需登录。

推荐阅读