SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

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 会:

  1. 直接识别 exports 字段
  2. 自动解析指向的 .ts 源文件
  3. 从源码中推断出完整的类型信息

无需额外配置 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;
});

为什么这样处理

  1. 职责分离:request 层只负责 HTTP 通信,不应该知道 store 的存在
  2. 依赖单向:保持 request ← api ← store 的单向依赖
  3. 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;
}

为什么这样做

  1. 环境变量统一管理:根目录一份 .env,所有应用共享
  2. Vite 打包时替换:子包是源码形式被打包,Vite 会自动替换 import.meta.env.VITE_*
  3. 类型声明:让 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/ 下):

  1. 修改 store 文件 → Vite 检测到变化
  2. Vite 尝试 HMR,但 store 模块没有 import.meta.hot.accept 处理
  3. HMR 边界无法确定 → 回退到整页刷新
  4. 页面重新加载 → store 重新初始化 → 看起来"修改生效了"

Monorepo 子包(store 在 packages/store/ 下):

  1. 修改 store 文件 → Vite 检测到变化
  2. 子包通过 workspace symlink 被引用,Vite 将其视为源码的一部分
  3. 配置了 acceptHMRUpdate → Vite 知道如何处理这个模块的热更新
  4. 真正的 HMR 成功 → 页面不刷新,只更新 store 定义
  5. 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):

  1. 使用标准 Vite 配置,全量扫描开启
  2. element-plus 及其所有子依赖自动预构建
  3. 构建成功

uni-app 项目(apps/uni):

  1. @dcloudio/vite-plugin-uni 限制了依赖扫描范围
  2. element-plus 的子依赖未被预构建
  3. 构建时找不到 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/webWeb 应用✅ element-plus、echarts 等 Web 库
apps/uniuni-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-plusqrcode
表面现象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浏览器 DOMH5 ✅ 小程序 ❌
Node.js 库qrcode, fs-extraNode.js 运行时❌ 全平台不兼容
uni-app 专用库uqrcodejs, @dcloudio/uni-uiuni-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>

存在的问题:

  1. 作用域隔离:@use 创建的变量只在当前文件有效,无法跨文件共享
  2. 顺序问题:必须确保在每个文件最顶部添加,且在其他 @use 之前
  3. 第三方组件无法修改:
    node_modules/@dcloudio/uni-ui/lib/uni-drawer/uni-drawer.vue
    ↑ 你无法在这个文件里手动添加 @use
  4. 维护困难:项目有几十上百个组件,每个都要手动添加,容易遗漏

推荐方式:使用 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 installsymlink 实时生效
循环依赖重构代码,保持单向依赖request 不依赖 store
环境变量类型报错envDir + env.d.ts统一管理,Vite 自动替换
Pinia HMRacceptHMRUpdatestate 保留是设计规范
element-plus 依赖解析失败换用跨平台 UI 库或仅在应用层使用uni-app 依赖扫描策略 + 环境兼容性
qrcode 依赖解析失败换用 uqrcodejs 等跨平台库Node.js 库在浏览器/小程序环境不可用
uni-app 找不到 SCSS 变量vite.config.ts 配置 additionalDataSCSS 变量是编译时概念,需在编译时注入
Vite alias 模块解析失败在 src 创建转发入口(当前方案)alias 是纯字符串替换,不读取 exports
或不配置 alias(最简单,需测试)指向包根会导致子路径失败,指向 src 需转发入口

参考资料

  • Pinia HMR 官方文档
  • Vite Monorepo Discussion
  • pnpm Workspace 文档
  • uni-app uni.scss 配置文档
  • Vite CSS 预处理器配置
最后更新时间:
贡献者: 何风顺
上一页
PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
下一页
前端工程化:深入理解 npm 依赖管理机制