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项目实战(移动端)#
- 相关资料
- (一) 创建项目
- (二) 禁用Eslint
- (三) devtool
- (四) 添加less支持
- (五) vue路由配置(背诵)
- (六) 父子组件通信(背诵)
- (七) axios拦截器(背诵)
- (八) Sticky 粘性布局
- (九) 图片懒加载
- (十) 全局注册组件
- (十一) slot插槽
- (十二) 使用ui库需要关注的三点
- (十三) 三种路由传参方式(背诵)
- (十四) 模拟数据
- (十五) 计算属性computed和属性观察watch
- (十六) vuex(背诵)
- (十七) 浏览器缓存cookie,sessionStorage,localStorage
- (十八) token(令牌)和session(会话)
- (十九) vue过滤器
- (二十) 微信支付-轮询和websocket
- (二十一) 进入组件, 滚动条不在顶部的问题
- (二十二) keep-alive(背诵)
- (二十三) 配置环境变量
- (二十四) rem移动端适配
- (二十五) mixin(混入)
- (二十六) watch监听对象
- (二十七) props检查类型
- (二十八) ref获取dom节点和子组件实例
- (二十九) nextStick
- (三十) 配置跨域和模拟数据
相关资料#
- vue-cli脚手架: vue2脚手架
- vue3脚手架: vite
- vue官网: [介绍 — Vue.js
- 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开发调试工具
- 下载 http://soft.huruqing.cn
- 添加到chrome扩展程序里
(四) 添加less支持#
-
npm install less less-loader@6.0.0 --save-dev
-
在vue文件这样写即可, scoped表示样式只在当前文件有效, 不会影响其他组件
ps: less-loader要安装6.0版本, 不然有兼容问题
<style lang="less" scoped> .box .text color: red; </style>
(五) vue路由配置(背诵)#
(1)一个简单路由配置#
npm i vue-router
安装路由插件- 在src创建views文件夹, 创建各个模块的组件
- 在src内创建router文件夹, 新建index.js(代码如下)
- 在main.js里, 把router挂载到vue的实例
- 配置路由出口, 详见下方第(2)点router-view
- 使用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#
- 路由出口
- 路由匹配到的组件将渲染在这里
- 在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组件中的属性,用来做选中样式的切换;
- 只要路由中包含to里面的路由, 就能匹配到, 就会高亮, 比如: /product, /product/list, /product/detail都会使下面的第二个router-link高亮
- 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配置方式
-
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
-
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面
-
使用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')
(六) 父子组件通信(背诵)#
知识点(背诵):
- 父传子: 父组件通过(绑定)属性的方式传数据给子组件, 子组件使用props接收数据
- 子传父: 父组件在子组件上绑定一个自定义事件, 子组件通过$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. 子传父#
- 父组件在子组件上绑定一个自定义事件(事件名称我们自己定义的, vue本身是没有这个事件的)
- 父组件给自定义事件绑定一个函数, 这个函数可以接受来自子组件的数据
- 子组件使用$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拦截器(背诵)#
- 对ajax请求进行拦截
- 在请求头添加token
- 对ajax响应数据进行拦截
- 统一处理请求失败的情况, 这样就不需要在每个组件里处理失败的情况
- 有些接口需要登录才能访问, 在没登录的情况下跳转到登录页面
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 粘性布局#
(九) 图片懒加载#
(十) 全局注册组件#
// 注册全局组件除了多了个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为例
- 属性, 该组件提供了哪些绑定属性
- 事件, 该组件提供了哪些事件
- 插槽, 该组件提供了哪些插槽
(十三) 三种路由传参方式(背诵)#
知识点:
- 通过params传参, 使用$route.params接收参数
- 动态路由传参, 使用$route.params接收参数
- 通过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;
(十四) 模拟数据#
- 文档地址: json-server - npm
- npm i json-server -g //全局安装
- 根目录创建db.json
- 启动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"
- 访问接口
http://localhost:3000/posts/1
- 将命令添加到package.json, 可以使用 npm run json 启动项目
"scripts":
"json": "json-server --watch db.json"
,
(十五) 计算属性computed和属性观察watch#
- computed的作用
- watch的作用
- 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创建的对象#
- 普通对象
- 创建对象
- 定义对象的属性
- 修改对象的属性
- 读取对象属性
- vuex
- 创建仓库
- 定义状态
- 修改状态
- 读取状态
(2) 相关概念#
- 概念vuex是什么: 创建一个仓库, 然后在仓库里定义若干状态, 并且管理这些状态. Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- vuex有哪几个核心概念, 都是用来做什么的
- state 定义状态
- getters 派生状态
- mutation 修改状态(同步)
- action 修改状态(异步)
- module 模块化
- 如何使用vuex进行跨组件通信
- 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 修改状态(异步)
- 定义状态
- 定义mutation, 通过mutation来修改状态
- 定义action , 通过action来提交(commit)mutation
- 用户派发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应用#
- 创建仓库
- 需要先安装vuex
npm i vuex --save
- 创建仓库
- 挂载仓库
- 需要先安装vuex
// 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,
,
);
- 获取状态
- 直接获取
this.$store.state.username
- 直接获取
<template>
<div>
<p>username: $store.state.username</p>
</div>
</template>
<script>
export default
created()
console.log(this.$store.state);
;
</script>
- 通过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>
项目应用
- 定义一个状态 phone, 值为空
- 登录成功之后, 修改phone的状态
- 在个人中心页面, 获取phone状态
- 若有phone, 显示phone
- 若没有, 就显示立即登录
vuex持久化
- 安装插件
npm i vuex-persistedstate -S
- 应用插件
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) 对比
- 三者都是浏览器缓存,可以将数据存储在浏览器上, 其中后两者是html5的新特性
- cookie存储容量较小,一般浏览器4KB, 后两者5M
- sessionStorage:临时存储, 浏览器关闭就销毁, localStorage: 永久存储, 销毁需要手动销毁
(2) 操作
- cookie使用相关js库
_js_-_cookie_
- sessionStorage,localStorage使用其自带方法
// 存储数据
localStorage.setItem(key,value); // 比如:localStorage.setItem('username','张三')
// 获取数据
localStorage.getItem(key); // 比如: localStorage.getItem('username');
// 清除数据
localStorage.clear();
(十八) token(令牌)和session(会话)#
相同点: 两者都是用来识别用户的
- session会话, sessionId
- 对于特定接口, 前端需要登录才能访问, 所以第一次访问时需要登录, 登录成功, 服务器会返回一个sessionId
- 下次前端再访问同一个接口的时候, 把sessionId带上(cookie), 这样服务器就能识别是谁在访问, 如果这个人已经登录过, 就不再需要再登录, session一般设有效期
- token令牌, 或叫同行证
- 前端在登录成功时, 服务器会把用户的相关信息加密, 得到一个密文, 这就是token, 返回给前端
- 前端再次访问接口时, 把token带上, 服务器端收到token就对它进行解密, 得到用户信息
项目应用
- 在vuex里定义token状态和相关的mutation
- 在登录成功的时候, 把token存入vuex
- 在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) 微信支付流程#
- 用户点击提交订单, 商户(服务器端)创建订单, 并返回订单信息和支付二维码给用户
- 用户扫码支付(货值调起微信支付)
- 支付平台收到钱后, 返回支付信息给用户, 同时通知商户(服务器端)已收到用户的钱
- 商户(服务器端)修改订单的状态
- 用户(web端)获取支付结果, 得到结果后做相应操作
- 轮询方式
- websocket
(2) 获取支付结果的两种方式#
获取支付结果, 可以使用轮询或者websocket
- 轮询, 定时给服务器请求, 询问结果, 直到有结果为止, 轮询不需要服务器特别的支持
- 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生命周期) **注: **
- 被keep-live包裹的组件会被缓存
- 使用keep-alive的组件crated和mounted只会执行一次
- 离开组件会触发deactivated生命周期(只有被缓存的组件才有的生命周期)
- 进入组件会触发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适配原理#
- 设置一个设备参考值(比如iPhone6)
- 跟据设备宽度等比缩放根标签字体大小
(4) vue项目配置rem#
- 安装插
npm i amfe-flexible --save
- 在main.js导入插件
import 'amfe-flexible'
- px自动转rem
- 安装插件
以上是关于Vue实战(六)通用Table组件的主要内容,如果未能解决你的问题,请参考以下文章
Vue 开发实战基础篇 # 12:常用高级特性provide/inject
- 安装插件