封裝圖表組件
-
我們選擇免費的,功能比較多的 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
源碼地址:[email protected]:sunhailiang/vue-public-ui.git
歡迎加微信一起學習:13671593005
未完待續…