Vue实战(六)通用Table组件

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue实战(六)通用Table组件相关的知识,希望对你有一定的参考价值。

参考技术A

本文是 Vue实战 系列的第六篇文章,主要介绍Falcon项目中通用 Table 组件的开发和使用 。Falcon项目地址: https://github.com/thierryxing/Falcon

随着业务的发展和功能的增多,我们发现不少页面都具备相似的功能,这里举几个比较俗的例子:可以多选的下拉菜单,带输入的对话框,日期选择器等等,于是我们会想办法将这些共有的功能抽取成一个个公共组件,以便能够在不同的页面或业务中使用。

对于一个中后台类的项目,一个比较常见的展示形式就是Table了,相信大家都不陌生,如下图所示:

一个Table通常由如下几个部分构成:

除此之外,由于 Table 中的数据往往都是从后端获取的,所以这个包含 Table 的页面还需要发起一个请求,并且将最终的内容渲染在表格之内,请求的过程由于是异步的,所以需要给用户展示一个 Loading 动画;当请求数据为空时,需要显示一个占位的空元素控件。

在 Falcon 项目的实践中,我们发现,每一个页面中的 Table 除了行数,列数,及单元格的内容不同之外,其它的地方,包括样式,分页及数据处理逻辑都是一样的,每次新增一个这样的页面无非就是拷贝粘贴了,那么在这种情况下,我们抽取出了一个通用的 Table 组件,取名为:TableBox。

说到这里插一个题外话:

关于这个问题我认为,如果一个功能只出现在了一个或两个页面中,往往是没有必要处理的,因为一两个功能的重复还不足以说明问题,也很难看出其中的共性,如果强行抽取的话,反而会增加维护的负担;如果出现的地方超过了两处,那么我们就需要考虑将这个功能进行抽取了,我也常常和 Team 的人说:“如果一个功能你拷贝粘贴了1次,没关系,不用纠结;2次的话,就得考虑其复用性和组件化了”。

当然,以上内容只适用于那些初期开发过程中无法预测未来变化的项目,如果刚开始产品设计的时候,就能够充分的预见和考虑未来的业务发展,并且给出详细的产品及UI设计方案,那么就另当别论了。

回到我们的主题,抽取这个 TableBox 其实并不是一气呵成的,而是在业务迭代中,不断地发现新的场景,新的问题,带着这些问题我们不断的优化 TableBox,最终达到一个较为完整的状态。这也符合 Vue 本身渐进式的理念。接下来我们花些时间,一起探讨一下这些场景和问题:

我们发现,对于不同的页面,只要带有 Table 的,其数据都需要从远端服务器获取,一般情况下,我们会在每个业务中都去写一下这个网络获取数据的逻辑,但是,如果仔细想想,你就会发现,其实这类列表数据获取和处理的逻辑都是一样的。所以针对这个情况,我们只要和后端协商好列表相关的统一 API 数据结构,如:

那么数据获取,渲染,Loading 动画展示隐藏,分页加载等操作都可以在 TableBox 中完成。
这个组件需要的只是向外暴露出数据请求的 API 地址及各种参数:

然后写好对应的获取数据的 fetchData 方法:

这样对于调用者来说,只需要简单的传入相关 API 地址及参数就可以了,数据加载的事情让 TableBox 去处理就好了,非常的方便。

因为 TableBox 组件本身是和业务无关的,所以其肯定无法知道我的这个 Table 的表头是什么,有多少行,也无法知道每一行展示什么数据,这些内容全部应该由父组件告知 TableBox。

要实现以上的功能,我们可以借助于 Vue 本身提供的强大的工具 Slot,如果简单点说,大家可以把 Slot 理解为一个坑位,因为大多数情况下,组件自己无法预先知道某块区域放置什么内容,那么组件可以先将个区域放置一个 Slot,就是挖个坑,当父组件引入子组件时,会告诉子组件往这个坑位中填充什么样的内容。

回到我们的 TableBox 组件,我们首先挖两个坑(放置两个 Slot ),命名为 ths 和 item ,分别用于表头和行列表内容:

这样对于表头的数据,可以由引入 TableBox 的父组件来指定,用法如下,其中 slot=\'ths\' 就是刚才我们在 TableBox
中放置的 Slot

同样,对于每一行的内容,也是由引入 TableBox 的父组件来指定,用法如下:

在开发业务的过程中,遇到一个场景:当页面数据已经全部加载完毕后,在某些场景下需要改变 Table 中某些数据的状态(删除某列或改变某一列的数据)。

这里具体举个 Falcon 中的实际例子:
我们允许用户给每个项目分配多个环境,以区分测试,生产,开发和各种自定义的场景,在每个环境下,用户可以设置不同的 Git Branch 。用户点击 Choose Branch 按钮后,会触发一个请求到后端,变更当前环境的 Git Branch, 修改成功后该列表项的按钮会显示为 Current Branch

由于以上逻辑都是在引入了 TableBox 的父组件中完成的,其能够控制数据的刷新,由于 场景1 中我们已经把数据请求的逻辑都封装在了 TableBox 中,所以我们需要让其向外暴露出一个 Boolean 属性:reloadData,当此属性为 true 时,TableBox 会重新请求一次API,并刷新列表。

同理,由于操作数据是由父组件发起的,所父组件中也需要有同样的属性,并且和 TableBox 中的 reloadData 保持数据同步,这里用到了 Vue 2.3 版本增加的一个 .sync 修饰符进行处理 。

这样,当 reloadData 在数据更新完毕后还原为 false 状态时,我们可以显示的触发一个 emit 事件:

由于目前所有的数据获取都是在 TableBox 内部处理的,所以父组件本身是无法直接获取到数据的。但是在某些情况下,我们又希望父组件能够获取到数据,以便能够在顶层进行更灵活的处理,这时我们就需要在 TableBox 内部将数据抛出。

抛出的方式也很简单,我们可以使用 emit 方法抛出一个事件。根据这个思路我们改造一下上文提到的 fetchData 方法:

然后在父组件中监听这个事件,这样就能获取到完整的数据了。

解决了以上4个场景的问题后,我们这个 TableBox 可以说告一段落了,后续如果有遇到新的场景,新的问题,我们只需要不断的去优化去完善这个组件即可。
到目前为止,TableBox 已经应用到了我们内部的三个后台项目约几十个页面中,可以说大大节省了我们的时间,提升了整体效率。
并且随着这样的组件越来越多,甚至我们的后端工程师经过简短的培训,也可以上手部分前端页面的开发了。
最后附上 TableBox 的地址: https://github.com/thierryxing/Falcon/blob/mock/src/components/global/TableBox.vue

Vue项目实战-vue2(移动端)

Vue项目实战(移动端)#

相关资料#

  1. vue-cli脚手架: vue2脚手架
  2. vue3脚手架: vite
  3. vue官网: [介绍 — Vue.js
  4. vscode插件
    • vetur 必备工具
    • vue-helper 一些辅助功能
    • Vue VSCode Snippets 片段

(一) 创建项目#

01 安装vue-cli脚手架#

npm install -g @vue/cli

02 查看vue脚手架版本#

出现版本号表示成功

vue --version

03 创建一个新项目#

创建项目

vue create hello-world  // 1.创建项目

运行项目

cd hello-world  // 2.进入项目文件夹
npm run serve		// 3.运行项目

(二) 禁用Eslint#

// 根目录新增vue.config.js
module.exports = 
    lintOnSave: false

如果vue组件提示红色错误,如下图 

 解决办法: 文件 -> 首选项 -> 设置 然后输入eslint -> 选择Vetur -> 把√取消即可 

(三) devtool#

vue开发调试工具

  1. 下载 http://soft.huruqing.cn
  2. 添加到chrome扩展程序里

(四) 添加less支持#

  1. npm install less less-loader@6.0.0 --save-dev

  2. 在vue文件这样写即可, scoped表示样式只在当前文件有效, 不会影响其他组件

    ps: less-loader要安装6.0版本, 不然有兼容问题

    <style lang="less" scoped> 
    .box 
      .text 
        color: red;
      
     
    </style>
    

(五) vue路由配置(背诵)#

(1)一个简单路由配置#

  1. npm i vue-router  安装路由插件
  2. 在src创建views文件夹, 创建各个模块的组件
  3. 在src内创建router文件夹, 新建index.js(代码如下)
  4. 在main.js里, 把router挂载到vue的实例
  5. 配置路由出口, 详见下方第(2)点router-view
  6. 使用router-link进行跳转, 详见下方第(3)点路由跳转
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router); 
// 路由数组
const routes = [
    
        path: '/product',
        component: ()=>import('@/views/product/index.vue')
    ,
    
        path: '/cart',
        component: ()=>import('@/views/cart/index.vue')
    ,
]

const router = new  Router(
    routes
)
export default router;
// main.js 代码
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'

Vue.config.productionTip = false

new Vue(
  // 把router挂载到vue实例
  router,
  render: h => h(App),
).$mount('#app')

(2) router-view#

  1. 路由出口
  2. 路由匹配到的组件将渲染在这里
  3. 在app.vue配置
<template>
  <div id="app"> 
    <!-- 路由出口 -->
    <router-view></router-view>
  </div>
</template>

<script>
export default 
  name: "App",
  components: ,
;
</script>

(3) 路由跳转#

// 方式一
<router-link to="/cart">cart</router-link>

// 方式二
this.$router.push('/cart');

(4) 子路由配置#

使用子路由进行模块路由配置,结构比较分明 比如我们的网站有商品模块,有列表页面和详情页面, 路由如下 /product   商品模块总路由 /prodcut/list   子路由 /product/detail   子路由


    path: '/product',
    component: () => import('@/views/product/index'),
    children: [
        
            path: 'list',
            component: ()=>import('@/views/product/children/list')
        ,
        
            path: 'detail',
            component: ()=>import('@/views/product/children/detail')
        
    ]

(5) active-class#

active-class是vue-router模块的router-link组件中的属性,用来做选中样式的切换;

  1. 只要路由中包含to里面的路由, 就能匹配到, 就会高亮, 比如: /product, /product/list, /product/detail都会使下面的第二个router-link高亮
  2. exact 表示精确匹配, 只有路由完全一样才能被匹配
<router-link to="/" active-class="on" exact>首页</router-link>
<router-link to="/product" active-class="on">product</router-link>
<router-link to="/cart" active-class="on">cart</router-link>
<router-link to="/my" active-class="on">my</router-link>
<router-link to="/order" active-class="on">order</router-link>

(6) history模式#

vue2配置方式

  1. vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。

  2. 如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面

  3. 使用history需要后端支持, vue-cli创建的devServer可以支持

    const router = new VueRouter(
      mode: 'history',  // 默认hash
      routes: [...]
    )
    

vue3配置方式

const router = createRouter( 
 	history: createWebHistory(),  // history模式
 	//history: createWebHashHistory(), // hash模式
  	routes
);     

(7) redirect重定向#

当访问 '/', 我们使用redirect使它默认跳到 '/product'


    path: '/',
    redirect: '/product'
,

(8) 404配置#

假如用户访问了一个没有的路由, 我们让它跳转到404页面

  
    path: '*',
    component:()=>import('@/components/NotFound')
  

(六) 父子组件通信(背诵)#

知识点(背诵):

  1. 父传子: 父组件通过(绑定)属性的方式传数据给子组件, 子组件使用props接收数据
  2. 子传父: 父组件在子组件上绑定一个自定义事件, 子组件通过$emit触发该自定义事件, 同时可以传入数据

1.父传子#

  • 父组件给子组件绑定属性, 属性的值是需要传递的信息
  • 子组件通过props接收父组件的信息
 // 例子1: 使用普通属性
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son msg="hello world" username="张三"/>
  </div>
</template>

<script>
import Son from "./Son";
export default 
  components: 
    Son,
  ,
;
</script> 

// Son.vue
<template>
  <div>
    <h4>子组件</h4>
    <p>msg:  msg </p>
    <p>username:  username </p>
  </div>
</template>

<script>
export default 
  props: ["msg", "username"],
;
</script> 

// 例子2: 使用绑定属性(可传变量)
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son :msg="msg" :username="username" />
  </div>
</template>

<script>
import Son from "./Son";
export default 
  components: 
    Son,
  ,
  data() 
    return 
        msg: '哈哈哈',
        username: '李四'
    ;
  ,
;
</script> 

// Son.vue
<template>
  <div>
    <h4>子组件</h4>
    <p>msg:  msg </p>
    <p>username:  username </p>
  </div>
</template>

<script>
export default 
  props: ["msg", "username"],
;
</script> 

父传子实践: 把首页拆分为多个组件 技巧: 如果某个部分只是做展示用, 尽量把它变成子组件

2. 子传父#

  1. 父组件在子组件上绑定一个自定义事件(事件名称我们自己定义的, vue本身是没有这个事件的)
  2. 父组件给自定义事件绑定一个函数, 这个函数可以接受来自子组件的数据
  3. 子组件使用$emit触发(调用)该事件, 并把数据以参数形式传给父组件
// 例子1: 一个简单的例子
// demo.vue
<template>
  <div>
    <h3>父组件</h3>
    <hr />
    <Son @aaa="say"/>
  </div>
</template>

<script>
import Son from "./Son";
export default 
  components: 
    Son,
  ,
  data() 
    return  
    ;
  ,
  methods: 
    say(data) 
      alert(data)
    
  

;
</script> 

// 子组件
<template>
  <div>
    <h4>子组件</h4>
    <button @click="$emit('aaa','我是子组件')">点击</button>
  </div>
</template>

<script>
export default 
  props: ["msg", "username"],
;
</script>  

(七) axios拦截器(背诵)#

  1. 对ajax请求进行拦截
    1. 在请求头添加token
  2. 对ajax响应数据进行拦截
    1. 统一处理请求失败的情况, 这样就不需要在每个组件里处理失败的情况
    2. 有些接口需要登录才能访问, 在没登录的情况下跳转到登录页面
import axios from "axios";
import Vue from "vue";
import  Toast  from "vant";
Vue.use(Toast);

const service = axios.create(
  baseURL: "http://huruqing.cn:3003",
  timeout: 50000, // 请求超时时间(因为需要调试后台,所以设置得比较大)
);

// request 对请求进行拦截
service.interceptors.request.use(
  (config) => 
    // 开启loading
    Toast.loading(
      message: "加载中...",
      forbidClick: true,
      loadingType: "spinner",
    );
    // 请求头添加token
    config.headers["token"] =
      "gg12j3h4ghj2g134kj1g234gh12jh34k12h34g12kjh34kh1g";
    return config;
  ,
  (error) => 
    Promise.reject(error);
  
);

// response 响应拦截器
service.interceptors.response.use(
  (response) => 
    Toast.clear();
    const res = response.data;
    if (res.code == 666) 
      return res;
     else 
      // 成功连接到后台, 但是没有返回正确的数据
      Toast.fail(res.msg);
    
  ,
  (error) => 
    Toast.clear();
    // 跟后台连接失败
    Toast.fail("网络异常,请稍后再试");
  
);

export default service;

(八) Sticky 粘性布局#

(九) 图片懒加载#

二、vue2.x进阶教程 | 清流

(十) 全局注册组件#

// 注册全局组件除了多了个template之外,其它跟平时写组件类似
// 在main.js,实例化vue组件之前执行以下代码
Vue.component('button-counter', 
  data: function () 
    return 
      count: 0
    
  ,
  template: '<button v-on:click="count++">你打了我  count  次</button>'
)

// 在其他组件就可以使用
<template>
	<div>
  	<button-counter></button-counter>
  </div>  
</template>
// 改造checkbox, 官网例子
Vue.component('base-checkbox', 
  model: 
    prop: 'checked',
    event: 'change'
  ,
  props: 
    checked: Boolean
  ,
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
)

// 然后就可以像下面这样来使用
<template>
  <div> 
   <base-checkbox v-model="flag"></base-checkbox>
		<p>flag</p>
  </div>
</template>

<script>
export default 
  data: function () 
    return 
      flag: false
    ;
  ,

</script>
// 另外需要在根目录的vue.config.js中开启运行时编译
module.exports = 
    runtimeCompiler: true

(十一) slot插槽#

元素作为承载分发内容的出口 一个内存插槽, 当内存插上之后,插槽就可以接收来自内存的信息, slot取名插槽含义也贴切, 在子组件配置插槽slot, 当父组件"插"信息进来的时候, 插槽slot就能接收到这个信息. slot插槽大大的扩展子组件的功能。 

1. vant有赞ui库中slot的例子#

<van-nav-bar title="标题" left-text="返回" left-arrow> 
   <p slot="right">
     <van-icon name="search" size="18" />
   </p>
</van-nav-bar>

2. 普通插槽#

// 父组件demo.vue代码
<template>
  <div>
    <h3>父组件</h3>
    <hr>
    <Son><button>按钮</button></Son>
  </div>
</template>

<script>
import Son from "./Son";
export default 
  components: 
    Son,
  
;
</script> 

// 子组件Son.vue
<template>
  <div>
    <slot></slot>
  </div>
</template> 

3. 具名插槽#

// father.vue代码
<template>
  <div>
    <h3>这是父组件</h3>

    <Child>
      <header slot="header" style="background: yellow">这是头部</header>
      <footer slot="footer" style="background: green;">这是底部</footer>

      <div style="border:1px solid;">
        <button>a</button>
        <button>b</button>
        <button>c</button>
        <button>d</button>
      </div>
    </Child>
  </div>
</template>

<script>
import Child from "@/components/Child";
export default 
  components: 
    Child
  
;
</script>

接收父组件带 slot="footer" 的内容
接收不带slot="xxx" 的内容

// Child.vue代码
<template>
  <div style="margin-top: 30px;background: gray;height: 200px;">
    <h5>这是子组件</h5>
		<!--接收父组件带 slot="header" 的内容-->
    <slot name="header"></slot>
		<!--接收父组件带 slot="footer" 的内容-->
    <slot name="footer"></slot>
		<!--接收剩余内容-->
    <slot></slot>
  </div>
</template>

自定义组件

// demo.vue
<template>
  <div> 
    <NavBar title="首页" @click-left="clickLeft" @click-right="clickRight"></NavBar>
  </div>
</template>

<script> 
import NavBar from './Nav-Bar.vue'
export default 
    components: 
      NavBar
    ,

    methods:
      clickLeft() 
        alert('左边被点击了'); 
      ,
      clickRight() 
        alert('右边被点击了')
      
    

</script>


// Nav-Bar.vue
<template>
    <div class="nav flex jc-sb pl-15 pr-15 bg-fff aic">
      <p class="blue flec aic" @click="$emit('click-left')">
        <van-icon name="arrow-left" />
        <span>返回</span>
      </p>
      <p>title?title:'标题'</p>
      <slot name="right"> <span  class="blue" @click="$emit('click-right')">按钮</span></slot>
    </div>
</template>

<script>
export default 
  props: ['title']

</script> 

<style lang="less">
.nav 
  height: 50px;
  .blue 
    color: #1989fa;
  


</style>

(十二) 使用ui库需要关注的三点#

以vant 的导航栏组件van-nav-bar为例

  1. 属性, 该组件提供了哪些绑定属性
  2. 事件, 该组件提供了哪些事件
  3. 插槽, 该组件提供了哪些插槽

(十三) 三种路由传参方式(背诵)#

知识点:

  1. 通过params传参, 使用$route.params接收参数
  2. 动态路由传参, 使用$route.params接收参数
  3. 通过query传参, $route.query接收参数

注意: router和route不是一回事 ​

1.通过name+params传参#

// 1.配置路由的时候添加name
  
        path: "detail",
        name: 'product-detail',
        component: () => import("@/views/order/children/detail"),
  ,


// 2.跳转
 this.$router.push(
        // 要跳转到的路由名称
        name: 'product-detail',
         params:  productId: '123' 
      )

// 3.接收参数
this.$route.params.productId

2.动态路由传参#

// 1.配置路由

  path: "detail/:productId", 
  component: () => import("@/views/product/children/detail.vue"),
,
  
// 2. 跳转
this.$router.push('/product/detai/22222')
<router-link to="/product/detail/333333">传参</router-link>

  
// 3.接收参数
 created() 
    let params = this.$route.params;
    console.log('params',params); 
  ,
  

3.通过path+query传参#

// 带查询参数,query传参会把参数拼接到地址栏,变成 /register?plan=aaa, 使用了path,参数不能通过params传递
this.$router.push( path: '/register', query:  plan: 'aaa' )
// 获取参数
this.$route.query;

(十四) 模拟数据#

  1. 文档地址:  json-server - npm
  2. npm i json-server -g    //全局安装
  3. 根目录创建db.json
  4. 启动json-server
json-server --watch db.json
// db.json

  "posts": [
     "id": 1, "title": "json-server", "author": "typicode" 
  ],
  "comments": [
     "id": 1, "body": "some comment", "postId": 1 
  ],
  "profile":  "name": "typicode" 

  1. 访问接口
http://localhost:3000/posts/1
  1. 将命令添加到package.json, 可以使用 npm run json 启动项目
 "scripts": 
    "json": "json-server --watch db.json" 
  ,

(十五) 计算属性computed和属性观察watch#

vue中computed和watch区别 - 简书

  1. computed的作用
  2. watch的作用
  3. computed和watch的区别
// computed
<template>
  <div>
    <p>姓:  xing </p>
    <p>名:  ming </p>

    <p>姓名:  xingming </p>

    <button @click="change">修改xing</button>
  </div>
</template>  

<script>
export default 
  data() 
    return 
      xing: "张",
      ming: "无忌",
    ;
  ,

  // 计算属性
  computed: 
    // xingming这个属性是由xing属性和ming计算得来
    xingming() 
      return this.xing + this.ming;
    ,
  ,

  methods: 
    change() 
      this.xing = "李";
    ,
  ,
;
</script>

(十六) vuex(背诵)#

(1) 普通对象 VS vuex创建的对象#

  1. 普通对象
    1. 创建对象
    2. 定义对象的属性
    3. 修改对象的属性
    4. 读取对象属性
  2. vuex
    1. 创建仓库
    2. 定义状态
    3. 修改状态
    4. 读取状态

(2) 相关概念#

  1. 概念vuex是什么: 创建一个仓库, 然后在仓库里定义若干状态, 并且管理这些状态. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. vuex有哪几个核心概念, 都是用来做什么的
    1. state 定义状态
    2. getters 派生状态
    3. mutation 修改状态(同步)
    4. action 修改状态(异步)
    5. module 模块化
  3. 如何使用vuex进行跨组件通信
  4. vuex持久化

// getters派生状态


// 1.  在 src/store/index.js
state: 
    token: "",
    username: "张三",
    age: 100,
    phone: "123456789",
  ,

  getters: 
    // 派生状态
    str(state) 
      return `我叫$state.username,我的年龄是$state.age`
    
  ,
    
// 2. 在组件里使用
    
<template>
	<div> 
  str
  </div>  
  
</template>    
    
import mapGetters from 'vuex';
export default 
 computed:
 		...mapGetters(['str'])
  




// action 修改状态(异步)

  1. 定义状态
  2. 定义mutation, 通过mutation来修改状态
  3. 定义action , 通过action来提交(commit)mutation
  4. 用户派发action
import Vue from "vue";
import Vuex from "vuex";
import $http from '@/utils/http';
// 导入持久化插件
import createPersistedState from "vuex-persistedstate";

Vue.use(Vuex);
// 创建仓库
const store = new Vuex.Store(
  plugins: [createPersistedState()],
  // 1.定义状态
  state: 
    token: "",
    phone: "123456789",
    username: "张三",
    age: 100,
  ,

  getters: 
    // 派生状态
    str(state) 
      return `我叫$state.username,我的年龄是$state.age`
    
  ,


  // 2.定义mutaion
  mutations: 
    // 修改token
    set_token(state,payload) 
      state.token = payload
    ,

    // 修改phone的状态
    set_phone(state, payload) 
      state.phone = payload;
    ,
    /**
     * 定义修改username的muation
     * @param * state 状态
     * @param * payload 传入的新数据
     */
    set_username(state, payload) 
      state.username = payload;
    ,

    // 定义修改age的mutation
    set_age(state, payload) 
      state.age = payload;
    ,
  ,

  // 3.定义action
  actions: 
    LOGOUT(store,payload) 
      $http.post('/user/logout').then(res=> 
          // 清除token和phone
          store.commit('set_token','');
          store.commit('set_phone','');
      )
    
   
);

export default store;


// 4.退出登录时派发action
 <p class="red" @click="logout2">退出登录</p>

methods: 
	 logout2() 
      this.$store.dispatch('LOGOUT');
       this.$router.push('/my');
    ,

// 模块化

// 1.定义模块的state getters mutaions actions
// src/store/modules/cart.js
export default 
    state: 
       cartNum: 100 
    ,

    getters:  ,
    mutaions:  ,
    actions: 

// src/store/modules/type.js
export default 
    state: 
       aaa: 333 
    ,

    getters:  ,
    mutaions:  ,
    actions: 



// 2.合并模块
import cart from './modules/cart';

const store = new Vuex.Store(
  modules:
    cart,
    type, 
  ,

                             
// 3.使用(在任何一个组件内)
<template>
	<div>
  	   $store.state.cart.cartNum 
      $store.state.type.aaa                            
  </div>                             
</template>                             
                             


(3) vuex应用#

  • 创建仓库
    1. 需要先安装vuex npm i vuex --save
    2. 创建仓库
    3. 挂载仓库
// 1. src/store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);

// 创建仓库
const store = new Vuex.Store(

);

export default store;


// 2. 挂载到根实例 /src/main.js
import router from "./router/index";
import store from './store/index';

Vue.use(Vant);  

Vue.config.productionTip = false;
new Vue(
  store,
  router,
  render: (h) => h(App),
).$mount("#app");
  • 定义状态
const store = new Vuex.Store(
  // 定义状态
  state: 
    username: "张三",
    age: 100,
  ,
);
  • 获取状态
    1. 直接获取 this.$store.state.username
<template>
  <div>
    <p>username: $store.state.username</p>
  </div>
</template>   
<script> 
export default 
    created() 
      console.log(this.$store.state);
     
;
</script>
  1. 通过mapState获取, mapState是vuex提供的方法, 可以让我们更方便的获取属性
<template>
  <div>
    <p>username: username</p>
    <p>age: age</p>
  </div>
</template>  

<script>
import mapState from 'vuex';
export default  
    computed: 
      ...mapState(['username','age'])
    
;
</script>
  • 修改状态: 通过mutation进行修改
    • 修改状态只能通过mutation来修改, 不可以直接修改
    • mutation只支持同步操作
  • 步骤:
    • 定义mutation
    • 提交mutation
// 1.定义mutation
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
// 创建仓库
const store = new Vuex.Store(
  // 定义状态
  state: 
    username: "张三",
    age: 100,
  ,
  // 定义mutaion
  mutations: 
      /**
       * 定义修改username的muation 
       * @param * state 状态
       * @param * payload 传入的新数据
       */
      set_username(state,payload) 
        state.username = payload;
      ,

      // 定义修改age的mutation
      set_age(state,payload) 
        state.age = payload;
      
   
);

export default store;


// 2. 提交mutaion
<template>
  <div>
    <p>username: $store.state.username</p>
    <button @click="change">修改状态</button>
  </div>
</template>   
<script> 
export default   
    methods: 
      change() 
        // 提交mutation,参数1 mutation的名称, 参数2 新的数据
        this.$store.commit('set_username','李四'); 
      
    
;
</script>

项目应用

  1. 定义一个状态 phone, 值为空
  2. 登录成功之后, 修改phone的状态
  3. 在个人中心页面, 获取phone状态
    1. 若有phone, 显示phone
    2. 若没有, 就显示立即登录

vuex持久化

  1. 安装插件 npm i vuex-persistedstate -S
  2. 应用插件
import Vue from "vue";
import Vuex from "vuex";
// 导入持久化插件
import createPersistedState from "vuex-persistedstate"; 
Vue.use(Vuex); 
an 
const store = new Vuex.Store(
  plugins: [createPersistedState()],
)

(十七) 浏览器缓存cookie,sessionStorage,localStorage#

(1) 对比

  1. 三者都是浏览器缓存,可以将数据存储在浏览器上, 其中后两者是html5的新特性
  2. cookie存储容量较小,一般浏览器4KB, 后两者5M
  3. sessionStorage:临时存储, 浏览器关闭就销毁, localStorage: 永久存储, 销毁需要手动销毁

(2) 操作

  1. cookie使用相关js库 _js_-_cookie_
  2. sessionStorage,localStorage使用其自带方法
// 存储数据
localStorage.setItem(key,value);  // 比如:localStorage.setItem('username','张三')
// 获取数据
localStorage.getItem(key);        // 比如: localStorage.getItem('username');
// 清除数据
localStorage.clear();

(十八) token(令牌)和session(会话)#

相同点: 两者都是用来识别用户的

  1. session会话, sessionId
    1. 对于特定接口, 前端需要登录才能访问, 所以第一次访问时需要登录, 登录成功, 服务器会返回一个sessionId
    2. 下次前端再访问同一个接口的时候, 把sessionId带上(cookie), 这样服务器就能识别是谁在访问, 如果这个人已经登录过, 就不再需要再登录, session一般设有效期
  2. token令牌, 或叫同行证
    1. 前端在登录成功时, 服务器会把用户的相关信息加密, 得到一个密文, 这就是token, 返回给前端
    2. 前端再次访问接口时, 把token带上, 服务器端收到token就对它进行解密, 得到用户信息

项目应用

  1. 在vuex里定义token状态和相关的mutation
  2. 在登录成功的时候, 把token存入vuex
  3. 在axios的拦截器里, 把token放入请求头, 这样, 每次发请求的时候, 都会自动带上token
// 1.  在vuex里定义token状态和相关的mutation
state: 
    token: "",
    username: "张三",
    age: 100,
    phone: "123456789",
  ,
  // 定义mutaion
  mutations: 
    // 修改token
    set_token(state,payload) 
      state.token = payload
    ,
  
  
  
  // 2. 在登录成功的时候, 把token存入vuex
   $http.post('/user/login',data).then(res=> 
        // 把手机号码存入store, 修改phone状态
        this.$store.commit('set_phone',this.phone);
        // 把token存入store
        this.$store.commit('set_token',res.result.token);
        // 从哪里来回哪里去
        this.$router.go(-1); 
      )

  // 3. 在axios的拦截器里, 把token放入请求头, 这样, 每次发请求的时候, 都会自动带上token
  
  
import axios from "axios";
import Vue from "vue";
import  Toast  from "vant";
// 导入store
import store from '@/store/index';
Vue.use(Toast);
  
  
// request 对请求进行拦截
service.interceptors.request.use(
  (config) => 
    // 获取token
    let token = store.state.token;  
    // 开启loading
    Toast.loading(
      message: "加载中...",
      forbidClick: true,
      loadingType: "spinner",
    );
    // 请求头添加token
    config.headers["user-token"] = token;
    return config;
  ,
  (error) => 
    Promise.reject(error);
  
);

(十九) vue过滤器#

作用: 格式化数据

// 组件内的过滤器
<template>
  <div>
      num | f
  </div>
</template>   

<script>
export default 
  data() 
    return 
      num: 10
    
  , 
  filters: 
    f(num) 
      return Number(num).toFixed(2);
    
  

</script>

// 全局过滤器
Vue.filter('fMoney', (money)=> 
  let num = money/100;
  return num.toFixed(2);
)
new Vue()

// 定义好全局过滤器后, 组件内可以直接使用
<template>
  <div>
      num | fMoney
  </div>
</template>   

<script>
export default 
  data() 
    return 
      num: 1000
    
  , 

</script>

(二十) 微信支付-轮询和websocket#

(1) 微信支付流程#

  1. 用户点击提交订单, 商户(服务器端)创建订单, 并返回订单信息和支付二维码给用户
  2. 用户扫码支付(货值调起微信支付)
  3. 支付平台收到钱后, 返回支付信息给用户, 同时通知商户(服务器端)已收到用户的钱
  4. 商户(服务器端)修改订单的状态
  5. 用户(web端)获取支付结果, 得到结果后做相应操作
    1. 轮询方式
    2. websocket

(2) 获取支付结果的两种方式#

获取支付结果, 可以使用轮询或者websocket

  1. 轮询, 定时给服务器请求, 询问结果, 直到有结果为止, 轮询不需要服务器特别的支持
  2. websocket, 前端只需跟后台建立连接即可(长连接), 有了结果服务器可以给前端主动推送信息, websocket是长连接, 而http请求是一次性连接, websocket需要服务器端创建socket接口, 很多网站的客服服务就是使用websocket做的
// 轮询
<template>
  <div class="payment pay"></div>
</template>

<script>
export default 
  data() 
    return 
      timer: null,
      orderId: 'sdfasdfasdfasdfasdfasdfas'
    ;
  ,

  created() 
    this.waitResult();
  ,

  beforeDestroy() 
    // 销毁定时器
    clearInterval(this.timer);
  ,

  methods: 
    async waitResult() 
      // 创建定时器
      this.timer = setInterval(async () => 
        let res = await this.$axios.post("/order/detail", 
          orderId: this.orderId,
        );
        if (res.result.orderStatus === "01") 
          clearInterval(this.timer);
          // 支付成功, 返回首页
          this.$router.push("/");
        
      , 2000);
    ,
  ,
;
</script> 
// webSocket
<template>
  <div>result</div>
</template>

// webSocket
<template>
  <div>result</div>
</template>

<script>
export default  
  data() 
    return 
      result: ''
    
  ,

  created() 
    this.connect();
  ,
  methods: 
    connect() 
      this.result = '等待支付结果...';
      // 跟后端建立连接
      var ws = new WebSocket("ws://huruqing.cn:3003/socket");
      // onopen连接结果
      ws.onopen = () => 
        console.log("连接成功");
      ;
      // 等待后端推送信息
      ws.onmessage = (res) =>   
        this.result = res.data;
      ;
    ,
  ,
;
</script> 

(二十一) 进入组件, 滚动条不在顶部的问题#

解决办法

// router/index.js
const routes = [...];
const router = new Router(
  mode: "history",
  scrollBehavior: () => (
    y: 0
  ),
  routes
);

(二十二) keep-alive(背诵)#

问题: 用户从列表的第3页, 点击某个商品进入了商品详情, 当用户点击返回的时候, 默认会返回到列表页的第一页而不是第3页, 这样的体验很不好, 所以我们希望可以回到列表页的原来位置, 这样的用户体验会比较好. 分析: 之所以会回到第一页, 是因为返回到列表页的时候, 组件会重新创建, 从新执行created方法, 所以页面页重新渲染 解决: 使用keep-alive可以缓存组件的状态, 具体做法: (1) 对列表页使用keep-alive, 使其即使离开了组件, 也不会销毁 组件挂载完毕的时候绑定滚动事件, 记录滚动的位置 (2) 从详情页返回的时候, 滚动的原来的位置(在activated生命周期) **注: **

  1. 被keep-live包裹的组件会被缓存
  2. 使用keep-alive的组件crated和mounted只会执行一次
  3. 离开组件会触发deactivated生命周期(只有被缓存的组件才有的生命周期)
  4. 进入组件会触发activated生命周期
// 方法1 APP.vue
<template>
  <div id="app"> 
    <keep-alive>
      <router-view></router-view>
    </keep-alive>
  </div>
</template> 

// 方法2, 给路由配置keepAlive属性
// (1) /router/index.js
 
    path: "/product",
    component: () => import("@/views/product/index.vue"),
    redirect: "/product/list",
    children: [
      
        path: "list",
        // 缓存次组件
        meta: 
          keepAlive: true,
          tittle: '列表'
        ,
        component: () => import("@/views/product/children/list2.vue"),
      ,
      
        path: "detail/:productId",
        component: () => import("@/views/product/children/detail.vue"),
      ,
    ],
  ,

// APP.vue
<template>
  <div id="app"> 
      <!-- 渲染需要缓存的组件 -->
     <keep-alive>  
        <router-view v-if="$route.meta.keepAlive"></router-view>
     </keep-alive>

      <!-- 渲染不需要缓存的组件 -->
      <router-view v-if="!$route.meta.keepAlive"></router-view>
  </div>
</template> 

// 上面需求的实现
(1) 在mounted绑定window.scroll事件, 滚动的时候保存滚动条的位置
(2) 返回时候, 重新滚动到原来保存的位置 

  mounted() 
    window.addEventListener('scroll',()=>  
      // 保存滚动条位置
      if (window.scrollY>0) 
          this.scrollHeight = window.scrollY;
      
    ,false);
  ,  

  // 进入组件
  activated()  
    // 滚动到最初的位置
    setTimeout(()=> 
      window.scrollTo(0,this.scrollHeight); 
    ,0)
  ,

(二十三) 配置环境变量#

项目开发的时候, 一般会有多个环境, 比如开发环境, 测试环境, 生产环境, 我们调用接口的时候, 不同环境调用不同的接口, 所以要配置环境, ,方便访问。

// utils/http.js 核心代码

let env = process.env.NODE_ENV;
let baseURL;
// 开发环境
if (env === "development") 
  baseURL = "http://localhost:3003";
 else 
  baseURL = "http://huruqing.cn:3003";


const service = axios.create(
  // 如果换了新的项目, 需要更换为新的接口地址
  baseURL: baseURL,
  timeout: 50000, // 请求超时时间(因为需要调试后台,所以设置得比较大)
);

(二十四) rem移动端适配#

(1) 元素单位有哪些:#

(2) rem和根标签字体大小的关系#

// rem例子 demo1.html
<!DOCTYPE html>
<html lang="en" style="font-size: 100px;">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style> 
        div
            width: 1rem;
            height: 1rem;
            background-color: gray;
        
    </style>
</head>
<body>
    <div>

    </div>
</body>
</html>

// rem例子 demo1.html
<!DOCTYPE html>
<html lang="en" style="font-size: 112px;">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style> 
        div
            width: 1rem;
            height: 1rem;
            background-color: green;
        
    </style>
</head>
<body>
    <div>

    </div>
</body>
</html>

(3) 移动端rem适配原理#

  1. 设置一个设备参考值(比如iPhone6)
  2. 跟据设备宽度等比缩放根标签字体大小

(4) vue项目配置rem#

  1. 安装插 npm i amfe-flexible --save
  2. 在main.js导入插件 import 'amfe-flexible'
  3. px自动转rem
    1. 安装插件 

      以上是关于Vue实战(六)通用Table组件的主要内容,如果未能解决你的问题,请参考以下文章

      Vue实战封装一个简单的列表组件,实现增删改查

      Vue 开发实战基础篇 # 12:常用高级特性provide/inject

      vue实战:三分钟实现优雅弹框 [ 项目级组件封装 ][ 超详细 ]

      VUE项目实战38实现商品分类的树形表格

      vue 组件开发实战

      Vue 开发实战学习笔记48篇(完结)