vite-plugin-uni-root
UniApp 虚拟根组件 Vite 插件,零额外依赖,纯字符串操作实现。
解决的问题
UniApp 不支持原生的根组件包裹机制,无法在全局统一注入 u-config-provider、u-toast、u-modal 等需要在每个页面根部存在的组件。传统方案需要在每个页面手动添加,维护成本高。
本插件通过 Vite 构建时注入,自动为每个页面包裹 <global-root-view> 根组件,无需修改任何页面代码本身。
技术方案
架构
App.root.vue(源码) → 在 main.ts 中导入并注册为全局组件 global-root-view
→ 页面模板被包裹 <global-root-view> 页面内容 </global-root-view>
→ 渲染时页面内容填入 <slot />
工作流程
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ main.ts │ → │ 注入 import │ → │ 全局注册组件 │
│ │ │ + 注册代码 │ │ global-root-view │
└─────────────┘ └──────────────┘ └─────────────────┘
│
┌─────────────┐ ┌──────────────┐ │
│ page.vue │ → │ 包裹模板 │ │
│ │ │ <global-...> │ ←─────────┘
└─────────────┘ └──────────────┘
│
┌─────────────┐ │
│ App.root.vue│ → <slot /> 接收页面内容
└─────────────┘
三步实现
-
main.ts 注入(
root.ts -> registerRootApp)- 在 main.ts 中注入
import GlobalRootView from "./App.root.vue" - 注入
app.component("global-root-view", GlobalRootView)注册为全局组件 - 注:
GlobalRootView仅为局部变量名,实际组件就是App.root.vue
- 在 main.ts 中注入
-
页面模板包裹(
page.ts -> transformPage)- 解析 SFC,定位
<template>标签的起止位置 - 提取 PageMeta 节点(如有则放到包裹层外部)
- 将模板内容包裹在
<global-root-view>...</global-root-view>中
- 解析 SFC,定位
-
App.root.vue 加载(
index.ts -> load)- 拦截
App.root.vue的模块请求,从文件读取并返回 - 模板中的
<slot />接收并渲染页面内容
- 拦截
跨端兼容
插件是纯 Vite 构建时插件,在 UniApp 编译之前完成源代码转换,不依赖任何平台特定 API,所有 UniApp 支持的平台均可正常工作:
| 平台 | 支持 |
|---|---|
| H5 | ✅ |
| 微信小程序 | ✅ |
| 支付宝小程序 | ✅ |
| 头条/抖音小程序 | ✅ |
| Android App | ✅ |
| iOS App | ✅ |
| 鸿蒙 App | ✅ |
使用方式
1. 创建 App.root.vue
在 src/ 目录下创建 App.root.vue 文件:
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { useTheme, useLocale } from 'uview-pro';
const { darkMode, themes, currentTheme } = useTheme();
const { currentLocale } = useLocale();
onLoad(() => {
console.log('darkMode->', darkMode.value);
console.log('theme->', currentTheme.value?.name);
console.log('locale->', currentLocale.value?.name);
});
</script>
<template>
<u-config-provider
:dark-mode="darkMode"
:themes="themes"
:current-theme="currentTheme?.name"
:current-locale="currentLocale?.name"
>
<slot />
<u-toast global></u-toast>
<u-modal global></u-modal>
</u-config-provider>
</template>
关键点:<slot /> 是页面内容的渲染位置,必须保留。
2. 配置 vite.config.ts
// npm 安装
import { UniRoot } from 'uview-pro/plugins';
// uni_modules 安装
import { UniRoot } from './src/uni_modules/uview-pro/plugins';
export default defineConfig({
plugins: [
UniRoot(), // 放在其他插件之前
// ...其他插件
],
});
3. 运行
npm run dev
不需要修改任何页面代码,所有页面自动被 <global-root-view> 包裹。
文件结构
src/uni_modules/uview-pro/plugins/root/
├── index.ts # 插件入口,Vite 插件生命周期
├── root.ts # main.ts 注入 + App.root.vue 处理
├── page.ts # 页面模板包裹转换
└── utils.ts # 工具函数(SFC 解析、pages.json 加载、AST 查找等)
配置项
UniRoot({
rootFileName: 'App.root', // 根组件文件名(不含扩展名),默认 App.root
})
HMR 支持
修改 pages.json 后自动重载页面列表,无需手动重启开发服务器。
注意事项
- Vue 版本需 >= 3.2.13(使用
vue/compiler-sfc的parseAPI,HBuilderX 环境下自动降级为正则匹配) - 页面模板中如有
<page-meta>组件,会被提取到包裹层外部,确保微信小程序兼容 - 页面模板中如有嵌套
<template #slot>具名插槽,不影响根模板的正确识别
致谢
本插件的实现参考了 @uni-ku/root(MIT License,作者 skiyee),核心思路受其启发并进行了重新实现。