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 = 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

源码地址:[email protected]:sunhailiang/vue-public-ui.git

欢迎加微信一起学习:13671593005
未完待续…

你可能感兴趣的:(vue实战,前端项目,前端)