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‘ }
        ]
    }
}

Vue.js项目总结

这样就可以通过不同的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:当前页变化时触发的事件函数

看下图:

Vue.js项目总结

然后自己根据业务进行灵活使用。

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‘属性实现格式化。

Vue.js项目总结

8.element-ui中的loading效果

Element 提供了两种调用 Loading 的方法:指令和服务。对于自定义指令v-loading,只需要绑定Boolean即可。默认状况下,Loading 遮罩会插入到绑定元素的子节点,通过添加body修饰符,可以使遮罩插入至 DOM 中的 body 上。

解决请求没回来之前,用户界面可交互问题;防止请求过慢,用户在分页按钮上面狂点。

请求期间,最好禁用会发送请求的按钮。

Vue.js项目总结

发请求的时候将isLoading变量改为true,请求成功或失败后改为false即可。

9.Vue中router的几种传参方式

10.element-ui中的表单验证

11.element-ui中的图片上传

12.补充对象解构的骚操作

默认获取的数据

Vue.js项目总结

Vue.js项目总结

解构后获取的数据

Vue.js项目总结

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>

页面效果:

Vue.js项目总结

3.定制图片上传

由于element-tiptap默认本地上传图片使用的是base64,因此需要进行定制化,前端上传图片给服务器,服务器返回一个外链的url,将url返回给element-tiptap渲染。

Vue.js项目总结

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
        })
      })
    }
  },

Vue.js项目总结

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个功能:

  1. 用户点击菜单,切换激活效果(element已经支持)
  2. 用户刷新页面的时候要保留激活效果
  3. 用户手动切换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的新特性。

代码如下:

Vue.js项目总结

23.Vue中修改配置文件不生效的解决方法

Vue.js项目总结

直接删除node_modules/.cache文件夹就可以了。

24.Vue中的非父子组件通信

新建一个名字叫 global-bus.js的文件。

假设 a 要给 b 发送数据,那么 b 就需要注册一下事件,a 来发布。

反之,b 给 a 发送数据,那么就是 a 要注册事件,b来发布。

注册事件使用 $on API,发布事件使用 $emit API。

注册事件名和发布事件名必须要一致!

总结:在Vue中非父子组件通信中,接收数据的组件要使用 $on 来注册事件,发送数据的组件要使用$emit 来发布。

举个例子:

Vue.js项目总结

Vue.js项目总结

Vue.js项目总结

Vue.js项目总结

25.在Vue中给axios添加响应拦截器

对请求错误的响应码进行统一处理,比如用户伪造token登录到后台管理系统,但是发送请求报错,此时应该让用户跳转到登录页面。

Vue.js项目总结

Vue.js项目总结

26.Vue中Echarts结合百度地图完成复杂图表功能

27.Vue中在组件上使用v-model

当你给子组件提供的数据既要使用还要修改,这个时候我们可以使用 v-model 简化数据绑定。

繁琐的写法:

Vue.js项目总结

简化写法:

Vue.js项目总结

Vue.js项目总结

Vue.js项目总结

参考Vue官方文档

28.Vue组件功能定制化

Vue.js项目总结

子组件中的props推荐使用对象的方式定义接收的数据,这样可以配置接收数据的类型、默认值、是否必传等,而使用数组的方式定义prop则不行,数组的方式不够严谨,功能不强大,建议以后封装组件,定义props的时候全部使用对象的方式定义。

Vue.js项目总结

Vue.js项目总结

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云盒子

Vue.js项目总结


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 单位,推荐使用以下两个工具:

下面我们分别将这两个工具配置到项目中完成 REM 适配。

(1)使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)

安装依赖:

# yarn add amfe-flexible
npm i amfe-flexible

然后在 main.js 中加载执行该模块:

import ‘amfe-flexible‘

最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签 font-size 的变化。

Vue.js项目总结

例如在 iPhone 6/7/8 设备下,html 标签字体大小为 37.5 px

Vue.js项目总结

例如在 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

Vue.js项目总结

这是没有配置转换之前的。

Vue.js项目总结

这是转换之后的,可以看到 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 代码,再交由插件来进行处理,它的插件体系非常强大,所能进行的操作是多种多样的,例如:

PostCSS 一般不单独使用,而是与已有的构建工具进行集成。

Vue CLI 默认集成了 PostCSS,并且默认开启了 autoprefixer 插件。

Vue CLI 内部使用了 PostCSS。

你可以通过 .postcssrc 或任何 postcss-load-config 支持的配置源来配置 PostCSS。也可以通过 vue.config.js 中的 css.loaderOptions.postcss 配置 postcss-loader

我们默认开启了 autoprefixer。如果要配置目标浏览器,可使用 package.jsonbrowserslist 字段。

Autoprefixer 插件的配置

Vue.js项目总结

autoprefixer 是一个自动添加浏览器前缀的 PostCss 插件,browsers 用来配置兼容的浏览器版本信息,但是写在这里的话会引起编译器警告。

Vue.js项目总结

警告意思就是说你应该将 browsers 选项写到 package.json.browserlistrc 文件中。

browserslist:

你会发现有 package.json 文件里的 browserslist 字段 (或一个单独的 .browserslistrc 文件),指定了项目的目标浏览器的范围。这个值会被 @babel/preset-envAutoprefixer 用来确定需要转译的 JavaScript 特性和需要添加的 CSS 浏览器前缀。

参考官方文档中的语法,我们将 .browserslistrc 修改如下:

Android >= 4.0
iOS >= 8

兼容Android版本大于等于4.0,IOS版本大于等于8的。

postcss-pxtorem 插件的配置

Vue.js项目总结

  • 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 中打开单位与标尺设置面板:菜单栏 -> 编辑 -> 首选项 -> 单位与标尺。

Vue.js项目总结

将单位中的标尺和文字的单位修改为

打开设置图像大小面板:

  • 菜单栏 -> 图像 -> 图像大小

  • 快捷键:Alt + Ctrl + I

Vue.js项目总结

  • 关闭重新采样

  • 将宽度单位设置为

  • 将高度单位设置为

  • 将宽度修改为 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属性设置路由链接即可进行点击跳转。

效果:

Vue.js项目总结

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是倒计时结束时触发的函数,比如可以在倒计时结束后隐藏倒计时按钮。

需要注意的细节:

Vue.js项目总结

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

Vue.js项目总结

严格模式下使用直接给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

将用户登录成功后端返回的tokenrefresh_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,则没有这个记录。

Vue.js项目总结

总结:

综上所述,在vuex中,最好设置成严格模式,并且按照文档的要求,通过commit提交mutation的方式来修改state,而不要直接修改state。不然,控制台会报错,并且vue调试工具不会记录state的变化,无法调试。

参考资料:

38.解决0.1 + 0.2 != 0.3的问题

Vue.js项目总结

39.Vant组件库van-grid加van-cell快速开发

实现效果:

Vue.js项目总结

<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单文件组件中的样式作用域

Vue.js项目总结

如果希望当前组件中的样式不影响其他组件,可以通过添加作用域实现;

在组件文件中的style标签上添加scoped属性即可。

Vue.js项目总结

如果要操作子组件中层级深的样式,可以通过深度作用操作符/deep/

Vue.js项目总结

比如在结合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获取当前时间戳的方法

  1. Date.now()
  2. +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>

处理效果:

Vue.js项目总结

48.Vue中的Watch应用

  watch: {
    // 属性名:要监视的数据的名称,这种写法的缺点是第一次不会触发。
    /* searchValue () {
      console.log(123)
    } */

    // 监听的完整写法
    searchValue: {
      // 当数据发生变化则会执行 handler 处理函数
      handler () {
        // 请求接口获取数据
        // 将数据渲染到模版中
      },
      immediate: true // 该回调将会在侦听开始之后被立即调用
    }
  },

watch详细语法参考

实际应用:

删除搜索的历史记录,这个业务中有多个地方要对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实现函数防抖效果

函数防抖经典的场景就是搜索输入框。

Vue.js项目总结

如果用户输入的关键词太频繁,就会导致频繁发送请求,导致后端服务器压力变大,因此建议添加防抖效果。

安装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插件使用

一般在文章详情业务中使用。

使用之前:

Vue.js项目总结

使用之后:

Vue.js项目总结

github-markdown.css地址

Vue.js项目总结

保存这个github-markdown.css文件到项目中,然后引入,给文章详情的盒子设置一个markdown-body类名即可。

Vue.js项目总结

51.this.$nextTick() 应用

官方原话:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

一句话总结:在Vue中修改数据会影响视图 (DOM) 更新,视图更新会有一定延迟,如果直接去获取视图中的数据会拿不到,此时需要使用$nextTickAPI解决。

实际业务场景代码:

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 数据 = 新的数据,应该让父组件最修改;

先写这么多,后期再分享。

需要额外复习的知识


1.img标签的object-fit属性

2.css3 border属性 渐变、背景图

3.搞懂Vue中的.sync修饰符

4.安装markdown vue主题

5.正则表达式

6.移动端适配知识

6.ES5的this和ES6箭头函数的this区别

看 《JS高程3》和阮一峰的ES6。

7.Vant定义主题颜色

8.实现移动端一键换肤效果

Vue.js项目总结

上一篇:httpservletresponse


下一篇:js反爬原理