主题
路由懒加载与图片懒加载(Lesson22 3/3)
本页对应 Lesson22,共 5 段视频,约 1 小时 41 分。
学习目标
- 能定位并修复支付页面中“请求进行中取消定时器后仍继续轮询”的 BUG,并通过状态码与定时器状态条件阻止无效请求继续执行。
- 能使用路由守卫与路由 meta 配置,对加入购物车成功页、提交订单/支付页、支付成功页等页面进行来源与登录权限校验。
- 能使用 vee-validate 为登录/注册表单配置必填、手机号、密码长度等校验规则,并在提交前根据校验结果阻止非法提交。
- 能执行
npm run build完成项目打包,并在 Node 服务中配置 history 路由回退与接口代理,使页面刷新和接口请求正常。 - 能实现“我的订单”页面:调用订单接口获取数据、渲染订单列表、展示分页,并让页码与 URL 查询参数保持同步。
知识点
路由懒加载要解决的问题
为什么需要:如果路由文件里把所有页面组件都用同步 import 引入,首屏就要把首页、登录、注册、搜索、详情、购物车、交易、支付等页面代码一次性打进来。用户第一次打开页面时,请求的文件更大、等待更久。路由懒加载的目标,是让某个路由组件只在真正访问该路由时再加载。
怎么想:路由表里的 component 最终需要的是一个组件。同步引入是“先拿到组件再配置路由”;懒加载是“先配置一个返回 import() 的函数,等路由命中时再执行”。所以写法上不是改路由表结构,而是改组件变量的定义方式。
怎么做:在 src/router/index.js 中,把页面组件定义成箭头函数返回动态 import():
js
const Home = ()=>import("@/pages/Home");
const Login = ()=>import("@/pages/Login");
const Register = ()=>import("@/pages/Register");
const Search = ()=>import("@/pages/Search");
const Detail = ()=>import("@/pages/Detail");
const AddCartSuccess = ()=>import("@/pages/AddCartSuccess");
const Cart = ()=>import("@/pages/Cart");
const Trade = ()=>import("@/pages/Trade");
const Pay = ()=>import("@/pages/Pay");
const PaySuccess = ()=>import("@/pages/PaySuccess");之后路由表里仍然写 component: Home 这样的形式即可。import() 返回 Promise,路由解析时会等待它完成。
易错点:不要把懒加载写成同步 import Home from "@/pages/Home";@ 别名要确保在项目里已配置;变量名和路由表里的 component 要对应;同一个组件不要既同步引入又动态引入,否则懒加载可能失效。
优点和缺点的权衡
为什么需要:笔记里明确写了优点:减少网络请求、降低请求文件的大小。但也要记住缺点:如果某一个文件过大,会影响用户体验。懒加载不是“无脑优化”,它把首屏压力拆到访问时,若某个页面本身很大,用户点进去仍会等。
怎么想:按路由拆分是第一步,第二步要看拆出来的文件是否合理。首页、详情、支付等页面体量不同,不能认为加了 ()=>import() 就万事大吉。公共依赖、图片、第三方库也可能让某个 chunk 变大。
怎么做:改造后进入不同路由,打开浏览器网络面板观察请求;打包时执行 npm run build,转写稿里明确说该命令会在项目根目录生成 dist 目录。通过打包产物和网络请求判断是否按路由拆分,再决定是否继续拆组件或优化资源。
易错点:把懒加载当成“所有性能问题都解决了”;只看开发环境不验证打包结果;某个页面文件过大时仍不继续拆分;忽略公共依赖被重复打包或集中打进大文件的问题。
打包上线后的验证与常见坑
为什么需要:懒加载改完后,开发环境能跳转不代表上线没问题。转写稿视频4里打包后生成 dist,并强调“不是直接打开 index.html 就能运行,需要服务支持”。路由采用 history 模式时,刷新子路由会 404,需要服务端把找不到的地址回退到 index.html。
怎么想:先区分两类问题:一类是懒加载是否生效,看网络请求和打包产物;另一类是路由上线配置,看刷新、代理、静态资源。转写稿里点击“注册”后刷新 404,随后用 connect-history-api-fallback 作为中间件解决;代理问题则用 http-proxy-middleware 配置。调试思路是:先看地址和请求,再定位是路由回退还是代理转发。
怎么做:打包后把 dist 交给服务环境,不要直接双击 index.html;history 模式配置回退;接口代理按转写稿思路在服务端配置。验证懒加载时,可像转写稿视频1调试请求那样打开网络面板,观察进入不同路由是否产生新的资源请求。
易错点:把懒加载和路由权限、代理配置混为一谈;打包后直接打开本地文件;刷新子路由 404 却只改前端路由;代理没配就判断接口坏了。懒加载解决的是加载时机和体积拆分,不替代上线服务配置。
本节目标与转写稿说明
本节要完成的是“图片懒加载”:让图片在需要显示时再下载,减少首屏请求压力,提升用户体验。笔记给出的实现路径是:安装 vue-lazyload@1,在入口文件注册插件并配置占位图,最后把需要懒加载的图片从普通 src 改成 v-lazy 指令。
需要说明:提供的转写稿主要记录的是支付定时器问题、路由权限、表单验证、打包和“我的订单”等内容,并没有出现图片懒加载的安装、注册或调试过程。因此下面不把转写稿里无关的支付、路由调试硬套到本节,也不虚构懒加载调试步骤;只按本节笔记和代码展开。转写稿中“路由首位”应纠正为“路由守卫”,“音袋子/因袋子”一类说法应理解为 index 文件,但这些属于其他小节内容,不并入本节。
安装 vue-lazyload@1
为什么需要:图片懒加载不是浏览器默认行为,需要借助插件提供 v-lazy 指令和加载状态管理。笔记指定了 @1 版本,说明本项目要使用与当前 Vue 项目匹配的 1.x 版本,避免直接装到不兼容的大版本。
怎么想:先确认项目能正常安装依赖,再在项目根目录执行安装命令。不要先改代码再装包,否则入口文件引入时会直接报“找不到模块”。
怎么做:按笔记执行:
shell
cnpm install vue-lazyload@1易错点:命令里的 @1 不要漏;如果包管理器不是 cnpm,要换成项目实际使用的 npm、yarn 或 pnpm,但版本约束仍要保留。安装后检查 package.json 中是否出现对应依赖。
在入口文件注册插件并配置占位图
为什么需要:v-lazy 是插件注册后才能识别的自定义指令。配置 loading 后,图片尚未加载完成时可以显示一张本地占位图,避免空白或布局突兀。
怎么想:入口文件 src/index.js 是全局注册插件的位置。先引入插件,再引入本地 loading.gif,最后通过 Vue.use 注册。占位图路径要使用项目里真实存在的文件。
怎么做:按笔记写入:
js
import VueLazyLoad from "vue-lazyload";
import loading from "@/assets/images/loading.gif"
Vue.use(VueLazyLoad,{
// 图片未加载前给予呈现的图片
loading
})易错点:@/assets/images/loading.gif 的路径、文件名大小写要和实际文件一致;Vue.use 只需在入口执行一次,不要在每个组件里重复注册;如果图片不存在,控制台会出现 404,占位效果也不会出现。
在搜索页给图片加 v-lazy
为什么需要:搜索页通常有多张商品图。如果全部用普通 src,页面一打开就会集中请求图片;改成 v-lazy 后,可以把加载时机交给懒加载插件控制。
怎么想:找到需要懒加载的 <img>,确认图片地址来自数据字段。笔记中搜索页 pages/Search/index.vue 使用 item.defaultImg,因此把原来的图片地址绑定改为 v-lazy 指令绑定。
怎么做:按笔记修改:
vue
<img v-lazy="item.defaultImg" />易错点:不要写成 :src="item.defaultImg" 后又期待懒加载生效;指令名是 v-lazy,不是 v-lazyload;要确认 item.defaultImg 在渲染时确实有值,否则不是懒加载问题,而是数据字段问题。
验证与常见问题
为什么需要:改完代码不等于生效,需要确认图片请求是否按预期出现,以及占位图是否显示。
怎么想:打开搜索页,观察图片区域;再打开浏览器网络面板,滚动页面时看图片请求是否逐步出现。若未加载前能看到 loading.gif,说明占位配置生效。
怎么做:先重启开发服务,再进入搜索页测试;如果无效果,依次检查依赖是否安装、入口是否注册、组件是否使用 v-lazy、图片字段是否存在。
易错点:只改模板不重启、路径写错、字段名写错、把懒加载指令和普通 src 混用,都会导致“看起来没生效”。本节不涉及转写稿中的支付定时器、路由守卫、表单验证和打包部署,那些问题应放到对应小节处理。
本节要解决什么问题
本节的目标是给特定页面增加“路由权限”:不是所有页面都允许用户随便输入地址进入,而是必须从指定来源页面跳转过来。笔记里列了三个需求:
1- 加入购物车成功界面(指定:搜索(search),详情(detail))----完成
2- 提交订单进入到的支付页面(指定:购物车界面(cart))(作业)
3- 支付成功页面(指定:支付界面(pay))(作业)
为什么需要:像“加入购物车成功”“支付页”“支付成功页”这类页面,本身依赖上一步操作产生的数据或状态。如果用户直接在地址栏输入 /addCartSuccess 也能打开,就是一个 bug。
怎么想:在进入路由之前判断“你从哪来”,来源合法才放行,否则重定向回首页。
怎么做:可以用组件内的 beforeRouteEnter,也可以用路由配置里的 beforeEnter。
易错点:转写稿里出现的“首卫/手位”应纠正为“守卫”,“mete”应纠正为 meta,“form”应纠正为 from,“内幕”应纠正为 name。这些拼写错误会直接导致守卫不生效。
先看错误现象:直接打开也能进
转写稿中的调试过程是:进入购物车、结算、提交订单后能到支付页;但如果在地址栏随便打开某些页面,也能打开,这本身就不允许。对于加入购物车成功页,理想情况是:只有从搜索页 search 或详情页 detail 加入购物车后,才允许进入;直接打开或刷新时,应该被拦回首页。
调试时重点看三件事:
- 直接打开
/addCartSuccess是否被拦截; - 从搜索页加入购物车是否能进入;
- 从详情页加入购物车是否能进入;
- 如果给来源路由加了
meta标记,但拼写错了,会出现“从搜索页也进不去”的现象。转写稿里就出现过“少了个 meta”导致进不去,补上后才正常。
判断来源:from.name 与 meta 标记
为什么需要:最直接的写法是判断 from.name 是否在 ["detail","search"] 里。笔记方案一里也保留了这种注释写法:
js
// if(["detail","search"].includes(from.name)){
// next();
// }else next("/")怎么想:这种写法能完成当前需求,但后面如果再加一个来源页面,就要回来改数组,不够灵活。转写稿里建议改用 meta:在允许进入的来源路由上增加一个标记,例如 meta: { goCartSuccess: true },守卫里只判断 from.meta.goCartSuccess。这样以后新增来源,只要在对应路由加标记,不用改守卫逻辑。
怎么做:在 search 和 detail 的路由配置里加 meta 标记,然后在守卫里判断。
易错点:meta 要加在“来源路由”上,不是加在目标路由上;from.meta 可能为空,实际项目里可写成 from.meta && from.meta.goCartSuccess,但笔记代码按原样保留。
方案一:组件内 beforeRouteEnter
位置:src/pages/AddCartSuccess/index.vue。
js
beforeRouteEnter(to,from,next){
// 只有来自于搜索页以及详情页才可以允许进入到该路由
// if(["detail","search"].includes(from.name)){
// next();
// }else next("/")
if(from.meta.goCartSuccess) next();
else next("/")
}为什么需要:守卫写在页面组件内部,和这个页面关系最紧,适合只控制单个页面。
怎么想:to 是目标路由,from 是来源路由,next() 放行,next("/") 重定向首页。
怎么做:在来源路由加 meta.goCartSuccess,目标页写上述守卫。
易错点:beforeRouteEnter 里不能直接访问 this,不过这里不需要;next 只能调用一次;from.meta 拼错、goCartSuccess 大小写不一致都会失败。
方案二:路由独享守卫 beforeEnter
位置:src/router/index.js 中对应路由配置。
js
{
// 成功加入购物车界面
path:"/addCartSuccess",
component:AddCartSuccess,
beforeEnter(to,from,next){
if(from.meta.goCartSuccess) next();
else next("/")
}
}为什么需要:把权限判断集中到路由配置里,路由结构更清晰,适合团队统一管理。
怎么想:beforeEnter 是“路由独享守卫”,只对当前这条路由生效。
怎么做:把判断逻辑从组件里搬到路由对象中;来源路由仍然要配置 meta.goCartSuccess。
易错点:beforeEnter 要写在目标路由对象内部,不是全局 beforeEach;不要和方案一重复写两份逻辑,否则容易重复 next。
调试与作业
调试顺序建议:先直接打开 /addCartSuccess,应回首页;再从搜索页加入购物车,应能进入;再从详情页加入购物车,应能进入;最后检查刷新或新窗口打开时是否被拦截。转写稿里“从首页直接打开不行、从搜索/详情可以、刷新不行”都是正常现象。
两个作业按同样思路做:
- 支付页:只允许从
cart进入,可在cart路由加meta标记,在支付路由的beforeEnter或支付页beforeRouteEnter判断from.meta。 - 支付成功页:只允许从
pay进入,同样在pay路由加标记,在支付成功页判断。
易错点:作业里不要只写 from.name === "cart" 就结束,建议按本节思路用 meta 标记,后续扩展更灵活;重定向地址要写项目里真实存在的首页路由。
安装与引入:先让项目具备校验能力
为什么需要:登录页的手机号、密码不能只靠后端报错,前端要在提交前给出即时提示。手写校验会分散在组件里,规则难复用。使用 vee-validate 可以把“规则定义”和“模板提示”分开。
怎么想:本项目是 Vue2,所以选择 vee-validate@2。转写稿里特别强调:Vue2 对应 v2,Vue3 对应 v4;v3 是过渡版本,不要混用。安装时如果漏写版本,可能装成 4.x,需要重新指定版本安装。
怎么做:先安装,再在 src/utils/validate.js 中统一注册插件和规则,最后在 src/index.js 引入一次。
shell
cnpm i vee-validate@2js
import "./utils/validate";易错点:Vue.use(VeeValidate) 只应执行一次;规则文件要被入口引入,否则模板里的 v-validate 不生效。安装后组件实例会多出 $validator、errors 等能力,可用 mounted(){ console.log(this) } 观察,但不要把调试代码留在最终页面。
重写 required:让错误提示变成中文
为什么需要:插件自带提示是英文,且默认“必填”未必符合项目要求。这里要求空字符串、纯空格都算不合法。
怎么想:Validator.extend("required", {...}) 可以覆盖同名规则。validate 返回布尔值:true 表示符合规则,false 表示不符合。getMessage 在不符合时返回提示文本,参数可拿到 input 的 name。
怎么做:在 validate.js 中重写 required,同时扩展手机号、密码规则。
js
import Vue from "vue";
import VeeValidate,{Validator} from "vee-validate"
import {phoneReg, pwdReg} from "@/utils/reg";
Vue.use(VeeValidate);
// 重写规则:
Validator.extend("required",{
// 该函数的返回值是一个布尔,为true说明符合规则,false不符合规则
validate:value=>value.trim().length>0,
// 当规则为false时,返回的值即是错误显示信息
// name是input->name属性值
getMessage(file) {
return file+"不允许为空!"
}
});
// 验证手机号:
Validator.extend("phone_rule",{
// 该函数的返回值是一个布尔,为true说明符合规则,false不符合规则
validate:value=>phoneReg.test(value),
// 当规则为false时,返回的值即是错误显示信息
// name是input->name属性值
getMessage:()=>"请输入正确的手机号!"
});
// 验证密码:
Validator.extend("pwd_rule",{
// 该函数的返回值是一个布尔,为true说明符合规则,false不符合规则
validate:value=>pwdReg.test(value),
// 当规则为false时,返回的值即是错误显示信息
// name是input->name属性值
getMessage:()=>"请输入6至21位密码!"
});易错点:value 可能是空值,直接 .trim() 前要确认输入场景;规则名要和模板里完全一致,phone_rule 不能写成 phoneRule。转写稿调试时曾出现“规则写了但没提示”,原因是模板没有使用该规则,或 input 没有 name,错误无法关联到字段。
模板绑定与错误显示:把规则挂到输入框
为什么需要:规则定义好后,必须让具体 input 声明“用哪些规则”,并指定错误显示位置。
怎么想:v-validate="{required:true,phone_rule:true}" 表示该字段同时应用两条规则;name="phone" 是错误信息的关联键;errors.first("phone") 取该字段第一条错误。
怎么做:在登录组件中绑定手机号、密码,并在提交时先统一校验。
vue
<template>
<!-- 登录 -->
<div class="login-wrap">
<div class="login">
<div class="loginform">
<div class="content">
<form @submit.prevent="login">
<div class="input-text clearFix">
<i></i>
<input
v-validate="{required:true,phone_rule:true}"
type="text"
name="phone"
placeholder="手机号">
<span class="error-msg">{{errors.first("phone")}}</span>
</div>
<div class="input-text clearFix">
<i class="pwd"></i>
<input v-validate="{required:true,pwd_rule:true}" type="text" name="password" placeholder="请输入密码">
<span class="error-msg">{{errors.first("password")}}</span>
</div>
<button class="btn">登 录</button>
</form>
<div class="call clearFix">
<router-link to="/register" class="register">立即注册</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
methods:{
// 1
async login(e){
// 所有的规则全部满足为true,有一个不满足为false
const result = await this.$validator.validateAll();
if(result){
const phone = e.target.phone.value.trim();
const password = e.target.password.value.trim();
await this.$store.dispatch("user/postLoginAsync",{
phone,
password
})
}
}
},
mounted(){
console.log(this)
}
}
</script>易错点:validateAll() 返回 Promise,必须 await 后再判断 result;不要在未校验通过时直接发登录请求。转写稿中调试时看到 result 为 false 才阻止提交,为 true 才继续。若用 e.target.phone 取值,input 必须有 name="phone";错误提示里的字段名也要与 name 一致。本节定位是“了解”,重点是理解规则返回值、getMessage 和提交前统一校验的流程。
打包命令与 dist 目录
为什么需要:项目开发完成后不能直接把源码目录交给运维或上线,生产环境需要的是打包后的静态资源。笔记里给出的命令是:
shell
cnpm run build转写稿里也提到等价的 npm run build。执行完该命令后,会在项目根目录中生成一个名字为 dist 的目录。
怎么想:打包的本质是把 Vue 项目里的组件、样式、图片等编译、压缩成浏览器可直接请求的静态文件。看到终端输出结束后,再去项目根目录确认是否出现 dist。
怎么做:执行命令,等待打包完成,然后进入 dist 查看生成的 index.html、CSS、JS、图片等资源。
易错点:不要以为打包完就能直接双击 dist/index.html 打开。转写稿里明确调试过:直接打开时页面资源路径不对,因为打包后的资源通常按根目录 / 去找,必须由服务托管。另一个易错点是命令要写在项目根目录,且依赖已安装。
把 dist 复制到 site 目录
为什么需要:笔记要求“复制 dist 目录内的文件至目录 site”。这样 site 就是实际部署目录,里面既有前端静态文件,也有后面要写的 server.js。
怎么想:不要把整个项目源码和 node_modules 直接丢给运维;上线目录应只保留打包产物和服务启动文件。转写稿里也强调“打完包不是啪就能运行,需要服务的支持”。
怎么做:把 dist 里面的文件复制到 site,然后在 site 下准备 server.js。
易错点:复制的是 dist 目录“内的文件”,不是把 dist 文件夹再套一层,否则静态资源路径容易多一层。server.js 要放在 site 目录下,后面 express.static(__dirname) 才能正确指向当前目录。
用 Express 托管静态资源
为什么需要:打包后的页面必须通过 HTTP 服务访问,不能直接本地文件打开。笔记中的 site/server.js 先引入 Express:
js
const express = require("express");
// 解决路由404问题第一步下载:cnpm install connect-history-api-fallback
// 解决路由404问题第二步引入:
const history = require("connect-history-api-fallback");
// 解决代理问题第一步下载:cnpm i http-proxy-middleware
// 解决代理问题第二步引入:
const {createProxyMiddleware} = require("http-proxy-middleware")
const app = express();
// 解决路由404问题第三步:将history当作中间件使用,当找不到指定的路由时,会将index.html作为渲染的界面
app.use(history());
// 将当前目录设置为静态资源
app.use(express.static(__dirname));
// 解决代理问题第三步:配置代理
app.use("/api",createProxyMiddleware({
target:"http://sph-h5-api.atguigu.cn",
changeOrigin:true
}))
app.use("/my",createProxyMiddleware({
target:"http://zhangpeiyue.com:8082",
changeOrigin:true,
pathRewrite:{
"^/my":""
}
}))
app.listen(80,()=>{
console.log("success");
})怎么想:先让服务能返回 index.html 和静态资源,再处理前端路由刷新 404,最后处理接口代理。转写稿中的调试顺序也是这样:先启动服务访问页面,发现刷新注册等页面 404,再加 history;发现“全部商品”等接口拿不到数据,再配代理。
怎么做:在 site 下安装依赖:
shell
cnpm install express
cnpm install connect-history-api-fallback
cnpm i http-proxy-middleware然后运行 server.js,访问 http://localhost 验证。
易错点:app.use(history()) 要放在 express.static(__dirname) 之前。它的作用是当找不到指定路由时,把 index.html 作为渲染界面返回;如果顺序反了,刷新子路由仍可能 404。express.static(__dirname) 表示把当前目录作为静态资源目录,__dirname 不要写错。
路由 404 与 history 模式
为什么需要:转写稿里点击“注册”后刷新页面就没了,原因是前端使用 history 路由,刷新时浏览器会向服务器请求 /register 这类地址,而服务器目录里并没有这个真实文件。笔记注释写得很清楚:connect-history-api-fallback 用于“当找不到指定的路由时,会将 index.html 作为渲染的界面”。
怎么想:服务端不认识前端路由,需要中间件兜底到 index.html,再由前端路由解析地址。
怎么做:按笔记三步:下载、引入、app.use(history())。启动后刷新子页面验证是否还在。
易错点:不要把它当成接口代理;它只解决前端路由刷新 404。若页面能打开但接口没数据,应检查代理,而不是继续改 history。
代理配置与启动验证
为什么需要:打包后前端请求 /api 等接口时,如果没有代理,请求会打到当前静态服务器上,拿不到真实后端数据。笔记用 http-proxy-middleware 配置了两个代理:/api 指向 http://sph-h5-api.atguigu.cn,/my 指向 http://zhangpeiyue.com:8082,并用 pathRewrite 把 /my 前缀去掉。
怎么想:代理三步与路由问题类似:下载模块、引入 createProxyMiddleware、用 app.use 配置。changeOrigin:true 用于改变请求源;pathRewrite 用于重写路径。
怎么做:安装并引入后,把笔记中的代理代码放入 server.js,重启服务,打开 Network 面板观察接口是否返回数据。
易错点:改完 server.js 必须重启服务。转写稿还提醒:如果前端页面路径以 /my 开头,可能被 /my 代理拦截,所以订单等页面命名要避开代理前缀,或调整代理配置。最后启动成功时控制台会输出 success,再访问页面、刷新子路由、检查接口,三项都通过才算部署验证完成。
课堂画面





代码示例
1. 路由懒加载 + 路由权限(src/router/index.js)
js
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
/* ---------------- 路由懒加载:用到哪个路由才去请求哪个 chunk ---------------- */
const Home = () => import("@/pages/Home");
const Login = () => import("@/pages/Login");
const Register = () => import("@/pages/Register");
const Search = () => import("@/pages/Search");
const Detail = () => import("@/pages/Detail");
const AddCartSuccess = () => import("@/pages/AddCartSuccess");
const Cart = () => import("@/pages/Cart");
const Trade = () => import("@/pages/Trade");
const Pay = () => import("@/pages/Pay");
const PaySuccess = () => import("@/pages/PaySuccess");
const routes = [
{ path: "/", name: "home", component: Home },
{ path: "/login", name: "login", component: Login },
{ path: "/register", name: "register", component: Register },
// 允许作为“加入购物车成功页”来源的两个页面
{ path: "/search", name: "search", component: Search, meta: { goCartSuccess: true } },
{ path: "/detail/:skuId", name: "detail", component: Detail, meta: { goCartSuccess: true } },
// 允许作为“结算(提交订单)页”来源的页面
{ path: "/cart", name: "cart", component: Cart, meta: { goTrade: true } },
// 允许作为“支付成功页”来源的页面
{ path: "/pay", name: "pay", component: Pay, meta: { goPaySuccess: true } },
// 路由独享守卫:只有来源路由的 meta 打了标记才放行
{
path: "/addCartSuccess",
name: "addCartSuccess",
component: AddCartSuccess,
beforeEnter(to, from, next) {
if (from.meta && from.meta.goCartSuccess) next();
else next("/");
}
},
{
path: "/trade",
name: "trade",
component: Trade,
beforeEnter(to, from, next) {
if (from.meta && from.meta.goTrade) next();
else next("/");
}
},
{
path: "/paySuccess",
name: "paySuccess",
component: PaySuccess,
beforeEnter(to, from, next) {
if (from.meta && from.meta.goPaySuccess) next();
else next("/");
}
}
];
export default new VueRouter({ routes });关键行与易错点
() => import("@/pages/Home"):返回 Promise,webpack 自动拆包。首屏只加载 Home 的 chunk,减少首次请求体积;缺点是某个 chunk 过大时,进入该页会有一段白屏,需要配合 loading。- 标记打在来源路由的
meta上(search/detail),守卫写在目标路由上(addCartSuccess),判断的是from而不是to——写反就永远进不去。 next("/")与next()只能二选一且必须调用一次;既调next()又调next("/")会报错。- 直接输入地址或刷新时,
from是空路由,from.meta为{},条件不成立 → 跳首页,这正是预期行为。 - 另一种等价方案是在组件里写
beforeRouteEnter(to, from, next),注意此时this拿不到组件实例。 - 转写稿里还演示过
if (["detail","search"].includes(from.name))的写法,但加页面就得改数组、不够灵活,所以最终改成meta标记方案。
2. 图片懒加载 + 表单验证(src/index.js、src/utils/validate.js、src/pages/Login/index.vue)
js
// src/index.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import VueLazyLoad from "vue-lazyload";
import loading from "@/assets/images/loading.gif";
import "./utils/validate";
Vue.use(VueLazyLoad, {
// 图片未加载前给予呈现的图片
loading
});
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");js
// src/utils/validate.js
import Vue from "vue";
import VeeValidate, { Validator } from "vee-validate";
import { phoneReg, pwdReg } from "@/utils/reg";
Vue.use(VeeValidate);
// 重写 required 规则
Validator.extend("required", {
// 返回 true 说明符合规则,false 不符合
validate: value => value.trim().length > 0,
// 规则为 false 时,返回值即错误提示信息
getMessage(file) {
return file + "不允许为空!";
}
});
// 验证手机号
Validator.extend("phone_rule", {
validate: value => phoneReg.test(value),
getMessage: () => "请输入正确的手机号!"
});
// 验证密码
Validator.extend("pwd_rule", {
validate: value => pwdReg.test(value),
getMessage: () => "请输入6至21位密码!"
});vue
<!-- src/pages/Login/index.vue(样式略,不影响逻辑) -->
<template>
<div class="login-wrap">
<div class="login">
<div class="loginform">
<div class="content">
<form @submit.prevent="login">
<div class="input-text clearFix">
<i></i>
<input
v-validate="{required:true,phone_rule:true}"
type="text"
name="phone"
placeholder="手机号">
<span class="error-msg">{{errors.first("phone")}}</span>
</div>
<div class="input-text clearFix">
<i class="pwd"></i>
<input
v-validate="{required:true,pwd_rule:true}"
type="text"
name="password"
placeholder="请输入密码">
<span class="error-msg">{{errors.first("password")}}</span>
</div>
<button class="btn">登 录</button>
</form>
<div class="call clearFix">
<router-link to="/register" class="register">立即注册</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
methods: {
async login(e) {
// 所有规则全部满足为 true,有一个不满足为 false
const result = await this.$validator.validateAll();
if (result) {
const phone = e.target.phone.value.trim();
const password = e.target.password.value.trim();
await this.$store.dispatch("user/postLoginAsync", { phone, password });
}
}
}
};
</script>关键行与易错点
- 图片懒加载:安装
vue-lazyload@1后Vue.use(VueLazyLoad, { loading }),loading是占位图;模板里把src换成v-lazy,如 Search 页的<img v-lazy="item.defaultImg" />。不要再同时写:src,两者会互相覆盖。 Validator.extend(名字, { validate, getMessage }):validate返回true才算通过,getMessage返回的是不通过时显示的文案。v-validate的值是对象{required:true, phone_rule:true},键名必须和extend的第一个参数完全一致,否则规则写了也不生效(转写稿里就踩过"定了规则没用上"的坑)。errors.first("phone")的参数必须等于 input 的name;把name改成中文,模板里的errors.first("phone")也得同步改。- 重写
required用的是value.trim().length > 0,比内置的 required 更严格(内置不拦纯空格)。 - 提交必须用
await this.$validator.validateAll()拿到布尔值再发请求,不能只靠按钮样式或errors.any()判断。 - 易错:
e.target.phone.value依赖 input 的name="phone",转写稿中特别强调——如果改用 ref 或 v-model 收集数据,这里就要跟着改,否则取不到值。
3. 打包部署(site/server.js)
js
// 复制 dist 目录内的文件至 site 目录,在 site 下执行 node server.js
const express = require("express");
// 解决路由404问题第一步:cnpm install connect-history-api-fallback
const history = require("connect-history-api-fallback");
// 解决代理问题第一步:cnpm i http-proxy-middleware
const { createProxyMiddleware } = require("http-proxy-middleware");
const app = express();
// 解决路由404问题第二步:当找不到指定路由时,将 index.html 作为渲染的界面
app.use(history());
// 将当前目录设置为静态资源
app.use(express.static(__dirname));
// 解决代理问题第三步:配置代理
app.use("/api", createProxyMiddleware({
target: "http://sph-h5-api.atguigu.cn",
changeOrigin: true
}));
app.use("/my", createProxyMiddleware({
target: "http://zhangpeiyue.com:8082",
changeOrigin: true,
pathRewrite: {
"^/my": ""
}
}));
app.listen(80, () => {
console.log("success");
});关键行与易错点
- 打包命令
cnpm run build,产物在项目根目录的dist;把dist里的文件复制到site,再在site下起服务。 app.use(history())必须写在express.static之前,否则刷新子路由仍然是 404。- history 中间件的作用:找不到对应资源时回退渲染
index.html,交给前端路由处理;这也是 history 模式上线必须配置的原因。 - 代理用
createProxyMiddleware(解构引入,新版本 API),/api直接转发,/my通过pathRewrite去掉前缀。 - 易错:直接双击
index.html打开会白屏——打包后资源是绝对路径/js/xxx.js,必须有服务托管才能访问。 - 易错:接口前缀
/my会和前端路由(比如“我的订单”/myOrder)冲突,转写稿里就是把/my代理去掉或给路由改名来解决的。 - 端口 80 需要管理员权限;
app.use("/api", ...)与express.static的顺序保持上面这样,避免静态资源拦截接口请求。
常见错误
- 支付页点击取消/关闭后仍继续发请求,慢网下更明显 → 只
clearTimeout了当前定时器,未让在途请求回调失效,回调返回后又setTimeout→ 增加isCancelled/isTimeout标志,取消时置位并clearTimeout、timer=null;请求成功/失败回调先判断标志再继续,异常状态码也阻断后续轮询。 - 加入购物车成功页直接输入 URL 仍能进入 → 来源路由未配置
meta.goCartSuccess,或守卫只判断from.name但路由没配name,动态path又不好硬判 → 来源路由统一加meta标记,目标路由用beforeEnter读from.meta,未授权next("/");不要在组件里散落硬编码判断。 v-lazy图片不懒加载或不显示占位图 → 入口未Vue.use(VueLazyLoad,{loading})、版本不匹配,或指令写成v-lazy:src、loading路径错误 → Vue2 使用vue-lazyload@1,入口注册并引入占位图,模板使用v-lazy="item.defaultImg"。- 表单验证规则不生效、错误提示不出、校验失败仍提交 →
Validator.extend规则名与v-validate不一致,input name与errors.first()不一致,或未await validateAll()判断结果 → 规则名、name、错误 key 三处对齐;提交方法用async,先const ok=await this.$validator.validateAll(); if(!ok) return;再发请求。 - 打包后刷新子路由 404 →
history路由模式,Express 未挂connect-history-api-fallback→app.use(history())后再express.static,让找不到的路由回退到index.html。 - 打包后接口 404 或跨域 → 开发环境的
devServer.proxy不会随打包生效,生产服务未配代理 → Express 中用http-proxy-middleware配/api代理,设置changeOrigin,按需pathRewrite。 - 订单分页刷新回到第一页、页码与请求数据不一致 → 页码只存组件
data,未同步 URL query,也未监听路由变化 → 初始化从$route.query.page取默认 1,切页pushquery,watch $route.query.page重新拉数据。 - 订单表格跨行错位 → 详情条数动态变化,但首列
rowspan写死 → 用item.orderDetailList.length动态绑定rowspan,只在首行渲染跨行单元格。
练习
- 修复支付页轮询:实现点击取消/关闭后不再发请求;用慢网复现“在途请求返回后又起定时器”的场景,并说明你的标志位清理点。
- 完成
/trade与/paySuccess的路由权限:/trade只允许从cart进入,/paySuccess只允许从pay进入;直接输入 URL 必须被重定向,分别用路由独享守卫实现。 - 给订单列表加分页并与 URL query 同步:刷新保留当前页,切页请求对应页,从详情返回列表能恢复页码。
- 部署 Express 服务:配置 history fallback 与
/api代理,验证刷新/myOrder不 404、接口请求不跨域。 - 完成登录/注册表单验证:手机号与密码规则、错误提示、
validateAll不通过不提交;注册页照登录页迁移并验证规则名与name对齐。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 解决BUG | 09:27 |
| 2 | 增加路由权限 | 13:12 |
| 3 | 表单验证 | 27:50 |
| 4 | 打包 | 18:07 |
| 5 | 我的订单 | 33:03 |
小结
- 懒加载是体积与体验的权衡:路由按页拆分、图片用占位,但要警惕单个 chunk 过大。
- 权限校验要收口到路由层,用
meta做来源标记,避免组件内散落硬编码和动态path判断。 - 异步清理类 bug 的核心是“在途请求 + 定时器 + 标志位”,取消必须让后续回调失效,而不只是清当前定时器。
- 表单验证是提交前防线:规则名、
name、错误 key 必须一致,最终仍要服务端校验。 - 打包后能打开不等于可部署,优先排查 history 路由回退与生产代理。
- 列表分页、筛选等状态优先同步 URL,组件状态只负责渲染,刷新和返回才可恢复。
- 动态表格的
rowspan/colspan必须由数据长度计算,不能写死。 - 修复前先用慢网、直接输 URL、刷新、取消操作复现边界,再改代码验证。