首页 / 资讯中心 / 文章详情

Vue 3全家桶实战指南:Vite构建、Pinia状态管理与迁移避坑

Vue 3全家桶实战指南:Vite构建、Pinia状态管理与迁移避坑 ★ FEATURED ARTICLE
1. 从 Vue 2 到 Vue 3 全家桶绕不开的那道坎Vue 3.0 正式发布到现在已经过了不少年头如果你还在观望或者刚接手一个 Vue 2 的老项目准备迁移那这篇文章就是给你写的。Vue 3 全家桶并不是单纯把框架换了个版本号而是整套生态工具链的一次集体升级构建工具从 webpack 换成了 Vite状态管理从 Vuex 换成了 Pinia路由从 Vue Router 3 换成了 Vue Router 4再加上 Composition API 这套全新的逻辑组织方式说实话变动幅度比我预想的大得多。我自己从 Vue 2 项目迁到 Vue 3 的时候踩过的坑不算少。最典型的就是代码写着写着reactive解构之后数据不响应了页面怎么都不更新还有路由守卫里拿不到组件的this整个逻辑得重写更别提 Vite 的生态兼容问题很多老的 npm 包在 ESM 模式下直接跑不起来。这些事网上零零散散都有提但很少有人把全家桶的完整使用场景串在一起讲清楚。这篇文章以我个人实际使用的经验为主从项目搭建开始把 Vue 3 Vite Vue Router 4 Pinia 这一整套东西的选型思路、核心用法、注意事项和排查技巧完整梳理一遍。适合的人群主要有两类一类是从 Vue 2 迁移过来的老手想快速知道两版之间到底差在哪、迁移时要改什么另一类是刚开始学 Vue 3 的新人希望直接上手一套规范且能落地的全家桶方案。如果你正准备在公司项目里推 Vue 3这篇文章也能给你做决策时提供一些参考。2. 全家桶方案选型为什么是 Vite 加 Pinia2.1 构建工具Vite 对比 webpack不只是快这么简单很多人在做技术选型时会纠结公司内部还有一堆老项目走 webpack要不要统一我的结论很直接新项目直接用 Vite别犹豫。Vite 启动项目的速度是秒级的大型 webpack 项目冷启动可能要十几秒甚至更久Vite 基于浏览器原生的 ESM 模块只在启动时按需加载当前页面涉及到的模块省去了整包编译的过程这个差异用一次就回不去了。但这不只是快的问题。Vite 的依赖预构建机制会把 CommonJS 格式的依赖统一转换为 ESM这个机制帮我提前发现了不少老包的问题。比如有些 npm 包内部引用了window对象、process.env等 Node 环境变量在浏览器原生 ESM 下直接报错这时候就需要在 Vite 配置里做兼容处理。还有一个前车之鉴项目里用了某款老版本的富文本编辑器组件打包后字体文件路径死活不对排查半天发现是 Vite 的静态资源处理规则和 webpack 不同public目录和相对路径的解析方式有差异解决方案是把资源改放到src/assets里通过模块方式引入。2.2 状态管理Pinia 取代 Vuex 的理由Vuex 在 Vue 2 时代几乎是无处不在的存在但到了 Vue 3 时代Pinia 明显更顺手。核心原因有几个第一Pinia 全面拥抱 Composition APIstore 的定义方式天然和 setup 语法契合。你用ref、computed怎么写逻辑在 Pinia 里就怎么写 store不需要学一套额外的 Vuex 风格 API。第二Pinia 的 TypeScript 推导非常友好不需要像 Vuex 那样手写繁琐的模块类型声明。第三Pinia 不再有mutations的概念改状态直接调actions里的方法少了一层概念。我有个实际迁移的经历某管理后台项目原来用 Vuex有十几个 module每个 module 里都写着 getters、mutations、actions 三件套光模块文件就快一百个。迁移到 Pinia 后模块数量没变但每个文件的代码量少了一半以上因为不再需要根据同步异步去拆分 mutations 和 actions也不用写一堆 namespaced 配置。团队新人上手的难度明显降低。2.3 全家桶版本怎么配这里给出我验证过的稳定组合可以直接照抄工具推荐版本说明Vue3.4.x 及以上3.4 之后的模板编译器和响应式系统都比较稳定Vite5.x如果还在用 4.x 也不用急着升但新项目直接用 5Vue Router4.2.x匹配 Vue 3 的 API 设计Pinia2.x配合 Vue 3.3 有更好的类型推导Vite 插件vitejs/plugin-vue必须装不然 .vue 文件没法编译需要提醒的是版本不要追新到 beta 或者 alpha。我有过一次教训项目里用了当时还处于 RC 阶段的某个 Vite 插件版本结果和一个核心依赖产生了兼容问题排查到深夜才发现是版本冲突。全家桶方案讲究的是整体稳定用 release 版本里最新的那个次版本号就够了。3. Vue 3 核心响应式从 ref 到 reactive 的完整理解3.1 ref 和 reactive 到底怎么选Vue 3 的响应式系统是整个组合式 API 的地基但很多新手一上来就栽在ref和reactive的选择上。先说结论默认用ref只有当你确定要定义一个对象且需要整体替换时才考虑reactive。为什么ref的底层虽然也走 reactive但它给值包了一层 Ref 对象这个包装让它在任何赋值场景下都能保持响应式连接。而reactive直接代理原始对象当你把对象里的某个属性解构出来单独使用响应式连接就断了。我举个实际例子。我之前写过一个表单组件初始值是从接口拿到的对象const form reactive({ name: , age: 18, address: {} }) // 不能这样解构 const { name, age } form // 此时 name 和 age 只是普通字符串和数字修改它们不会触发视图更新正确做法是用toRefs或直接全程用refconst formRef ref({ name: , age: 18, address: {} }) // 修改时通过 formRef.value 操作实际上在我写过的业务代码里reactive的使用频率远低于ref因为ref在模板里自动解包写起来并不费劲。只有像表单这种整个对象嵌套很深的场景我会优先用reactive配合toRefs来代替旧版的data()。3.2 深层监听和 shallowRef 的取舍Vue 3 的watch默认是浅层的还是深层的很多人没搞明白。实际上watch监听一个ref时默认不会做深度监听但如果你监听的是reactive对象它又会自动开启深度监听。这个行为之间的差异如果不注意很容易写出看起来应该响应但实际没反应的 bug。举个例子const list ref([{ id: 1, count: 1 }]) // 这样监听不会触发因为默认是浅监听 watch(list, (newVal) { console.log(list changed) }) // 需要加 deep 选项 watch(list, (newVal) { console.log(list changed) }, { deep: true })反过来如果页面里监听的是一个很大的数据对象每次变更都要做深度比较性能会受影响。这种场景我推荐shallowRef它只代理对象第一层深层变化不会触发响应适合那些数据结构很大、但只有整体替换时才需要更新的情况。典型的场景是富文本编辑器的初始内容、大表格组件的静态配置项用普通ref会让性能白白浪费。3.3 computed 和 watch 的最佳实践computed适合派生状态watch适合副作用处理这个概念要分清。我见过不少项目把computed当成 watch 用在 computed 里发请求改数据这是典型的反模式。正确的做法是一个值如果能从其他状态计算得到就用computed不要额外定义一个变量去手动同步。比如列表的筛选结果const keyword ref() const allList ref([]) const filteredList computed(() { const kw keyword.value.trim().toLowerCase() if (!kw) return allList.value return allList.value.filter(item item.name.includes(kw)) })这里filteredList不需要手动维护keyword或allList一变它就自动更新。而watch的使用场景是搜索防抖、路由参数变化后的数据请求、表单某个字段变化后的联动逻辑等。我在实际项目里的经验先想想这个值能不能算出来如果能就不要用 watch 去同步它代码会干净很多。4. Composition API 的项目级组织方式4.1 别再整个页面塞一个 setup 了Vue 3 引入 Composition API 的初衷是把同一逻辑的代码聚合到一起但如果你只是在setup里从上到下把变量和方法全部写完效果跟 Vue 2 的 options 写反了——代码比 before 还乱。我推荐按业务场景封装成组合式函数就是所谓的composables。比如用户列表页可以把加载、筛选、分页、删除这四件事拆成多个 hooks这样每个函数文件只做一件事测试和维护都简单。一个典型的可复用组合式函数// composables/useTableList.js import { ref, computed } from vue export function useTableList(fetcher, { pageSize 10 } {}) { const list ref([]) const loading ref(false) const currentPage ref(1) const total ref(0) const totalPages computed(() Math.ceil(total.value / pageSize)) async function loadList() { loading.value true try { const { rows, count } await fetcher({ page: currentPage.value, pageSize }) list.value rows total.value count } finally { loading.value false } } function changePage(page) { currentPage.value page loadList() } return { list, loading, currentPage, total, totalPages, loadList, changePage } }页面里的用法就是const { list, loading, changePage } useTableList(fetchUserList)这种模式特别适合中后台系统因为大量页面的列表行为是同构的抽成 composable 后新页面的列表功能十分钟就能写完。但这里我要泼一盆冷水不要一上来就抽 hook只有当你发现至少两三个页面都在用同一段逻辑时再抽才值得。过度抽象在业务代码里是很常见的技术债来源。4.2 setup 语法糖带来的双刃剑script setup极大地简化了组件的写法不需要写export default { setup() {...} }那一层变量和函数可以直接在模板里使用。但方便归方便它也有需要注意的地方。最典型的坑有两个一是组件不能再通过config的方式被外部直接访问如果你在 Vue 2 里习惯了通过this.$refs.child拿子组件实例再调用子组件的方法那在 script setup 里你得通过defineExpose主动暴露方法否则外部拿到的实例是封闭的。script setup function openDialog() { ... } defineExpose({ openDialog }) /script二是defineProps的默认值写法变了。Vue 2 写props: { name: { type: String, default: vue } }Vue 3 用类型标注时要注意script setup const props withDefaults(defineProps{ name?: string age?: number }(), { name: vue, age: 18 }) /scriptwithDefaults帮你处理了默认值问题但它只是一个编译期的工具运行时的类型校验还需要额外处理。如果你需要更严格的 props 校验逻辑比如自定义 validator用纯 TypeScript 类型定义就不够用了需要在defineProps里写完整的运行时配置。我自己项目里的做法是组件的 props 尽量用 TS 类型定义配合withDefaults给默认值简单场景已经够用只有当组件需要对外提示传参格式时才回头用运行时校验。4.3 生命周期和 Vue 2 的对应关系Vue 3 把大部分生命周期钩子都改名了但逻辑上是平移的拿张对应表查就行Vue 2 选项Vue 3 组合式 API说明beforeCreate不需要setup 阶段setup 本身就替代了这个阶段created不需要setup 阶段同上beforeMountonBeforeMount挂载前mountedonMounted挂载完成beforeUpdateonBeforeUpdate更新前updatedonUpdated更新后beforeDestroyonBeforeUnmount卸载前destroyedonUnmounted卸载完成这里有个容易忽视的点setup的执行时机比created早得多在组件实例创建之前所以setup里拿不到this也不能访问组件的 data 和 methods。这逼着你把所有逻辑外移到独立的函数或者直接用参数传递。还要注意一个问题如果项目里有第三方插件依赖实例化时绑定的事件比如某个图表库监听页面的 resize 事件你必须在onBeforeUnmount里清理掉否则组件卸载后事件还在页面会多出很多幽灵监听性能慢慢就吃光了。5. Vue Router 4 与 Pinia 的完整落地5.1 路由配置的差异和动态路由处理Vue Router 4 的配置 API 大框架没变但有几个细节很不一样。首先是创建方式从new VueRouter()变成了createRouter({ history: createWebHistory(), routes })简单理解就是路由变成了函数式工厂这也是整个 Vue 3 风格统一的一部分。动态路由这个东西中后台项目几乎天天用。我的做法是登录后根据后端返回的权限标识动态添加路由。const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: LoginPage }, { path: /, component: LayoutPage, children: [] } ] }) function setupDynamicRoutes(permissionList) { const routes permissionList.map(perm ({ path: perm.path, name: perm.name, component: () import(/views${perm.componentPath}.vue), meta: { title: perm.title, icon: perm.icon } })) routes.forEach(route router.addRoute(LayoutPage, route)) }这段代码有个坑router.addRoute添加的路由如果和已有路由的 name 冲突它不会覆盖原来的路由而是完全忽略新路由。所以动态添加路由前最好先router.removeRoute(xxx)清理掉旧的否则权限更新后会看到路由一直不生效的诡异问题。此外动态 import 的路径不能是纯变量拼接这是 Vite 的限制。import(/views${perm.componentPath}.vue)这种写法Vite 需要在构建时静态分析依赖路径纯变量拼出来的路径它根本分析不到构建产物里就没有那个组件。解决方法是维护一张完整的组件映射表const viewModules { system/user: () import(/views/system/user.vue), system/role: () import(/views/system/role.vue) }这个坑我印象太深了因为一开始没看 Vite 文档动态路由加完后页面一直在转圈控制台报Failed to resolve import排查了大半天才发现是构建工具的限制。5.2 路由守卫里的异步顺序路由守卫是每个后台系统的必经环节最常见的需求是进入页面前检查登录状态、拉取用户信息、判断权限。但这里非常容易出现死循环因为beforeEach里一旦做异步操作要特别注意别让路由跳转重复触发守卫。我的做法是在路由守卫里通过pinia中的 store 判断用户信息是否已加载如果没加载就先拉取加载完再next()放行。要注意的是 放行后不会再触发一次守卫 这个概念所以在守卫里要避免再次跳转到当前页。router.beforeEach(async (to, from, next) { const useUserStore getUserStore() const userStore useUserStore() if (!userStore.token) { if (to.path ! /login) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } return } // 已登录但没有用户信息先拉取 if (!userStore.userInfo) { try { await userStore.fetchUserInfo() next() } catch (e) { userStore.clearToken() next({ path: /login }) } return } next() })这里必须强调的是useUserStore()要在beforeEach回调里调用不能放在模块顶层。因为在模块顶层执行时Pinia 实例可能还没初始化会直接报 getActivePinia was called with no active Pinia。这个报错我见了不少人问过根源就在这里。5.3 Pinia store 的正确拆法Pinia 的 store 定义一个大文件放所有状态是我极力反对的。我建议按业务模块拆比如用户相关、订单相关、配置相关都各自独立成一个 store 文件每个文件只维护自己的状态和动作。store 之间如果需要互相访问直接引用对方的实例不需要像 Vuex 那样做模块间复杂映射。一个用户模块的 Pinia store 示例// store/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: , userInfo: null, permissionList: [] }), getters: { isLogin: (state) !!state.token, isAdmin: (state) state.userInfo?.role admin }, actions: { setToken(token) { this.token token }, async fetchUserInfo() { const res await getUserInfoApi() this.userInfo res.data }, async logout() { await logoutApi() this.token this.userInfo null this.permissionList [] } } })用起来很方便组件里const userStore useUserStore() // 模板中直接用 userStore.isAdmin、userStore.userInfo可能你已经注意到 Pinia 的使用方式相比 Vuex 更像一个带状态的管理对象这也是大家说 Pinia 是Vuex 的最终形态的原因。没有 mutations 以后一个人就能把一个模块的状态管理全部搞定也减少了团队成员之间理解 API 的成本。5.4 持久化方案不装插件也能写Pinia 本身不带持久化功能如果你要刷新页面后保留登录状态就得自己处理。这一块有两个方向一个是用插件比如某个叫 pinia-plugin-persistedstate 的库另一个是自己在 action 里手动存。我个人的经验是不要在项目里为了一个持久化需求引入太多插件因为插件本身也有兼容性和体积问题。简单做法是在关键 state 变更时手动写入 localStorage初始化时读取export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, clearAll() { this.token this.userInfo null localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })如果你项目里的 store 状态特别多、又希望一次搞定所有字段的持久化那用插件确实省事但要注意插件的存储键名冲突和 JSON 序列化的性能问题。我在自己的项目里试过两种方案最终选择了手写因为可以精确控制哪些字段不持久化比如临时角色切换的状态不需要存。6. Vite 构建配置与生产环境优化6.1 路径别名和代理配置Vite 配置里最常见的两个需求一个是路径别名指向src另一个是开发环境代理后端接口。我给出一个经过验证的基础配置import { defineConfig } from vite import vue from vitejs/plugin-vue import path from node:path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }, build: { sourcemap: false, chunkSizeWarningLimit: 1500 } })路径别名这里要特别注意__dirname在 ESM 的配置文件中不一定可用如果报错就改用fileURLToPath(new URL(./src, import.meta.url))的方式。Vite 的配置文件默认是 ESM 格式这是它和 webpack 配置文件最大的区别之一。代理配置中rewrite的作用是把请求路径里的/api前缀去掉再发给后端这要根据你后端的实际接口路径决定不要照抄。如果后端接口本身就带/api那就不需要 rewrite直接 target 指过去就行。6.2 按需加载与分包策略首屏加载速度优化Vue 3 和 Vite 时代有两个利器懒加载组件和手动分包。路由组件用动态 import 就已经实现了按需加载但页面上有些业务组件没有路由概念比如详情弹窗这种只有在用户点击后才出现的组件应该也用动态 import 包一层script setup import { defineAsyncComponent } from vue const DetailDialog defineAsyncComponent(() import(/components/DetailDialog.vue)) /script分包方面Vite 默认会把所有第三方依赖打进一个 vendor 包但如果你项目里有比较大的第三方库比如图表库、编辑器我建议手动拆开避免某个库一更新整个缓存都失效。build: { rollupOptions: { output: { manualChunks: { vue-vendor: [vue, vue-router, pinia], charts: [echarts], editor: [wangeditor/editor] } } } }手动分包有个潜在的坑如果两个 chunk 之间有循环依赖控制台会报警告严重时可能到页面白屏。遇到这种情况优先把公共模块单独抽到一个 chunk 里而不是把所有第三方全部塞进一个 vendor。我踩过一次把两个内部库一起放进了vendor块里结果它们互相依赖构建后跑起来报 Cannot access before initialization最后拆开才解决。6.3 开发环境与生产环境的差异开发阶段和生产构建的差异性需要特别注意。我在生产环境遇到过一个奇怪的问题本地开发一切正常部署到服务器上之后刷新二级路由页面 404。这个和 Vite 本身没关系是服务器没有做 history rewrite也就是所有请求都回到了服务器根路径找不到就返回 404。解决方法是让服务器配置所有路径都指向 index.html不同服务器的配置方式不一样但思路是统一的。另一件常被忽略的事情是Vite 的public目录里的文件会被原样拷贝到构建产物根目录如果你在组件里用/images/logo.png这种绝对路径引用构建后必须保证服务器根目录下有这个文件。我建议所有需要打包处理、压缩或改名的静态资源都放在src/assets里通过 import 引入不要放到 public。这样构建时能正确加 hash还能通过构建工具压缩。7. 全家桶项目的常见问题排查实录7.1 模板 ref 访问不到Vue 3 中访问模板 ref如果你在onMounted里拿到的 ref 是null或者拿到的不是 DOM 元素大概率是因为模板 ref 的绑定时机问题。一个常见场景是v-for渲染的列表项做 ref拿到的是一个数组但如果你把 ref 定义成了普通 ref不会自动转换。template div v-foritem in list refitemRefs :keyitem.id{{ item.name }}/div /template script setup const itemRefs ref([]) /script这里必须注意v-for里的 ref 会自动收集成一个数组但你需要在组件挂载后再读取itemRefs.value否则可能拿到的还是空数组。如果你在路由组件里使用可能还会遇到路由切换后 ref 不更新的情况这基本是因为v-for的 key 没配对导致 Vue 复用了旧的 DOM 节点。7.2 watch 监听不到对象内部的变化这个问题在 Vue 3 里很典型就是你以为对象的属性变了 watch 总能触发但实际上监听的是引用地址。如果你监听一个ref且这个 ref 指向一个 object那不写deep: true只有整个对象被替换时才会触发。这种情况我踩过好几次尤其是接口返回超大数据、然后你在回调里改了对象某个字段watch 竟然没反应。解法有两种加deep: true或者监听对象的某个具体字段watch( () form.value.address?.province, (newVal) { console.log(province changed to, newVal) } )这种监听方式只关心具体某个字段不触发深层比较性能和逻辑清晰度都更好。我的经验是尽量监听最小粒度的数据不要为了图省事监听整个大对象。7.3 Pinia store 中调用路由或组件实例在 Pinia 的 action 里要做路由跳转第一反应是引入 router但要注意 Pinia store 文件里直接import router from /router这在大多数场景下没问题但如果你把 store 定义到了组件内部域里用工厂函数创建的 storerouter 的导入时机和 store 的实例化时机可能会有冲突。我以前遇到一个诡异问题在登录页调用 user store 里的loginaction这个 action 里要跳转首页但跳转后页面空白。排查到最后发现是因为另一个 store 文件里引用了同一个 router 实例但那个 store 在路由初始化之前就被调用了导致 router 还没有完整的路由表。稳妥的做法是不要在 action 里跳路由把跳转逻辑放在组件层。组件里先调用 action 处理状态成功后再用router.push跳转这样职责也清晰排查也容易。7.4 Vite 构建后资源路径 404这个问题是 Vite 项目的新手三连坑之一。明明本地预览都好部署上线后图片和 JS 全 404。原因通常是base配置不对。如果你部署在服务器根目录base: /没问题如果部署在子目录比如https://example.com/admin/那base要设成/admin/或相对路径./。export default defineConfig({ base: process.env.NODE_ENV production ? /admin/ : / })但注意base: ./在部分路由和服务端渲染场景下会有问题因为相对路径计算的是当前页面路径如果路由是/admin/user/list相对路径就会算错。最稳妥的还是写死一个绝对路径前缀然后让部署层面保证资源在那个目录下。7.5 问题排查速查表现象可能原因解决方案reactive 对象解构后不响应解构拿的是普通值用 toRefs 或直接用 ref页面白屏某个包在 Vite 下报 ESM 错误在 vite.config 的 optimizeDeps.exclude 排除该包路由切换了但页面没变route.name 冲突检查 addRoute 的 name先 removeRoutePinia 报 no active Piniastore 在 app.use(pinia) 前被调用确保 useStore 在组件/路由守卫回调内部打包后资源 404base 配置不对根据部署路径设置 base动态路由组件加载失败Vite 无法分析动态 import使用组件映射表替代字符串拼接这个表是我自己压箱底的排查手册每一个都对应着一次真实踩坑希望能帮后来者省下一点排查时间。实际项目里你还可能会遇到别的奇怪问题但有一条通用排查思路先确认版本兼容再查构建配置最后看运行时调用顺序百分之八十的问题都能按这个顺序定位出来。8. 项目实战中的几条个人建议最后说几条我从实战中总结出来的建议。全篇说的都是我的真实体验不是从文档里抄出来的结论能用得上就收下。第一新项目不要急着全家桶一步到位。你先用 Vue 3 Vite 搭一个空壳工程跑通一个最简单的页面再逐步引入 Router、Pinia、UI 组件库。一次引入全部依赖出问题时很难分清是哪个环节导致的。第二组件库的选择要谨慎。Vue 3 时代的组件库生态已经成熟了但是不同组件库的样式定制能力、打包体积、兼容性差异很大。我建议先看看团队最常用的组件库对 Vue 3 的支持程度别因为某个组件库的单点功能就放弃主力选择这会直接影响后期开发效率。第三TypeScript 全家桶势在必行。Vue 3 对 TS 的支持明显比 Vue 2 好组合式函数天然适合做类型推导。如果你还在写纯 JavaScript我的建议是在迁移到 Vue 3 的同时把 TS 也引进来一次到位不要先用 JS 以后再补。第四也是最重要的一条多写组合式函数少写组件。组件只负责模板和布局业务逻辑全部下沉到 composables。这个习惯养成后你会发现组件体积骤减、复用率提高、测试也更好写了。我在项目里把用户、权限、字典、文件上传这些公共能力都抽成了 composables后续新功能的开发速度明显提升。Vue 3 全家桶的生态已经非常稳定如果你还在犹豫是否要迁移我认为关键是先在一个非核心项目里试点跑通全流程积累一套适合自己团队的踩坑清单。等这些坑都提前排掉正式的全面迁移就会顺利得多。
阅读完成 · 觉得有帮助?
咨询建站