part03~开发通用前端UI框架(图表封装,高效mock,请求封装,表单校验,图标管理)

封装图表组件

  • 我们选择免费的,功能比较多的 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

    1. 引入 axios
    2. 写请求数据的方法
    // 引入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 = nullswitch (method) {case 'GET':res = [200, 40, 44, 12, 34, 200]breakdefault: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-varsmessage: 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-itemlabel="Form Layout":label-col="formItemLayout.labelCol":wrapper-col="formItemLayout.wrapperCol"><a-radio-groupdefault-value="horizontal"@change="handleFormLayoutChange"><a-radio-button value="horizontal">Horizontala-radio-button><a-radio-button value="vertical">Verticala-radio-button><a-radio-button value="inline">Inlinea-radio-button>a-radio-group>a-form-item><a-form-itemlabel="Field A":label-col="formItemLayout.labelCol":wrapper-col="formItemLayout.wrapperCol"><a-input placeholder="input placeholder" />a-form-item><a-form-itemlabel="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">Submita-button>a-form-item>a-form>
template>
<script>
export default {data() {return {formLayout: 'horizontal'}},computed: {formItemLayout() {const { formLayout } = thisreturn formLayout === 'horizontal'? {labelCol: { span: 4 },wrapperCol: { span: 14 }}: {}},buttonItemLayout() {const { formLayout } = thisreturn 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-itemlabel="Form Layout":label-col="formItemLayout.labelCol":wrapper-col="formItemLayout.wrapperCol"><a-radio-groupdefault-value="horizontal"@change="handleFormLayoutChange"><a-radio-button value="horizontal">Horizontala-radio-button><a-radio-button value="vertical">Verticala-radio-button><a-radio-button value="inline">Inlinea-radio-button>a-radio-group>a-form-item><a-form-itemlabel="姓名":label-col="formItemLayout.labelCol":wrapper-col="formItemLayout.wrapperCol":validateStatus="userErrorStatus":help="userHelpText"><a-input placeholder="请输入用户名称" v-model="userName" />a-form-item><a-form-itemlabel="手机":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">Submita-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 } = thisreturn formLayout === 'horizontal'? {labelCol: { span: 4 },wrapperCol: { span: 14 }}: {}},buttonItemLayout() {const { formLayout } = thisreturn 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()// 添加要替换的 loadersvgRule.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
未完待续…


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部