学 Vue 的语法是一回事,把一个项目从零跑到上线是另一回事。前者看文档就够了,后者遇到的全是文档不写的东西:本地请求接口被浏览器拦了、切了路由定时器还在跑、打包出来一个 6MB 的产物里有一半是 sourcemap、移动端点按钮要等三百毫秒才有反应。这些问题单拎出来都不难,但你不知道它存在的时候能耗掉一整天。这篇把我在 Vue 2 项目里踩得最密集的十个点整理出来,每个都写清楚成因、解法和现在的做法,方便对着排查。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- 列表进详情传参,
query和params到底该选哪个 - 本地开发跨域,
proxyTable是怎么绕过同源策略的 axios封装和接口统一管理该封装到什么程度- UI 库按需加载的原理,
babel-plugin-import干了什么 - 定时器不清理为什么会拖垮页面,两种清理写法的取舍
- rem 适配脚本的原理,以及现在更省事的方案
- 生产环境为什么要关掉 sourcemap,关了之后线上报错怎么查
- 移动端 300ms 点击延迟的来历,以及它现在还在不在
- 路由懒加载的写法演进,从
require回调到动态import - gzip 为什么必须前后端一起配才生效
# 一、列表进入详情页传参
例如商品列表页面前往商品详情页面,需要传一个商品 id:
<router-link :to="{path: 'detail', query: {id: 1}}">前往detail页面</router-link>
详情页的路径为 http://localhost:8080/#/detail?id=1,可以看到传了一个参数 id=1,并且就算刷新页面 id 也还会存在,通过 this.$route.query.id 就能拿到。
Vue 的传参方式有两种,query 和 params 加动态路由。query 通过 path 切换路由,params 通过 name 切换路由:
<!-- query 通过 path 切换路由 -->
<router-link :to="{path: 'Detail', query: { id: 1 }}">前往Detail页面</router-link>
<!-- params 通过 name 切换路由 -->
<router-link :to="{name: 'Detail', params: { id: 1 }}">前往Detail页面</router-link>
接收也是两套 API:
// query 通过 this.$route.query 接收
created () {
const id = this.$route.query.id;
}
// params 通过 this.$route.params 接收
created () {
const id = this.$route.params.id;
}
URL 上的表现差别很直观,query 是 /detail?id=1&user=123,params 加动态路由是 /detail/123。
用 params 有个前提,一定要在路由中定义参数,跳转时必须带上,否则就是空白页面:
{
path: '/detail/:id',
name: 'Detail',
component: Detail
}
注意,params 传参时如果没有在路由中定义参数,也是可以传过去的,同时也能接收到,但是一旦刷新页面这个参数就不存在了。对于需要依赖参数进行某些操作的行为,这是行不通的:
<!-- 路由中只定义了 id 参数,token 没有定义 -->
<router-link :to="{name: 'Detail', params: { id: 1, token: '123456' }}">前往Detail页面</router-link>
created () {
// 两个都能拿到
// 但页面刷新后 id 依然可以获取,token 就不存在了
const id = this.$route.params.id;
const token = this.$route.params.token;
}
原因说穿了很简单:没写进 URL 的参数只活在内存里,刷新等于整个应用重新加载,内存里的东西全清空,只有地址栏上的字符能活下来。
所以这一条的结论是尽量使用 query 来传参。它刷新不丢、可复制、可分享,出了问题让同事把链接发过来就能直接复现。params 留给「这个值本身就是资源标识」的场景,比如商品 id 拼成 RESTful 风格的路径。路由本身的完整用法我在 Vue 路由 vue-router 那篇里系统梳理过,这里只讲落到项目里的取舍。
# 二、请求服务器接口跨域
本地开发项目请求服务器接口的时候,因为浏览器的同源策略,会遇到跨域问题。
vue-cli 初始化的项目在配置文件里提供了 proxyTable 来解决本地开发的跨域。在 config/index.js 里找到 proxyTable 选项,做如下配置:
proxyTable: {
// 用 '/api' 开头,代理所有请求到目标服务器
'/api': {
target: 'http://jsonplaceholder.typicode.com', // 接口域名
changeOrigin: true, // 是否改写请求头里的 Host
pathRewrite: {
'^/api': '' // 转发时把前缀去掉
}
}
}
这样请求 /api/posts/1 会被转发到 http://jsonplaceholder.typicode.com/posts/1,本地环境就能正常调后台接口了。
它为什么能绕过跨域?关键在于同源策略是浏览器的限制,服务器之间互相请求不受这条约束。加了代理之后,浏览器发出的请求打的是 localhost:8080/api/xxx,和页面同源,浏览器不拦;webpack-dev-server 收到之后在 Node 这一层转发给真实后端,再把响应原样带回来。整条链路里浏览器从头到尾以为自己只跟本地服务器打过交道。
三个配置项各管一件事:target 是转发目标;changeOrigin: true 会把转发请求头里的 Host 改成目标域名,很多后端服务或者 Nginx 会按 Host 做虚拟主机路由,不改就匹配不上;pathRewrite 负责把用来识别的前缀去掉,因为真实接口路径上并没有 /api 这一段。
这里有个坑要注意,代理只在开发服务器上生效。打包出来的静态文件跑在 Nginx 上,proxyTable 这些配置压根不在产物里。上线的跨域要么后端加 CORS 响应头,要么 Nginx 配一段反向代理,两条路都得提前和后端约定好,不能等到发布当天才发现。
Vue CLI 3 之后配置文件换成了 vue.config.js,字段名从 proxyTable 改成了 devServer.proxy,Vite 里则是 server.proxy,三者的选项结构基本一致,迁移的时候照着改字段名就行。
# 三、axios 的封装和 API 接口的统一管理
axios 的封装主要是为了做请求拦截和响应拦截。
请求拦截里能干的事情有:统一带上 userToken、设置 post 请求头、用 qs 对 post 提交数据做序列化、给需要 loading 的请求打开遮罩。响应拦截里则可以根据状态码做统一的错误处理,比如 401 跳登录、500 弹提示、业务码非 0 统一 toast。
不封装会怎样?每个接口调用点都得自己写一遍 token 注入和错误处理,一个中型项目几百个接口,改一次登录态失效的处理逻辑要改几百处。这不是能不能维护的问题,是一定会漏改的问题。
接口的统一管理是做项目时必须的流程。把所有请求方法按模块收进 api 目录,业务组件里只 import 方法名,不出现任何 URL 字符串。这样接口路径变更时不必再回到业务代码里去改,改一处即可。顺带还有两个好处:接口有没有被用到,编辑器直接能查引用;接口的入参出参可以在这一层做类型标注,用 TypeScript 的话收益更明显。
封装到什么程度合适?我的经验是别过度。把「所有请求都要做的事」放进拦截器,把「URL 和参数拼装」放进 api 层,剩下的留在业务里。见过封装到最后连 loading、缓存、重试、并发控制全塞进一个 request.js 的,改一行牵动整站,比不封装还难维护。这块的具体写法在 Vue 中使用 axios 那篇里有展开。
# 四、UI 库的按需加载
这里以 vant 的按需加载为例,演示 Vue 里 UI 库怎样按需加载。
先装依赖:
npm i vant -S
npm i babel-plugin-import -D
在 .babelrc 文件中添加插件配置:
{
"plugins": [
["import",
{
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}
]
]
}
原文这段配置开头多出了一个 libraryDirectory {,是复制时串行了,直接粘进项目 Babel 会报解析错误,这里去掉了。
然后在 main.js 里按需引入需要的组件:
// 按需引入 vant 组件
import {
DatetimePicker,
Button,
List
} from 'vant';
注册:
// 使用 vant 组件
Vue.use(DatetimePicker)
.use(Button)
.use(List);
最后在页面中使用:
<van-button type="primary">按钮</van-button>
babel-plugin-import 到底做了什么?它在编译阶段把上面那句解构导入改写成了逐个的路径导入,同时补上对应的样式文件导入。也就是说源码里写的一句 import { Button } from 'vant',编译后会变成从 vant/es/button 导入组件、从 vant/es/button/style 导入样式。
为什么要这么绕一圈?因为直接从包入口解构导入,打包工具拿到的是整个入口模块,虽然现代打包器的 tree-shaking 能砍掉没用到的 JS,但样式文件是靠 import 副作用引入的,摇不掉。结果就是 JS 只留了一个按钮,CSS 却把整个组件库的样式全打进去了。
libraryDirectory 填 es 而不是 lib,是因为 es 目录是 ESM 格式的产物,能配合 tree-shaking;lib 是 CommonJS,静态分析做不了。style: true 引入的是未编译的样式源文件(vant 是 less),需要项目里配好对应的预处理器;填 "css" 则引入编译好的 css,省去 loader 配置。
现在的组件库大多已经内置了按需加载方案,Element Plus、Vant 4 都推荐用 unplugin-vue-components 这类插件,连 import 语句都不用写,模板里直接用标签,插件扫描后自动补导入。手动配 babel-plugin-import 这一套主要是老项目还在用。
# 五、定时器问题
在 a 页面写一个定时器,让它每秒打印一个 1,然后跳转到 b 页面,可以看到定时器依然在执行。这是非常消耗性能的。