主题
启动项目DEMO与通过脚手架搭建项目(Lesson14 1/3)
本页对应 Lesson14,共 6 段视频,约 1 小时 32 分。
学习目标
- 能独立将首页结构迁移到
App.vue,并通过router-view正确渲染对应路由组件。 - 能抽离
Header、Footer为局部组件,在App.vue中引入注册,并修正组件内图片等静态资源路径。 - 能创建首页、登录、注册、搜索四个一级路由,使用
router-link和编程式导航完成头部跳转。 - 能复现并解释“重复导航”异常,至少使用
catch、失败回调或重写导航方法三种方式之一完成处理。 - 能通过备份原生方法并重写
VueRouter.prototype.push/replace实现编程式导航增强,保证参数、this指向和返回值正确。
知识点
本节目标:先把 DEMO 跑起来
这一节要做的不是写业务代码,而是把给定的项目目录 sph 在本地跑起来。为什么需要先做这件事?因为后面的组件拆分、路由配置、页面跳转,都建立在一个能正常启动的项目上;如果依赖没装、启动脚本没跑通,后面出现的报错就很难判断是业务代码问题还是环境问题。怎么想?顺序应当是:拿到项目目录 → 放到本地合适位置 → 进入项目根目录 → 安装依赖 → 读懂 package.json 里的启动脚本 → 执行启动命令 → 根据终端结果验证。怎么做?先把项目目录中的 sph 放置到本地;转写稿里也提醒过,创建项目目录不要出现中文,所以本地路径尽量用英文目录。放好后进入 sph 目录,准备安装依赖。易错点:不要在压缩包里直接运行,不要在外层目录运行安装命令,也不要把项目放在中文路径下后再排查“莫名其妙”的模块找不到问题。
安装依赖:cnpm i
为什么需要安装依赖?因为 package.json 里声明了项目运行所需的模块,但本地还没有生成对应的依赖目录;不安装就直接启动,常见结果就是终端提示某个模块找不到。怎么想?安装命令必须在项目根目录执行,也就是能看到 package.json 的 sph 目录里执行。怎么做?进入 sph 后执行:
shell
cnpm i转写稿中也提到“执行了一下 npm i,把所需要安装的模块重新安装了一份”,说明安装依赖是启动前的必要步骤;如果本机使用 npm,也可以用 npm i 完成同样的依赖安装。易错点:第一,终端当前目录不是 sph,导致装到了别处;第二,cnpm 未安装却直接执行;第三,安装过程中断后没有重新安装,启动时缺模块;第四,看到报错不要先改代码,应先确认依赖是否完整。
读懂 package.json:启动脚本和运行依赖
为什么需要读 package.json?因为 npm start 并不是“魔法命令”,它只是执行 scripts 中配置的命令。本节笔记中的 package.json 如下:
json
{
"scripts":{
"start":"node server.js"
},
"dependencies": {
"connect-history-api-fallback": "^2.0.0",
"express": "^4.18.2",
"http-proxy-middleware": "^2.0.6"
}
}怎么想?先看 scripts.start,它告诉我们执行 npm start 时实际运行的是 node server.js;再看 dependencies,说明这个 DEMO 依赖 express、connect-history-api-fallback、http-proxy-middleware 等模块。怎么做?安装依赖后核对这三个模块是否都在依赖列表中,启动时若提示 Cannot find module 'express' 一类错误,就回到依赖安装步骤检查。易错点:不要把 start 脚本名写错;不要误以为 npm start 会自动安装依赖;也不要随意改动 server.js 路径,否则 node server.js 会找不到入口文件。转写稿中“找到显示 NPM 脚本,把项目启动一下”说的就是通过脚本启动,而不是手动乱敲文件。
启动项目:npm start
为什么用 npm start?因为它读取 package.json 中约定的启动脚本,团队里统一用这个入口最不容易出错。怎么想?依赖装完后,在 sph 根目录执行启动命令,观察终端是否正常输出、是否报模块缺失或端口占用。怎么做?执行:
shell
npm start启动后根据终端提示在浏览器中访问对应地址进行验证。转写稿里的操作顺序是:关闭旧项目,找到 NPM 脚本,重新安装所需模块,然后启动项目。这个顺序值得照做。易错点:终端不在 sph 目录;依赖没装完就启动;端口被占用;启动报错后不看第一行错误信息,直接反复执行命令。若出现模块找不到,优先检查 cnpm i 是否在项目根目录成功执行;若路径含中文,按转写稿提醒改为英文路径再试。
调试记录:从“启动失败”倒推
这一节最常见的调试不是改业务代码,而是倒推环境。转写稿中反复出现“把所需依赖安装上”“重新安装了一份”“找到 NPM 脚本启动”,这说明启动 DEMO 的关键链路是:目录正确 → 依赖完整 → 脚本正确 → 命令正确。遇到启动失败时,先看终端报错关键词:是“找不到模块”,就查依赖;是“找不到文件”,就查当前目录和 server.js 是否存在;是端口问题,就换端口或关闭占用进程。不要一上来改 package.json 的依赖版本,也不要删 node_modules 后不重新安装。易错点:把“安装依赖”和“启动项目”混成一步;在错误目录执行命令;看到报错只截图不读信息。按本节流程跑通后,再进入后续首页、组件和路由的练习,才不会把环境问题误判成代码问题。
1. 用脚手架创建项目:先定 Vue2 版本
为什么需要: 本节要先把项目骨架搭起来,后面的组件、路由才有地方放。转写稿里反复强调,这一节按 Vue2 讲,所以创建时要选 Vue2 默认模板,而不是随手选最新模板。
怎么想: 先明确“脚手架负责生成基础工程”,包括 babel、eslint 等默认配置;路由不是本次创建时勾选的,后面要手动装。创建目录还要避开中文路径,转写稿明确提醒“创建项目目录不允许出现中文”。
怎么做: 在目标目录执行笔记中的命令,交互时选择默认 Vue2:
shell
vue create first # 选择默认安装 vue2 -->Default ([Vue 2] babel, eslint)易错点: 不要因为“最新版本”就选 Vue3 模板;转写稿后面安装 vue-router 时特别说明“最新版本对 Vue2 不支持”,要装 3.x。另一个易错点是项目路径含中文,转写稿明确说不要再这样建项目。
2. 创建后先启动、整理入口
为什么需要: 脚手架生成后只是空壳,要先确认依赖和启动脚本正常,再把静态首页结构放进 App.vue,否则页面出不来。
怎么想: 转写稿的做法是:进入项目目录后先 npm i 安装依赖,再通过 npm 脚本启动;然后把首页结构放到 App.vue 中呈现。样式方面,把 reset.css 放到 src 下,并在 src/index.js 或 App.vue 中引入。转写稿提到有同学“怎么不一样”,原因就是 reset.css 没引入。
怎么做: 先安装依赖、启动项目;把 reset.css 放到 src 下,在入口处引入;再把首页主体结构剪到 App.vue 中。图片路径不要直接沿用旧路径,转写稿调试时出现图片 404,最后是把组件独享图片放到组件自己的 images 目录,再改相对路径。
易错点: reset.css 忘记引入会导致样式不一致;图片路径写错会报模块找不到。转写稿中就是通过看报错、找 src 下图片、改相对路径解决的。
3. 抽离公共头部和底部组件
为什么需要: 首页、登录、注册、搜索都有公共头部和底部,如果每个页面复制一遍,后面改导航要改很多处。
怎么想: 转写稿先对比页面:头部在多个页面都有,底部也有公共部分,所以先抽成公共组件。组件先按局部组件用,因为当时只确定在 App.vue 中使用;不要一上来就全局注册。
怎么做: 在 src/components 下建 Header/index.vue 和 Footer/index.vue,把 App.vue 中对应结构和样式剪过去,再在 App.vue 中引入。转写稿还提到组件名不要都叫 index,name 要改成 Header、Footer,方便调试工具识别。
易错点: ESLint 会报“组件名必须多个单词”,也会报“定义了未使用”。转写稿的解决思路是:要么用起来,要么按规则忽略;不要遇到一个报错就无脑关规则。图片也要跟着组件走,否则又会 404。
4. 手动安装 vue-router 并创建基本路由
为什么需要: 脚手架创建时没有选路由,但后面要实现首页、登录、注册、搜索四个页面切换,所以必须手动安装 vue-router。
怎么想: 转写稿先确定一级路由:/ 首页、/login 登录、/register 注册、/search 搜索;再在 src/router/index.js 中引入 Vue、VueRouter,Vue.use(VueRouter),配置 routes,最后 export default router。页面呈现位置放在头部和底部之间的 <router-view/>。
怎么做: 进入项目目录安装 3.x 大版本,转写稿建议安装“3 这个版本”,不要装最新版:
shell
npm i vue-router@3然后创建 src/router/index.js,配置四个路由组件,并在 App.vue 的头部、底部之间放 <router-view/>。转写稿调试时出现“模块找不到”,第一时间检查是否没装 vue-router。
易错点: Vue2 配 vue-router@3,不要装成最新大版本;router-view 不放,路由切换了也没地方显示;路由地址和组件要一一对应。
5. 编程式导航重复跳转的调试
为什么需要: 转写稿中点击“搜索”时,如果当前已经在搜索页,再用 this.$router.push('/search') 会报“重复导航”异常。这是路由联调中很典型的问题。
怎么想: 先打印 this.$router.push(...) 的返回值,发现它返回的是 Promise;跳到相同地址时 Promise 失败。转写稿解释:vue-router 3.1 之后对相同路由跳转做了判断,并采用 Promise 形式。
怎么做: 简单处理可以在 push 后加 .catch(() => {}),或给 push 传失败回调;转写稿还演示了“原生加强”:先备份 VueRouter.prototype.push,再重写 push,在重写方法中调用备份方法,并 return 原方法结果。replace 同理。
易错点: 重写 push 时如果不 return 原方法结果,返回值会丢;不指定 this 会出错;转写稿中就是通过 call(this, ...) 把当前路由实例和参数传回备份方法。
替换站点图标:让浏览器标签显示尚品汇
为什么需要:项目初始化后浏览器标签还是 Vue 默认图标,和“尚品汇”项目不一致;favicon 是浏览器标签、收藏夹里显示的站点标识,替换它是最基础的整理工作。
怎么想:Vue CLI 模板把站点图标放在 public/favicon.ico,public/index.html 里通过 <link rel="icon"> 引用。因此不需要改 src 里的业务代码,只要替换 public 下的图标文件,再让模板正确引用即可。
怎么做:把尚品汇图标复制到 public 目录,并命名为 favicon.ico,覆盖原文件;启动项目查看浏览器标签。转写稿里特别提醒:如果启动后站标没有变化,先清除缓存,然后重新打开浏览器再检查。
易错点:文件名必须是 favicon.ico,不要放成 favicon.png 却仍写 .ico;要放在 public 根目录,不要放到 src/assets;浏览器缓存会导致“换了但没生效”,先清缓存再重开;index.html 中的引用路径不要写死,交给 BASE_URL 拼接。
检查 public/index.html:图标和标题的模板变量
为什么需要:public/index.html 是页面模板,head 里的图标和标题决定浏览器标签显示什么;Vue CLI 会通过 html-webpack-plugin 把配置注入模板。
怎么想:模板里不是普通 HTML 写死路径,而是使用 <%= BASE_URL %> 和 <%= htmlWebpackPlugin.options.title %>。前者和 vue.config.js 的 publicPath 有关,后者来自 pages.index.title。所以标题不应该在 index.html 里硬编码,而应回到 vue.config.js 配置。
怎么做:保留本节笔记中的模板代码:
html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>易错点:模板变量拼写要准确,htmlWebpackPlugin.options.title 不能少写层级;BASE_URL 末尾是否带斜杠由 publicPath 决定,不要手动再拼错;<div id="app"></div> 是 Vue 挂载点,不要删;标题如果没生效,先检查 vue.config.js 的 pages.index.title 是否配置。
配置 vue.config.js:开发服务器与页面入口/标题
为什么需要:开发时希望自动打开浏览器、使用固定域名和端口;同时项目入口和网页标题也需要统一配置,避免每次手动改模板。
怎么想:devServer 控制开发服务器行为,open 自动打开浏览器,host 和 port 指定访问地址;pages.index.entry 指定页面入口,title 设置网站标题;publicPath 被注释掉,表示需要更改 BASE_URL 时再打开。转写稿里把入口写成 src/index.js,所以项目入口文件要对应,不能还停留在默认的 src/main.js。
怎么做:保留本节笔记中的配置代码:
js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// publicPath:"./",// 更改BASE_URL
devServer:{
open:true,
host:"zhangpeiyue.com",
port:80
},
pages: {
index: {
// page 的入口
entry: 'src/index.js',
// 设置网站标题
title:"尚品汇"
}
}
})易错点:entry 改成 src/index.js 后,入口文件必须真实存在,否则编译报错;title 要放在 pages.index 里,不要误以为 index.html 里写死就行;host 和 port 改了以后,访问地址要跟着变;publicPath 一旦改成 ./,会影响模板里的 BASE_URL,图标等 public 资源路径要一起检查;启动后图标未变,仍按笔记先清缓存、重开浏览器。
整理入口与重置样式:转写稿里的配套动作
为什么需要:转写稿里把首页结构先放到 App.vue 中呈现,并把重置样式 reset.css 放到 src/assets/css 下,再在 src/index.js 里引入。否则不同页面样式基线不一致,容易出现“代码一样但显示不一样”的问题。
怎么想:公共重置样式属于全局基础样式,应放在 src/assets/css 这类公共目录,由入口统一引入;首页结构先放进 App.vue,是为了先看到整体效果,再继续拆组件和路由。转写稿里同学出现样式不一致,排查后指出原因就是 reset.css 没有引入。
怎么做:把 reset.css 放到 src/assets/css/reset.css,在 src/index.js 中引入;将首页结构放入 App.vue 中呈现。这里不额外编造代码,按转写稿的操作顺序执行即可。
易错点:只复制文件不引入等于没做;因为 pages.index.entry 已改成 src/index.js,引入位置要写在这个入口里;样式引入顺序会影响覆盖关系;不要过度依赖文档,转写稿里也强调最后要能自己从头敲一遍。
本节要解决的问题:保存时 ESLint 报错
这一节的核心不是业务功能,而是把开发过程中频繁出现的 ESLint 报错处理掉。转写稿里一开始就遇到两类提示:一类是“定义了没有使用”,另一类是组件名必须多个单词。它们不是浏览器运行错误,而是 ESLint 在保存或编译时给出的代码规范提示。为什么需要处理?因为不处理会打断开发节奏,甚至让编译结果被 lint 阻断。怎么想?先看报错里的规则名,再判断是改代码、局部忽略规则,还是整体关闭保存时检查。怎么做?笔记给了两个解决方向:在 package.json 里配置规则忽略,以及在 vue.config.js 里整体忽略 lint 检查。易错点:转写里的“esling”应写作 ESLint;忽略规则不是修复代码,团队项目要慎用整体关闭。
先看入口文件 src/index.js 与未使用变量告警
笔记中的入口代码是:
js
import Vue from "vue";
import App from "@/App";
const vm = new Vue({
el:"#app",
render:h=>h(App)
})转写稿里调试时说,报错信息是“定义了没有使用”,并举例就是这里的 const vm = new Vue(...):变量 vm 定义了但没有被使用,于是触发 no-unused-vars。为什么需要理解这一点?因为这类报错不是 Vue 本身坏了,而是 ESLint 认为声明了变量却不用,可能是冗余代码。怎么想?有三条路:第一,真的使用 vm;第二,不需要变量就不要声明;第三,如果确认是练习或临时写法,就忽略这条规则。怎么做?笔记给出的“解决一”是在 package.json 中配置忽略:
json
{
....
"rules": {
"no-unused-vars":0,
"vue/multi-word-component-names": 0
}
.....
}其中 "no-unused-vars": 0 表示关闭“未使用变量”检查。易错点:笔记只截取了 rules 片段,实际 Vue CLI 项目通常要把 rules 放在 package.json 的 eslintConfig 下面;不要误以为顶层随便写一个 rules 就一定生效。另外,转写稿也提醒:遇到一个报错就忽略一个并不是好习惯,能改代码时优先改代码。
组件名多单词规则:vue/multi-word-component-names
第二个常见规则是组件名必须由多个单词组成。转写稿里说“组件名字是不是必须得多个单词?一个单词不行”,例如 Home、Search 这类单词组件名会触发 vue/multi-word-component-names。为什么需要这条规则?因为 Vue 希望组件名和原生 HTML 标签区分开,多单词命名更安全、语义更清楚。怎么想?如果这是正式项目,优先把组件名改成 HomeView、SearchView 这类多单词形式;如果只是练习阶段,想先跑通流程,可以按笔记忽略规则。怎么做?仍然是在 package.json 的 rules 中加入:
json
"vue/multi-word-component-names": 0易错点:这条规则来自 eslint-plugin-vue,不是 ESLint 核心规则;忽略后虽然不报错,但不代表单词组件名就是好命名。转写稿还提到“定义完组件之后,把组件引入进来,没报错,但是 Vue 当中又制定了一个规则,你一旦注册这个组件,你必须得用”,这说明要看清楚报错到底来自“未使用变量”还是“组件命名/使用”规则,不能把所有报错都当成同一种问题。
整体忽略保存时 lint:vue.config.js 的 lintOnSave
如果不想逐条配置规则,笔记给出的“解决二”是整体忽略 lint 检查。完整代码如下:
js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 忽略lint检查
lintOnSave:false,
// publicPath:"./",// 更改BASE_URL
devServer:{
open:true,
host:"zhangpeiyue.com",
port:80
},
pages: {
index: {
// page 的入口
entry: 'src/index.js',
// 设置网站标题
title:"尚品汇"
}
}
})为什么需要?转写稿里的场景是保存代码后 lint 报错影响继续开发,lintOnSave:false 可以让保存时不再执行 lint 检查。怎么想?这是“整体开关”,和上面的“单条规则忽略”不同:上面是某条规则不查,这里是保存时整体不查。怎么做?把 lintOnSave:false 写进 vue.config.js 的导出配置中,改完重启开发服务。易错点:lintOnSave 是 Vue CLI 的配置项,不要写到 package.json 的 rules 里;它不会删除 ESLint 依赖,也不会自动修代码;团队项目长期关闭会失去保存时的规范检查,建议只用于临时排错或练习。笔记里同一份配置还包含 devServer 和 pages,它们和 lint 不是同一件事,阅读时要区分。
调试复盘:先定位规则,再决定忽略层级
转写稿的调试顺序可以总结为:先看到“定义了没有使用”,定位到 src/index.js 里的 vm;再看到组件名单词问题,定位到 vue/multi-word-component-names;最后如果不想逐条处理,才用 lintOnSave:false 整体关闭保存时检查。为什么需要这样复盘?因为 ESLint 报错最怕“看到红字就关总开关”。怎么想?先读规则名,再判断是代码问题还是规范约束。怎么做?单条规则用 package.json 的 rules 处理,整体保存检查用 vue.config.js 的 lintOnSave 处理。易错点:同音字要纠正,“esling”是 ESLint;配置改完要重启服务;忽略规则后仍要保证代码能运行,不要把“看不见报错”误当成“问题已解决”。
资源准备:swiper 样式与 reset.css 的放置
为什么需要:首页要用轮播图,swiper 的 CSS 必须先加载;同时浏览器默认样式不一致,需要 reset.css 做重置。怎么想:public 下的资源不会被 webpack 重新处理,适合放第三方静态 CSS;项目级重置样式放 src/assets/css,由入口统一引入。怎么做:把 swiper.min.css 放到 public/css/swiper.min.css,在 public/index.html 中用 <%= BASE_URL %> 引入;把 reset.css 放到 src/assets/css/reset.css,在 src/index.js 中 import "@/assets/css/reset.css";。易错点:不要写死 /css/...,要带 BASE_URL;reset.css 忘记引入会导致页面间距、默认样式异常。
html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="<%= BASE_URL %>css/swiper.min.css">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>js
import Vue from "vue";
import App from "@/App";
import "@/assets/css/reset.css";
new Vue({
el: "#app",
render: h => h(App)
});把首页静态结构放入 App.vue
为什么需要:先把静态首页跑通,再拆组件和路由,能快速定位是样式问题还是组件问题。怎么想:App.vue 是根组件,先把最外层结构、头部、分类导航、轮播、楼层、底部放进去;图片路径先按笔记原路径写。怎么做:在 src/App.vue 的 template 中放 .outer,内部包含 header.header、.type-nav、.list-container、.today-recommend、.floor、.brand、.footer。图片 alt 改为中文。易错点:刷新后图片 404,通常不是图片丢了,而是相对路径不对;组件独享图片应放到组件自己的 images 目录,并把 ./assets/images/logo.png 改为 ./images/logo.png。
vue
<template>
<div class="outer">
<header class="header">
<div class="top">
<div class="container">
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p><span>请</span><a>登录</a><a class="register">免费注册</a></p>
</div>
</div>
</div>
<div class="bottom">
<h1 class="logoArea">
<a class="logo" title="尚品汇" target="_blank">
<img src="./assets/images/logo.png" alt="尚品汇Logo">
</a>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input type="text" id="autocomplete" class="input-error input-xxlarge" />
<button class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
</form>
</div>
</div>
</header>
<!-- 分类导航、今日推荐、楼层、品牌等静态结构按笔记原样保留 -->
<div class="list-container">
<div class="sortList clearfix">
<div class="center">
<div class="swiper-container" id="mySwiper">
<div class="swiper-wrapper">
<div class="swiper-slide"><img src="./assets/images/home/banner1.jpg" alt="首页轮播图1"></div>
<div class="swiper-slide"><img src="./assets/images/home/banner2.jpg" alt="首页轮播图2"></div>
<div class="swiper-slide"><img src="./assets/images/home/banner3.jpg" alt="首页轮播图3"></div>
<div class="swiper-slide"><img src="./assets/images/home/banner4.jpg" alt="首页轮播图4"></div>
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</div>
<div class="right">
<div class="ads"><img src="./assets/images/home/ad1.png" alt="广告图"></div>
</div>
</div>
</div>
<div class="footer">
<div class="footer-container">
<div class="footerItem">
<h4>帮助中心</h4>
<img src="./assets/images/wx_cz.jpg" alt="微信二维码">
</div>
</div>
</div>
</div>
</template>
<script>
export default { name: "App" };
</script>拆分 Header/Footer 并放 router-view
为什么需要:登录、注册、搜索、首页都有公共头部和底部,如果每个页面复制一遍,改一处要改多处。怎么想:先找共同点,头部和底部抽成公共组件;中间变化部分交给路由。怎么做:新建 src/components/Header/index.vue、src/components/Footer/index.vue,在 App.vue 中引入,并在头部和底部之间放 <router-view></router-view>。易错点:router-view 必须放在公共头部和公共底部之间;组件名不要只写 index,name 要写 Header、Footer,避免调试工具里全是 index。
vue
<template>
<div class="outer">
<Header />
<router-view></router-view>
<Footer />
</div>
</template>
<script>
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default { name: "App", components: { Header, Footer } };
</script>创建路由与导航链接
为什么需要:点击登录、注册、搜索要切换页面。怎么想:Vue2 项目要安装 vue-router@3,最新版不兼容 Vue2。怎么做:新建 src/router/index.js,配置 /、/login、/register、/search;Header 中登录、注册用 router-link,logo 回首页;搜索按钮用编程式导航。易错点:报 module not found 就是没装 vue-router;不要在中文目录创建项目。
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
const routes = [
{ path: "/", component: Home },
{ path: "/login", component: Login },
{ path: "/register", component: Register },
{ path: "/search", component: Search }
];
export default new VueRouter({ routes });vue
<router-link to="/login">登录</router-link>
<router-link to="/register" class="register">免费注册</router-link>
<router-link to="/" class="logo"><img src="./images/logo.png" alt="尚品汇Logo"></router-link>
<button @click="goSearch">搜索</button>编程式导航重复跳转异常与加强
为什么需要:已经在 /search 时再点搜索,push 到相同地址会报 NavigationDuplicated。怎么想:vue-router 3.1 之后 push 返回 Promise,重复导航返回失败 Promise,不处理就报错。怎么做:最简单在 push 后 .catch(() => {});更通用是备份原生 push 后重写,返回原生调用结果。易错点:重写时要用 call(this, ...) 保留原 this,并 return 原生结果。
js
const nativePush = VueRouter.prototype.push;
VueRouter.prototype.push = function (location, onResolve, onReject) {
if (onResolve || onReject) {
return nativePush.call(this, location, onResolve, onReject);
}
return nativePush.call(this, location).catch(err => err);
};先拆 Header 与 Footer:为什么需要、怎么想
为什么需要:首页、登录、注册、搜索页都有相同的头部和底部;如果每个页面复制一遍,改一处就要改多处。怎么想:先找共同点,把头部、底部抽成公共组件,页面中间内容交给路由。转写稿里说的“will root”即 VueRouter,“布式”即 push。怎么做:在 src/components 下建 Header/index.vue、Footer/index.vue,把静态页对应结构剪进去;组件名写 Header、Footer,不要都叫 index。易错点:不确定全局还是局部时先做局部组件;目前 Header/Footer 只在 App.vue 用,先局部注册即可。
Header 组件代码与图片处理
为什么需要:头部有登录、注册、Logo、搜索按钮,后续要挂路由链接。怎么想:组件独享图片放组件自己的 images 目录,避免全局资源混乱。怎么做:保留结构,Logo 的 alt 改成中文;图片裂开时看控制台路径,把图片剪切到 Header/images,路径写成 ./images/logo.png。易错点:不要随手删全局 assets 图片;路径相对于当前 .vue 文件。
vue
<template>
<!-- 头部 -->
<header class="header">
<!-- 头部的第一行 -->
<div class="top">
<div class="container">
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p>
<span>请</span>
<a>登录</a>
<a class="register">免费注册</a>
</p>
</div>
<div class="typeList">
<a>我的订单</a>
<a>我的购物车</a>
<a>我的尚品汇</a>
<a>尚品汇会员</a>
<a>企业采购</a>
<a>关注尚品汇</a>
<a>合作招商</a>
<a>商家后台</a>
</div>
</div>
</div>
<!--头部第二行 搜索区域-->
<div class="bottom">
<h1 class="logoArea">
<a class="logo" title="尚品汇" target="_blank">
<img src="./images/logo.png" alt="尚品汇Logo">
</a>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input type="text" id="autocomplete" class="input-error input-xxlarge" />
<button class="sui-btn btn-xlarge btn-danger" type="button">搜索</button>
</form>
</div>
</div>
</header>
</template>
<script>
export default {
name: "Header"
}
</script>Footer 组件代码与图片处理
为什么需要:底部版权、帮助中心二维码在各页面复用。怎么想:同样把 Footer 结构、样式剪进组件;二维码图放 Footer/images。怎么做:微信图 alt 改中文,路径改 ./images/wx_cz.jpg。易错点:图片裂开先查路径;组件目录名与 name 对应,便于调试工具识别。
vue
<template>
<!-- 底部 -->
<div class="footer">
<div class="footer-container">
<div class="footerList">
<div class="footerItem">
<h4>购物指南</h4>
<ul class="footerItemCon">
<li>购物流程</li>
<li>会员介绍</li>
<li>生活旅行/团购</li>
<li>常见问题</li>
<li>购物指南</li>
</ul>
</div>
<div class="footerItem">
<h4>配送方式</h4>
<ul class="footerItemCon">
<li>上门自提</li>
<li>211限时达</li>
<li>配送服务查询</li>
<li>配送费收取标准</li>
<li>海外配送</li>
</ul>
</div>
<div class="footerItem">
<h4>支付方式</h4>
<ul class="footerItemCon">
<li>货到付款</li>
<li>在线支付</li>
<li>分期付款</li>
<li>邮局汇款</li>
<li>公司转账</li>
</ul>
</div>
<div class="footerItem">
<h4>售后服务</h4>
<ul class="footerItemCon">
<li>售后政策</li>
<li>价格保护</li>
<li>退款说明</li>
<li>返修/退换货</li>
<li>取消订单</li>
</ul>
</div>
<div class="footerItem">
<h4>特色服务</h4>
<ul class="footerItemCon">
<li>夺宝岛</li>
<li>DIY装机</li>
<li>延保服务</li>
<li>尚品汇E卡</li>
<li>尚品汇通信</li>
</ul>
</div>
<div class="footerItem">
<h4>帮助中心</h4>
<img src="./images/wx_cz.jpg" alt="微信二维码">
</div>
</div>
<div class="copyright">
<ul class="helpLink">
<li>关于我们<span class="space"></span></li>
<li>联系我们<span class="space"></span></li>
<li>关于我们<span class="space"></span></li>
<li>商家入驻<span class="space"></span></li>
<li>营销中心<span class="space"></span></li>
<li>友情链接<span class="space"></span></li>
<li>关于我们<span class="space"></span></li>
<li>营销中心<span class="space"></span></li>
<li>友情链接<span class="space"></span></li>
<li>关于我们</li>
</ul>
<p>地址:北京市昌平区宏福科技园综合楼6层</p>
<p>京ICP备19006430号</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Footer"
}
</script>挂到 App.vue:组件与路由出口
为什么需要:头尾固定,中间内容随路由变化。怎么想:App.vue 引 Header/Footer,中间放 router-view;笔记里的 .... 在实际路由阶段就是出口位置。怎么做:局部注册组件。易错点:vue-router 在 Vue2 中不要装最新版,装 vue-router@3;项目目录不要有中文。
vue
<template>
<!-- 项目的最外层 -->
<div class="outer">
<!-- 头部 -->
<Header></Header>
....
<Footer></Footer>
</div>
</template>
<script>
import Home from "@/pages/Home";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default {
name: "App",
components: {Footer, Header, Home}
}
</script>导航链接与重复跳转调试
为什么需要:点头部“登录/注册”、Logo、搜索按钮要跳转。怎么做:登录、注册、Logo 用 router-link,搜索按钮用 @click 调方法。易错点:当前已在 /search 再点搜索,Vue Router 3.1+ 会返回失败 Promise,控制台报 NavigationDuplicated。解决:.catch(()=>{});或 push 第二、第三参数传成功/失败回调;或重写原型方法。重写思路是“先备份原生方法,再重写,在重写里 call 调备份并返回结果,补失败回调/.catch”,replace 同理。转写稿用数组 push 加一解释“原生加强”:先保存原生 push,再重写并调用备份,不能直接递归调用自己。
先明确路由表:为什么需要、怎么想
单页应用里,页面切换不能靠刷新整个 HTML,而是让 URL 对应一个组件,再由 <router-view/> 把组件渲染出来。转写稿里先看静态页面:首页、登录、注册、搜索,先只做头部能点到的这几项,不一次性把所有导航都做完。所以先定一级路由:
首页:
/
登录:/login
注册:/register
搜索:/search
怎么做:先装 Vue2 对应的路由版本,不能装最新版。转写稿里出现 Module not found 时,第一时间判断“模块没装或没装对”,然后安装:
shell
cnpm install vue-router@3易错点:Vue2 项目要用 vue-router@3;转写稿里也提醒项目目录不要出现中文。路由 path 的大小写要统一,笔记代码里注册写成了 /Register,而规划里是 /register,实际项目必须统一,否则 router-link 跳不过去。
编写路由配置:为什么需要、怎么做
为什么需要:把“地址”和“组件”建立映射,入口文件只负责挂载 router。
怎么想:在 src/router/index.js 中先 Vue.use(VueRouter),再写 routes,最后 new VueRouter 并导出。
怎么做:保留笔记代码如下:
js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
const routes = [
{
path:"/",
component:Home
},
{
path:"/login",
component:Login
},
{
path:"/Register",
component:Register
},
{
path:"/search",
component:Search
}
];
const router = new VueRouter({
mode:"history",
routes
});
export default router;易错点:mode:"history" 时地址更干净,但部署时服务端要配合;开发阶段先保证路由能切换。component 不要写成字符串,要写导入的组件对象。
页面组件:先能渲染,再谈样式
为什么需要:路由指向的必须是组件。转写稿里先给登录、注册、搜索写最简结构,方便验证路由是否生效。
怎么做:三个页面组件保留笔记代码:
vue
<template>
<div>
<h3>注册界面</h3>
</div>
</template>
<script>
export default {
name: "Register"
}
</script>
<style scoped>
h3{
padding:50px;
background: green;
}
</style>vue
<template>
<div>
<h3>登陆界面</h3>
</div>
</template>
<script>
export default {
name: "Login"
}
</script>
<style scoped>
h3{
padding:50px;
background: skyblue;
}
</style>vue
<template>
<div>
<h3>搜索界面</h3>
</div>
</template>
<script>
export default {
name: "Search"
}
</script>
<style scoped>
h3{
padding:50px;
background: yellow;
}
</style>Home 笔记中写“略”,实际就是把首页主体内容放进 src/pages/Home/index.vue。易错点:组件 name 建议与文件名一致;页面组件最外层最好只有一个根节点。
App.vue 留出口:头部、内容、底部
为什么需要:转写稿先分析出头部和底部是多个页面共用的,于是抽成 Header、Footer,中间用 <router-view/> 显示路由组件。
怎么做:笔记中的 App.vue 如下:
vue
<template>
<!-- 项目的最外层 -->
<div>
<!-- 头部 -->
<Header></Header>
<router-view/>
<!-- 底部 -->
<Footer></Footer>
</div>
</template>
<script>
import Home from "@/pages/Home";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default {
name: "App",
components: {Footer, Header, Home}
}
</script>
<style lang="less" scoped>
</style>易错点:<router-view/> 放错位置,页面就不会出现在头部和底部之间;import Home 如果没使用,ESLint 会报“定义了未使用”,转写稿里就处理过这类报错。要么用掉,要么删掉。
入口挂载 router
为什么需要:new Vue 时注入 router,this.$router、<router-link>、<router-view/> 才可用。
怎么做:
js
import Vue from "vue";
import App from "@/App";
import router from "@/router";
import "@/assets/css/reset.css";
new Vue({
el:"#app",
router,
render:h=>h(App)
})易错点:忘记 Vue.use(VueRouter) 或忘记在 new Vue 里写 router,都会导致路由不工作。
导航链接与调试:转写稿里的补充
头部登录、注册、logo、搜索按钮要改成 <router-link to="...">,不要再用普通 a 标签。转写稿调试编程式导航时,连续点击当前路由会报“冗余导航”,这里的“荣余”应纠正为“冗余”。原因是 [email protected] 之后 push/replace 返回 Promise,跳到相同地址会失败;可用 .catch()、失败回调,或对 push/replace 做“原生方法备份→重写→调用备份并 return”的加强。易错点:加强时不要直接递归调用自己;要保存原生方法,并用 call(this, ...) 调回备份。
课堂画面






代码示例
示例一:项目配置与入口文件(vue.config.js / public/index.html / src/index.js)
js
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
// 忽略 lint 检查(整体关闭)
lintOnSave: false,
// publicPath: "./", // 更改 BASE_URL
devServer: {
open: true,
host: "zhangpeiyue.com",
port: 80
},
pages: {
index: {
// page 的入口
entry: "src/index.js",
// 设置网站标题
title: "尚品汇"
}
}
})html
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="<%= BASE_URL %>css/swiper.min.css">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
</body>
</html>js
// src/index.js
import Vue from "vue";
import App from "@/App";
import router from "@/router";
import "@/assets/css/reset.css";
new Vue({
el: "#app",
router,
render: h => h(App)
})关键行与易错点
pages.index.title与index.html里的<%= htmlWebpackPlugin.options.title %>是一一对应的,改了vue.config.js里的 title 才会改浏览器标签页文字。- 静态资源必须放在
public下,并用<%= BASE_URL %>拼接路径;直接写/css/swiper.min.css在publicPath改变后会失效。 render: h => h(App)不能用template: '<App/>'代替,因为脚手架默认引入的是运行时版 Vue(不带模板编译器),会报编译错误。@是src的别名,import "@/assets/css/reset.css"必须在入口处引入一次,否则全局重置样式不生效。- 若 eslint 报
no-unused-vars、vue/multi-word-component-names,两种方案:package.json的eslintConfig.rules里置 0,或vue.config.js中lintOnSave: false。 - 站标改了却没变,是浏览器缓存问题,清缓存后重开浏览器。
devServer.host改成自定义域名时,需要本机 hosts 映射,否则打不开页面。
示例二:路由表 + App.vue + Header 组件(含路由跳转)
js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "@/pages/Home";
import Login from "@/pages/Login";
import Register from "@/pages/Register";
import Search from "@/pages/Search";
Vue.use(VueRouter);
const routes = [
{ path: "/", component: Home },
{ path: "/login", component: Login },
{ path: "/register",component: Register },
{ path: "/search", component: Search }
];
const router = new VueRouter({
mode: "history",
routes
});
export default router;vue
<!-- src/App.vue -->
<template>
<!-- 项目的最外层 -->
<div class="outer">
<!-- 头部 -->
<Header />
<!-- 路由出口:Home / Login / Register / Search 在这里渲染 -->
<router-view />
<!-- 底部 -->
<Footer />
</div>
</template>
<script>
import Header from "@/components/Header";
import Footer from "@/components/Footer";
export default {
name: "App",
components: { Header, Footer }
}
</script>vue
<!-- src/components/Header/index.vue -->
<template>
<header class="header">
<!-- 头部的第一行 -->
<div class="top">
<div class="container">
<div class="loginList">
<p>尚品汇欢迎您!</p>
<p>
<span>请</span>
<router-link to="/login">登录</router-link>
<router-link class="register" to="/register">免费注册</router-link>
</p>
</div>
</div>
</div>
<!-- 头部第二行 搜索区域 -->
<div class="bottom">
<h1 class="logoArea">
<router-link class="logo" title="尚品汇" to="/">
<img src="./images/logo.png" alt="">
</router-link>
</h1>
<div class="searchArea">
<form action="###" class="searchForm">
<input type="text" id="autocomplete" class="input-error input-xxlarge" />
<button class="sii-btn btn-xlarge btn-danger" type="button" @click="goSearch">搜索</button>
</form>
</div>
</div>
</header>
</template>
<script>
export default {
name: "Header",
methods: {
goSearch() {
this.$router.push("/search");
}
}
}
</script>关键行与易错点
- 必须
Vue.use(VueRouter),否则<router-link>、<router-view>不识别;vue-router要装 3.x(cnpm install vue-router@3),装 4.x 对 Vue2 不兼容。 - 路径大小写敏感:
/Register与/register是两个不同地址,router-link的to必须和routes里的path完全一致。 - 组件
name不要写成index,否则 devtools 里所有组件都叫 index,无法区分。 - 组件内引用图片用相对路径(
./images/logo.png),图片要跟随组件一起放到components/Header/images,别再用./assets/images/...。 Header/Footer现在只在App中使用,所以注册为局部组件即可;若多个页面复用再考虑全局注册。- 头部导航条(服装城、美妆馆…)并非所有页面都有,不要放进
Header,应抽成独立公共组件或按路由v-if控制。 - 通过
<router-link>跳到当前路由不会报错,但用this.$router.push('/search')跳到当前路由会抛 NavigationDuplicated 异常,这是示例三要解决的问题。
示例三:对 push / replace 做“原生加强”
js
// src/router/index.js —— 追加在 new VueRouter 之后、export default 之前
// 1. 先把原生方法备份
const originPush = VueRouter.prototype.push;
const originReplace = VueRouter.prototype.replace;
// 2. 重写 push
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
// 调用方自己传了回调,直接透传
originPush.call(this, location, resolve, reject);
} else {
// 调用方没传回调,补上空回调,吞掉重复导航的失败
return originPush.call(this, location, () => {}, () => {});
}
};
// 3. 重写 replace
VueRouter.prototype.replace = function (location, resolve, reject) {
if (resolve && reject) {
originReplace.call(this, location, resolve, reject);
} else {
return originReplace.call(this, location, () => {}, () => {});
}
};关键行与易错点
- 顺序不能反:先备份、后重写。如果先重写再备份,备份拿到的是自己,会无限递归。
- 必须用
originPush.call(this, ...),不能用originPush(...)。直接调用时this丢失,路由实例拿不到,跳转失效;call(this)把当前的 router 实例传回去。 - 重写后要
return原生方法的返回值。$router.push()返回的是一个 Promise,不返回会导致外部拿不到结果,甚至影响后续逻辑。 - 补空回调
() => {}就是在“消费”这个失败的 Promise,避免控制台出现Uncaught (in promise) NavigationDuplicated。 - 报错根因:vue-router 3.1 之后,用编程式导航跳到与当前相同地址时,返回的是失败的 Promise;而
<router-link>内部做了处理,所以点链接不报错、写push才报错。 - 其他可选方案:装 3.1 之前的版本(不推荐)、每次
push().catch(()=>{})(太啰嗦)、给push传第二个参数成功回调/第三个参数失败回调。加强一次,全项目所有$router.push都受益。 - 面试常问:这就是“你项目中遇到过什么问题、怎么解决的”的典型素材——先说现象(重复导航报错),再说原因(3.1 后用 Promise 且判断地址),最后说方案(重写原型方法)。
常见错误
1. 控制台报 Module not found: Error: Can't resolve 'vue-router',页面白屏
- 现象:
npm start编译失败或运行时报模块找不到,路由相关代码完全不生效。 - 原因:
vue create时选的是默认Default ([Vue 2] babel, eslint),没有勾选 Router,依赖里根本没有vue-router;或者装了但装成了 4.x。 - 修法:进项目目录执行
cnpm install vue-router@3(大版本必须锁 3,4.x 不兼容 Vue2);装完package.json里确认是"vue-router": "^3.x";再检查src/index.js里有没有new Vue({ router })注入。
2. 头部/底部抽成组件后,logo 和微信二维码图片 404
- 现象:页面结构正常,但 logo、帮助中心图片显示裂图,控制台
Failed to resolve component之外的 404 请求。 - 原因:把
Header、Footer从App.vue剪到src/components/Header/index.vue后,src="./assets/images/logo.png"这个相对路径是相对当前组件文件解析的,层级变了自然找不到。 - 修法:组件专属的静态图片跟随组件走——在组件目录下建
images/,把logo.png、wx_cz.jpg拷进去,路径改成./images/logo.png;批量迁移时可用编辑器的全局替换把./assets/images/换成../assets/images/(层级对得上时),但要逐个验证。
3. 点击"搜索"或重复点击"登录",控制台爆红 Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location
- 现象:第一次跳转正常,第二次点同一个链接就报错,页面其实没坏,但控制台一片红。
- 原因:vue-router 3.1 之后
push内部改用 Promise 实现,跳转到当前已所在的路由被判为失败并 reject,而没人处理这个 reject。 - 修法:三种任选——①
this.$router.push('/search').catch(() => {});②this.$router.push('/search', () => {}, () => {})传空失败回调;③ 统一在src/router/index.js里对VueRouter.prototype.push/replace做加强(见下条)。
4. 加强 push 之后,链式 .catch() 报 Cannot read properties of undefined,或跳转后行为异常
- 现象:路由能跳,但原来写
.catch()的地方报错;或者某些页面跳过去后返回结果丢了。 - 原因:重写的函数里只调用了
nativePush.call(this, location, ...)却没有return,原生返回的 Promise 被丢弃,外层拿到undefined。 - 修法:先备份再重写,且必须返回原生结果:
js
const nativePush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, resolve, reject) {
if (resolve && reject) {
return nativePush.call(this, location, resolve, reject)
}
return nativePush.call(this, location, () => {}, () => {})
}5. 图标换了但浏览器标签页还是旧的 Vue logo
- 现象:
public/favicon.ico已替换,刷新无效。 - 原因:favicon 被浏览器强缓存,且
public/index.html里如果还写着<link rel="icon" href="/favicon.ico">而没走<%= BASE_URL %>,路径在某些配置下也会偏。 - 修法:确认
index.html中是<link rel="icon" href="<%= BASE_URL %>favicon.ico">,然后清缓存 + 硬刷新(Ctrl+Shift+R),或换端口重开一次浏览器。
6. eslint 报 'vm' is defined but never used 和 vue/multi-word-component-names,编译直接中断
- 现象:代码逻辑没问题,但终端一片红,页面不刷新。
- 原因:脚手架默认带
eslint+eslint-plugin-vue,no-unused-vars和组件名单词数规则默认开启。 - 修法:优先改代码——
const vm = new Vue(...)改成new Vue(...)不接收返回值,组件name写成Header、Footer这类多单词;确实不想管,就在package.json的eslintConfig.rules里置 0,或在vue.config.js里lintOnSave: false。
7. devServer.host 写成 zhangpeiyue.com 后,npm start 起来但浏览器打不开
- 现象:终端显示编译成功,访问地址却连不上。
- 原因:
host不是随便写的域名,本机hosts里没有该域名到127.0.0.1的映射。 - 修法:临时改回
host: "localhost";或确实要用自定义域名,先改hosts文件加一行127.0.0.1 zhangpeiyue.com。另外port: 80在 Mac/Linux 下需要管理员权限,报权限错就换8080。
练习
练习 1|路由骨架自查 不看笔记,从零给项目装上 vue-router@3,建出 /、/login、/register、/search 四条路由,并在 App.vue 里让内容区正确渲染。完成后回答:如果漏了 router-view,页面会是什么表现?如果漏了 new Vue({ router }),$router 会是什么?
练习 2|组件抽取与图片迁移 把 Header、Footer 从 App.vue 中抽成 src/components/Header/index.vue 和 src/components/Footer/index.vue,要求抽完后零 404、样式与抽取前完全一致。记录你一共改了几处图片路径、用了什么方式改的。
练习 3|复现并修复 NavigationDuplicated 故意在 Header 里用 this.$router.push('/search') 实现搜索按钮,反复点击直到复现报错。然后用三种不同方案各修一次(catch / 失败回调 / 原型加强),对比哪种改动量最小、哪种最一劳永逸。
练习 4|加强 replace 仿照课上对 push 的加强,在 src/router/index.js 里对 replace 做同样的处理,并验证:连续两次 this.$router.replace('/login') 时控制台是否还报错、返回值是否是 Promise。
练习 5|独立复刻 合上笔记和文档,从 vue create 开始完整重做一遍:脚手架 → 配置 vue.config.js(title、favicon、devServer)→ 抽 Header/Footer → 配四条路由 → 头部导航跳转。中途卡住先自己查,卡超过 10 分钟再看笔记。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 将首页放置到App.vue组件中进行呈现 | 07:43 |
| 2 | 头部与尾部组件划分 | 20:01 |
| 3 | 创建基本路由 | 17:42 |
| 4 | 实现头部切换 | 07:36 |
| 5 | 原生加强 | 24:42 |
| 6 | 解决编程式导航问题 | 14:56 |
小结
- 脚手架创建时没选的依赖,后期补装必须锁大版本:Vue2 项目就是
vue-router@3,装 4 会直接跑不起来。 - 组件一旦被移动到新目录,组件内的相对路径全部失效,静态资源应当跟着组件走,放进组件自己的
images/目录。 public/下的资源走<%= BASE_URL %>引用,src/assets/下的资源走import或@别名引用,两者不要混着写。- 页面结构的"公共部分"先抽组件、后配路由:先确定 Header/Footer 是全局还是局部,再决定路由组件里该渲染什么,顺序反了会反复返工。
NavigationDuplicated不是 bug 而是 vue-router 3.1+ 的设计变更,本质是"Promise 被 reject 但没人接"。- 原型方法加强的固定套路只有三步:备份原生 → 重写 → 在重写中
call备份并return结果,缺了return就会丢掉返回值。 - eslint 报错优先"改代码",其次"改规则",最后才"整体关掉"——
lintOnSave: false是兜底而不是首选。 - 本地起不来的问题八成在
host/port/hosts三处,跟业务代码无关,排查时先看终端地址再看代码。