ARTICLE · 1117394
uni-app 项目初始化:unibest 上手实录
写 uni-app 的第一个项目,光花在基建上的时间可能比写业务还多:pages.json 手写、请求封装自己撸、登录拦截自己写、三个端各配一遍环境变量、HBuilderX 和命令行来回切……每个新项目重来一遍。这两年 uni-app 自己也出了官方的 uni-app x 工程化方案,但在 Vue 3 + Vite 这条路线上,unibest 是目前我看到的完成度最高的模板。这篇记的是我实际用它跑通一个项目的过程:它替你做了什么、怎么装、三端怎么跑、以及几个我真踩过的坑。
一、它到底是什么
unibest(作者菲鸽)不是一个 UI 库,也不是 uni-app 的升级版,它是一个跨端项目模板 + 配套 CLI。技术栈本身现在都不算新鲜:
uni-app + Vue 3 + TypeScript Vite 5 构建(不依赖 HBuilderX 写代码) UnoCSS 原子化样式 Pinia + pinia-plugin-persistedstate 状态管理 alova 请求库(适配 uni.request) z-paging 列表分页、vue-i18n 多语言、dayjs Vitest、ESLint、husky、commitlint、lint-staged
它真正的价值不在这些依赖本身,而在把这些东西按一套约定接好,并且暴露为你能改的配置文件。核心是三层:

最关键的一层是最上面那层:pages.json 和 manifest.json 是构建产物,不是你手写的源文件
.config.ts,不用去翻 pages.json。二、环境准备:先确认版本
这一步我被绊了一下。网上包括不少 CSDN 教程写的都是 node>=18、pnpm>=7.30,但 v4 版的 package.json 里 engines 字段已经收紧了,官方「快速开始」页也是新口径:
// package.json(v4.x) "engines": { "node": ">=20", "pnpm": ">=9" }
>=20 | ||
>=9 | npm i -g pnpm | |
v3 代码块,建议 VS Code | ||
preinstall 里是 npx only-allow pnpm——如果你顺手用 npm install 或 yarn,会被直接拦下来。这不是 bug,是刻意的:uni-app 的依赖对包管理器行为敏感,别混用。三、创建项目:三种方式
推荐用 CLI 创建,因为可以顺便把平台、UI 库、登录策略、多语言一起定下来。
# 方式一:交互式(第一次用推荐,能看到所有选项) pnpm create unibest my-project cd my-project pnpm install pnpm dev # 方式二:命令行参数直接指定,不进交互 pnpm create unibest my-project --i18n --login # 方式三:项目建好后再补功能 cd my-project pnpm create unibest add i18n pnpm create unibest add login pnpm create unibest add i18n login lime-echart ucharts
CLI 底层是从仓库的 base 分支克隆模板,所以给你的一定是最新基线版本。参数表:
-p, --platform | h5mp-weixinappmp-alipaymp-toutiao | |
-u, --ui | nonewot-uiwot-ui-v2uview-plusuv-uisard-uniappuview-protdesign | |
-l, --login | ||
-i, --i18n |
# 一次性指定平台和 UI 库(v4 新增 wot-ui-v2) pnpm create unibest my-project -u wot-ui-v2 -p h5,mp-weixin # 补加功能时想覆盖已有配置 pnpm create unibest add i18n --force
wot-ui-v2 时,CLI 会自动注入 wot-ui-resolver.ts 并改好 vite.config.ts,避免 wd-button 没样式。如果你照老文章以为「装了 unibest 就一定有 wot-ui」,到 pnpm dev 时才发现组件全是方的,就是版本认知的问题。四、目录结构:约定式路由怎么算的
建出来的项目长这样,源码全在 src/ 下:

路由是「文件位置」推导出来的,新建页面只要建目录,不用去注册
页面的导航栏标题这类配置,不写在一个集中式 JSON 里,而是写在页面文件自己的 definePage 块里:
<script setup lang="ts"> // src/pages/detail/index.vue definePage({ style: { navigationBarTitleText: '商品详情', // 需要自定义导航栏时打开(H5/小程序表现不同,慎用) // navigationStyle: 'custom', }, }) </script>
全局级的页面配置写在 pages.config.ts:
// pages.config.ts import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages' export default defineUniPages({ globalStyle: { navigationBarTextStyle: 'black', navigationBarBackgroundColor: '#FFFFFF', }, })
应用级配置(appid、平台配置、h5 的 router base)写在 manifest.config.ts:
// manifest.config.ts import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest' export default defineManifestConfig({ 'mp-weixin': { appid: VITE_WX_APPID, setting: { // 开发期关掉合法域名校验,方便本地联调 urlCheck: false, }, usingComponents: true, }, h5: { // 部署在子目录时改这里,比如 /admin/ router: { base: '/' }, }, locale: VITE_FALLBACK_LOCALE, })
env/,就是 unibest 的配置体系:pages.config.ts 管路由,manifest.config.ts 管应用,env/ 管多环境。构建时 loadEnv 按 mode 读对应文件,变量通过 import.meta.env 消费。想区分微信的 develop / trial / release 环境,官方给了平台级变量命名约定,比如 VITE_SERVER_BASEURL__WEIXIN_DEVELOP、..._WEIXIN_TRIAL、..._WEIXIN_RELEASE,不用自己写判断逻辑。五、几个我最先用上的内置能力
1. 请求层已经是适配好的
unibest v4 用的是 alova 而不是自己撸 uni.request 包装,配了 uniapp 适配器。简化后大致是:
// src/http/http.ts(示意) import { createAlova } from 'alova' import AdapterUniapp from '@alova/adapter-uniapp' export const alovaInstance = createAlova({ baseURL: import.meta.env.VITE_SERVER_BASEURL, ...AdapterUniapp(), })
配套的 src/http/interceptor.ts 是请求拦截器,注入 token、开 loading 计数、统一错误提示都挂在这里;src/router/interceptor.ts 是路由拦截器,登录守卫写在这里——它有个「登录策略」开关,两种模式:DEFAULT_NO_NEED_LOGIN 是黑名单策略(默认,可以直接进 APP),DEFAULT_NEED_LOGIN 是白名单策略(默认进不去,必须先登录),配套一个 EXCLUDE_PAGE_LIST 排除列表,语义跟着模式走。这两个文件是「业务相关」的部分,它只给你骨架和位置,token 怎么存、错误怎么提示、登录态怎么刷新都得自己写——不然容易以为「用了 unibest 就自动有登录功能」。
2. 启动顺序有讲究,别乱改
// src/main.ts(示意) import { createSSRApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' import { routeInterceptor } from './router/interceptor' import { requestInterceptor } from './http/interceptor' export function createApp() { const app = createSSRApp(App) app.use(createPinia()) // 顺序必须:store → 路由拦截 → 请求拦截 // 两个拦截器都要读 store,不先注册 pinia 会拿不到 token app.use(routeInterceptor) app.use(requestInterceptor) return { app } }
useTokenStore() 拿到的是空的,表现为「首屏所有请求都不带 token」,而且控制台不一定报明显的错,很难查。3. 状态持久化是白名单式的
token、user 这类 store 内置了 persist,直接落盘:
// src/store/token.ts(示意) import { defineStore } from 'pinia' export const useTokenStore = defineStore('token', { state: () => ({ token: '', }), actions: { setToken(token: string) { this.token = token }, }, persist: true, })
具体存哪些字段、怎么存,还是得按自己业务定。这块我在专栏的 Pinia 持久化那篇里展开讲过——核心是别把 loading、弹窗开关这类临时状态也 persist 进去,不然会出现「带着上次的 loading 启动」这类诡异问题。
4. v3 代码块和自动导入
在 .vue 里输入 v3 按 Tab,能直接生成页面骨架(script setup + template + style)。另外 unplugin-auto-import 配好了,uni-app 的 API 可以直接用,不用手写 import:
<script setup lang="ts"> // 不用 import,直接用 const list = ref([]) onLoad((options) => { console.log(options) }) onShow(() => {}) </script>
六、三端运行与发布
命令是固定的,我整理成一张表,实际项目里就是照这个跑:
pnpm devpnpm dev:h5 | pnpm build:h5 | dist/build/h5 | |
pnpm dev:mp | pnpm build:mp | dist/build/mp-weixin | |
pnpm dev:app | pnpm build:app | dist/build/app |
几个容易搞混的细节:
- H5
pnpm dev之后开http://localhost:9000/,端口在env/里配(VITE_APP_PORT)。 - 微信小程序
先跑 pnpm dev:mp,然后在微信开发者工具里导入dist/dev/mp-weixin,不是导入项目根目录。构建完是dist/build/mp-weixin,导入后点「上传」。 - App
跑 pnpm dev:app之后,用 HBuilderX 导入dist/dev/app,选运行到模拟器或基座。安卓和鸿蒙有更省事的做法——把整个项目导入 HBuilderX,用它的菜单直接运行,不用走产物目录。 - 其他平台
支付宝、抖音、快手、小红书、飞书、QQ、百度、快应用都有对应脚本, dev:mp-alipay、dev:mp-toutiao、dev:mp-xhs……命名规律就是dev:<平台>,用之前记得在package.json的 scripts 里确认一下。
# 其他常用脚本 pnpm type-check # vue-tsc 类型检查 pnpm lint # ESLint pnpm lint:fix # 自动修 pnpm test # Vitest 跑单测 pnpm openapi # 用 openapi-ts-request 从接口文档生成类型化请求代码 pnpm upload:mp # miniprogram-ci 脚本,配合 CI 发布用
feat: / fix: 这类前缀。顺便说一句,模板里 lint-staged 的配置是作者写的一句彩蛋——所有文件匹配到的命令是 echo 祝菲鸽身体健康,所以装完之后 lint-staged 实际上什么都不检查。你大概率要自己把这块改成 eslint --fix。七、踩过的坑
坑一:照着老教程装,环境版本对不上
搜到的多数第三方教程(包括几篇 CSDN 的)还写着 node 18 / pnpm 7.30。v4 已经收紧了,pnpm 8 装完会有 engines 警告。直接照官方文档的 >=20 / >=9 走,别信第三方教程里的版本号。
坑二:以为 UI 库是内置的
老文章说 unibest = uniapp + wot-ui。现在 v4 的 CLI 是在创建时选 UI 库的,-u none 就是纯 UnoCSS 不带组件库。选完之后 node_modules 里有没有那套组件,取决于你创建时的选择——装完记得先确认这一点,再 pnpm dev。
坑三:H5 首屏底部 tabbar 没出来
官方注意事项里第一条就写了:代码跑起来后 H5 底部没有 tabbar,刷新浏览器或者重跑一次 pnpm dev 即可。属于首屏编译时序的小问题,不用排查。
坑四:自动导入的 API 报「未定义」
同样在官方注意事项里:跑一次 pnpm dev 就好。这些 API 的类型声明是构建时生成的,刚 clone 完仓库还没生成,第一次 dev 会报一片红,看着吓人,实际重启就好。
坑五:没有 Git,install 阶段就挂了
husky 需要 Git 仓库。如果你是下载 zip 而不是 clone,pnpm install 会在 prepare 阶段报错。git init 一下就行。
坑六:H5 部署到子目录后白屏
产物路径变了但 base 还是 /,资源全 404。改 manifest.config.ts 里的 h5.router.base,和 nginx 的 location 对齐。
坑七:把 unibest 当成了「零配置框架」
这个是观念上的坑。它替你做的是工程约定和接线,业务决策一件没替你做——登录怎么登、token 怎么刷新、接口怎么组织、错误怎么提示、多语言文案翻哪几份,都得自己写。可以省掉的基建,省不掉业务。想清楚这点,才不会觉得「用模板就等于什么都没做」。
八、什么情况下适合用、什么情况下别用
.vue,两套东西的工程结构、组件写法、构建流程都不一样,网上很多「uni-app 工程化」文章把两者混着讲,看的时候注意区分。九、上线前过一遍的清单
engines和本机 Node / pnpm 版本对得上,别停在 18 / 8; lint-staged那句彩蛋换成真正的检查命令,否则 hooks 形同虚设; manifest.config.ts里 urlCheck: false只在开发期开,发版前确认关掉;微信小程序的合法域名、App 端的证书和备案,配齐了再提审; H5 部署子目录的 h5.router.base和服务器配置一致;persist的字段是白名单还是全量,临时状态排干净了没; 拦截器的注册顺序没被后人改乱(store → route → request); pnpm type-check和 pnpm lint在 CI 里跑一遍,别只靠 IDE 提示。
· · ·
unibest 解决的是一个很朴素的问题:让每个 uni-app 项目的起点是一样的。它不折腾花样,把 CLI 化开发、多端构建、约定式路由、TS 配置、hooks 检查串成了能跑通的闭环;一个人要同时顾 H5 / 小程序 / App,省下来的时间相当可观。
边界也很清楚——业务还是自己的,uni-app x 也不在范围内。
后续我打算再写一篇实战的:把 unibest 模板改造成自己项目的私脚手架过程(换 UI 库、删不需要的 feature、调整目录约定),如果你也在用,评论区可以交流你踩了哪些坑。
觉得有用的话,点赞、分享、推荐三连支持一下。