Skip to content

卖座网数据调用与猫眼网数据调用(跨域)(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 request

API 模块:

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

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码示例 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,业务层又多取一层;修法:统一响应结构,拦截器返回什么,业务层就按什么取,避免重复解包。

练习 ​

  1. 完成卖座“正在热映/即将上映”切换:请求头补齐、类型参数正确、列表能渲染电影图片和名称。
  2. 增加全局 Loading 组件,请求开始显示、成功/失败都关闭,并验证异常场景下不会一直转圈。
  3. 完成分页:显示当前页/总页数,实现上一页、下一页,处理第一页和最后一页边界;切换类型时页码重置为 1 且只请求一次。
  4. 将卖座请求拆成 request 实例与 api/maizuo.js 两层,APP 中只调用 API 方法,响应拦截器统一处理返回数据。
  5. 按路由需求配置 /、/home 重定向、/newsList、/news、/goodsList、/my 和 404 页面,并创建对应组件;确认 404 兜底规则放在最后。

原视频对照 ​

#视频时长
1调用数据24:52
2封装117:00
3完成渲染12:57
4完成渲染213:33
5完成卖座网分页17:10
6封装axios14:17

小结 ​

  • 调接口先看 Network,把请求方式、地址、参数、请求头四项对齐,再写代码。
  • 跨域报错不要猜,按服务端要求补请求头;请求头值要注意字符串化和特殊字符。
  • 页面切换优先用状态驱动视图,数据放 data,模板用 v-for/v-if 渲染。
  • 分页核心是 page、pageSize、totalPage;切类型重置页码,避免 watch 重复触发。
  • 封装分两层:request 统一 baseURL、timeout、headers、拦截器;api 层只暴露业务方法。
  • loading、错误提示、分页边界要统一收口,成功和失败都要复位状态。
  • 路由需求先建页面组件再配规则,404 兜底规则放最后;调试时先排除前端请求四要素问题。