Vue.js项目总结
0.Vue项目的一些经验总结
做了很多个Vue技术栈项目了,总结一些经常会用到的技术点,方便以后快速回顾,也当作是一种输出方式了。
talk is cheap, show me the code!
1.在Vue中给axios添加请求拦截器
import axios from 'axios'
// 创建一个名字叫request的axios实例,和axios本身的功能一样
// 推荐使用axios.create方式创建实例,因为如果一个系统里面有多个api地址的时候可以给不同的实例设置不同地址
// 如果直接使用axios.default.baseurl的方式就只能设置一个api地址,很局限
const request = axios.create({
baseURL: 'http://api-toutiao-web.itheima.net'
})
// 请求拦截器
request.interceptors.request.use(
// 任何所有请求会经过这里
// config 是当前请求相关的配置信息对象
// config 是可以修改的
function (config) {
const userToken = JSON.parse(window.localStorage.getItem('user_token'))
if (userToken) {
// 用户已经登录了,在headers请求头带上token
config.headers.Authorization = `Bearer ${userToken.token}`
}
// 然后我们就可以在允许请求出去之前定制统一业务功能处理
// 例如:统一的设置 token
// 当这里 return config 之后请求在会真正的发出去
return config
},
// 请求失败,会经过这里
function (error) {
return Promise.reject(error)
}
)
export default request
2.在Vue中给router添加导航守卫
vue-router的导航守卫也叫路由拦截器
router.beforeEach((to, from, next) => {
// 在路由拦截器中判断是否有token,如果有token就next放行,否则next重定向到登录页面
const userToken = window.localStorage.getItem('user_token')
if (!userToken && to.path !== '/login') {
return next('/login')
}
next()
})
3.在Vue中配置Filters过滤器
filters: {
// 文章审核状态过滤器
auditStatus: value => {
switch (value) {
case 0:
return '草稿'
case 1:
return '待审核'
case 2:
return '审核通过'
case 3:
return '审核失败'
case 4:
return '已删除'
default:
return '未知'
}
}
},
在模版中使用拦截器,| 符号前面的参数是给拦截器使用的,后面的auditStatus
是拦截器。
<el-table-column
align="center"
width="120px"
label="状态">
<template slot-scope="scope">
<el-tag type="success">{{ scope.row.status | auditStatus}}</el-tag>
</template>
</el-table-column>
4.element-ui中的image组件
图片容器,在保留原生img的特性下,支持懒加载,自定义占位内容、加载失败、大图预览等。
代码胜千言。
<el-image
style="width: 150px; height: 100px"
:src="scope.row.cover.images[0]"
lazy
:preview-src-list="scope.row.cover.images"
fit="cover"
>
<!-- 占位内容 -->
<div slot="placeholder" class="image-slot">
加载中<span class="dot">...</span>
</div>
<!-- 加载失败内容 -->
<div slot="error" class="image-slot">
<img
class="article-error-cover"
src="./error.3f7b1f94.gif"
alt="error"
/>
</div>
</el-image>
5.通过数组的方式处理文章状态
比较low的方式
v-if、过滤器写switch...case
逼格高的方式
<el-table-column align="center" width="120px" label="状态">
<template slot-scope="scope">
<el-tag :type="articleStatusList[scope.row.status].type">{{
articleStatusList[scope.row.status].name}}
</el-tag>
</template>
</el-table-column>
data() {
return {
articleStatusList: [
// 文章状态列表
{ status: 0, name: '草稿', type: 'info' },
{ status: 1, name: '待审核', type: 'primary' },
{ status: 2, name: '审核通过', type: 'success' },
{ status: 3, name: '审核失败', type: 'warning' },
{ status: 4, name: '已删除', type: 'danger' }
]
}
}
这样就可以通过不同的status状态来切换对应的文本以及el-tag
标签的type类型。
6.element-ui中的分页组件
引入分页组件el-pagination
<el-pagination
class="article-pagination"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage4"
:page-sizes="[100, 200, 300, 400]"
:page-size="100"
layout="total, sizes, prev, pager, next, jumper"
:total="400"
>
</el-pagination>
参数介绍:
属性
background: 设置背景颜色
current-page: 设置当前页码
page-sizes: 设置每页显示多少条记录数,接收一个数组
page-size: 设置当前每页显示多少条记录数,一般为page-sizes属性的第一项
layout: 分页组件显示的内容
total: 总共有多少条记录数
方法
@size-change:每页分页数量变化时触发的事件函数,
@current-page:当前页变化时触发的事件函数
看下图:
然后自己根据业务进行灵活使用。
7.element-ui中的日期选择器
自定义日期格式
<el-form-item label="日期">
<el-date-picker
v-model="form.dateList"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
>
</el-date-picker>
</el-form-item>
v-model双向绑定的数据是一个数组,数组下标0的数据是开始日期,数组下标1的数据是结束日期
默认返回的时间格式是一个Date实例,通常转换成 2021-1-22 这种格式数据比较好,那么如何转换呢?可以给组件传入value-format='yyyy-MM-dd'
属性实现格式化。
8.element-ui中的loading效果
Element 提供了两种调用 Loading 的方法:指令和服务。对于自定义指令v-loading
,只需要绑定Boolean
即可。默认状况下,Loading 遮罩会插入到绑定元素的子节点,通过添加body
修饰符,可以使遮罩插入至 DOM 中的 body 上。
解决请求没回来之前,用户界面可交互问题;防止请求过慢,用户在分页按钮上面狂点。
请求期间,最好禁用会发送请求的按钮。
发请求的时候将isLoading
变量改为true,请求成功或失败后改为false即可。
9.Vue中router的几种传参方式
10.element-ui中的表单验证
11.element-ui中的图片上传
12.补充对象解构的骚操作
默认获取的数据
解构后获取的数据
13.Vue中富文本编辑器使用
常见的富文本编辑器有:
-
百度富文本编辑器(UEditor)
-
CKEditor
-
Vue-quill-Editor
-
tiptap(需要深度定制的话就用它)
-
element-tiptap(基于tiptap封装的富文本编辑器,与element-ui样式类似,适合不需要深度定制,开箱即用的情况)
Vue官方推荐周边插件资源:https://github.com/vuejs/awesome-vue,可以在这里面找到业务需要的开源插件,例如日历、轮播图、富文本编辑器等。
下面讲解element-tiptap在vue中的使用
1.安装element-tiptap
官方仓库地址:https://github.com/Leecason/element-tiptap/blob/master/README_ZH.md
npm install element-tiptap
2.初始配置
这里使用局部引入方式
import {
ElementTiptap,
Doc,
Text,
Paragraph,
Heading,
Bold,
Underline,
Italic,
Image,
Strike,
ListItem,
BulletList,
OrderedList,
TodoItem,
TodoList,
HorizontalRule,
Fullscreen,
Preview,
CodeBlock
} from 'element-tiptap'
import 'element-tiptap/lib/index.css'
export default {
name: 'PublishIndex',
components: {
'el-tiptap': ElementTiptap
},
props: {},
data () {
return {
extensions: [
new Doc(),
new Text(),
new Paragraph(),
new Heading({ level: 3 }),
new Bold({ bubble: true }), // 在气泡菜单中渲染菜单按钮
new Image(),
new Underline(), // 下划线
new Italic(), // 斜体
new Strike(), // 删除线
new HorizontalRule(), // 华丽的分割线
new ListItem(),
new BulletList(), // 无序列表
new OrderedList(), // 有序列表
new TodoItem(),
new TodoList(),
new Fullscreen(),
new Preview(),
new CodeBlock()
]
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<el-tiptap v-model="form.content" lang="zh" :extensions="extensions"></el-tiptap>
页面效果:
3.定制图片上传
由于element-tiptap默认本地上传图片使用的是base64,因此需要进行定制化,前端上传图片给服务器,服务器返回一个外链的url,将url返回给element-tiptap渲染。
base64补充:它仅适用于非常小的图像。 Base64编码文件比原始文件大。其优势在于不必打开另一个连接并向服务器发送图像的HTTP请求。这种好处很快就会消失,所以只有大量非常小的个人图像才有优势。
富文本中的图片base64和外链方式对比。
base64的图片:
如果图片较大且很多,虽然可以减少http请求,但是也会造成页面很卡,因为要等到全部的图片全部都加载完了之后才开始渲染。
外链的图片:
可以在页面渲染完成之后再进行加载,不会阻塞页面的渲染,体验好,但是会发送http请求。
根据自己的业务需求选择吧。
注意:一般文件上传的接口都要求把请求头中的 Content-Type 设置为 multipart/form-data,但是我们使用 axios 上传文件的话就不要需要手动设置了,你只需要给 data 一个 FormData 对象即可,axios 内部做了对应的封装。
extensions: [
new Doc(),
new Text(),
new Paragraph(),
new Heading({ level: 3 }),
new Bold({ bubble: true }), // 在气泡菜单中渲染菜单按钮
new Image({
// 自定义图片上传函数 返回Promise
uploadRequest (file) {
const fd = new FormData()
fd.append('image', file)
// 这里 return 是返回 Promise 对象
return uploadImage(fd).then(({ data: res }) => {
// 这个 return 是返回最后的结果
return res.data.url
})
}
}),
new Underline(), // 下划线
new Italic(), // 斜体
new Strike(), // 删除线
new HorizontalRule(), // 华丽的分割线
new ListItem(),
new BulletList(), // 无序列表
new OrderedList(), // 有序列表
new TodoItem(),
new TodoList(),
new Fullscreen(),
new Preview(),
new CodeBlock()
],
14.element-ui中的响应式布局
参考官方文档。
15.Vue中compute计算属性应用
计算属性会观测内部依赖数据的变化而重新求值。
<div class="all-channel-wrap">
<van-grid :gutter="10">
<van-grid-item
v-for="(item, index) in recommendChannels"
:key="index"
:text="item.name"
@click="handleRecommendChannelClick(item, index)"
/>
</van-grid>
</div>
computed: {
...mapState(['user']),
recommendChannels () {
/*
使用所有频道减去用户频道就能算出推荐频道的数据;
思路:从所有频道数据中筛选出不为用户频道的数据;
只要this.userChannels值或者this.allChannels值改变就会触发计算属性执行
*/
return this.allChannels.filter(item => {
return !this.userChannels.find(item2 => {
return item2.name === item.name
})
})
}
},
16.element-ui中的upload组件
<el-upload
class="upload-demo"
drag
action="http://123456abc.xyz/user/images"
:on-success="handleUploadSuccess"
name="image"
:headers="{Authorization: `Bearer ${userToken.token}`}"
multiple>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</el-upload>
// 文件上传成功
handleUploadSuccess (res) {
this.$message.success('上传成功!')
this.uploadDialogVisible = false // 关闭dialog对话框
this.loadUserImages() // 加载页面数据
},
使用el-upload的时候需要注意headers的配置,使用action的方式上传图片默认不会走你自己封装的axios请求拦截器,因此这里需要手动配置headers加上token。
更多的配置参考element文档:https://element.eleme.cn/#/zh-CN/component/upload#methods
17.Vue中监听路由变化
有一个小需求,我们的后台管理项目的左侧菜单栏要支持下面3个功能:
- 用户点击菜单,切换激活效果(element已经支持)
- 用户刷新页面的时候要保留激活效果
- 用户手动切换url时激活效果要同步
实现代码:
<el-menu
:default-active="defaultSelectPath"
class="el-menu-vertical-demo"
background-color="#002033"
text-color="#fff"
active-text-color="#ffd04b"
:collapse-transition="false"
router
>
<el-menu-item index="/">
<i class="el-icon-s-home"></i>
<span>首页</span>
</el-menu-item>
<el-menu-item index="article">
<i class="el-icon-document"></i>
<span slot="title">内容管理</span>
</el-menu-item>
</el-menu>
<script>
export default {
name: 'AppAside',
components: {},
props: {},
data () {
return {
defaultSelectPath: '/'
}
},
computed: {},
watch: {
$route (to, from) {
if (to.path !== '/') {
this.defaultSelectPath = this.$route.path.substring(1)
} else {
this.defaultSelectPath = this.$route.path
}
}
},
created () {
},
mounted () {
if (this.$route.path !== '/') {
this.defaultSelectPath = this.$route.path.substring(1)
} else {
this.defaultSelectPath = this.$route.path
}
},
methods: {
}
}
</script>
18.Vue中图片裁剪插件
cropper.js
19.Vue中的数据可视化
20.如何寻找一手资料
1.直接去github搜索,用英文搜,例如 搜索 vue crop.
2.使用vue-awesome资源列表搜索.
21.html label标签的应用场景
22.window.URL.createObjectURL API的应用
思考下面一个场景:
用户点击自己的头像,选择新的头像图片进行修改,修改完成之后需要弹出一个dialog对话框,在对话框中显示用户的头像图片,此时头像还没有上传到服务器哦,这个时候怎么办呢?可以使用window.URL.createObjectURL
API来实现。
API返回的是一个blob地址,这个API是HTML5的新特性。
代码如下:
23.Vue中修改配置文件不生效的解决方法
直接删除node_modules/.cache
文件夹就可以了。
24.Vue中的非父子组件通信
新建一个名字叫 global-bus.js
的文件。
假设 a 要给 b 发送数据,那么 b 就需要注册一下事件,a 来发布。
反之,b 给 a 发送数据,那么就是 a 要注册事件,b来发布。
注册事件使用 $on API,发布事件使用 $emit API。
注册事件名和发布事件名必须要一致!
总结:在Vue中非父子组件通信中,接收数据的组件要使用 $on 来注册事件,发送数据的组件要使用$emit 来发布。
举个例子:
25.在Vue中给axios添加响应拦截器
对请求错误的响应码进行统一处理,比如用户伪造token登录到后台管理系统,但是发送请求报错,此时应该让用户跳转到登录页面。
26.Vue中Echarts结合百度地图完成复杂图表功能
27.Vue中在组件上使用v-model
当你给子组件提供的数据既要使用还要修改,这个时候我们可以使用 v-model
简化数据绑定。
繁琐的写法:
简化写法:
28.Vue组件功能定制化
子组件中的props推荐使用对象的方式定义接收的数据,这样可以配置接收数据的类型、默认值、是否必传等,而使用数组的方式定义prop则不行,数组的方式不够严谨,功能不强大,建议以后封装组件,定义props的时候全部使用对象的方式定义。
29.Vue项目打包上线部署
打包项目命令:npm run build
,会在Vue项目根目录生成dist目录,该目录下面的html、css、JavaScript代码已经通过webpack进行压缩。
可以使用serve运行打包的项目。
安装:npm install -g serve
使用方法:
cd 到dist文件夹,运行serve即可,或者在项目根目录运行:serve -s dist\
serve会开启一个默认端口号为5000的node.js Web服务器。
免费部署平台:
1、GithubPages
2、21云盒子
30.Vue引入Vant组件库
常见的Vue移动端组件库:https://github.com/vuejs/awesome-vue#mobile
Vant:https://github.com/youzan/vant
1.安装
npm i vant
2.引入组件
一共有4种导入方式,这里使用导入所有组件的方式,其他方式看文档。
导入所有组件(用得比较多,建议全部导入比较简单)
Vant 支持一次性导入所有组件。
import Vue from 'vue';
import Vant from 'vant';
import 'vant/lib/index.css';
Vue.use(Vant);
31.Vue移动端rem适配
物理像素和逻辑像素
在Vant中进行rem适配
Vant 中的样式默认使用 px
作为单位,如果需要使用 rem
单位,推荐使用以下两个工具:
- postcss-pxtorem 是一款 postcss 插件,用于将 px 单位转化为 rem
- lib-flexible 用于设置 rem 基准值
下面我们分别将这两个工具配置到项目中完成 REM 适配。
(1)使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)
安装依赖:
# yarn add amfe-flexible
npm i amfe-flexible
然后在 main.js
中加载执行该模块:
import 'amfe-flexible'
最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签 font-size
的变化。
例如在 iPhone 6/7/8 设备下,html 标签字体大小为 37.5 px
例如在 iPhone 6/7/8 Plus 设备下,html 标签字体大小为 41.4 px
(2)使用 postcss-pxtorem 将 px 转为 rem
安装依赖:
# yarn add -D postcss-pxtorem
# -D 是 --save-dev 的简写, 开发环境下的依赖
npm install postcss-pxtorem -D
然后在项目根目录中创建 postcss.config.js
文件:
module.exports = {
plugins: {
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 8']
},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
配置完毕,重新启动服务。
最后测试:刷新页面,审查元素样式查看是否已将 px
转换为 rem
。
这是没有配置转换之前的。
这是转换之后的,可以看到 px 都被转换为了 rem
需要注意的是:
- 该插件不能转换行内样式中的 px,例如
<div style="width: 200px;"></div>
关于 PostCSS 配置文件
module.exports = {
plugins: {
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 8']
},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
postcss.config.js
是 PostCSS 的配置文件。
PostCSS 介绍
PostCSS 是一个允许使用 JS 插件转换样式的工具。 这些插件可以检查(lint)你的 CSS,支持 CSS Variables 和 Mixins, 编译尚未被浏览器广泛支持的先进的 CSS 语法,内联图片,以及其它很多优秀的功能。
PostCSS 被广泛地应用,其中不乏很多有名的行业领导者,如:维基百科,Twitter,阿里巴巴, JetBrains。PostCSS 的 Autoprefixer 插件是最流行的 CSS 处理工具之一。
PostCSS 接收一个 CSS 文件并提供了一个 API 来分析、修改它的规则(通过把 CSS 规则转换成一个抽象语法树的方式)。在这之后,这个 API 便可被许多插件利用来做有用的事情,比如寻错或自动添加 CSS vendor 前缀。
截止到目前,PostCSS 有 200 多个功能各异的插件。你可以在 插件列表 或 搜索目录 找到它们。
PostCSS 是一个处理 CSS 的处理工具,本身功能比较单一,它主要负责解析 CSS 代码,再交由插件来进行处理,它的插件体系非常强大,所能进行的操作是多种多样的,例如:
-
Autoprefixer 插件可以实现自动添加浏览器相关的声明前缀
-
PostCSS Preset Env 插件可以让你使用更新的 CSS 语法特性并实现向下兼容
-
postcss-pxtorem 可以实现将 px 转换为 rem
-
...
PostCSS 一般不单独使用,而是与已有的构建工具进行集成。
Vue CLI 默认集成了 PostCSS,并且默认开启了 autoprefixer 插件。
Vue CLI 内部使用了 PostCSS。
你可以通过
.postcssrc
或任何 postcss-load-config 支持的配置源来配置 PostCSS。也可以通过vue.config.js
中的css.loaderOptions.postcss
配置 postcss-loader。我们默认开启了 autoprefixer。如果要配置目标浏览器,可使用
package.json
的 browserslist 字段。
Autoprefixer 插件的配置
autoprefixer 是一个自动添加浏览器前缀的 PostCss 插件,browsers
用来配置兼容的浏览器版本信息,但是写在这里的话会引起编译器警告。
警告意思就是说你应该将 browsers
选项写到 package.json
或 .browserlistrc
文件中。
browserslist:
你会发现有
package.json
文件里的browserslist
字段 (或一个单独的.browserslistrc
文件),指定了项目的目标浏览器的范围。这个值会被 @babel/preset-env 和 Autoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。
参考官方文档中的语法,我们将 .browserslistrc
修改如下:
Android >= 4.0
iOS >= 8
兼容Android版本大于等于4.0,IOS版本大于等于8的。
postcss-pxtorem 插件的配置
-
rootValue
:表示根元素字体大小,它会根据根元素大小进行单位转换 -
propList
用来设定可以从 px 转为 rem 的属性 -
- 例如
*
就是所有属性都要转换,width
就是仅转换width
属性
- 例如
rootValue 应该如何设置呢?
如果你使用的是基于 lib-flexable 的 REM 适配方案,则应该设置为你的设计稿的十分之一。
例如设计稿是 750 宽,则应该设置为 75。
大多数设计稿的原型都是以 iPhone 6 为原型,iPhone 6 设备的宽是 750。
但是 Vant 建议设置为 37.5,为什么呢?
因为 Vant 是基于逻辑像素 375 写的,所以如果你设置为 75 的话,Vant 的样式就小了一半。
所以如果设置为 37.5
的话,Vant 的样式是没有问题的,但是我们在测量设计稿的时候都必须除2才能使用,否则就会变得很大。
有没有更好的办法不用除以2呢?当然有了,这里给大家介绍两种方式,一种不用写代码,一种需要写代码。
(1)不用写代码的方式
在 Photoshop 中打开单位与标尺设置面板:菜单栏 -> 编辑 -> 首选项 -> 单位与标尺。
将单位中的标尺和文字的单位修改为
点
打开设置图像大小面板:
-
菜单栏 -> 图像 -> 图像大小
-
快捷键:
Alt + Ctrl + I
关闭重新采样
将宽度单位设置为
点
将高度单位设置为
点
将宽度修改为
375
,高度不用动(它会适应宽度自动调整)点击确定完成修改。
调整之后,我们可以看到图像的大小变成了 375 点 x 667 点(144 ppi)。
在 iPhone 6/7/8 设备下,1个点 = 2个物理像素,所以你看到我们导出的图片还是原来的二倍图。
(2)写代码的方式(自行了解)
通过查阅文档我们可以看到 rootValue
支持两种参数类型:
-
数字:固定值
-
函数:动态计算返回
-
- 有一个默认参数:一个对象,其中包含一个 file 属性(编译的文件路径)
所以我们可以这样来处理它:
module.exports = {
plugins: {
'postcss-pxtorem': {
rootValue ({ file }) {
// 如果是 Vant 的样式就按照 37.5 处理转换
// 如果是我们自己的样式就按照 75 处理转换
return file.indexOf('vant') !== -1 ? 37.5 : 75
},
propList: ['*']
}
}
}
这种方式不方便调试。因为在调试面板中看到的都是逻辑像素大小,它和 750 物理像素设计稿不一致,无法很好的利用调试工具。
32.Vue中Vant的tabbar组件使用
<template>
<div class="layout-container">
<!-- 子路由出口 -->
<router-view />
<!-- 底部导航栏 -->
<van-tabbar v-model="active" route>
<van-tabbar-item to="/" name="home" icon="home-o">首页</van-tabbar-item>
<van-tabbar-item to="/qa" name="qa" icon="comment-o">问答</van-tabbar-item>
<van-tabbar-item to="/video" name="video" icon="video-o">视频</van-tabbar-item>
<van-tabbar-item to="/my" name="my" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
export default {
name: 'LayoutIndex',
components: {},
props: {},
data () {
return {
active: 'home'
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {}
}
</script>
<style scoped lang="less"></style>
route
属性意味着开启路由模式,通过在每个tabbar-item上的to属性设置路由链接即可进行点击跳转。
效果:
33.使用try...catch捕获async函数异常
// 用户登录
async handleUserLogin () {
try {
const { data: response } = await login(this.user)
console.log(response)
} catch (err) {
console.log(err)
}
}
34.Vue中Vant的Toast组件使用
Vant 中内置了Toast 轻提示组件,可以实现移动端常见的提示效果。
// 简单文字提示
Toast("提示内容");
// loading 转圈圈提示
Toast.loading({
duration: 0, // 持续展示 toast
message: "加载中...",
forbidClick: true // 是否禁止背景点击
});
// 成功提示
Ttoast.success("成功文案");
// 失败提示
Toast.fail("失败文案");
提示:在组件中可以直接通过
this.$toast
调用。
另外需要注意的是:Toast 默认采用单例模式,即同一时间只会存在一个 Toast,如果需要在同一时间弹出多个 Toast,可以参考下面的示例。
Toast.allowMultiple();
const toast1 = Toast('第一个 Toast');
const toast2 = Toast.success('第二个 Toast');
toast1.clear();
toast2.clear();
项目中登录模块的应用。
async onLogin () {
// 开始转圈圈
this.$toast.loading({
duration: 0, // 持续时间,0表示持续展示不停止
forbidClick: true, // 是否禁止背景点击
message: '登录中...' // 提示消息
})
try {
const res = await request({
method: 'POST',
url: '/app/v1_0/authorizations',
data: this.user
})
console.log('登录成功', res)
// 提示 success 或者 fail 的时候,会先把其它的 toast 先清除
this.$toast.success('登录成功')
} catch (err) {
console.log('登录失败', err)
this.$toast.fail('登录失败,手机号或验证码错误')
}
}
35.Vue中Vant的Form表单组件使用
直接参考官网。
需要注意的是如果form表单中有类似发送验证码的button按钮,需要使用.prevent
修饰符取消它的默认行为,否则你点击按钮的话会提交表单。
36.Vue中countdown倒计时组件使用
<van-count-down
:time="1000 * 60"
v-if="isCountDownShow"
format="ss s"
@finish="isCountDownShow = false"
/>
finish
是倒计时结束时触发的函数,比如可以在倒计时结束后隐藏倒计时按钮。
需要注意的细节:
37.Vuex入门和使用
什么情况下需要使用Vuex?
1.如果数据需要在组件之间传递调用(需要经过多个组件的传递),可以考虑使用vuex
2.vuex可以统一管理应用的所有数据(包括请求)
安装Vuex
npm install vuex
创建store
仓库
import Vue from 'vue'
import Vuex from 'vuex'
// 注册Vuex插件
Vue.use(Vuex)
// 创建Vuex实例
const store = new Vuex.Store({
// 设置严格模式,如果不是使用mutations方式修改状态就会throw error
strict: true,
// 仓库的数据
state: {
user: "",
color: "#000"
}
})
// 暴露store实例
export default store
在main.js
中导入store
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
store
}).$mount('#app')
操作State
在组件中获取store中state的数据:this.$store.state
改变store中数据状态的方法:this.$store.state.xxx = xxx
这种方式无法被Vue追踪,因此也就无法使用快照之类的功能,官方是不建议使用这种方式的,推荐mutations
方式。
可以在创建Vuex实例的时候传入严格模式来限制修改状态必须使用mutations。
import Vue from 'vue'
import Vuex from 'vuex'
// 注册Vuex插件
Vue.use(Vuex)
// 创建Vuex实例
const store = new Vuex.Store({
// 设置严格模式,如果不是使用mutations方式修改状态就会throw error
strict: true,
// 仓库的数据
state: {
user: "",
color: "#000"
}
// 同步修改State中的值
/* mutations: {
// mutations下的方法第一个参数是固定的state
// 第二个参数是传递进来的参数
setColor (state, color) {
state.color = color
}
} */
})
// 暴露store实例
export default store
严格模式下使用直接给state赋值的方式后,虽然数据改了,但是Vue会报错。
使用mutations修改state的写法。
1.先在store中定义mutations,创建要修改state状态的函数。
const store = new Vuex.Store({
// 设置严格模式,如果不是使用mutations方式修改状态就会throw error
strict: true,
// 仓库的数据
state: {
user: "",
color: "#000"
},
// 同步修改State中的值
mutations: {
// mutations下的方法第一个参数是固定的state
// 第二个参数是传递进来的参数
setColor (state, color) {
state.color = color
}
}
})
setColor函数的第一个参数state是固定的值,color是接收的参数
2.在组件中使用this.$store.commit('setColor', 'red')
方式调用mutations方法修改状态。
// 修改颜色函数
handleChangeColor (color) {
console.log(color);
// 不推荐这样修改state中的数据
// this.$store.state.color = color
this.$store.commit('setColor', color)
}
this.$store.commit
第一个参数是mutations
中的函数,第二个参数是传递过去的参数
项目中使用Vuex
将用户登录成功后端返回的token
和refresh_token
保存在Vuex里面,然后通过localStorage
做持久化存储。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
user: JSON.parse(window.localStorage.getItem('user')) // 当前登录用户的登录状态
},
mutations: {
setUser (state, data) {
state.user = data
// 为了防止页面刷新导致数据丢失,我们还需要把数据放到本地存储中,这里仅仅为了持久化数据
window.localStorage.setItem('user', JSON.stringify(state.user))
}
},
actions: {
},
modules: {
}
})
// 用户登录函数
async handleUserLogin () {
// 启动loading效果
this.$toast.loading({
duration: 0, // 持续时间,0表示持续展示不停止
forbidClick: true, // 是否禁止背景点击
message: '登录中...' // 提示消息
})
try {
const { data: res } = await userLogin(this.user)
console.log('登录成功', res)
this.$toast.success('登录成功')
// 将后端返回的用户登录状态(token) 放到 Vuex 容器中
this.$store.commit('setUser', res.data)
} catch (error) {
if (error.response.status === 400) {
console.log('登录失败', error)
this.$toast.fail('登录失败,手机号或验证码错误')
}
}
}
拓展知识
为什么推荐使用mutations
来修改state中的数据?why???
通过commit 提交 mutation 的方式来修改 state 时,vue的调试工具能够记录每一次state的变化,这样方便调试。但是如果是直接修改state,则没有这个记录。
总结:
综上所述,在vuex中,最好设置成严格模式,并且按照文档的要求,通过commit提交mutation的方式来修改state,而不要直接修改state。不然,控制台会报错,并且vue调试工具不会记录state的变化,无法调试。
参考资料:
- https://blog.csdn.net/weixin_40402192/article/details/80052887
- https://blog.csdn.net/zhq2005095/article/details/78359883
38.解决0.1 + 0.2 != 0.3的问题
39.Vant组件库van-grid加van-cell快速开发
实现效果:
<template>
<div class="my-container">
<van-cell-group class="my-info" v-if="user">
<van-cell
center
class="base-info"
:border="false"
>
<van-image
slot="icon"
class="avatar"
round
fit="cover"
:src="currentUser.photo"
/>
<div slot="title" class="name">{{currentUser.name}}</div>
<van-button class="update-btn" size="small" round>编辑资料</van-button>
</van-cell>
<van-grid
:border="false"
class="data-info"
>
<van-grid-item class="data-info-item">
<div slot="text" class="text-wrap">
<div class="count">{{currentUser.art_count}}</div>
<div class="text">头条</div>
</div>
</van-grid-item>
<van-grid-item class="data-info-item">
<div slot="text" class="text-wrap">
<div class="count">{{currentUser.follow_count}}</div>
<div class="text">关注</div>
</div>
</van-grid-item>
<van-grid-item class="data-info-item">
<div slot="text" class="text-wrap">
<div class="count">{{currentUser.fans_count}}</div>
<div class="text">粉丝</div>
</div>
</van-grid-item>
<van-grid-item class="data-info-item">
<div slot="text" class="text-wrap">
<div class="count">{{currentUser.like_count}}</div>
<div class="text">获赞</div>
</div>
</van-grid-item>
</van-grid>
</van-cell-group>
<div v-else class="not-login">
<div @click="$router.push('/login')"><img class="mobile" src="./phone.png" /></div>
<div class="text">登录 / 注册</div>
</div>
<!-- 收藏历史部分 start -->
<van-grid :column-num="2" class="nav-grid mb5">
<van-grid-item class="nav-grid-item" icon-prefix="toutiao" icon="shoucang" text="收藏" />
<van-grid-item class="nav-grid-item" icon-prefix="toutiao" icon="lishi" text="历史" />
</van-grid>
<!-- 收藏历史部分 end -->
<van-cell title="消息通知" is-link to="/" />
<van-cell class="mb5" title="小智同学" is-link to="/" />
<van-cell v-if="user" class="login-out" @click="handleLogout" title="退出登录" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getCurrentUser } from '@/api/user'
export default {
name: 'MyIndex',
components: {},
props: {},
data () {
return {
currentUser: {} // 当前用户信息
}
},
computed: {
...mapState(['user']) // 将state里面的user数据映射到当前组件的data中方便使用
},
watch: {},
created () {
this.loadCurrentUser()
},
mounted () {},
methods: {
// 加载当前用户信息
async loadCurrentUser () {
try {
const { data: res } = await getCurrentUser()
this.currentUser = res.data
} catch (error) {
console.log(error)
}
},
// 退出登录函数
async handleLogout () {
// 提示用户是否确认退出
// Promise写法
/* this.$dialog.confirm({
title: '退出提示',
message: '确认退出吗?'
}).then(_ => { // 确认执行这里
// 清除用户登录状态,因为Vuex中的状态是响应式的,页面会随着变化
this.$store.commit('setUser', null)
}).catch(err => { // 退出执行这里
console.log(err)
}) */
// async...await写法
try {
await this.$dialog.confirm({
title: '退出提示',
message: '确认退出吗?'
})
// 确认退出
console.log('用户确认退出.')
this.$store.commit('setUser', null)
} catch (err) {
// 用户取消退出
}
}
}
}
</script>
<style scoped lang="less">
.my-container {
.my-info {
background: url("./banner.png") no-repeat;
background-size: cover;
.base-info {
background-color: unset;
height: 115px;
box-sizing: border-box;
padding-top: 38px;
padding-bottom: 11px;
.avatar {
box-sizing: border-box;
width: 66px;
height: 66px;
border: 1px solid #fff;
margin-right: 11px;
}
.name {
font-size: 15px;
color: #fff;
}
.update-btn {
height: 26px;
font-size: 10px;
color: #666;
}
}
/deep/ .van-grid-item__content {
background-color: unset;
}
.data-info {
.data-info-item {
height: 65px;
color: #fff;
.text-wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.count {
font-size: 18px;
}
.text {
font-size: 11px;
}
}
}
}
}
/deep/.nav-grid {
.nav-grid-item {
height: 70px;
.toutiao {
font-size: 22px;
}
.toutiao-shoucang {
color: #eb5253;
}
.toutiao-lishi {
color: #ff9d1d;
}
.van-grid-item__text {
font-size: 14px;
color: #333;
}
}
}
.login-out {
text-align: center;
color: #d86262;
}
.mb5 {
margin-bottom: 5px;
}
.not-login {
height: 180px;
background: url('./banner.png') no-repeat;
background-size: cover;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.mobile {
width: 66px;
height: 66px;
}
.text {
font-size: 14px;
color: #fff;
}
}
}
</style>
可以看到直接使用van-cell+van-grid比自己手写一堆div和一堆span然后逐个调样式效率要高非常多,刚开始要使用各种插槽会有点不习惯,但是一旦习惯之后会非常爽,多写就完事了~
40.Vue单文件组件中的样式作用域
如果希望当前组件中的样式不影响其他组件,可以通过添加作用域实现;
在组件文件中的style标签上添加scoped
属性即可。
如果要操作子组件中层级深的样式,可以通过深度作用操作符/deep/
。
比如在结合vant-ui
组件库开发项目的时候,自定义vant组件的样式没有生效,这个时候就可以使用/deep/
来解决问题。
41.封装本地存储操作模块
有时候我们在做项目的时候会频繁操作本地存储,如果每次都window.localstorage.setItem('xxx', xxx)
或者window.localstorage.getItem('xxx')
还有window.localstorage.removeItem('xxx')
就比较麻烦,因此把这些繁琐的操作封装成一个模块中的函数会比较高效一些。
/* 封装操作本地存储模块 */
// 获取本地存储数据
export const getItem = k => {
const data = window.localStorage.getItem(k)
// 为什么把 JSON.parse 放到try-catch中?
// 因为 data 可能不是 JSON 格式字符串,例如'123asd'
try {
// 尝试把 data 转为 JavaScript 对象
return JSON.parse(data)
} catch (error) {
// data 不是JSON格式字符串,直接原样返回
return data
}
}
// 设置本地存储数据
export const setItem = (k, v) => {
// 如果 v 是对象,就把 v 转换成JSON 格式字符串再存储
if (typeof v === 'object') {
v = JSON.stringify(v)
}
window.localStorage.setItem(k, v)
}
// 删除本地存储指定数据
export const removeItem = (k) => {
window.localStorage.removeItem(k)
}
// 删除本地存储所有数据
export const removeAll = _ => {
window.localStorage.clear()
}
在Vuex中应用:
import Vue from 'vue'
import Vuex from 'vuex'
import { getItem, setItem } from '@/utils/storage'
Vue.use(Vuex)
const USER_KEY = 'user-token' // 定义常量
export default new Vuex.Store({
state: {
user: getItem(USER_KEY)
// user: JSON.parse(window.localStorage.getItem('user')) // 当前登录用户的登录状态
},
mutations: {
setUser (state, data) {
state.user = data
// 为了防止页面刷新导致数据丢失,我们还需要把数据放到本地存储中,这里仅仅为了持久化数据
setItem(USER_KEY, state.user)
// window.localStorage.setItem('user', JSON.stringify(state.user))
}
},
actions: {
},
modules: {
}
})
42.搞懂Vue中的插槽
什么是插槽?
插槽怎么用?
插槽有哪些应用场景?
插槽是如何实现的?
待我看懂Vue源码再来回答此问题
43.Vant中的List组件
List 列表:瀑布流滚动加载,用于展示长列表,当列表即将滚动到底部时,会触发事件并加载更多列表项。
<template>
<div class="article-list">
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell v-for="item in list" :key="item" :title="item" />
</van-list>
</div>
</template>
<script>
export default {
name: 'ArticleList',
components: {},
props: {
channel: {
type: Object,
required: true
}
},
data () {
return {
list: [],
loading: false,
finished: false
}
},
computed: {},
watch: {},
created () {},
mounted () {},
methods: {
onLoad () {
// 异步更新数据
// setTimeout 仅做示例,真实场景中一般为 ajax 请求
setTimeout(() => {
for (let i = 0; i < 10; i++) {
this.list.push(this.list.length + 1)
}
// 加载状态结束
this.loading = false
// 数据全部加载完成
if (this.list.length >= 40) {
this.finished = true
}
}, 1000)
}
}
}
</script>
<style scoped lang="less"></style>
List 组件通过 loading 和 finished 两个变量控制加载状态,
当组件初始化或滚动到到底部时,会触发 load 事件并将 loading 设置成 true,此时可以发起异步操作并更新数据,数据更新完毕后,将 loading 设置成 false 即可。
若数据已全部加载完毕,则直接将 finished 设置成 true 即可。
-
load 事件
: -
- List 初始化后会触发一次 load 事件,用于加载第一屏的数据。
-
- 如果一次请求加载的数据条数较少,导致列表内容无法铺满当前屏幕,List 会继续触发 load 事件,直到内容铺满屏幕或数据全部加载完成。
-
loading 属性
:控制加载中的 loading 状态 -
- 非加载中,loading 为 false,此时会根据列表滚动位置判断是否触发 load 事件(列表内容不足一屏幕时,会直接触发)
-
- 加载中,loading 为 true,表示正在发送异步请求,此时不会触发 load 事件
-
finished 属性
:控制加载结束的状态 -
- 在每次请求完毕后,需要手动将 loading 设置为 false,表示本次加载结束
-
- 所有数据加载结束,finished 为 true,此时不会触发 load 事件
44.JavaScript获取当前时间戳的方法
Date.now()
+new Date()
45.Vant中的下拉刷新组件
使用到 Vant 中的 PullRefresh 下拉刷新 组件。
使用下拉刷新组件将van-list
组件包裹住:
<van-pull-refresh
v-model="isRefreshLoading"
:success-text="refreshSuccessText"
:success-duration="1500"
@refresh="onRefresh"
>
<van-list
v-model="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<van-cell
v-for="(article, index) in articles"
:key="index"
:title="article.title"
/>
</van-list>
</van-pull-refresh>
下拉刷新时会触发组件的 refresh
事件,在事件的回调函数中可以进行同步或异步操作,操作完成后将 v-model
设置为 false
,表示加载完成。
async onRefresh () {
// 下拉刷新,组件自己就会展示 loading 状态
// 1. 请求获取数据
const { data } = await getArticles({
// 模拟异步请求
})
// 2. 把数据放到数据列表中(往顶部追加)
const { results } = data.data
this.articles.unshift(...results)
// 3. 关闭刷新的状态 loading
this.isRefreshLoading = false
this.refreshSuccessText = `更新了${results.length}条数据`
}
46.处理第三方图片资源403问题
为什么列表数据中的好多图片资源请求失败返回 403?
因为第三方平台对图片资源做了防盗链保护处理。
第三方平台怎么处理图片资源保护的?
服务端一般使用 Referer 请求头识别访问来源,然后处理资源访问。
Referer 是什么东西?
扩展参考:http://www.ruanyifeng.com/blog/2019/06/http-referer.html
Referer 是 HTTP 请求头的一部分,当浏览器向 Web 服务器发送请求的时候,一般会带上 Referer,它包含了当前请求资源的来源页面的地址。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
需要注意的是 referer 实际上是 "referrer" 误拼写。参见 HTTP referer on Wikipedia (HTTP referer 在维基百科上的条目)来获取更详细的信息。
怎么解决?
不要发送 referrer ,对方服务端就不知道你从哪来的了,姑且认为是你是自己人吧。
如何设置不发送 referrer?
用
<a>
、<area>
、<img>
、<iframe>
、<script>
或者<link>
元素上的referrerpolicy
属性为其设置独立的请求策略,例如:
<img src="http://……" referrerPolicy="no-referrer">
或者直接在 HTMl 页面头中通过 meta 属性全局配置:
<meta name="referrer" content="no-referrer" />
47.Vue中处理相对时间
推荐两个第三方库:
两者都是专门用于处理时间的 JavaScript 库,功能差不多,因为 Day.js 的设计就是参考的 Moment.js。但是 Day.js 相比 Moment.js 的包体积要更小一些,因为它采用了插件化的处理方式。
Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样,如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js 。
-
Day.js 可以运行在浏览器和 Node.js 中。
-
🕒 和 Moment.js 相同的 API 和用法
-
💪 不可变数据 (Immutable)
-
🔥 支持链式操作 (Chainable)
-
🌐 国际化 I18n
-
📦 仅 2kb 大小的微型库
-
👫 全浏览器兼容
下面是具体的操作流程。
安装 dayjs:
npm i dayjs
创建封装 utils/dayjs.js
:
import Vue from 'vue'
import dayjs from 'dayjs'
// 加载中文语言包
import 'dayjs/locale/zh-cn'
import relativeTime from 'dayjs/plugin/relativeTime'
// 配置使用处理相对时间的插件
dayjs.extend(relativeTime)
// 配置使用中文语言包
dayjs.locale('zh-cn')
// 全局过滤器:处理相对时间
Vue.filter('relativeTime', value => {
return dayjs().from(dayjs(value))
})
在 main.js
中加载初始化:
import './utils/dayjs'
使用过滤器:
<span>{{ article.pubdate | relativeTime }}</span>
处理效果:
48.Vue中的Watch应用
watch: {
// 属性名:要监视的数据的名称,这种写法的缺点是第一次不会触发。
/* searchValue () {
console.log(123)
} */
// 监听的完整写法
searchValue: {
// 当数据发生变化则会执行 handler 处理函数
handler () {
// 请求接口获取数据
// 将数据渲染到模版中
},
immediate: true // 该回调将会在侦听开始之后被立即调用
}
},
实际应用:
删除搜索的历史记录,这个业务中有多个地方要对searchHistory
属性进行赋值修改,然后更新localStorage,此时可以使用watch监视这个属性,统一操作。
<template>
<div class="search-container">
<!-- 搜索框 start -->
<!--
在 van-search 外层增加 form 标签,且 action 不为空,即可在 iOS 输入法中显示搜索按钮。
-->
<form action="/">
<van-search
v-model="searchValue"
show-action
placeholder="请输入搜索关键词"
@search="onSearch(searchValue)"
@cancel="$router.back()"
@focus="isResultShow = false"
/>
</form>
<!-- 搜索框 end -->
<!-- 搜索结果组件 start -->
<search-result
v-if="isResultShow"
:search-value="searchValue"
/>
<!-- 搜索结果组件 end -->
<!-- 搜索联想建议组件 start -->
<search-suggestion
v-else-if="searchValue"
:search-value="searchValue"
@search="onSearch"
/>
<!-- 搜索联想建议组件 end -->
<!-- 搜索历史组件 start -->
<search-history
v-else
:searchHistory="searchHistory"
@onDelete="handleDeleteHistoryItem"
@onDeleteAll="searchHistory = $event"
@search="onSearch"
/>
<!-- 搜索历史组件 end -->
</div>
</template>
<script>
import searchHistory from './components/search-history'
import searchSuggestion from './components/search-suggestion'
import searchResult from './components/search-result'
import { setItem, getItem } from '@/utils/storage'
import { mapState } from 'vuex'
export default {
name: 'SearchIndex',
components: {
searchHistory,
searchSuggestion,
searchResult
},
props: {},
data () {
return {
searchHistory: [], // 搜索历史记录
isResultShow: false, // 是否有搜索结果
searchValue: '' // 搜索文本内容
}
},
computed: {
...mapState(['user'])
},
watch: {
// 监视搜索历史记录的变化,更新到本地存储
searchHistory () {
setItem('search-histories', this.searchHistory)
}
},
created () {
this.loadSearchHistory()
},
mounted () {
},
methods: {
// 删除单项历史记录
handleDeleteHistoryItem (index) {
this.searchHistory.splice(index, 1)
// 更新本地存储
},
// 加载历史记录
async loadSearchHistory () {
this.searchHistory = getItem('search-histories') || []
},
// 监听搜索事件
onSearch (searchValue) {
this.searchValue = searchValue
this.isResultShow = true
// 保存历史记录
const index = this.searchHistory.indexOf(searchValue)
if (index !== -1) {
// 有重复历史记录,去重
this.searchHistory.splice(index, 1)
}
// 把最新搜索历史记录放到顶部
this.searchHistory.unshift(searchValue)
// 更新本地存储
}
}
}
</script>
<style scoped lang="less"></style>
49.Vue结合lodash实现函数防抖效果
函数防抖经典的场景就是搜索输入框。
如果用户输入的关键词太频繁,就会导致频繁发送请求,导致后端服务器压力变大,因此建议添加防抖效果。
安装lodash:
# yarn add lodash
npm i lodash
防抖处理:
// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"
不建议下面这样使用,因为这样会加载整个模块。
import _ from 'lodash' _.debounce()
// debounce 函数
// 参数1:函数
// 参数2:防抖时间
// 返回值:防抖之后的函数,和参数1功能是一样的
loadSearchSuggestion: debounce(async function () {
try {
const { data: response } = await getSearchSuggestion(this.searchValue)
this.searchSuggestionList = response.data.options
} catch (error) {
console.log(error)
}
}, 200)
手写防抖函数demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖</title>
<style>
input {
width: 100%;
height: 30px;
border: 3px solid pink;
outline: none;
}
</style>
</head>
<body>
<input type="text">
<script>
const inp = document.querySelector("input");
// 初始化一个定时器ID值
let intervalId = -1;
inp.addEventListener('input', function(e){
// 触发input事件后立刻清除一次定时器
clearInterval(intervalId)
// 设置定时器,如果用户在1500ms内没有再次触发input事件,则发送请求,否则会再次执行清除定时器的操作导致请求
intervalId = setTimeout(_=>{
getList(e.target.value);
}, 1500)
})
// 搜索关键词
function getList(word){
console.log("发请求..." + word)
}
</script>
</body>
</html>
50.github-markdown插件使用
一般在文章详情业务中使用。
使用之前:
![](https://img2020.cnblogs.com/blog/2217722/202103/2217722-20210309121604546-604516502.gif)
使用之后:
![](https://img2020.cnblogs.com/blog/2217722/202103/2217722-20210309121743499-1872612464.gif)
保存这个github-markdown.css
文件到项目中,然后引入,给文章详情的盒子设置一个markdown-body
类名即可。
51.this.$nextTick() 应用
官方原话:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
一句话总结:在Vue中修改数据会影响视图 (DOM) 更新,视图更新会有一定延迟,如果直接去获取视图中的数据会拿不到,此时需要使用$nextTick
API解决。
实际业务场景代码:
import { getArticleDetail } from '@/api/article'
import { ImagePreview } from 'vant'
/*
在组件中获取动态路由参数:
方式一:this.$route.params.articleId
方式二:props 传参,推荐
*/
export default {
name: 'ArticleDetail',
components: {},
props: {
articleId: {
type: [Object, String, Number],
required: true
}
},
data () {
return {
article: {} // 文章详情内容
}
},
computed: {},
watch: {},
created () {
this.loadArticleDetail()
},
mounted () {
},
methods: {
// 获取文章详情
async loadArticleDetail () {
try {
const { data: response } = await getArticleDetail(this.articleId)
this.article = response.data
/*
Vue中数据改变会影响视图(DOM数据)更新,但是不是立即的。
所以如果需要在修改数据之后马上操作被该数据影响的视图 DOM,
需要把这个代码放到$nextTick方法的回调函数中。
this.$nextTick是 Vue 提供的一个方法。
*/
this.$nextTick(_ => {
this.handlePreviewImage()
})
} catch (error) {
console.log('error: ', error)
}
},
// 图片预览函数
// 1. 获取文章内容 DOM 容器
// 2. 得到所有的img标签
// 3. 循环img标签,给 img 注册点击事件
// 4. 在事件处理函数中调用 ImagePreview() 预览
handlePreviewImage () {
const articleContent = this.$refs['article-content']
console.log(articleContent)
const imgs = articleContent.querySelectorAll('img')
console.log('imgs: ', imgs)
const imgPaths = [] // 收集所有的图片路径
imgs.forEach((item, index) => {
imgPaths.push(item.src)
item.onclick = function () {
ImagePreview({
images: imgPaths, // 预览图片路径列表
startPosition: index // 起始位置
})
}
})
}
}
}
</script>
52.Vue中props补充
1.动态路由参数映射到props
在Vue的router中配置props: true
将动态路由参数映射到组件的 props 中,无论是访问还是维护性都很方便。
router.js
{
path: '/article/:articleId',
name: 'article',
component: () => import('@/views/article'),
props: true
}
2.子组件不要将props重新赋值
prop 数据如果是引用类型 (数组、对象) 可以修改;
注意这个修改指的是:user.name = 'Jack'、arr.push(123)、arr.splice(0, 1);
但是任何 prop 数据是不能重新赋值的:xxx = xxx;
如果你想要让 prop 数据 = 新的数据,应该让父组件最修改;