封装图表组件
-
我们选择免费的,功能比较多的 Echart,当然了你也可以选择 AntV,也有 highChart
- 安装 echart: npm install echarts --save
- 新建 chart 组件库:components->chart->Chart.vue
<template> <div ref="chart" style="width: 600px;height:400px;"></div> </template> <script> import echarts from 'echarts' export default { name: 'Chart', mounted() { var myChart = echarts.init(this.$refs.chart) // 指定图表的配置项和数据 var option = { title: { text: 'ECharts 入门示例' }, tooltip: {}, legend: { data: ['销量'] }, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] } // 使用刚指定的配置项和数据显示图表。 myChart.setOption(option) } } </script> <style lang="less" scoped></style>
- 但是此时有些问题,就是这个组件的数据渲染的一些功能,有很多异步的操作,所以你想针对这个 dom 去操作时就会有问题,怎么办呢?
- 推荐一个 vue 中监听 dom 元素大小的库
- npm i --save resize-detector
<template> <div ref="chart" style="height:400px;"></div> </template>
import echarts from 'echarts' import { addListener, removeListener } from 'resize-detector' export default { name: 'Chart', mounted() { this.chart = echarts.init(this.$refs.chart) // 指定图表的配置项和数据 var option = { title: { text: 'ECharts 入门示例' }, tooltip: {}, legend: { data: ['销量'] }, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] } // 使用刚指定的配置项和数据显示图表。 this.chart.setOption(option) // 监听数据dom变化 addListener(this.$refs.chart, this.resize) }, methods: { resize() { console.log('变化了') this.chart.resize() }, removeChart() { console.log('卸载') } }, beforeDestroy() { // 卸载时移除监听事件 removeListener(this.$refs.chart, this.removeChart) // 始放图表组件,防止内存泄漏 this.chart.dispose() this.chart = null } } </script>
- 现在你改变页面布局你会发现一个问题,元素变化确实收到了,但是你仔细看控制台,一次页面的布局大小的变化要触发好多次,resize 事件
- 怎么解决这个问题?对!防抖函数!这样可以提升代码性能
- 我们之前引入的 lodash,lodash 就有一个防抖函数 debounce
import { debounce } from 'lodash' // 在created中添加一个debounce防抖函数 created() { this.resize = debounce(this.resize, 200) }
- 此时你在打开页面改变页面布局大小,就会发现多次触发 resize 的事件不在了
封装成通用的图表组件
- components->chart->Chart.vue
<script> import echarts from 'echarts' import { addListener, removeListener } from 'resize-detector' import { debounce } from 'lodash' export default { props: {// 关于图表的类型,咱们通过组件调用传参过来即可 option: { type: Object, default: () => {} } }, mounted() { this.renderChar() // 监听数据dom变化 addListener(this.$refs.chart, this.resize) }, methods: { // 纯粹的自定义组件 renderChar() { // 基于准备好的dom初始化chart示例 this.chart = echarts.init(this.$refs.chart) this.chart.setOption(this.option) }, resize() { console.log('变化了') this.chart.resize() } }, watch: { option(val) { // 这样有一个问题:option没有变化,但是option中的data数组如果变了是监视不到的,怎么办呢?用深度监听? this.chart.setOption(val) } // option: { // // 深度监听的写法:但是依旧很耗性能,怎么办呢?那我们还是采取第一种监听方式 // handler(val) { // this.chart.setOption(val) // }, // deep: true // // } }, beforeDestroy() { removeListener(this.$refs.chart, this.resize) // 始放图表组件,防止内存泄漏 this.chart.dispose() this.chart = null }, created() { this.resize = debounce(this.resize, 200) } } </script>
- Analysis.vue
<div><Chart :option="opitons" style="height:400px" /></div>
<script> // 引入公共的图表组件 import Chart from '@/components/chart/Chart' // 使用随机数 import { random } from 'lodash' export default { data() { return { // 指定图表的配置s项和数据 fuck: 'FUCK', opitons: { title: { text: 'ECharts 入门示例' }, tooltip: {}, legend: { data: ['销量'] }, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] } ] } } }, mounted() { setInterval(() => { this.opitons.series[0].data = this.opitons.series[0].data.map(() => random(100) ) // 重新赋值,是要数据发生变化就更新数据 this.opitons = { ...this.opitons } }, 800) }, components: { Chart } } </script>
前后分离之 MOCK 数据
- 就当前来看,项目开发中依旧推崇前后分离,也就是其实前端后端在最开始碰需求的时候,只要把数据结构和字段名称等等信息约定好以后,大家各自开发自己的
- 前后端并行,这样能提高开发效率,那么此时前端想模拟数据接口怎么办?是不是要跟后端要?
- 不!其实我们最开始已经约定数据结构和字段类型等等信息,那么我们可以通过 mock 的方式模拟接口,这样子,等到前后端对接数据的时候我们只要换掉接口即可立马打通数据
安装 axios->cnpm i axios
新建 service 文件夹->mock->index.js
-
Analysis.vue
- 引入 axios
- 写请求数据的方法
// 引入axios import axios from 'axios' mounted() { // 调用mock接口 this.getCharData() setInterval(() => { this.getCharData() // this.opitons.series[0].data = this.opitons.series[0].data.map(() => // random(100) // ) // // 重新赋值,是要数据发生变化就更新数据 // this.opitons = { ...this.opitons } }, 800) }, methods: { // 模拟mock数据 getCharData() { axios .get('/service/mock/chartData', { params: { ID: 12346 } }) .then(res => { this.opitons = { title: { text: 'ECharts 入门示例' }, tooltip: {}, legend: { data: ['销量'] }, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: res.data } ] } }) } },
service->mock->index
function chartData(method) { let res = null switch (method) { case 'GET': res = [200, 40, 44, 12, 34, 200] break default: res = null } return res } module.exports = { chartData }
配置 webpack->vue.config.js
- devServer
- https://webpack.js.org/configuration/dev-server/#devserverproxy
devServer: {
proxy: {
'/service': {
target: 'http://localhost:3000',
bypass: function(req, res) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.')
return '/index.html'
} else {
const name = req.path.split('/')[3]
const mock = require(`./service/mock/index`)[name]
const result = mock(req.method)
delete require.cache[require.resolve(`./service/mock/index`)] //清除缓存这样,每次你只要一修改mock数据页面及时刷新
return res.send(result)
}
}
}
}
}
- 但是此时还有一个问题,就是如果你改了 mock 数据,页面并不会立马更新,因为有缓存,
delete require.cache[require.r
esolve(`./service/mock/index`)] //清除缓存这样,每次你只要一修改mock数据页面及时刷新
与服务端发生交互快速切换 mock 和正式环境
- 说白了这一步就是区分一下环境变量,根据设置不同的环境变量来区分环境
- package.json
- 新增一个命令,设置 mock 环境标志,这样运行时就是 mock 状态
- 先安装 cnpm i cross-env 运行跨平台设置和使用环境变量的脚本
"scripts": {
"serve": "vue-cli-service serve",
// 新增serve:mock命令此时就会将MOCK设置成环境变量cross-env设置跨平台环境变量设置
"serve:mock": "cross-env MOCK=true vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
- vue.config.js
- 根据环境变量来切换是否走 mock 接口
devServer: {
proxy: {
"/service": {
target: "http://localhost:3000",
bypass: function(req, res) {
if (req.headers.accept.indexOf("html") !== -1) {
console.log("Skipping proxy for browser request.");
return "/index.html";
} else if(process.env.MOCK==='true') { // 通过环境变量来执行下面mock代理
const name = req.path.split("/")[3];
const mock = require(`./service/mock/index`)[name];
const result = mock(req.method);
delete require.cache[require.resolve(`./service/mock/index`)]; //清除缓存这样,每次你只要一修改mock数据页面及时刷新
return res.send(result);
}
}
}
}
}
统一管理接口,二次封装请求文件
- 新建 utils 工具箱
- utils 里面新建 request.js 文件用封装 axios
import axios from 'axios'
import { Notification } from 'ant-design-vue'
function request(options) {
return axios(options)
.then(res => {
return res
})
.catch(error => {
const {
response: { status, statusText }
} = error
// 请求失败提醒
Notification.error({
message: status,
description: statusText
})
// 返回reject的好处就是你在使用的时候,直接通过catch去捕捉,不会在进入then里面让你处理相关逻辑
return Promise.reject(error)
})
}
export default request
- 回到 Analysis.vue 中
// 引入封装好的方法
import request from '@/utils/request'
methods: {
// 模拟mock数据
getCharData() {
// 使用该方法
request({
url: '/service/mock/chartData',
method: 'get',
params: { ID: 12346 }
}).then(res => {
this.opitons = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: res.data
}
]
}
})
}
},
- 此时呢你运行页面你会发现,数据请求成功,如果你改变请求地址,你还会发现错误信息提醒
- 如果只不过呢,有一个问题如果我想给提示信息写一些特殊的样式怎么办?
- 很明显这个 js 文件没法写单文件组件,那么 render?还是 jsx?前者写法比较复杂,那么咱们引入 jsx 吧
怎么用呢? 看这:https://github.com/vuejs/jsx
npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props
babel.config.js 添加配置
module.exports = {
presets: ['@vue/cli-plugin-babel/preset', '@vue/babel-preset-jsx'] // 添加jsx配置
}
- request.js
import axios from 'axios'
import { Notification } from 'ant-design-vue'
function request(options) {
return axios(options)
.then(res => {
return res
})
.catch(error => {
const {
response: { status, statusText }
} = error
// 请求失败提醒
Notification.error({
// 注意了:下面的这句注释,是用来告诉eslint不用校验了,否则h没使用过就会报错
//eslint-disable-next-line no-unused-vars
message: h => (
// 注意看这里:咱们就可以使用jsx语法定义想要的样式了
<div>
请求错误:<span style="color:red">{status}</span>
<br />
{options.url}
</div>
),
description: statusText
})
// 返回reject的好处就是你在使用的时候,直接通过catch去捕捉,不会在进入then里面让你处理相关逻辑
return Promise.reject(error)
})
}
export default request
关于表单和表单校验(Antd)
- 最简单粗暴的方式
- 去 antd 复制粘贴一个基础表单出来
- Forms->BasicForm.vue
- 应该是这样的
<template>
<a-form :layout="formLayout">
<a-form-item
label="Form Layout"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-radio-group
default-value="horizontal"
@change="handleFormLayoutChange"
>
<a-radio-button value="horizontal">
Horizontal
</a-radio-button>
<a-radio-button value="vertical">
Vertical
</a-radio-button>
<a-radio-button value="inline">
Inline
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
label="Field A"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-input placeholder="input placeholder" />
</a-form-item>
<a-form-item
label="Field B"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-input placeholder="input placeholder" />
</a-form-item>
<a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
<a-button type="primary">
Submit
</a-button>
</a-form-item>
</a-form>
</template>
<script>
export default {
data() {
return {
formLayout: 'horizontal'
}
},
computed: {
formItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
}
: {}
},
buttonItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 }
}
: {}
}
},
methods: {
handleFormLayoutChange(e) {
this.formLayout = e.target.value
}
}
}
</script>
- 接下来我们去自定义校验
- 刚好 antd 也提供了自定义校验的东西
// 你看官方提供了这么写属性供咱们使用
validateStatus: 校验状态,可选 ‘success’, ‘warning’, ‘error’, ‘validating’。
hasFeedback:用于给输入框添加反馈图标。
help:设置校验文案
注意了:我们根据官方提供的这些属性改造一下表单校验
<template>
<a-form :layout="formLayout">
<a-form-item
label="Form Layout"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-radio-group
default-value="horizontal"
@change="handleFormLayoutChange"
>
<a-radio-button value="horizontal">
Horizontal
</a-radio-button>
<a-radio-button value="vertical">
Vertical
</a-radio-button>
<a-radio-button value="inline">
Inline
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
label="姓名"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:validateStatus="userErrorStatus"
:help="userHelpText"
>
<a-input placeholder="请输入用户名称" v-model="userName" />
</a-form-item>
<a-form-item
label="手机"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:validateStatus="phoneErrorStatus"
:help="phoneHelpText"
>
<a-input type="number" placeholder="请输入手机号码" v-model="phone" />
</a-form-item>
<a-form-item :wrapper-col="buttonItemLayout.wrapperCol">
<a-button type="primary" @click="submitHandle">
Submit
</a-button>
</a-form-item>
</a-form>
</template>
<script>
export default {
data() {
return {
userErrorStatus: '',
userHelpText: '',
phoneErrorStatus: '',
phoneHelpText: '',
userName: '',
phone: '',
formLayout: 'horizontal'
}
},
watch: {
// 监听校验
userName(val) {
if (val.length < 2) {
;(this.userErrorStatus = 'error'),
(this.userHelpText = '昵称长度不得少于两位')
} else {
;(this.userErrorStatus = ''), (this.userHelpText = '')
}
},
phone(val) {
if (val.length < 11) {
;(this.phoneErrorStatus = 'error'),
(this.phoneHelpText = '手机不得少于11位')
} else {
;(this.phoneErrorStatus = ''), (this.phoneHelpText = '')
}
}
},
computed: {
formItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 }
}
: {}
},
buttonItemLayout() {
const { formLayout } = this
return formLayout === 'horizontal'
? {
wrapperCol: { span: 14, offset: 4 }
}
: {}
}
},
methods: {
// 提交校验
submitHandle() {
if (this.userName.length < 2) {
;(this.userErrorStatus = 'error'),
(this.userHelpText = '昵称长度不得少于两位')
return
}
if (this.phone.length < 11) {
;(this.phoneErrorStatus = 'error'),
(this.phoneHelpText = '手机不得少于11位')
return
}
},
handleFormLayoutChange(e) {
this.formLayout = e.target.value
}
}
}
</script>
- 嗯…看起来没什么问题,好像实现了但是是不是有点繁琐了????很明显不够人性,智能化,如果是个大表单,有的忙了
- 不写了,自己去 Antd 看官方的动态校验规则(仔细研究文档,一切答案都有)
复杂的分布表单
- 结合 vuex
- store 中新建 moudles->form.js
- 看上源码三个地方
- store->moudles->form
- store-> index
- componens->ReceiverAccount.vue
- views->Dashboard->Forms->Step1~
如果你看不懂,你就留言给我,我带你看~关于组件的使用
关于项目中图标的管理
- 添加项目需要用的 iconfont
去阿里矢量图库->图标管理->我的项目->新建项目
关于 iconfont 的使用
-
已阿里 icon 库为例:https://www.iconfont.cn/
-
这是本地化操作
- 去 icon 官网找到适合的 iconfont
- 添加到购物车
- 将购物车中的要用的 icon 添加到项目
- 下载到本地
- 解压文件夹,将所有的字体文件和 iconfont.css 分别放到资源文件夹下
- 修改 iconfont.css 中字体路径
- 删除默认图标,直接使用 64 位或者 16 进制
- main.js 中引入 iconfont.css
-
这是使用 cdn 的方式
- 将你需要的 icon 选中添加购物车
- 将购物车内的 icon 添加到项目
- 选择 Symbol 类型
- 查看在线链接
- 去 main.js
// 使用Icon import { Icon } from 'ant-design-vue' // 将cdn地址换成阿里图标我们的地址 const IconFont = Icon.createFromIconfontCN({ scriptUrl: '//at.alicdn.com/t/font_1729142_92zhgmdrlj8.js' }) // 全局注册 Vue.component('IconFont', IconFont)
-
然后你可以选择在任何地方使用这个图片,这个 type 就是你图标库中的图标的名称
-
注意:你可以改图标库中的名称等信息,但是你改完之后,会重新生成一个地址,你只要把那个地址重新覆盖到我们本地项目中就行了
-
我使用了 404 的图标放在 404 页面,你可以去看
<IconFont type="iconicon-404"></IconFont>
特殊 ICon
- 以上呢是现有满足我们的 icon,那如果我们设计师给我们特殊的 icon 呢?
- 假设你已经拿到设计师设计好的 SVG 文件
- 你可以直接引入这个 svg,然后给 img 的 src 属性就行了
- 这里我们采用组件式的 SVG 更加方便一点,何为组件式?意思是一旦这样配置之后,我们将会向用组件一样用 SVG
- 首先我们需要去 vue.config.js 中添加一个vue-svg-loader配置,如果没有需要安装一下
- vue.config.js
chainWebpack: config => {
const svgRule = config.module.rule('svg')
// 清除已有的所有 loader。
// 如果你不这样做,接下来的 loader 会附加在该规则现有的 loader 之后。
svgRule.uses.clear()
// 添加要替换的 loader
svgRule.use('vue-svg-loader').loader('vue-svg-loader')
}
- 然后呢,你照常 import 这个 svg 就像这样
- 404 页面
<template>
<div style=" text-align:center">
<!-- 看,当组件用了,是不是方便一些 -->
<Man />
</div>
</template>
import Man from '@/assets/man.svg' // 注意哦,此时你引入的是一个组件哦,所以需要干啥子?没错就是要注册
export default {
components: {
// 注册组件
Man
}
}
如何查看你配置的 loader 等配置项呢?
- vue inspect > output.js
源码地址:git@github.com:sunhailiang/vue-public-ui.git
欢迎加微信一起学习:13671593005
未完待续…