☰
三次迭代Vue2新闻资讯移动端项目实战:从请求封装到二次开发
2026/11/5 2:30:23 网站建设 项目流程

简介:这是一份基于 Vue.js 的新闻类前端项目第三次迭代源码包,面向已掌握 Vue 基础、希望练习组件化开发与工程化流程的学习者。压缩包共 32 个文件,以 18 个 .vue 单文件组件和 5 个 .js 脚本为主,另含样式、JSON、HTML 等配置与说明文件,整体约 220KB。目录覆盖视图、路由、状态管理、静态资源等常见模块,并带有项目依赖、构建工具与自定义配置,便于理解依赖安装、开发服务和生产打包背后的组织方式,也能观察新闻页面从组件拆分到路由组织的实现思路。已有 121 人学习下载,可用于拆解页面结构、路由划分与组件复用,也可作为 Vue CLI 项目搭建和构建配置的参考样例。

1. 三次迭代后的样子:一个能直接改的 Vue 新闻资讯移动端

拿到 hm-news-vue-03 这个包,我最先翻的不是 README,而是src/views下面的文件数量和router/index.js的路由表——第三次迭代的项目,功能摊开的程度基本能从这两处看出来。这是一个用 Vue 2 写的移动端新闻资讯应用,频道切换、下拉刷新、上拉加载、详情页、搜索、登录鉴权、收藏这几条主线都齐了,不是那种只有两个页面的脚手架 demo。适合两类人:一是刚学完 Vue 基础、想找一个功能闭环的项目把路由守卫、请求封装、组件复用串起来练手;二是手里有内容接口,想找个现成的前端壳子接上去跑。它不追求工程化极限,但该有的分层都有,改起来不至于牵一发动全身。

2. 技术选型与目录结构:先看清骨架再动手

2.1 技术栈清单与版本约束

这类移动端资讯项目,Vue 2 时代最主流的组合基本就是这一套。我把package.json里真正影响运行的部分拎出来:

依赖版本在项目里承担什么
vue2.6.14视图层,全篇 Options API
vue-router3.5.1路由 + 登录守卫,history 模式
vuex3.6.2频道列表、用户 token、收藏状态
axios0.21.4请求,统一在utils/request.js收口
vant2.12.6骨架屏、List 上拉加载、PullRefresh、Tab、Search
less / less-loader4.x / 7.x样式预处理,配合全局变量文件
postcss-px-to-viewport1.1.1750 设计稿直接写 px,编译成 vw
@vue/cli-service4.5.15构建与本地 devServer

这里有个前提要说清:Vue 2 的响应式基于Object.defineProperty,所以数组下标赋值、对象新增属性都不会触发更新,项目里凡是改列表的地方都老老实实用了Vue.set或者整体替换数组,这不是写法老旧,是版本决定的。如果你打算升 Vue 3,第 6 章会讲迁移时最先崩的是哪几块。

版本这块我的建议是锁死。vant 2.x 的小版本之间对 vue 的 peer 依赖判断不完全一致,npm install装出两个 vue 实例的翻车现场我见过不止一次,症状是页面白屏、控制台只有一句Cannot read property 'prototype' of undefined,排查两小时最后发现是 lock 文件没提交。拿到项目第一件事:确认有没有package-lock.json,有就用npm ci而不是npm install。

2.2 目录结构与分层职责

hm-news-vue-03/ ├── public/ │ └── index.html # viewport meta 在这里,别在 App.vue 里重复写 ├── src/ │ ├── api/ # 一个业务域一个文件:article.js / user.js / channel.js │ │ └── article.js # 只放接口函数,不做数据处理 │ ├── assets/ # 静态图,走 webpack 处理 │ ├── components/ # 全局通用组件,按需在页面里 import │ │ ├── ArticleItem.vue # 新闻卡片 │ │ ├── NavBar.vue # 自定义导航栏,处理刘海屏安全区 │ │ └── EmptyState.vue # 空列表占位 │ ├── mixins/ │ │ └── listMixin.js # 分页列表的公共逻辑,详情页评论、搜索页复用 │ ├── router/ │ │ └── index.js # 路由表 + 全局前置守卫 │ ├── store/ │ │ ├── index.js # 入口,注册 modules │ │ ├── modules/channel.js │ │ └── modules/user.js │ ├── styles/ │ │ ├── variables.less # 主题色、字号,通过 vue.config.js 全局注入 │ │ └── reset.less │ ├── utils/ │ │ ├── request.js # axios 实例 + 双拦截器 │ │ ├── storage.js # localStorage 的读写封装,带过期时间 │ │ └── format.js # 时间戳转「3 分钟前」 │ ├── views/ │ │ ├── home/ # 首页:频道 Tab + 列表 │ │ ├── detail/ # 详情页 │ │ ├── search/ │ │ ├── login/ │ │ └── mine/ # 我的:收藏、历史 │ ├── App.vue │ └── main.js ├── .env.development ├── .env.production └── vue.config.js

这个分层的关键是依赖方向单一:views依赖components和api,api只依赖utils/request,utils不依赖任何业务代码。只要守住这条线,你换接口、换 UI 库都不会牵连到别处。

api/这一层值得单说。很多教学项目喜欢在组件的methods里直接axios.get('...'),这个项目没有。api/article.js长这样:

// src/api/article.js import request from '@/utils/request' // 获取新闻列表:channelId 为空时走推荐流 export function getArticleList(params) { return request({ url: '/articles', method: 'get', params // { channelId, page, pageSize } }) } // 获取详情 export function getArticleDetail(id) { return request({ url: `/articles/${id}`, method: 'get' }) }

注意它返回的是request(...)的结果,也就是拦截器处理完之后的res.data,组件里await getArticleList()拿到的直接就是业务数据,不需要再.data.data一层层剥。params用对象传,axios 会自己拼 query string,比手写模板字符串安全——channelId是数字还是字符串都不影响。

vue.config.js里有两处配置绕不开,一是 less 全局变量注入,二是 devServer 代理:

// vue.config.js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, css: { loaderOptions: { less: { // 全局注入变量,组件里就不用一行行 @import 了 additionalData: `@import "~@/styles/variables.less";` } } }, devServer: { port: 8081, proxy: { // 前端请求 /api 开头,转发到后端,绕开浏览器同源限制 '/api': { target: 'http://127.0.0.1:3000', changeOrigin: true, pathRewrite: { '^/api': '' } } } } })

changeOrigin: true是必须的,否则后端拿到的 Host 还是 localhost:8081,某些框架的防盗链校验会直接 403。pathRewrite看后端约定:后端接口本身就带/api前缀的话,这一行删掉。改完记得重启——vue.config.js不参与热更新,改了不重启是不生效的,这一点每年都要坑一批人。

3. 核心链路拆解:一个新闻列表请求从触发到渲染

3.1 axios 实例与双拦截器

整个项目所有请求都走同一个实例,这是保证「token 自动带、错误统一提示、401 统一跳转」的前提。

// src/utils/request.js import axios from 'axios' import { Toast } from 'vant' import router from '@/router' import { getToken, removeToken } from '@/utils/storage' const service = axios.create({ // 从环境变量读,开发环境是 /api,生产环境是真实域名 baseURL: process.env.VUE_APP_BASE_API, timeout: 8000 }) // 请求拦截:统一挂 token service.interceptors.request.use( config => { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截:拆壳 + 统一错误处理 service.interceptors.response.use( response => { const res = response.data // 后端约定 code === 200 才算成功,其余一律当失败处理 if (res.code !== 200) { Toast(res.message || '请求失败') if (res.code === 401) { removeToken() // 带上当前路径,登录完能回到原地 router.replace({ path: '/login', query: { redirect: router.currentRoute.fullPath } }) } return Promise.reject(new Error(res.message || 'Error')) } return res.data // 注意这里,组件拿到的就是 data 本身 }, error => { // 网络层错误:超时、断网、5xx const msg = error.code === 'ECONNABORTED' ? '请求超时,请重试' : '网络异常' Toast(msg) return Promise.reject(error) } ) export default service

三处细节值得停下来看。第一,baseURL走环境变量而不是硬编码,.env.development里写VUE_APP_BASE_API=/api,.env.production里写真实地址,注意只有VUE_APP_开头的变量才会被注入到客户端代码,写成BASE_API是拿不到的,这个坑很经典。第二,响应拦截器return res.data之后,业务层就再也拿不到code和message了,所以成功判断必须在这里做完,不能漏。第三,401 的处理用了router.replace而不是push,避免用户在登录页疯狂点返回时栈里堆一堆重复记录。

还有一个隐性收益:因为所有错误提示都在拦截器里Toast了,业务组件里就不需要每个catch都写一遍提示,try/finally里只管收尾 loading 状态就行。

3.2 列表分页与竞态处理

首页列表用的是 vant 的List组件,它会在滚动到底部时触发load事件,我们只管在回调里请求下一页。

// src/views/home/components/ArticleList.vue import { getArticleList } from '@/api/article' export default { name: 'ArticleList', props: { channelId: { type: [Number, String], required: true } }, data() { return { list: [], page: 1, pageSize: 10, loading: false, // 交给 van-list 控制底部 loading finished: false, // 没有更多了 reqSeq: 0 // 请求序号,解决快速切频道的竞态 } }, watch: { // 频道变了,整个列表重来 channelId() { this.reset() this.onLoad() } }, methods: { reset() { this.list = [] this.page = 1 this.finished = false }, async onLoad() { if (this.finished) return const seq = ++this.reqSeq this.loading = true try { const { results, total } = await getArticleList({ channelId: this.channelId, page: this.page, pageSize: this.pageSize }) // 回来的数据如果不是最新那次请求发的,直接丢弃 if (seq !== this.reqSeq) return this.list.push(...results) this.page += 1 // 两个终止条件:返回空 或 已经够总数 this.finished = results.length < this.pageSize || this.list.length >= total } catch (e) { // 错误提示已在拦截器统一处理,这里只需让 van-list 可以重试 this.finished = false } finally { if (seq === this.reqSeq) this.loading = false } } } }

reqSeq这段是我后来补的,原始写法没有。现象是:手指在频道 Tab 上快速左右滑,A 频道和 B 频道的请求同时在飞,谁后回来谁的数据就盖住当前频道——用户看到的是「财经」标签下出现了体育新闻。加了自增序号之后,旧请求回来发现seq !== this.reqSeq直接 return,问题消失。

finished的判定用了双保险:results.length < pageSize应对后端不返回总数的场景,list.length >= total应对后端总数不准但分页正确的场景。两个条件任意一个成立就停,避免无限触发load把接口打爆。

page的自增放在push之后而不是请求之前,是为了失败时不跳页——否则一次网络抖动,第 2 页就永远丢了。

3.3 路由表与登录守卫

路由这块分两部分看:静态路由表定义页面和懒加载,全局守卫决定谁能进。

// src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' import { getToken } from '@/utils/storage' Vue.use(VueRouter) const routes = [ { path: '/', // 首页不做懒加载,首屏更快;其余页面按需加载 component: () => import('@/views/home/index.vue'), meta: { keepAlive: true, showTab: true } }, { path: '/detail/:id', name: 'Detail', component: () => import('@/views/detail/index.vue'), props: true, // 把 :id 当 props 传进组件,组件不用碰 $route meta: { requiresAuth: false } }, { path: '/mine', component: () => import('@/views/mine/index.vue'), meta: { requiresAuth: true, keepAlive: true, showTab: true } }, { path: '/login', component: () => import('@/views/login/index.vue'), meta: { hideTab: true } }, { path: '*', redirect: '/' } ] const router = new VueRouter({ mode: 'history', routes, // 返回列表页时恢复滚动位置,不然从详情回来永远在顶部 scrollBehavior(to, from, savedPosition) { return savedPosition || { x: 0, y: 0 } } }) router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !getToken()) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } }) export default router

meta在这里承担了三类职责,落表说清楚:

字段作用谁在读
requiresAuth标记需要登录beforeEach守卫
keepAlive是否进keep-alive缓存App.vue的<keep-alive :include>
showTab/hideTab底部 Tab 栏显隐App.vue里v-if判断

props: true是个小技巧:详情页用props: ['id']接收,组件就和路由解耦了,以后写单测或者复用到别的地方都不用 mock$route。

App.vue里配合缓存是这样写的:

<!-- App.vue --> <keep-alive :include="cachedViews"> <router-view /> </keep-alive> <script> export default { computed: { // 只缓存 meta.keepAlive 为 true 的页面 cachedViews() { return this.$router.options.routes .filter(r => r.meta && r.meta.keepAlive) .map(r => r.name) .filter(Boolean) }, showTab() { return !this.$route.meta.hideTab } } } </script>

注意include匹配的是组件的 name,不是路由的 path 也不是 name。这里cachedViews用路由 name 去匹配,前提是每个页面的name和路由name写成一样的,否则缓存不生效——现象是「从详情页返回列表,列表又回到第一页重新加载」,很多人以为是scrollBehavior坏了,其实是keep-alive根本没命中。

4. 状态管理与组件复用:频道、卡片、收藏怎么收口

4.1 频道管理:Vuex + 本地持久化

首页顶部的频道 Tab 支持增删和排序,这部分状态必须放进 Vuex,因为「频道编辑页」和「首页列表」是两个组件,靠props传不过去。

// src/store/modules/channel.js import { getChannels, setChannels } from '@/utils/storage' // 用户没自定义过时的默认频道 const DEFAULT_CHANNELS = [ { id: 0, name: '推荐' }, { id: 1, name: '国内' }, { id: 2, name: '国际' }, { id: 3, name: '财经' } ] export default { namespaced: true, state: () => ({ // 优先读本地缓存,没有就用默认值 channels: getChannels() || DEFAULT_CHANNELS, activeIndex: 0 }), getters: { activeChannelId: state => (state.channels[state.activeIndex] || {}).id }, mutations: { SET_ACTIVE(state, index) { state.activeIndex = index }, SET_CHANNELS(state, list) { state.channels = list setChannels(list) // 顺手持久化,省得每个调用点都记着写 } }, actions: { // 删除频道:推荐频道(id=0)不允许删 removeChannel({ state, commit }, id) { if (id === 0) return const list = state.channels.filter(item => item.id !== id) commit('SET_CHANNELS', list) if (state.activeIndex > list.length - 1) { commit('SET_ACTIVE', list.length - 1) } }, // 添加频道:去重后追加到末尾 addChannel({ state, commit }, channel) { if (state.channels.some(item => item.id === channel.id)) return commit('SET_CHANNELS', [...state.channels, channel]) } } }

namespaced: true是必须的,否则多个 module 里有同名 mutation 会互相覆盖。持久化放在 mutation 里而不是 action 里,是刻意的——只要状态变了才写缓存,避免「没变也写」。

utils/storage.js我没用第三方插件,手写了一个带过期时间的版本,四十行就够:

// src/utils/storage.js const TOKEN_KEY = 'hm_token' const TOKEN_EXPIRE = 7 * 24 * 60 * 60 * 1000 // 7 天 export function setToken(token) { const data = { value: token, expire: Date.now() + TOKEN_EXPIRE } localStorage.setItem(TOKEN_KEY, JSON.stringify(data)) } export function getToken() { const raw = localStorage.getItem(TOKEN_KEY) if (!raw) return '' try { const { value, expire } = JSON.parse(raw) // 过期的 token 等于没有,顺手清掉,避免带着废 token 反复 401 if (Date.now() > expire) { localStorage.removeItem(TOKEN_KEY) return '' } return value } catch (e) { // 缓存被手动改坏的情况,宁可清空也别让整个应用崩在 JSON.parse localStorage.removeItem(TOKEN_KEY) return '' } }

那个try/catch不是凑数。用户装了某些清理插件,或者手动开了控制台乱改,localStorage里存的可能不是合法 JSON,JSON.parse一抛错,如果这段代码在router.beforeEach里跑,整个应用会白屏。这是我在别的项目上真踩过的。

4.2 新闻卡片组件:props 怎么切

ArticleItem.vue被首页列表、搜索结果页、收藏页三处复用,props 切得干净,复用才不会变成负担。

<!-- src/components/ArticleItem.vue --> <template> <div class="article-item" @click="$emit('click', article.id)"> <div class="content"> <h3 class="title">{{ article.title }}</h3> <p class="meta"> <span>{{ article.source }}</span> <span>{{ article.commentCount }} 评论</span> <span>{{ formatTime(article.publishTime) }}</span> </p> </div> <!-- 有图才渲染,避免占位留白 --> <van-image v-if="article.cover" :src="article.cover" fit="cover" lazy-load /> </div> </template> <script> import { formatTime } from '@/utils/format' export default { name: 'ArticleItem', props: { // 只接收必要字段,不要整个对象透传原始接口结构 article: { type: Object, required: true, validator: v => !!v.id && !!v.title } }, methods: { formatTime } } </script>

validator是我加的习惯:数据对不上的时候,控制台立刻报错,而不是渲染出一片空白让人猜。@click="$emit('click', article.id)"只往外抛 id,父组件自己去跳转,这样卡片组件不需要知道路由的存在。

图片用了lazy-load,vant 的van-image配合Lazyload插件时,滚到视口附近才发图片请求。列表页二十条数据里十几张图,全量加载会让 3G 模拟环境下首屏时间翻倍。

4.3 分页逻辑抽成 mixin

首页、搜索页、收藏页的分页逻辑几乎一样,抽出来能省一大坨重复代码:

// src/mixins/listMixin.js export default { data() { return { list: [], page: 1, pageSize: 10, loading: false, finished: false, reqSeq: 0 } }, methods: { // 子组件实现 fetchData(params),返回 { results, total } async loadPage() { const seq = ++this.reqSeq this.loading = true try { const { results = [], total = 0 } = await this.fetchData({ page: this.page, pageSize: this.pageSize }) if (seq !== this.reqSeq) return this.list.push(...results) this.page += 1 this.finished = results.length < this.pageSize || this.list.length >= total } finally { if (seq === this.reqSeq) this.loading = false } }, // 下拉刷新和切频道都走这个,把状态一次复位 resetList() { this.reqSeq++ // 让在途请求全部作废 this.list = [] this.page = 1 this.finished = false } } }

resetList里的this.reqSeq++是个容易漏的动作。切换频道时只是把数组清空、页码归 1,但上一个频道还在飞的请求回来之后照样会push进新列表,等于清了个寂寞。序号一加,在途请求全部判定为过期,这才是真正干净的重置。

mixin 的边界要守住:只放状态机(分页、加载中、是否到底),不放业务字段。一旦往里塞了channelId这类东西,搜索页就得覆写一遍,反而更乱。

5. 本地跑通与问题排查:从 install 到接口联调

5.1 三步启动与环境变量

先在机器上确认 Node 版本,Vue CLI 4 的项目在 Node 16 上最稳,Node 18 以上某些依赖的 OpenSSL 行为变了,会报ERR_OSSL_EVP_UNSUPPORTED。

node -v # 建议 16.x npm ci # 有 lock 文件就用 ci,保证依赖树和作者一致 npm run serve # 启动开发服务器,默认读 .env.development

.env.development内容:

# 只有 VUE_APP_ 前缀的变量才会被注入到客户端代码 VUE_APP_BASE_API=/api VUE_APP_TITLE=新闻资讯

如果你的后端不在本地,改vue.config.js里 proxy 的target就行;如果后端已经部署好、允许跨域,那就把VUE_APP_BASE_API直接改成完整域名,同时把devServer.proxy整段删掉——两个都留着容易出现「请求发了两遍」的错觉,其实是 baseURL 拼上了 proxy 前缀又拼了一次路径。

启动成功后终端会给地址,默认http://localhost:8081。想用手机真机调试,把 devServer 的host改成0.0.0.0,然后手机和电脑连同一个局域网,用电脑内网 IP 访问。

5.2 五条踩坑记录:现象、原因、解决

第一条:页面白屏,控制台报Cannot read property 'prototype' of undefined

现象:npm run serve成功,浏览器打开一片白,控制台就这一行报错,堆栈指到 vant 的入口文件。 原因:依赖树里出现了两个 Vue 实例。vant 的某个小版本对 peerDependencies 声明较松,npm install时又拉了一份 vue 进来,插件注册时拿到的 Vue 和组件里的 Vue 不是同一个构造函数。 解决:确认package-lock.json存在并用npm ci重装;还不行就npm ls vue看是不是有两个版本,在package.json里显式锁"vue": "2.6.14"(不要写^2.6.14),删掉node_modules重来。

第二条:npm run serve报端口被占用,EADDRINUSE

现象:启动直接失败,提示 8081 已被占用。 原因:上一次的 dev server 没退干净,或者本机有别的服务占了端口。Windows 上强关终端窗口经常留下僵尸进程。 解决:vue.config.js里devServer.port改一个;想找出元凶就lsof -i:8081(macOS/Linux)或netstat -ano | findstr 8081(Windows)拿到 PID 后 kill 掉。

第三条:接口全 404,Network 面板里请求地址是http://localhost:8081/articles

现象:请求发出去了但 404,看请求 URL 没有/api前缀。 原因:VUE_APP_BASE_API没读到。要么变量名没加VUE_APP_前缀,要么.env.development放错了目录(必须在项目根目录,和package.json同级),要么改了环境变量没重启。 解决:在main.js里console.log(process.env.VUE_APP_BASE_API)打印验证,打印出undefined就从上面三条逐个排。注意环境变量是编译时注入的,改完必须重启 dev server。

第四条:vant 组件整体变小,按钮和文字比设计稿小一圈

现象:自己写的样式尺寸正常,vant 的组件明显偏小。 原因:postcss-px-to-viewport把 vant 内部的 px 也按 750 设计稿的规则转成了 vw,但 vant 自身是按 375 的基准设计的,等于被缩了一半。 解决:配置里把 vant 排除掉:

// postcss.config.js module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 750, // 设计稿宽度 unitPrecision: 5, // 关键:node_modules 里的样式不转换,vant 按自己的基准来 exclude: [/node_modules/] } } }

第五条:从详情页返回,列表回到顶部且重新请求

现象:明明配了keep-alive和scrollBehavior,返回列表还是从头加载。 原因:<keep-alive :include="...">里的名字和组件的name对不上。include匹配的是组件选项里的name,路由表里的name只是路由的名字,两者不自动同步。 解决:给每个要缓存的页面组件显式写name,并且和路由name保持一致;或者干脆不用include,改用exclude排除不需要缓存的页面,反着写不容易错。

这五条里前两条属于环境,后三条属于配置。环境问题靠锁版本和看端口解决,配置问题一律靠「打印中间值」定位——console.log(process.env.xxx)、console.log(this.$route.meta),比盯着代码猜快得多。

6. 二次开发:把新闻流换成自己的数据源

多数人拿到这个包,最终目的都是接自己的接口。整个改造只需要动三处,别的地方一行都不用碰。

第一处是api/层,把 URL 换成你的;第二处是字段映射。你的后端多半不会刚好也叫title、source、publishTime,与其改遍所有组件,不如在 api 层加一个适配函数,把外部结构转成项目内部统一的article结构:

// src/api/article.js —— 只加适配,组件完全不用改 function adaptArticle(raw) { return { id: raw.article_id ?? raw.id, title: raw.subject || raw.title || '无标题', source: raw.media_name || '未知来源', cover: raw.thumb || '', commentCount: raw.comment_num ?? 0, // 后端返回的是秒级时间戳,统一转成毫秒,format.js 只认毫秒 publishTime: raw.pub_time ? raw.pub_time * 1000 : Date.now() } } export async function getArticleList(params) { const res = await request({ url: '/v2/feed', method: 'get', params }) return { results: (res.list || []).map(adaptArticle), total: res.total_count || 0 } }

适配层的价值在于「把变化关在一个文件里」。哪天后端把subject改成headline,你只改这一行,二十个页面组件纹丝不动。??是空值合并,只在null/undefined时兜底,0和''不会被误判,比||精确。

第三处是分页协议。上面代码假定后端返回list+total_count,如果你的后端是游标分页(返回next_cursor),那listMixin里的page += 1就得改成cursor = res.next_cursor,finished的判定也换成!res.next_cursor。改动点集中在 mixin 一处,所有列表页一起生效——这正是当初把它抽出来的原因。

改造完怎么验证?我一般按这个顺序走一遍:先看 Network 里首屏请求是否带了正确的参数和 token;再手动切三个频道,确认列表数据不串(验证reqSeq生效);然后断网刷新,确认有 Toast 提示而不是白屏;最后登录过期场景,把localStorage里的 token 手动改成一个废值,刷新页面,应该被踢回登录页并且登录后能回到原页面。这四步走完,基本能覆盖 90% 的联调问题。

最后提一句 Vue 3 迁移。这个项目里最先崩的不会是模板语法,而是三样:Vue.set全部要删(Vue 3 用 Proxy,不需要了)、Vuex 换成 Pinia 时namespaced概念消失、new VueRouter()变成createRouter()且mode: 'history'改成history: createWebHistory()。真要迁,建议先把api/和utils/这两层原样搬过去——它们和框架版本无关,搬完再逐个页面重写视图层,比一次性重写整个项目稳得多。

从那以后我每次接手一个「第 N 次迭代」的项目,都会先跑一遍npm ci && npm run serve,能起来再谈别的——起不来的项目,代码写得再漂亮都是纸上谈兵。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询