封裝圖表元件
- 我們選擇免費的,功能比較多的 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
<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 頁面,你可以去看
特殊 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
未完待續…