Skip to content

启动项目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>

为什么需要:登录、注册、搜索、首页都有公共头部和底部,如果每个页面复制一遍,改一处要改多处。怎么想:先找共同点,头部和底部抽成公共组件;中间变化部分交给路由。怎么做:新建 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);
};

为什么需要:首页、登录、注册、搜索页都有相同的头部和底部;如果每个页面复制一遍,改一处就要改多处。怎么想:先找共同点,把头部、底部抽成公共组件,页面中间内容交给路由。转写稿里说的“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/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, ...) 调回备份。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:项目配置与入口文件(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

小结 ​

  1. 脚手架创建时没选的依赖,后期补装必须锁大版本:Vue2 项目就是 vue-router@3,装 4 会直接跑不起来。
  2. 组件一旦被移动到新目录,组件内的相对路径全部失效,静态资源应当跟着组件走,放进组件自己的 images/ 目录。
  3. public/ 下的资源走 <%= BASE_URL %> 引用,src/assets/ 下的资源走 import 或 @ 别名引用,两者不要混着写。
  4. 页面结构的"公共部分"先抽组件、后配路由:先确定 Header/Footer 是全局还是局部,再决定路由组件里该渲染什么,顺序反了会反复返工。
  5. NavigationDuplicated 不是 bug 而是 vue-router 3.1+ 的设计变更,本质是"Promise 被 reject 但没人接"。
  6. 原型方法加强的固定套路只有三步:备份原生 → 重写 → 在重写中 call 备份并 return 结果,缺了 return 就会丢掉返回值。
  7. eslint 报错优先"改代码",其次"改规则",最后才"整体关掉"——lintOnSave: false 是兜底而不是首选。
  8. 本地起不来的问题八成在 host/port/hosts 三处,跟业务代码无关,排查时先看终端地址再看代码。