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
未完待續…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章