Skip to content

Vue3 中 uview-plus 线上组件失效:import.meta.glob 被运行时判断拦截的排查记录

这篇文章解决什么问题

这次遇到的问题很典型:

  • 本地开发环境里,uview-plus 组件都能正常显示。
  • 发布到线上后,所有 uview-plus 组件都失效。
  • 查看页面 HTML,发现类似 u-buttonu-form 这样的组件标签没有被 Vue 正确替换。
  • 页面没有马上给出特别直观的报错,第一眼看起来像样式丢了,实际不是。

最后定位到 uview-plusinstall 逻辑里有一段代码:

js
const canUseGlob = typeof import.meta !== 'undefined' && typeof import.meta.glob === 'function'
if (!canUseGlob) return components

生产环境执行到这里时,import.meta 存在,但 import.meta.glob 不存在,所以 canUseGlobfalse。函数提前返回空组件列表,后面的组件自动注册逻辑没有执行。

把这段 if 注释掉后,线上组件恢复正常。

这篇文章记录排查过程,并重点解释:import.meta 是什么,import.meta.glob 又是什么,为什么这个判断在生产环境里会把组件注册拦掉。

先说结论

这不是 Vue 模板解析问题,也不是组件样式问题。

真正原因是:

import.meta.glob 不是浏览器原生运行时 API,而是 Vite 提供的编译期能力。生产环境里用 typeof import.meta.glob === 'function' 做运行时判断,可能得到 false,从而提前中断逻辑。

在这个问题里,uview-plus 的组件注册链路大概是:

text
app.use(uviewPlus)
  -> uview-plus install()
  -> resolveComponents()
  -> import.meta.glob 扫描 ./components/u-*/u-*.vue
  -> Vue.component() 全局注册组件

线上失效时,链路断在 resolveComponents()

text
resolveComponents()
  -> canUseGlob === false
  -> return []
  -> install() 没有注册任何 uview-plus 组件
  -> 模板里的 u-button / u-form 保持原标签

所以 HTML 里还能看到原始组件标签,本质是“组件没有注册成功”。

现象:本地正常,线上全部失效

最容易误判的是:本地一切正常。

开发阶段打开页面,按钮、表单、弹窗、图标都能正常显示。于是第一反应通常会怀疑:

  • 是否线上 CSS 没加载?
  • 是否静态资源路径不对?
  • 是否 CDN 缓存了旧文件?
  • 是否组件按需引入失败?
  • 是否 Vue 编译模板时有差异?

但查看线上 HTML 后,关键线索出现了:组件标签没有被替换。

正常情况下,Vue 组件渲染后,浏览器 DOM 里不会保留一个完整的自定义组件结构。例如 u-button 应该被渲染成它内部的真实 DOM。

如果线上还能看到类似:

html
<u-button>提交</u-button>

这通常说明 Vue 没把它当成已注册组件处理。也就是说,问题重点不在样式,而在组件注册。

顺着 install 往下查

uview-plus 作为 Vue 插件使用时,一般会在入口处安装:

js
import uviewPlus from '@/uni_modules/uview-plus'

app.use(uviewPlus)

Vue 插件的核心是 install 方法。install 里如果注册了全局组件,业务页面才可以直接写:

vue
<template>
  <u-button>提交</u-button>
</template>

这次排查到的关键代码可以简化成下面这样:

js
let components = []

function resolveComponents() {
  if (components.length) return components

  const canUseGlob = typeof import.meta !== 'undefined' && typeof import.meta.glob === 'function'
  if (!canUseGlob) return components

  const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

  for (const key in importFn) {
    const component = importFn[key]?.default
    if (component?.name && component.name.indexOf('u--') !== 0) {
      components.push(component)
    }
  }

  return components
}

const install = (Vue) => {
  resolveComponents().forEach((component) => {
    Vue.component(component.name, component)
  })
}

逻辑意图很清楚:

  1. import.meta.glob 扫描 components/u-* 目录下的组件。
  2. 取出每个 .vue 文件的默认导出。
  3. 根据组件自己的 name 全局注册。
  4. 页面模板里就能直接使用这些组件。

但问题也出在这里:组件扫描依赖 import.meta.glob,而前面又用运行时方式判断它是否存在。

import.meta 是什么

import.meta 是 ES Module 里的一个元信息对象。

可以把它理解为:

当前模块运行时,由宿主环境提供给这个模块的一份补充信息。

在浏览器原生 ESM 里,常见的是:

js
console.log(import.meta.url)

它会拿到当前模块的 URL。

在 Node.js ESM 里,也有自己的 import.meta 能力,比如 import.meta.url。不同运行环境可以往 import.meta 上挂自己的字段。

重点是:import.meta 是标准能力,但 import.meta.glob 不是标准能力。

也就是说:

js
import.meta

和:

js
import.meta.glob

不是一回事。

前者是 ESM 语法里的元信息对象,后者是 Vite 扩展出来的功能。

import.meta.glob 是什么

import.meta.glob 是 Vite 提供的批量导入能力。

它常用于按路径批量拿文件:

js
const modules = import.meta.glob('./pages/*.vue')

默认情况下,这会生成懒加载导入:

js
{
  './pages/home.vue': () => import('./pages/home.vue'),
  './pages/about.vue': () => import('./pages/about.vue')
}

如果加上 eager: true

js
const modules = import.meta.glob('./components/*.vue', { eager: true })

就会在构建时变成静态导入风格,类似:

js
import * as module0 from './components/a.vue'
import * as module1 from './components/b.vue'

const modules = {
  './components/a.vue': module0,
  './components/b.vue': module1
}

这说明一个关键事实:

import.meta.glob 更像 Vite 的编译期宏,不应该把它当成浏览器运行时一定存在的函数。

开发时你写的是:

js
import.meta.glob('./components/u-*/u-*.vue', { eager: true })

构建后真正运行的代码,理想情况下已经不是原始的 import.meta.glob() 调用了,而是 Vite 转换后的模块映射。

为什么运行时判断会出问题

问题代码是:

js
const canUseGlob = typeof import.meta !== 'undefined' && typeof import.meta.glob === 'function'
if (!canUseGlob) return components

const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

它的意图是做兼容判断:如果当前环境不能使用 import.meta.glob,就不要继续执行,避免报错。

但这里混淆了两个阶段:

阶段该阶段发生什么是否适合用 typeof import.meta.glob 判断
编译期Vite 扫描源码,把 import.meta.glob() 转成模块映射不适合,这是构建工具能力
运行时浏览器执行打包后的 JS不适合,浏览器原生没有 import.meta.glob

如果构建工具已经把下面这句转换掉:

js
const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

那后面真正需要执行的其实只是一个普通对象映射。

但前面的判断:

js
typeof import.meta.glob === 'function'

可能仍然在运行时执行,并且得到 false

结果就是:明明后面的组件映射已经可以用了,却因为运行时检测失败,提前 return components

这就是这类问题最绕的地方:

出问题的不是 glob 扫不到组件,而是代码在真正使用扫描结果之前,被一个运行时 guard 拦截了。

为什么本地开发能正常,生产环境不正常

本地开发和生产构建不是完全同一条链路。

在开发环境中,Vite / uni-app 开发服务器会按需处理模块,HMR、条件编译、依赖处理也都在开发服务里参与运行。某些情况下,开发环境里 import.meta.glob 相关逻辑能正常通过。

生产环境中,代码会经历打包、压缩、条件编译、依赖处理,再交给浏览器运行。浏览器只认识标准的 import.meta,不认识 Vite 扩展的 import.meta.glob

所以生产环境 debugger 里看到:

js
import.meta.glob === undefined

并不奇怪。

关键要看两件事:

  1. 生产包里是否还存在原始的 import.meta.glob(...) 调用。
  2. 是否存在 typeof import.meta.glob === 'function' 这种运行时判断,并且它挡住了已经转换好的逻辑。

如果生产包里仍然有原始 import.meta.glob(...) 调用,说明这份代码没有被 Vite 正确转换,可能要检查 uni_modules、依赖转译、构建配置。

如果生产包里只有运行时判断失败,而真正的 glob 调用已经被转换成模块映射,那问题就是这个 guard 本身不适合放在这里。

这次更接近第二种:注释掉 if (!canUseGlob) return components 后,组件能正常注册。

最小修复方式

这次实际验证可用的修复是:移除运行时提前返回。

原代码:

js
function resolveComponents() {
  if (components.length) return components

  const canUseGlob = typeof import.meta !== 'undefined' && typeof import.meta.glob === 'function'
  if (!canUseGlob) return components

  const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

  // 注册组件
}

改成:

js
function resolveComponents() {
  if (components.length) return components

  const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

  // 注册组件
}

这样做的前提是:当前项目的 H5 构建链路确实由 Vite 处理,import.meta.glob 会在构建阶段被转换。

如果这是一个长期维护项目,不建议直接改 node_modules 或临时目录里的依赖源码。更稳的方式是:

  • 优先升级 uview-plus,看新版本是否已经修复。
  • 如果必须固定版本,用 patch-package 或项目已有补丁机制记录修改。
  • 如果依赖在 uni_modules 内被项目直接维护,可以把修改提交到项目源码里,并写清楚原因。
  • 发布前检查生产包,确认组件注册逻辑没有被 guard 拦截。

更稳的写法应该是什么

如果目标只支持 Vite 构建,最清晰的写法就是直接使用 import.meta.glob

js
const importFn = import.meta.glob('./components/u-*/u-*.vue', { eager: true })

不要再用运行时方式判断它是不是函数。

如果确实要兼容非 Vite 构建工具,那就不能只靠:

js
typeof import.meta.glob === 'function'

因为这只是运行时检测,无法替代构建期转换。

更稳的兼容策略通常是二选一:

策略适合场景代价
明确要求 Vite 构建uni-app + Vue3 + Vite 项目不兼容非 Vite
手工维护组件列表要兼容多种构建工具新增组件时要同步列表

例如手工维护组件列表:

js
import UButton from './components/u-button/u-button.vue'
import UForm from './components/u-form/u-form.vue'

const components = [
  UButton,
  UForm,
]

这种写法笨,但它是纯 ESM,构建工具差异更小。组件库内部为了自动注册,一般会选择 import.meta.glob;业务项目里如果踩到构建兼容坑,手工列表反而更容易排查。

排查这类问题的顺序

以后再遇到“本地组件正常、线上组件全部无效”,可以按下面顺序查:

1. 先看 DOM,不要先看样式

如果 DOM 里还有原始组件标签,优先怀疑组件没注册。

例如:

html
<u-button></u-button>

如果组件已经注册并渲染,DOM 一般会变成组件内部结构,而不是保留原标签。

2. 看插件 install 有没有执行

确认入口里是否执行了:

js
app.use(uviewPlus)

然后在 install 方法里打断点,看是否进入组件注册逻辑。

3. 看自动注册列表是否为空

重点看:

js
resolveComponents()

返回值是不是空数组。

如果返回空数组,继续查组件扫描逻辑,而不是查每个业务页面。

4. 看 import.meta.glob 是否被当成运行时 API 使用

搜索:

js
import.meta.glob

重点关注两类代码:

js
const modules = import.meta.glob(...)

和:

js
typeof import.meta.glob === 'function'

前者是 Vite 编译期能力,正常。

后者要小心,尤其是它后面如果有提前 return,就可能在生产环境拦掉正确逻辑。

5. 看生产包里的代码形态

生产包里如果还能搜到原始:

js
import.meta.glob('./xxx')

说明构建转换没发生,要查构建配置。

生产包里如果已经变成模块映射,但还有:

js
typeof import.meta.glob === 'function'

说明转换发生了,但运行时判断仍然可能失败。

这两种问题长得像,但修复方向不同。

这次问题的本质

这次问题本质上是“编译期能力”和“运行时能力”混用。

import.meta 是运行时对象。

import.meta.glob 是 Vite 在源码转换阶段识别的特殊语法。

如果把 import.meta.glob 当成浏览器运行时一定存在的函数,并用它控制是否继续执行,就可能出现:

text
构建期:glob 调用已经能被处理
运行时:import.meta.glob 不存在
判断:canUseGlob 为 false
结果:组件注册提前终止

所以这个问题表面看是 uview-plus 组件失效,实际暴露的是一个更通用的经验:

框架和构建工具提供的“编译期魔法”,不要随便按普通运行时函数去判断。

最后总结

本次线上组件失效的关键链路:

text
uview-plus install()
  -> resolveComponents()
  -> typeof import.meta.glob === 'function'
  -> 生产环境为 false
  -> 提前 return []
  -> Vue.component 没有注册
  -> 页面保留原始 u-* 标签

修复思路:

text
不要用运行时 canUseGlob 拦截 Vite 的 import.meta.glob 编译期能力。

如果项目确定走 Vite 构建,直接让 import.meta.glob 参与构建转换,比运行时判断更可靠。

如果项目要兼容非 Vite 构建,就不要依赖 import.meta.glob 自动扫描,应该准备手工导入列表或单独的构建适配方案。

基于 VitePress 的个人知识库骨架