Vue3 + Vite + pnpm Monorepo 迁移问题指南
记录将 Vue3 单体项目迁移到 pnpm workspace monorepo 架构过程中遇到的问题及解决方案。
项目背景
将原本的单体 Vue3 项目重构为 monorepo 架构,实现多应用(web、uni)共享代码包:
zkzq/
├── apps/
│ ├── web/ # Vue3 Web 应用
│ └── uni/ # UniApp 应用
├── packages/
│ ├── hooks/ # 可复用的 Composables
│ ├── utils/ # 工具函数
│ ├── store/ # Pinia Store 模块
│ ├── services/ # API 服务层
│ ├── types/ # TypeScript 类型定义
│ ├── enums/ # 枚举定义
│ └── constants/ # 常量定义
├── .env.development # 环境变量(根目录)
├── .env.production
└── pnpm-workspace.yaml问题一:TypeScript 子路径导入报错
问题描述
在应用中使用子路径导入时,TypeScript 报错找不到模块:
// ❌ 报错:Cannot find module '@zkzq/hooks/useTable'
import useTable from "@zkzq/hooks/useTable";原因分析
子包的 package.json 缺少 exports 字段配置,TypeScript 无法解析子路径。
解决方案
在子包的 package.json 中配置 exports 字段:
// packages/hooks/package.json
{
"name": "@zkzq/hooks",
"private": true,
"version": "1.0.0",
"type": "module",
"main": "index.ts",
"types": "index.ts",
"exports": {
".": "./index.ts",
"./useTable": "./src/useTable.ts",
"./useCountDown": "./src/useCountDown.ts",
"./useLoading": "./src/useLoading.ts",
"./usePagination": "./src/usePagination.ts"
}
}为什么这样做有效
应用的 tsconfig.json 配置了 "moduleResolution": "bundler",该模式下 TypeScript 会:
- 直接识别
exports字段 - 自动解析指向的
.ts源文件 - 从源码中推断出完整的类型信息
无需额外配置 typesVersions(仅在 moduleResolution: "node" 时需要)。
问题二:包与包之间如何相互引用
问题描述
@zkzq/utils 包中需要使用 @zkzq/enums 的枚举:
// packages/utils/permission.ts
import { PermissionEnums } from "@zkzq/enums";解决方案
步骤 1:在 package.json 中声明依赖
// packages/utils/package.json
{
"dependencies": {
"@zkzq/enums": "workspace:*"
}
}步骤 2:运行 pnpm install
pnpm 会在 node_modules 中创建符号链接(symlink),指向本地的 packages/enums 目录。
为什么这样做
| 疑问 | 解答 |
|---|---|
| 修改会实时生效吗? | ✅ 是的,因为是 symlink,指向同一份文件 |
| 必须声明依赖吗? | ✅ 是的,否则依赖关系不清晰,其他开发者难以理解 |
注意事项:避免循环依赖
包之间的依赖必须是单向的,形成有向无环图(DAG):
@zkzq/types ← 最底层,无依赖
@zkzq/enums ← 底层
@zkzq/constants
@zkzq/utils ← 依赖 enums、types
@zkzq/services ← 依赖 utils、types
@zkzq/store ← 依赖 services、utils
apps/web ← 依赖所有包问题三:Service 和 Store 循环依赖
问题描述
原项目中存在循环依赖:
service/request需要在 401 时调用userStore.logout()store/user需要调用api/user获取用户信息
service/request → useUserStore(处理 401)
↑ ↓
└── service/api/user ← store/modules/user解决方案
打破循环:让 request 不依赖 store,直接处理 401 逻辑。
// packages/services/request/index.ts
// ❌ 删掉 store 导入
// import useUserStore from "@zkzq/store/modules/user";
httpClient.interceptors.response.use((response) => {
if (response.data.code === 401) {
// ✅ 直接处理,不调用 store
LocalStorageService.removeItem("ZKZQ_ACCESS_TOKEN");
window.location.href = "/login";
return Promise.reject(response.data);
}
return response.data;
});为什么这样处理
- 职责分离:request 层只负责 HTTP 通信,不应该知道 store 的存在
- 依赖单向:保持
request ← api ← store的单向依赖 - logout 逻辑简化:401 时需要做的就是清 token + 跳转,不需要通过 store
问题四:子包中环境变量类型报错
问题描述
在子包中使用 import.meta.env.VITE_APP_API_BASE_URL 时,TypeScript 报错:
类型"ImportMeta"上不存在属性"env"解决方案
步骤 1:根目录放置环境变量文件
zkzq/
├── .env.development # 开发环境
├── .env.production # 生产环境
└── apps/# .env.development
VITE_APP_API_BASE_URL=http://dev.api.com步骤 2:各应用配置 envDir 指向根目录
// apps/web/vite.config.ts
import path from "path";
export default defineConfig({
envDir: path.resolve(__dirname, "../../"),
// ...
});步骤 3:子包添加类型声明文件
// packages/services/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_API_BASE_URL: string;
// 其他环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}为什么这样做
- 环境变量统一管理:根目录一份
.env,所有应用共享 - Vite 打包时替换:子包是源码形式被打包,Vite 会自动替换
import.meta.env.VITE_* - 类型声明:让 TypeScript 识别
import.meta.env
问题五:Pinia Store HMR 不生效
问题描述
修改子包中 store 的 state 初始值 actions 逻辑后,页面没有更新,普通项目修改后会触发项目的刷新:
// packages/store/modules/team.ts
state: () => ({
temp: "123" // 修改这里,保存后页面无变化
});
actions: {
setInfo(info: any) {
this.info = info;
this.info.XXX = "xxx" // 这里保存整个页面也没有重新触发
},
}但修改 getters 会立刻生效:
getters: {
displayTemp(): string {
return "新值"; // 修改这里,保存后立刻生效
}
}原因分析
这不是 bug,是 Pinia HMR 的设计规范。
| 对比项 | 普通项目 | Monorepo |
|---|---|---|
| 保存 store 后 | 整页刷新(控制台清空) | 真正的 HMR(页面不刷新) |
| state 初始值修改 | ✅ 生效(因为页面重置) | ❌ 不生效(state 被保留) |
| getters/actions 修改 | ✅ 生效 | ✅ 生效 |
普通项目之所以"看起来生效",是因为 HMR 失败导致整页刷新,state 被重新初始化。
Monorepo 是真正的 HMR 成功状态,Pinia 的设计原则是:
尽量保留运行时 state,避免热更新导致状态丢失
为什么子包需要配置 acceptHMRUpdate
普通项目 vs Monorepo 子包的 HMR 差异
普通项目(store 在 src/store/ 下):
- 修改 store 文件 → Vite 检测到变化
- Vite 尝试 HMR,但 store 模块没有
import.meta.hot.accept处理 - HMR 边界无法确定 → 回退到整页刷新
- 页面重新加载 → store 重新初始化 → 看起来"修改生效了"
Monorepo 子包(store 在 packages/store/ 下):
- 修改 store 文件 → Vite 检测到变化
- 子包通过 workspace symlink 被引用,Vite 将其视为源码的一部分
- 配置了
acceptHMRUpdate→ Vite 知道如何处理这个模块的热更新 - 真正的 HMR 成功 → 页面不刷新,只更新 store 定义
- Pinia 按设计保留 state,只替换 getters/actions
关键区别图示
普通项目 HMR 流程:
store 修改 → Vite HMR → 无 accept 处理 → 整页 reload → state 重置
↓
"看起来生效"
Monorepo 子包 HMR 流程:
store 修改 → Vite HMR → acceptHMRUpdate 处理 → 真正的 HMR
↓
state 保留 + getters/actions 更新为什么普通项目不配置也能"工作"
普通项目即使不配置 acceptHMRUpdate:
- 修改 store → HMR 失败 → 整页刷新 → 开发者感知不到问题
- 实际上是假的热更新,每次都在重新加载页面
但在 Monorepo 中,由于模块解析路径和打包方式的差异,Vite 能够正确处理子包的 HMR,这时候就必须显式配置 acceptHMRUpdate,否则:
- 不配置:store 模块更新了,但依赖它的组件不知道要重新渲染
- 配置后:Pinia 接管 HMR,正确更新 getters/actions,保留 state
解决方案
在每个 store 文件中添加 HMR 配置:
// packages/store/modules/team.ts
import { defineStore, acceptHMRUpdate } from "pinia";
const useTeamStore = defineStore("team", {
state: () => ({
/* ... */
}),
getters: {
/* ... */
},
actions: {
/* ... */
},
});
// 添加 HMR 支持
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useTeamStore, import.meta.hot));
}
export default useTeamStore;进阶(可选):不用每个 store 都写?
如果你嫌每个 store 都写这一段烦,可以:
封装一个 helper(推荐)
// packages/store/hmr.ts
import type { StoreDefinition } from "pinia";
import { acceptHMRUpdate } from "pinia";
export function withHMR<T extends StoreDefinition>(useStore: T): T {
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useStore, import.meta.hot));
}
return useStore;
}使用:
export const useTeamStore = withHMR(
defineStore('team', { ... })
)Vite 配置
不需要额外配置!不需要配置 alias、watch、optimizeDeps 等。
只要添加了 acceptHMRUpdate,Pinia 的 HMR 就会正常工作。
HMR 行为说明
配置后的行为:
| 修改内容 | 是否立即生效 | 说明 |
|---|---|---|
| state 初始值 | ❌ | 需要刷新页面或调用 $reset() |
| getters | ✅ | 自动重新计算 |
| actions | ✅ | 定义更新,需重新调用才能看到效果 |
如果需要 state 也生效
方案 1:手动刷新页面
方案 2:使用 $reset()
if (import.meta.hot) {
import.meta.hot.accept(() => {
useTeamStore().$reset();
});
}方案 3:手动同步特定字段
if (import.meta.hot) {
import.meta.hot.accept(() => {
const store = useTeamStore();
store.temp = "新值";
});
}最终项目配置
子包 package.json 示例
// packages/hooks/package.json
{
"name": "@zkzq/hooks",
"private": true,
"version": "1.0.0",
"type": "module",
"main": "index.ts",
"types": "index.ts",
"exports": {
".": "./index.ts",
"./useTable": "./src/useTable.ts",
"./*": "./src/*.ts" // 也可以使用通配符
},
"dependencies": {
"@zkzq/utils": "workspace:*"
}
}应用 vite.config.ts
// apps/web/vite.config.ts
import { defineConfig } from "vite";
import path from "path";
export default defineConfig({
envDir: path.resolve(__dirname, "../../"),
// 其他配置...
});应用 tsconfig.json
// apps/web/tsconfig.json
{
"compilerOptions": {
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}问题六:element-plus 在 uni-app 中依赖解析失败
问题描述
packages/core 子包或者在 uni 项目中引用了 element-plus,在 uni-app 项目启动时报告 98 个依赖解析错误:
X [ERROR] Could not resolve "lodash-unified"
X [ERROR] Could not resolve "@popperjs/core"
X [ERROR] Could not resolve "@ctrl/tinycolor"
X [ERROR] Could not resolve "normalize-wheel-es"
# ... 共 98 个错误所有错误都指向 element-plus 的子依赖(lodash-unified、@ctrl/tinycolor、@vueuse/shared 等)。
原因分析
1. uni-app Vite 插件的依赖扫描策略
根据 uni-app 官方 AI 的说明,关键差异在于:
普通 Vite 项目:
扫描 element-plus/package.json → dependencies
→ 自动预构建 lodash-unified、@vueuse/shared 等
→ ✅ 不会报错uni-app 项目:
@dcloudio/vite-plugin-uni 关闭了「全量依赖扫描」
→ 只预构建业务代码显式 import 的依赖
→ element-plus 的子依赖找不到
→ ❌ 报错为什么 uni-app 要这样设计?
- 目标环境复杂(H5、微信小程序、支付宝小程序、App...)
- 小程序有代码包大小限制(2MB 主包、20MB 分包)
- 需要更严格的依赖控制,减小包体积
2. 为什么 Web 项目没问题
Web 项目(apps/web):
- 使用标准 Vite 配置,全量扫描开启
- element-plus 及其所有子依赖自动预构建
- 构建成功
uni-app 项目(apps/uni):
@dcloudio/vite-plugin-uni限制了依赖扫描范围- element-plus 的子依赖未被预构建
- 构建时找不到 lodash-unified 等模块 → 报错
3. 更深层的环境兼容性问题
即使解决了依赖预构建问题,element-plus 在不同平台的兼容性也不同:
| 平台 | 兼容性 | 原因 |
|---|---|---|
| uni-app H5 | ✅ 完全兼容 | H5 就是浏览器环境,有 DOM API |
| uni-app 小程序 | ❌ 不兼容 | 小程序没有 document、window 等 DOM API |
| uni-app App | ❌ 不兼容 | 使用 WebView,但 DOM API 受限 |
结论:element-plus 只能在 uni-app H5 端使用,小程序和 App 端无法使用。
解决方案
方案 1:临时解决(仅在 H5 端使用 element-plus)(未实践,uniapp 不采用这个 ai,只是记录)
如果只需要在 uni-app H5 端使用 element-plus,可以按以下步骤配置:
步骤 1:安装 element-plus 及其子依赖,在项目层依赖里面
cd apps/uni
pnpm add element-plus lodash-unified @vueuse/shared @ctrl/tinycolor步骤 2:配置 Vite 预构建
// apps/uni/vite.config.ts
export default defineConfig({
plugins: [uni()],
optimizeDeps: {
include: [
"element-plus",
"lodash-unified",
"@vueuse/shared",
"@ctrl/tinycolor",
],
},
});步骤 3:清除缓存并重新启动
rm -rf node_modules/.vite
pnpm uni:dev:h5预期结果:
- ✅ H5 端构建成功,element-plus 组件可以正常使用
- ❌ 小程序端仍然无法使用(因为环境限制)
架构原则总结
| 层级 | 职责 | 可依赖的库 |
|---|---|---|
packages/* | 跨端业务逻辑 | ✅ 纯 JS 库(dayjs、lodash、axios) ❌ 平台专属库(element-plus、uni-ui) |
apps/web | Web 应用 | ✅ element-plus、echarts 等 Web 库 |
apps/uni | uni-app 应用 | ✅ @dcloudio/uni-ui、uqrcodejs 等跨端库 |
核心教训:
- element-plus 在 uni-app 中的问题是预构建策略 + 环境兼容性双重问题
- 可以通过配置让 H5 端能用,但无法让小程序端使用
- 最佳实践:共享包保持平台无关,UI 库由应用层单独引入
问题七:qrcode 在 uni-app 中依赖解析失败
问题描述
packages/utils 依赖了 qrcode 库,在 uni-app 项目启动时报错: uni项目 依赖了 qrcode 库,在 uni-app 项目启动时也报错:
16:03:42 [vite] Build failed with 1 error:
../../packages/utils/node_modules/qrcode/lib/core/segments.js:8:25:
ERROR: Could not resolve "dijkstrajs"
8 │ const dijkstra = require('dijkstrajs')
╵ ~~~~~~~~~~~~即使在所有包中都安装了 qrcode,仍然解析不出 dijkstrajs 依赖。
原因分析
这个报错是因为 qrcode 依赖的 dijkstrajs 模块在 uni-app 的构建环境(vite)中无法被解析。这是 qrcode 这个库本身的问题,它在 Node 环境下运行良好,但在浏览器/小程序等前端环境打包时会出现依赖解析失败。
1. 与 element-plus 问题的相似性
| 对比维度 | element-plus | qrcode |
|---|---|---|
| 表面现象 | Could not resolve "lodash-unified" | Could not resolve "dijkstrajs" |
| 触发原因 | uni-app 关闭全量依赖扫描 | uni-app 关闭全量依赖扫描 |
| Web 项目状态 | ✅ 正常(全量扫描开启) | ✅ 正常(全量扫描开启) |
2. 核心差异:环境兼容性
element-plus:
- 设计目标:Web 浏览器 DOM 环境
- 运行时依赖:浏览器 API(document、window)
- uni-app H5:✅ 完全兼容(H5 就是浏览器)
- uni-app 小程序:❌ 不兼容(无 DOM)
- 结论:预构建问题解决后,H5 端可以正常使用
qrcode:
- 设计目标:Node.js 服务器环境
- 运行时依赖:Node.js API(Buffer、Stream、require)
- uni-app H5:⚠️ 不一定兼容(浏览器没有 Node.js API)
- uni-app 小程序:❌ 不兼容(无 Node.js 运行时)
- 结论:即使解决预构建,运行时仍可能失败
3. 详细的问题层次
第一层问题(依赖解析失败):
原因:uni-app 关闭全量依赖扫描
表现:Vite 找不到 dijkstrajs
可以解决:通过 optimizeDeps.include 手动指定第二层问题(环境不兼容,难以解决):
qrcode 内部代码:
const pngjs = require('pngjs') // Node.js 图像库
const buffer = Buffer.from(...) // Node.js Buffer API
const stream = require('stream') // Node.js Stream API
浏览器环境:❌ 没有这些 API
→ 即使预构建了,运行时也会报错 "Buffer is not defined"解决方案
方案 1:临时修复(不推荐)(未采用,只是记录)
如果强行让 qrcode 在 uni-app H5 运行:
// apps/uni/vite.config.ts
export default defineConfig({
plugins: [uni()],
optimizeDeps: {
include: [
"qrcode",
"dijkstrajs", // 强制预构建
"pngjs", // qrcode 的其他依赖
],
},
build: {
rollupOptions: {
external: ["dijkstrajs"], // 标记为外部依赖
},
},
});pnpm add qrcode dijkstrajs pngjs预期结果:
- ✅ 构建可能成功
- ❌ 运行时可能报错(浏览器缺少 Node.js API)
方案 2:替换为 uni-app 兼容库(推荐)(准备采用这个方案)
根据 uni-app 官方文档,应该使用专为 uni-app 优化的二维码库。
步骤 1:移除 qrcode
cd packages/utils
pnpm remove qrcode
cd apps/uni
pnpm remove qrcode # 如果安装过步骤 2:安装 uqrcodejs
cd apps/uni
pnpm add uqrcodejs步骤 3:修改代码
// ❌ 旧代码(qrcode)
import QRCode from "qrcode";
QRCode.toDataURL("text", (err, url) => {
console.log(url);
});
// ✅ 新代码(uqrcodejs)
import UQRCode from "uqrcodejs";
const qrcode = new UQRCode();
qrcode.data = "text";
qrcode.make();
const canvas = qrcode.createCanvas();uqrcodejs 的优势:
- ✅ 专为 uni-app 设计,全平台兼容(H5、小程序、App)
- ✅ 无 Node.js 依赖,纯前端实现
- ✅ 无依赖解析问题
问题对比总结
| 问题 | 预构建问题 | 环境兼容问题 | 是否值得解决 |
|---|---|---|---|
| element-plus | ✅ 有 | ⚠️ H5 兼容,小程序不兼容 | ✅ H5 可以用 |
| qrcode | ✅ 有 | ❌ 都不兼容(缺 Node.js API) | ❌ 直接换库 |
架构原则
共享包(packages/*)只能依赖纯 JavaScript 库:
| 库类型 | 典型代表 | 依赖环境 | uni-app 兼容性 |
|---|---|---|---|
| 纯 JS 库 | dayjs, lodash, axios | 无特殊依赖 | ✅ 全平台兼容 |
| Web UI 库 | element-plus, ant-design | 浏览器 DOM | H5 ✅ 小程序 ❌ |
| Node.js 库 | qrcode, fs-extra | Node.js 运行时 | ❌ 全平台不兼容 |
| uni-app 专用库 | uqrcodejs, @dcloudio/uni-ui | uni-app 运行时 | ✅ 全平台兼容 |
核心教训:
- qrcode 是 Node.js 库,在浏览器/小程序环境本质上不可用
- 不应该尝试让 Node.js 库在前端运行,应该选择对应的前端库
- 选库时优先考虑目标环境兼容性,而不是"反正能 npm install"
问题八:uni-app 组件找不到 SCSS 变量
问题描述
使用 uni-drawer 等 uni-ui 组件时,编译报错:
[sass] Undefined variable.
╷
27 │ background-color: $uni-bg-color;
│ ^^^^^^^^^^^^^
╵
node_modules\@dcloudio\uni-ui\lib\uni-drawer\uni-drawer.vue 27:21原因分析
1. uni-app 的默认行为
根据 uni-app 官方文档,uni.scss 是一个特殊文件:
- 默认路径:
src/uni.scss(固定位置) - 自动识别:uni-app 会自动将其中的 SCSS 变量注入到所有组件
- 无需手动引入:所有
.vue文件编译时都能访问这些变量
2. 本项目的情况
项目中 uni.scss 被移动到了 src/styles/uni.scss,导致:
- ❌ uni-app 无法自动识别
- ❌
uni-drawer等组件找不到$uni-bg-color等变量 - ❌ 编译报错:
Undefined variable
3. 为什么在 main.ts 中 import 无效?
// ❌ 这样不行
import "@/styles/uni.scss";原因:SCSS 变量是编译时概念,不是运行时概念
import只会把编译后的 CSS 加入页面(运行时)- 每个
.vue文件的<style lang="scss">是独立编译的 uni-drawer.vue编译时,编译器不知道你在 main.ts 里 import 了什么- 它看不到
$uni-bg-color的定义 → 报错
解决方案
在 vite.config.ts 中配置 SCSS 预处理器:
// apps/uni/vite.config.ts
export default defineConfig({
css: {
preprocessorOptions: {
scss: {
// 手动引入 uni.scss,让所有组件编译时都能访问变量
additionalData: `@use "@/styles/uni.scss" as *;`,
},
},
},
});原理:
- Vite 在编译每个
.vue文件的<style lang="scss">时 - 自动在前面插入
@use "@/styles/uni.scss" as *; - 所有组件(包括
node_modules中的 uni-ui 组件)都能访问这些变量
为什么不在每个文件中手动 @use?
方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| vite.config.ts 配置 | ✅ 全局生效,自动注入 | - |
| ✅ 第三方组件也能访问 | ||
| ✅ 维护简单,只需配置一次 | ||
| 每个文件手动 @use | ✅ 显式声明依赖 | ❌ 作用域隔离,只在当前文件有效 |
| ❌ 必须在每个文件顶部手动添加 | ||
❌ 无法修改第三方组件(node_modules 中的 uni-drawer) | ||
| ❌ 维护困难,容易遗漏 |
如果手动引入:
<!-- 你的组件.vue -->
<style lang="scss">
@use "@/styles/uni.scss" as *;
.my-class {
background: $uni-bg-color; // ✅ 可以访问
}
</style>存在的问题:
- 作用域隔离:
@use创建的变量只在当前文件有效,无法跨文件共享 - 顺序问题:必须确保在每个文件最顶部添加,且在其他
@use之前 - 第三方组件无法修改:
node_modules/@dcloudio/uni-ui/lib/uni-drawer/uni-drawer.vue ↑ 你无法在这个文件里手动添加 @use - 维护困难:项目有几十上百个组件,每个都要手动添加,容易遗漏
推荐方式:使用 Vite 配置的 additionalData,这是管理全局 SCSS 变量的最佳实践。参考 Vite 官方文档 - CSS 预处理器。
SCSS 变量 vs CSS 变量
这个问题涉及到前端开发的重要概念区别:
SCSS 变量($variable)
// uni.scss
$uni-bg-color: #fff;
.my-class {
background: $uni-bg-color;
}编译后:
.my-class {
background: #fff; /* 变量被替换,变量本身消失 */
}特点:
| 特性 | 说明 |
|---|---|
| 处理时机 | ⏱️ 编译时(写代码时存在,编译后消失) |
| 浏览器中 | ❌ Elements 里看不到 |
| 能否修改 | ❌ 编译后无法修改 |
| 作用域 | 📦 文件级别,必须通过 vite 配置或 @use 一个一个文件引入 |
| 使用场景 | 编译时计算、代码复用、组件库配置 |
| 额外功能 | ✅ 支持函数(darken(), lighten() 等) |
CSS 变量(:root { --variable })
// theme.scss
:root {
--bg-color: #fff;
--text-primary: #333;
}
.my-class {
background: var(--bg-color);
}编译后:
:root {
--bg-color: #fff; /* 变量保留在 CSS 中 */
}
.my-class {
background: var(--bg-color); /* 变量引用也保留 */
}特点:
| 特性 | 说明 |
|---|---|
| 处理时机 | 🌐 运行时(在浏览器中真实存在) |
| 浏览器中 | ✅ Elements 里能看到 |
| 能否修改 | ✅ 可以用 JS 动态修改 |
| 作用域 | 🎨 CSS 级别,支持继承和局部覆盖 |
| 使用场景 | 主题切换、动态样式、响应式设计 |
| 额外功能 | ✅ 可以在媒体查询中改变值 |
对比示例
SCSS 变量的限制:
$primary-color: #007aff;
.button {
background: $primary-color;
}// ❌ 无法修改,因为变量已经不存在了
document.documentElement.style.setProperty("$primary-color", "red");CSS 变量的灵活性:
:root {
--primary-color: #007aff;
}
.button {
background: var(--primary-color);
}// ✅ 可以动态修改,页面立即更新
document.documentElement.style.setProperty("--primary-color", "red");实际项目中的选择
使用 SCSS 变量:
- uni-ui 组件库的默认配置(
$uni-bg-color等) - 需要编译时计算(
$large-size: $base-size * 1.5) - 不需要动态改变的常量
使用 CSS 变量:
- 主题切换(亮色/暗色模式)
- 需要 JS 动态修改的样式
- 需要作用域继承的场景
类比理解
| 方式 | 类比 | 效果 |
|---|---|---|
| main.ts import | 买了一本字典放在书架上 | 字典存在,但写作业时看不到 |
| 每个文件 @use | 每次写作业都带着字典 | 可以,但麻烦,且第三方组件无法修改 |
| vite.config.ts 配置 | 每次发作业本时自动夹一本字典 | 所有文件(包括第三方组件)都有字典 |
问题九:Vite alias 配置导致模块解析失败
问题描述
uni-app 配置 alias 后,出现模块解析问题。根据 alias 指向不同路径,失败模式也不同:
情况 1:alias 指向 src 目录(当前配置)
// apps/uni/vite.config.ts
"@zkzq/components": path.resolve(__dirname, "../../packages/components/src/")import { message } from "@zkzq/components"; // ❌ 失败:找不到 src/index.ts
import MyMessageBox from "@zkzq/components/myMessageBox"; // ✅ 成功:找到 src/myMessageBox/index.ts情况 2:alias 指向包根目录
"@zkzq/components": path.resolve(__dirname, "../../packages/components/")import { message } from "@zkzq/components"; // ✅ 成功:找到 index.ts
import MyMessageBox from "@zkzq/components/myMessageBox"; // ❌ 失败:找不到 myMessageBox/index.ts
// (实际在 src/myMessageBox/)核心原理
alias 是纯字符串替换,不会读取 package.json 的 exports 字段
// ❌ 常见误解
"@zkzq/components" → 读取 exports 字段 → 根据配置解析路径
// ✅ 实际行为
"@zkzq/components/myMessageBox"
→ 字符串替换:packages/components/ + myMessageBox
→ 查找文件:packages/components/myMessageBox/index.ts
→ ❌ 不存在(实际在 src/ 下)为什么情况 1 和情况 2 结果相反?
项目结构:
packages/components/
├── index.ts ← 包主入口
└── src/
├── myMessageBox/
│ └── index.ts ← 子模块
└── message/
└── index.ts情况 1(指向 src/):
@zkzq/components→src/→ 找src/index.ts导出message❌ 不存在@zkzq/components/myMessageBox→src/myMessageBox→ 找src/myMessageBox/index.ts✅ 存在
情况 2(指向包根):
@zkzq/components→packages/components/→ 找index.ts✅ 存在@zkzq/components/myMessageBox→packages/components/myMessageBox→ 找myMessageBox/index.ts❌ 不存在
结论:无论指向哪里,都无法同时满足主入口和子路径
解决方案对比
| alias 配置 | 主入口 | 子路径 | 可行性 | 说明 |
|---|---|---|---|---|
指向 src/ | ❌ | ✅ | ⚠️ 需转发入口 | 情况 1,创建 src/index.ts 转发 |
| 指向包根 | ✅ | ❌ | ❌ 不可行 | 情况 2,子路径必然失败 |
| 不配置 | ✅ | ✅ | ✅ 最简单 | 通过 exports 解析(需测试 uni-app) |
推荐方案:在 src 创建转发入口
步骤 1:创建转发文件
// packages/components/src/index.ts(新建)
export * from "../index";
export { default as message } from "../index";步骤 2:保持 alias 配置不变
// apps/uni/vite.config.ts
"@zkzq/components": path.resolve(__dirname, "../../packages/components/src/")工作原理:
@zkzq/components
→ alias 替换:packages/components/src/
→ 查找文件:packages/components/src/index.ts ✅
→ 执行转发:export * from "../index"
→ 最终加载:packages/components/index.ts(包主入口)
@zkzq/components/myMessageBox
→ alias 替换:packages/components/src/myMessageBox
→ 查找文件:packages/components/src/myMessageBox/index.ts ✅优点:
- ✅ 最小改动(一个文件)
- ✅ 主入口和子路径都工作
- ✅ uni-app 仍能访问源码(条件编译正常)
验证步骤:
pnpm uni:dev:h5 # uni-app 测试
pnpm dev # Web 测试总结
| 问题 | 解决方案 | 关键点 |
|---|---|---|
| TS 子路径导入报错 | 配置 exports 字段 | moduleResolution: "bundler" 自动识别 |
| 包间依赖 | workspace:* + pnpm install | symlink 实时生效 |
| 循环依赖 | 重构代码,保持单向依赖 | request 不依赖 store |
| 环境变量类型报错 | envDir + env.d.ts | 统一管理,Vite 自动替换 |
| Pinia HMR | acceptHMRUpdate | state 保留是设计规范 |
| element-plus 依赖解析失败 | 换用跨平台 UI 库或仅在应用层使用 | uni-app 依赖扫描策略 + 环境兼容性 |
| qrcode 依赖解析失败 | 换用 uqrcodejs 等跨平台库 | Node.js 库在浏览器/小程序环境不可用 |
| uni-app 找不到 SCSS 变量 | vite.config.ts 配置 additionalData | SCSS 变量是编译时概念,需在编译时注入 |
| Vite alias 模块解析失败 | 在 src 创建转发入口(当前方案) | alias 是纯字符串替换,不读取 exports |
| 或不配置 alias(最简单,需测试) | 指向包根会导致子路径失败,指向 src 需转发入口 |