主题
卖座网数据调用与猫眼网数据调用(跨域)(Lesson9 1/2)
本页对应 Lesson09,共 6 段视频,约 1 小时 39 分。
学习目标
- 能通过浏览器 Network 面板定位卖座网“正在热映/即将上映”接口,指出请求方式、请求地址、查询参数及
tab参数差异,并说明_k参数避免缓存的作用。 - 能使用 axios 发送携带查询参数和自定义请求头的 GET 请求,配置跨域所需请求头并验证接口返回数据。
- 能封装电影数据请求方法,通过传入
type、pageSize、page等参数复用请求逻辑,并返回可渲染的数据结果。 - 能使用 Vue 完成电影列表渲染、热映/即将上映切换、加载中状态展示及上下页分页,并处理页码边界与切换时页码重置。
- 能使用
axios.create创建实例,配置baseURL、timeout、请求头及响应拦截器,并将接口按模块抽离到 API 目录供组件调用。 - 能根据路由需求创建对应页面组件,并完成基础路由映射配置,使不同路由地址可渲染对应组件。
知识点
先找接口:从 Network 定位数据源
为什么需要:卖座网页面上的电影不是写死在 HTML 里的,而是页面加载后通过 Ajax 再取一次数据。要“调用数据”,第一步不是写代码,而是找到浏览器实际发出的请求。
怎么想:打开卖座网移动端,按 F12 打开 Network,筛选 XHR/Fetch;点击“正在热映”“即将上映”,观察新增请求。转写稿里反复强调:不要看页面地址,要看页面里发出的 Ajax 请求地址。
怎么做:在 Network 中选中返回电影列表的请求,查看 Request URL、Request Method、Query String Parameters、Request Headers、Response。请求方式为 GET,响应体里能看到 data.films、total 等字段。
易错点:页面地址和接口地址不是一回事;接口地址可能带查询字符串;不要纠结 cityId=310100 为什么是上海,它只是对方数据库里的城市编码。
分析参数:type 是切换关键
为什么需要:正在热映和即将上映不能写两套完全独立的逻辑,否则维护成本高。
怎么想:对比两个请求地址,域名、城市、页码、每页数量都一样,只有 type 不同:type=1 是正在热映,type=2 是即将上映;k 是随机数,用来避免缓存。
怎么做:把参数整理成对象,地址从 Network 复制,不要自己猜:
js
const params = {
cityId: 310100,
pageNum: 1,
pageSize: 10,
type: 1, // 1 正在热映,2 即将上映
k: Date.now()
}易错点:k 每次请求最好不同;pageNum 是页码,pageSize 是每页条数;切换类型时页码要回到 1。
跨域与请求头:把浏览器里的信息带齐
为什么需要:直接请求会报 CORS。转写稿中看到报错后,检查发现对方服务端会校验特定请求头。
怎么想:不是“前端随便加头就能跨域”,而是对方接口要求携带 X-Client-Info、X-Host 等头。值从 Network 的 Request Headers 复制。
怎么做:先用 axios 发一次请求:
js
import axios from 'axios'
axios.get('从 Network 复制的接口地址', {
params,
headers: {
'X-Client-Info': '从 Network 复制',
'X-Host': '从 Network 复制'
}
}).then(res => {
console.log(res.data)
}).catch(err => {
console.log(err)
})易错点:属性名带连字符必须加引号;请求头的值最终要是字符串,若是对象需 JSON.stringify;改完请求头仍失败,就依次检查请求方式、地址、参数。
先跑通一个,再封装切换
为什么需要:一上来就做切换容易乱,先把“正在热映”调通,再改 type 验证“即将上映”。
怎么做:把请求封装成方法,接收 type:
js
function getFilms(type = 1) {
return axios.get(url, { params: { ...params, type }, headers })
}易错点:模板里 @click="getFilms" 会把事件对象传进去,应写成 @click="getFilms(1)" 或包一层函数。
渲染列表与加载中
为什么需要:拿到 res.data.data.films 后要交给 Vue 状态,视图才会更新。
怎么做:
js
filmList.value = res.data.data.films
total.value = res.data.data.total模板中循环 filmList,图片字段用 poster,名称用 name。加载中可用 loading 布尔值控制,并抽成全局组件:
vue
<Loading v-if="loading" />
<div v-else>
<div v-for="item in filmList" :key="item.filmId">
<img :src="item.poster" height="200">
<p>{{ item.name }}</p>
</div>
</div>易错点:图片不显示先检查字段名和地址;组件引入顺序建议内置模块、第三方模块、自定义模块、样式。
分页:页码变化后重新请求
为什么需要:接口返回 total,需要计算总页数。
怎么做:totalPage = Math.ceil(total / pageSize);上一页、下一页修改 pageNum 后重新调用请求;切换 type 时把 pageNum 置为 1。
易错点:页码不能小于 1、不能大于总页数;如果在 watch 里既改页码又请求,容易重复请求甚至死循环,转写稿最后改为在翻页事件里显式调用请求方法。
封装 axios 与 API 层
为什么需要:多个组件都要调接口,不能到处写地址和请求头。
怎么做:建 request/maizuo.js 创建实例:
js
const request = axios.create({
baseURL: 'https://m.maizuo.com',
timeout: 10000,
headers: { /* 从 Network 复制 */ }
})
request.interceptors.response.use(res => res.data)
export default request再建 api/maizuo.js:
js
import request from '../request/maizuo'
export function getFilmList(type, pageNum) {
return request.get('接口路径', {
params: { cityId: 310100, pageNum, pageSize: 10, type, k: Date.now() }
})
}易错点:baseURL 只放域名,路径不要重复;拦截器返回 res.data 后,组件里不要再多层 .data;接口路径以 Network 为准,不要凭记忆编。
先搭最小可运行页面
为什么需要:先排除项目配置干扰,才能专心调接口。转写里先保留一份初始 src,再清空入口,只留一个 App 组件。
怎么想:项目能启动、能热更新、能自动打开浏览器,再谈抓数据。
怎么做:在 vite.config.js 里配置 server.open 和端口;改完配置必须重启。
易错点:配置文件改了不重启不生效;入口挂载点 #app 要和 index.html 对上。
js
// vite.config.js
export default {
server: {
open: true,
port: 8080
}
}js
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')抓包找接口:先看请求方式、地址、查询串
为什么需要:接口不能猜。转写里反复打开 Network,分别点“正在热映”“即将上映”,比较两个请求。
怎么想:先选 XHR/Fetch,找到返回电影数据的请求;再看请求方式是不是 GET,地址里哪些参数在变。
怎么做:转写中两个接口主要差在 type:type=1 是正在热映,type=2 是即将上映;还有 pageNum 页码、pageSize 每页条数、cityId 城市编码、k 随机数。k 的作用是让地址每次不同,避免浏览器缓存。
易错点:不要把页面地址当接口地址;查询串要放在 params 里,不要手拼错;转写里的“麦座/卖座”是同音口误,实际以抓包域名为准。
用 axios 发请求并处理请求头
为什么需要:要拿 JSON 数据,先让单个接口跑通。
怎么想:先写死一个 type,成功后再做切换。转写里先请求正在热映,再把 type 改成 2 验证即将上映。
怎么做:下载 axios,用 GET;请求头里带特殊字符的属性名要加引号,属性值最终必须是字符串。转写里 X-Client-Info 的值如果写成对象,会被转成 [object Object],要用 JSON.stringify 或写成字符串。
易错点:浏览器报 CORS 时,不要以为“前端随便加两个头”就能通用解决跨域;CORS 主要由服务端响应头决定。若接口文档/抓包要求携带 X-Host、X-Client-Info,要按原样带;开发阶段跨域也可用 Vite 代理。
js
import axios from 'axios'
const clientInfo = '<从抓包请求头原样复制>'
axios.get('https://m.maizuo.com/gateway', {
params: {
cityId: 310100,
pageNum: 1,
pageSize: 10,
type: 1,
k: Date.now()
},
headers: {
'X-Host': 'm.maizuo.com',
'X-Client-Info': clientInfo
}
}).then(res => {
console.log(res.data)
}).catch(err => {
console.log(err)
})封装请求与 API
为什么需要:多个组件都要调同一个服务,不能到处写死域名和请求头。
怎么想:一层封装 axios 实例,一层封装具体接口函数。
怎么做:axios.create 设 baseURL、timeout、公共请求头;响应拦截器返回 res.data;API 文件只暴露“取电影列表”函数。
易错点:拦截器返回 res.data 后,组件里不要再多写一层 res.data.data;导入顺序建议内置模块、第三方模块、自定义模块、样式。
js
// src/api/maizuo.js
import request from '../utils/request'
export function getFilmList({ type, pageNum, pageSize = 10 }) {
return request.get('/gateway', {
params: {
cityId: 310100,
type,
pageNum,
pageSize,
k: Date.now()
}
})
}渲染、切换、分页与加载中
为什么需要:只打印数据不算完成,要能切换、翻页、显示加载状态。
怎么想:用 type 控制正在热映/即将上映;用 pageNum 控制页码;用 total 算总页数;用 isLoading 控制加载组件。
怎么做:点击按钮改 type,watch 里重置页码并请求;翻页按钮改 pageNum 后请求;总页数用 Math.ceil(total / pageSize)。
易错点:watch 里又改 pageNum 容易重复请求甚至死循环;按钮 @click="getData" 不传参时第一个参数是事件对象,要写成 @click="getData(1)";页码要限制在 1 到总页数之间。
js
const filmList = ref([])
const type = ref(1)
const pageNum = ref(1)
const total = ref(0)
const isLoading = ref(false)
async function getData() {
isLoading.value = true
const res = await getFilmList({
type: type.value,
pageNum: pageNum.value
})
filmList.value = res.data.films
total.value = res.data.total
isLoading.value = false
}
watch(type, () => {
pageNum.value = 1
getData()
})html
<button @click="type = 1">正在热映</button>
<button @click="type = 2">即将上映</button>
<Loading v-if="isLoading" />
<div v-for="item in filmList" :key="item.filmId">
<img :src="item.poster" height="200" />
<p>{{ item.name }}</p>
</div>先明确目标:从卖座网抓“正在热映/即将上映”
为什么需要:做页面不能先写死数据,要先拿到真实接口,再谈渲染、切换和分页。
怎么想:先只调一个列表,跑通后再比较“正在热映”和“即将上映”的差异。
怎么做:新建项目,保留一份原始 src 备份;在 vue.config.js 中配置自动打开和端口,改完配置必须重启。
js
// vue.config.js
module.exports = {
devServer: { open: true, port: 80 }
}入口先保证能跑:
js
new Vue({ render: h => h(App) }).$mount('#app')易错点:配置文件改了不重启不生效;不要一上来就复制整套项目,先把入口、主组件写清楚。
找接口:在 Network 里看请求方式和参数
为什么需要:页面地址不等于接口地址,接口要靠浏览器网络面板找。
怎么想:打开开发者工具 Network,筛选 XHR/Ajax,点击“正在热映”,看真正返回电影列表的请求。转写里把接口地址复制到地址栏打不开,这是正常的,因为它还依赖请求头。
怎么做:对比“正在热映”和“即将上映”两个地址,域名、ct、pageNum、pageSize 基本一致,核心差异是 type:type=1 正在热映,type=2 即将上映;k 是随机数,用来避免缓存。
易错点:别把页面 URL 当接口;别忽略 type;k 每次变化,浏览器才认为地址不同,避免直接读缓存。
理解 URL:hash、查询字符串和端口
为什么需要:路由和请求都绕不开 URL。
怎么想:URL 可拆成协议、域名、端口、资源路径、查询字符串、# 片段。# 右边叫 hash/片段,不影响资源请求,前端路由的 hash 模式就利用它;查询字符串用于传参,也不影响最终访问哪个资源。
怎么做:https 默认端口 443,http 默认 80;写服务时端口不能省,访问时可省略默认端口。https 需要证书。
易错点:不要把查询字符串当成路径;不要以为 # 后面的内容会发给服务器。
用 axios 发请求:GET、params、headers、CORS
为什么需要:接口需要带查询参数和自定义请求头。
怎么想:先只请求“正在热映”,确认请求方式为 GET,再把查询字符串放进 params,把 Network 里的两个自定义请求头原样放进 headers。
js
axios.get(url, {
params: { ct: '310100', pageNum: 1, pageSize: 10, type: 1, k: Date.now() },
headers: {
// 两个自定义请求头按 Network 中的原名、原值填写
}
}).then(res => {
console.log(res.data.data.filmList)
}).catch(err => {
console.error(err)
})易错点:出现 CORS 跨域报错时,先检查请求头是否带全;请求头属性名含特殊字符要加引号,值最终应是字符串,对象值要 JSON.stringify。响应体通常在 res.data.data.filmList,别少一层 data。
封装 axios 与 API:别在每个组件写死地址
为什么需要:地址、超时、请求头重复写会难维护。
怎么想:用 axios.create 创建卖座专用实例,用响应拦截器统一返回响应体;再把具体接口放到 api 目录。
js
const request = axios.create({
baseURL: 'https://m.maizuo.com',
timeout: 10000,
headers: { /* 统一请求头 */ }
})
request.interceptors.response.use(res => res.data)
export default requestAPI 模块:
js
export const getFilmList = (type, pageNum = 1, pageSize = 10) =>
request.get('/gateway', { params: { type, pageNum, pageSize, ct: '310100', k: Date.now() } })易错点:拦截器返回 res.data 后,组件里不要再 .data.data;实例不影响全局 axios;引入顺序建议内置模块、第三方、自定义、样式。
渲染、切换、加载中与分页
为什么需要:数据拿到后要驱动视图,并处理切换、加载、页码边界。
怎么想:filmList 初始为空数组,请求成功后赋值;v-for 渲染海报和名称,:key 用电影 ID;按钮切换 type 后重新请求;isLoading 控制加载中,可抽成全局组件。分页维护 pageNum/pageSize/total,上一页下一页要判断边界。
怎么做:切换类型时重置页码并只请求一次;不要在 type 的 watch 里既改页码又调接口,否则会重复请求。
js
methods: {
changeType(type) {
this.type = type
this.pageNum = 1
this.getList()
}
}易错点:@click="fn" 不写括号会传事件对象;页码小于 1 或大于总页数要限制;图片过大加高度;加载中状态请求前设为 true,结束后设为 false。
先看本节需求清单
本节的核心不是马上写页面,而是先把“项目要响应哪些地址”列清楚。笔记里的需求清单如下,原样保留:
请求地址:/ ---------------------------------------->首页(完成)
请求地址:/ home---------------------------------------->首页--》重定向(完成)
请求地址:/newsList ---------------------------------------->新闻列表页(完成)
请求地址:/news ---------------------------------------->新闻列表页(完成)
请求地址:/goodsList ---------------------------------------->商品列表页(完成)
请求地址:/my---------------------------------------->我的个人中心(完成)
请求地址:/a;lskdjf;lakdsjf; ---------------------------------------->404页面(完成)
为什么需要:这份清单相当于前后端、页面之间的“地址契约”。没有它,首页、列表页、个人中心、404 的路径容易命名混乱,后面做导航、重定向和联调时会反复返工。
怎么想:先不急着写组件,而是逐条判断:这个地址对应哪个页面?是正常页面、重定向,还是兜底 404?清单后面的“完成”说明当前阶段已经逐项验证过。
怎么做:把每一行拆成“请求地址 → 目标页面 → 类型”。例如 / 是首页,/home 也是首页但要走重定向,/newsList 与 /news 都指向新闻列表页,/goodsList 是商品列表页,/my 是个人中心,最后一条乱码式地址用于验证 404 兜底。
易错点:不要把 /home 当成第二个首页组件重复开发;它只是重定向到首页。/newsList 和 /news 同时存在时,要明确二者是否等价,避免一个能开、一个 404。404 路由要放在最后匹配,否则会抢走正常地址。
首页与重定向要分开理解
为什么需要:/ 和 /home 都指向首页,但它们的职责不同。/ 是默认入口,/home 是另一个可访问地址,通过重定向回到首页,保证用户输入 /home 时不会白屏。
怎么想:重定向不是“再做一个首页”,而是“地址变了,最终仍落到首页”。转写稿里反复强调先分步走、不要急,先完成一个点再扩展;这里也应先把 / 跑通,再验证 /home 是否能跳回首页。
怎么做:在路由需求表中把 /home 标注为“重定向到 /”,验收时直接访问 /home,观察地址和页面是否落到首页。
易错点:重定向目标写错会形成循环;把 /home 配成独立组件会造成维护两份首页。转写稿提到配置文件修改后必须重启,路由配置变更后同样要重新验证,不能只看旧页面。
列表页双地址与个人中心
为什么需要:/newsList、/news 都指向新闻列表页,/goodsList 指向商品列表页,/my 指向个人中心。提前列出这些地址,后续导航、菜单、接口调试才有统一入口。
怎么想:先判断“是不是同一个页面多个入口”。如果是,就让两个地址都落到新闻列表页;商品列表和个人中心则各自独立。转写稿中调试接口时强调看请求方式、请求地址、请求参数,这个习惯同样适用于验收路由:地址是否命中、页面是否渲染、参数是否丢失。
怎么做:逐条访问清单地址,确认页面标题或内容能区分新闻列表、商品列表、个人中心;对 /newsList 与 /news 分别刷新一次,避免只在点击跳转时正常、直接输入地址却失败。
易错点:只测点击不测直接输入地址;地址大小写、末尾斜杠不一致;把 /my 误写成 /mine。转写稿里提到的“请求地址、请求方式、参数”三项检查法,可以用来排查页面空白时到底是路由没命中还是数据没回来。
404 兜底与验收习惯
为什么需要:最后一条 /a;lskdjf;lakdsjf; 明显是随意地址,用来验证未知路径是否进入 404 页面。没有兜底,用户输错地址会看到空白或默认错误。
怎么想:404 是“所有正常路由都不匹配后的最后一道门”,所以它必须放在路由匹配的最后。转写稿中讲跨域时提到请求头、讲 Network 时提到看状态和响应,这些都是验收思路:不要猜,打开开发者工具看实际地址和结果。
怎么做:访问清单最后一条乱码地址,确认出现 404 页面;再回归访问 /、/home、/newsList、/news、/goodsList、/my,确保 404 没有误伤正常地址。
易错点:404 路由放太前会拦截正常页面;只写 404 组件不配置通配地址;重定向和 404 同时存在时顺序混乱。转写稿里“配置改了要重启”“用 Network 看真实请求”的调试习惯,应贯穿本节验收。
先明确本节目标:路由表之前,先把页面组件准备好
为什么需要:路由的本质是“地址与组件之间的映射”。如果路由表里写了 component: Home,但项目里没有 Home.vue,路由配置就无法成立。转写稿里反复强调“分步走,不要急”,先保留初始 src,再把入口和主组件跑通,然后才逐步加功能;本节也是同一思路:先创建路由对应的页面组件,后面配置路由时才有东西可映射。
怎么想:把每个路由地址先对应成一个“页面骨架组件”,只写标题占位,不急着写业务逻辑。这样路由切换是否成功、页面是否渲染,一眼就能验证。
怎么做:在 src 下新建 pages 目录,依次创建 Home.vue、NewsList.vue、GoodsList.vue、My.vue、NotFound.vue。
易错点:不要把组件直接散落在 src 根目录;不要一上来就写复杂业务,否则路由不通时很难判断是路由问题还是组件问题。
Home.vue:首页组件
为什么需要:首页通常对应 / 或 /home,是路由的第一个验证点。
怎么想:只保留最小模板和 name,让组件可被路由识别即可。
怎么做:
vue
<template>
<div>
<h3>Home</h3>
</div>
</template>
<script>
export default {
name: "Home"
}
</script>
<style scoped>
</style>易错点:name 建议与文件名一致,便于调试;<style scoped> 先留空可以,但不要误写成 scope。
NewsList.vue:新闻列表组件
为什么需要:新闻列表是典型的一级页面,用来验证“列表页路由”是否能独立渲染。
怎么想:页面先写中文标题“新闻列表界面”,后续再补列表数据。
怎么做:
vue
<template>
<div>
<h3>新闻列表界面</h3>
</div>
</template>
<script>
export default {
name: "NewsList"
}
</script>
<style scoped>
</style>易错点:模板中只能有一个根 div;name 不要写成 Newslist 这类大小写不一致的形式。
GoodsList.vue:商品列表组件
为什么需要:商品列表和新闻列表结构相似,但路由地址不同,提前建好可以避免路由表引用不存在的组件。
怎么想:仍然只做占位组件,保证“地址—组件”关系清晰。
怎么做:
vue
<template>
<div>
<h3>商品列表界面</h3>
</div>
</template>
<script>
export default {
name: "GoodsList"
}
</script>
<style scoped>
</style>易错点:文件名、name、后续路由里的组件变量名要统一;不要出现 GoodsList.vue 却导出 Goods 的混乱。
My.vue:个人中心组件
为什么需要:个人中心常作为独立路由,用来验证“我的”页面入口。
怎么想:先写“我的个人中心界面”,不涉及登录状态。
怎么做:
vue
<template>
<div>
<h3>我的个人中心界面</h3>
</div>
</template>
<script>
export default {
name: "My"
}
</script>
<style scoped>
</style>易错点:My 是组件名,不要和路由路径混淆;组件内暂不写 router-link,避免把页面组件和导航组件耦合。
NotFound.vue:404 兜底组件
为什么需要:用户访问不存在的地址时,需要有兜底页面,否则路由匹配失败后页面空白,不利于调试。转写稿里也提到“有问题就输出、看报错”,404 组件就是路由层的“可观察反馈”。
怎么想:404 组件同样是普通页面组件,只是后续会配置到通配路由上。
怎么做:
vue
<template>
<div>
<h3>404,您找的界面不存在!</h3>
</div>
</template>
<script>
export default {
name: "NotFound"
}
</script>
<style scoped>
</style>易错点:不要把它命名成 404.vue,组件名和文件名建议用英文合法标识;404 文案要明确,方便确认兜底路由生效。
组件创建后的检查与后续衔接
为什么需要:组件写完后如果不检查,后续路由报错会很难定位。转写稿中强调“先调通一个,再封装、再扩展”,这里也一样:先确认五个组件都能被引入。
怎么想:检查三件事——文件在 src/pages 下、export default 存在、name 与文件名对应。
怎么做:后续路由表里再按需 import Home from '../pages/Home.vue' 等;本节不提前编造路由配置。
易错点:路径大小写、目录层级、组件名拼写是高频错误;scoped 样式先留空没问题,但不要删除 <style scoped> 导致后续样式作用域混乱。
课堂画面






代码示例
代码示例 1:卖座网数据调用(请求头跨域 + 切换 + 加载中 + 分页)
vue
<!-- src/App.vue -->
<template>
<div id="app">
<div class="btn">
<button @click="changeType(1)">正在热映</button>
<button @click="changeType(2)">即将上映</button>
</div>
<!-- 加载中 -->
<h3 v-if="isLoading">正在拼命加载中...</h3>
<div v-else>
<div v-for="item in filmList" :key="item.filmId" class="film">
<img :src="item.poster" height="200" alt="" />
<p>{{ item.name }}</p>
</div>
</div>
<!-- 分页 -->
<div class="page">
<a v-if="pageNum > 1" @click.prevent="prevPage">上一页</a>
<span>{{ pageNum }} / {{ totalPage }}</span>
<a v-if="pageNum < totalPage" @click.prevent="nextPage">下一页</a>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "App",
data() {
return {
type: 1, // 1 正在热映 2 即将上映
pageNum: 1, // 当前页码
pageSize: 2, // 每页显示条数
totalPage: 1, // 总页数
filmList: [],
isLoading: false
};
},
mounted() {
this.getData();
},
methods: {
changeType(type) {
this.type = type;
this.pageNum = 1; // 切换类型时回到第一页
this.getData();
},
prevPage() {
this.pageNum--;
this.getData();
},
nextPage() {
this.pageNum++;
this.getData();
},
getData() {
this.isLoading = true;
axios({
method: "get",
url: "https://m.maizuo.com/gateway", // 以抓包得到的地址为准
params: {
cityId: 310100, // 城市编码
pageNum: this.pageNum, // 页码
pageSize: this.pageSize, // 每页条数
type: this.type, // 1 正在热映 / 2 即将上映
k: Math.random() // 随机数:每次地址不同,绕过浏览器缓存
},
headers: {
// 属性名有中划线,必须加引号;属性值必须是字符串
"X-Client-Info":
'{"a":"3000","ch":"1002","v":"5.0.4","e":"..."}',
"X-Host": "mall.film-ticket.film.list"
}
})
.then(res => {
// res 是响应体对象,res.data 才是接口返回的 body,再取 body.data
const { films, total } = res.data.data;
this.filmList = films;
this.totalPage = Math.ceil(total / this.pageSize);
this.isLoading = false;
})
.catch(err => {
console.log(err);
this.isLoading = false;
});
}
}
};
</script>
<style>
.btn button { margin-right: 10px; }
.page a { cursor: pointer; margin: 0 10px; }
</style>关键行
headers里"X-Client-Info"、"X-Host":这两个请求头就是说的"跨域加的两个头",服务端靠它们判断你是否有资格访问。k: Math.random():查询字符串变了,完整地址就变了,浏览器不会走缓存,每次真正打到服务器。res.data.data:第一层data是 axios 的响应体,第二层data才是接口业务数据(films、total)。Math.ceil(total / pageSize):总页数要向上取整,41 条、每页 2 条应得 21 页。this.pageNum = 1:切换"正在热映/即将上映"时必须重置页码,否则会出现"第 21 页的即将上映"这种空数据。
易错点
- 请求头属性名写成
X-Client-Info: ...会报语法错误,必须加引号。 - 请求头的值如果直接写对象,最终会变成字符串
"[object Object]",服务端校验不过;要么用单引号把 JSON 字符串包起来,要么JSON.stringify(...)。 - 别把
type当成"页数"传,type只有 1 和 2 两个值。 @click="getData"和@click="changeType(1)"不一样:前者会把事件对象当第一个参数传进去,所以调用带参方法要显式写括号。- 如果打开控制台看到
CORS报错,先检查上面两个请求头有没有带上、值是不是字符串。
代码示例 2:axios 封装 + 接口统一管理
js
// src/request/maizuo.js —— 对 axios 实例进行封装
import axios from "axios";
const maizuo = axios.create({
baseURL: "https://m.maizuo.com", // 域名只写一次
timeout: 10000, // 超过 10 秒认为超时
headers: {
"X-Client-Info": '{"a":"3000","ch":"1002","v":"5.0.4","e":"..."}',
"X-Host": "mall.film-ticket.film.list"
}
});
// 响应拦截器:把响应体直接返回,后面就不用每次写 res.data
maizuo.interceptors.response.use(res => res.data);
export default maizuo;js
// src/api/maizuo.js —— 把该服务下的所有接口封装成函数
import maizuo from "../request/maizuo";
export const getFilmList = (pageNum, pageSize, type) => {
return maizuo.get("/gateway", {
params: {
cityId: 310100,
pageNum,
pageSize,
type,
k: Math.random()
}
});
};js
// src/App.vue 中的调用部分(其余模板同上)
import { getFilmList } from "./api/maizuo";
export default {
methods: {
async getData() {
this.isLoading = true;
// 拦截器已返回 res.data,所以这里拿到的是响应体
const res = await getFilmList(this.pageNum, this.pageSize, this.type);
const { films, total } = res.data;
this.filmList = films;
this.totalPage = Math.ceil(total / this.pageSize);
this.isLoading = false;
}
}
};js
// src/main.js 中注册 Loading 全局组件(建议的写法)
import Vue from "vue";
import App from "./App.vue";
import Loading from "./components/Loading.vue";
Vue.config.productionTip = false;
Vue.component("Loading", Loading); // 注册为全局组件,任何组件都能直接用
new Vue({
render: h => h(App)
}).$mount("#app");关键行
axios.create():创建一个新实例,不会影响axios本身,其他地方该怎么用还怎么用。baseURL:把https://m.maizuo.com抽出来,接口里只写路径/gateway。interceptors.response.use(res => res.data):统一剥一层壳,业务代码里少写一个.data。Vue.component("Loading", Loading):跨组件复用的东西才值得注册全局组件。
易错点
- 拦截器剥了一层壳之后,业务里就不能再多写一个
.data,否则报Cannot destructure property 'films' of undefined。 - 封装后
headers是写在实例配置里的,业务方法里不要再重复写一遍。 - 组件里
import顺序建议:内置模块 → 第三方模块(axios)→ 自定义模块 → 样式。 - 改了
vue.config.js(比如devServer: { open: true, port: 80 })必须重启,不重启不生效。
代码示例 3:路由配置(对应 15-1 需求 / 15-2 组件)
js
// src/router/index.js
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../pages/Home.vue";
import NewsList from "../pages/NewsList.vue";
import GoodsList from "../pages/GoodsList.vue";
import My from "../pages/My.vue";
import NotFound from "../pages/NotFound.vue";
Vue.use(VueRouter);
const routes = [
{ path: "/", component: Home }, // 首页
{ path: "/home", redirect: "/" }, // /home 重定向到首页
{ path: "/newsList", component: NewsList },// 新闻列表页
{ path: "/news", component: NewsList }, // 新闻列表页(另一个地址)
{ path: "/goodsList", component: GoodsList },// 商品列表页
{ path: "/my", component: My }, // 我的个人中心
{ path: "*", component: NotFound } // 404,必须放最后
];
const router = new VueRouter({
routes,
mode: "history"
});
export default router;vue
<!-- src/App.vue -->
<template>
<div id="app">
<h3>这是 App 根组件</h3>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App"
};
</script>js
// src/main.js
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App)
}).$mount("#app");关键行
Vue.use(VueRouter):必须先注册插件,否则new VueRouter()报错。{ path: "/home", redirect: "/" }:重定向,访问/home会直接跳回首页。{ path: "*", component: NotFound }:通配路由,必须写在数组最后,否则会吞掉后面所有路由。new Vue({ router }):把 router 挂到根实例上,组件里才能用<router-view>和$route。
易错点
path: "*"放前面会导致所有正常路由都进 404 页面。mode: "history"刷新子路由会 404(需要服务端配合),本地调试可先用默认的 hash 模式。- 忘记在
App.vue里写<router-view>,路由切换时页面不会变化。
常见错误
- 现象:卖座接口控制台报 CORS,页面无数据;原因:未按接口要求携带自定义请求头(如
X-Client-Info、X-Requested-With等);修法:对照 Network 的 Request Headers 补齐,特殊字符用引号包裹,对象值用JSON.stringify。 - 现象:请求头值显示为
[object Object];原因:headers中直接写对象;修法:先JSON.stringify再赋值,或写成合法字符串形式。 - 现象:点击“即将上映”仍请求“正在热映”,或参数变成事件对象;原因:
@click="getData"写成方法引用,默认传入event;修法:写成@click="getData(2)"、@click="getData(1)",需要事件时显式传$event。 - 现象:切换类型后页码不归 1,或请求发两次、进入重复请求;原因:
watch同时监听类型和页码,回调里又改页码并调用请求;修法:切换类型时先重置page=1,再只发一次请求;类型变化与页码变化分开处理。 - 现象:图片不显示或列表字段为空;原因:响应体字段名写错(如
poster、name、filmId)或图片未限制尺寸;修法:对照响应体逐项确认字段,:src绑定正确,设置高度/宽度并处理空值。 - 现象:loading 一直转或分页越界;原因:请求成功/失败未统一复位 loading,页码未做边界限制;修法:在
finally复位 loading;page<1置 1,page>totalPage置totalPage,按钮按边界禁用或隐藏。 - 现象:封装 axios 后仍写
res.data.data取不到数据;原因:响应拦截器已返回response.data,业务层又多取一层;修法:统一响应结构,拦截器返回什么,业务层就按什么取,避免重复解包。
练习
- 完成卖座“正在热映/即将上映”切换:请求头补齐、类型参数正确、列表能渲染电影图片和名称。
- 增加全局
Loading组件,请求开始显示、成功/失败都关闭,并验证异常场景下不会一直转圈。 - 完成分页:显示当前页/总页数,实现上一页、下一页,处理第一页和最后一页边界;切换类型时页码重置为 1 且只请求一次。
- 将卖座请求拆成
request实例与api/maizuo.js两层,APP中只调用 API 方法,响应拦截器统一处理返回数据。 - 按路由需求配置
/、/home重定向、/newsList、/news、/goodsList、/my和 404 页面,并创建对应组件;确认 404 兜底规则放在最后。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 调用数据 | 24:52 |
| 2 | 封装1 | 17:00 |
| 3 | 完成渲染 | 12:57 |
| 4 | 完成渲染2 | 13:33 |
| 5 | 完成卖座网分页 | 17:10 |
| 6 | 封装axios | 14:17 |
小结
- 调接口先看 Network,把请求方式、地址、参数、请求头四项对齐,再写代码。
- 跨域报错不要猜,按服务端要求补请求头;请求头值要注意字符串化和特殊字符。
- 页面切换优先用状态驱动视图,数据放
data,模板用v-for/v-if渲染。 - 分页核心是
page、pageSize、totalPage;切类型重置页码,避免watch重复触发。 - 封装分两层:
request统一baseURL、timeout、headers、拦截器;api层只暴露业务方法。 - loading、错误提示、分页边界要统一收口,成功和失败都要复位状态。
- 路由需求先建页面组件再配规则,404 兜底规则放最后;调试时先排除前端请求四要素问题。