前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版

Vue 项目开发中的十个痛点与解法(十四)

首页2018-08-28 17:10:30Front-End
Vue工程化性能优化

学 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 页面,可以看到定时器依然在执行。这是非常消耗性能的。

fe
  • 一、列表进入详情页传参
  • 二、请求服务器接口跨域
  • 三、axios 的封装和 API 接口的统一管理
  • 四、UI 库的按需加载
  • 五、定时器问题
    • 方案一,把定时器存到 data 上
    • 方案二,用 $once 监听销毁钩子
  • 六、rem 文件的导入问题
  • 七、打包后生成很大的 .map 文件
  • 八、fastClick 的 300ms 延迟
  • 九、路由懒加载
  • 十、开启 gzip 压缩
  • 总结
  • 参考

← vue过渡与动画(十五)Vue 实例属性与实例方法完全指南(十三) →