玩轉 CMS2 上篇研究了樣式、請求、evn、mock,感覺對效率的提升沒有太明顯作用。 比如某個工作需要2天,現在1天可以幹完,這就是很大的提升。 提高效率的方法有代碼復用、模塊化、低代碼工具。 目前可以考慮從代碼復用方面下手,即使最低級的代碼複製也可以。 要快速提高效率,需要對本地項目中的一些關 ...
玩轉 CMS2
上篇研究了樣式
、請求
、evn
、mock
,感覺對效率的提升沒有太明顯作用。
比如某個工作需要2天,現在1天可以幹完,這就是很大的提升。
提高效率的方法有代碼復用、模塊化、低代碼工具。
目前可以考慮從代碼復用方面下手,即使最低級的代碼複製
也可以。
要快速提高效率,需要對本地項目
中的一些關鍵流程和技術比較瞭解,清楚常用功能實現思路和手段:
- 如何快速開發產品提出的一些常用頁面(功能)
- 現存有哪些可復用組件
- 常用佈局
- 本地項目常見 Bug
Tip: 移動端的後續在安排,比如:H5中常用佈局、如何快速調試H5、端內H5問題排查...
常用頁面(功能)
客戶端下載配置頁
需求:以前安卓和ios的下載頁是在h5中寫死的,現在需要將這部分改成配置(h5項目中 download.vue 中的數據來自cms 配置的 json 數據),於是需要增加一個下載頁
。
技術手段:
- 創建 downloadPage.vue,
編輯下載頁
和新建下載頁
,兩個路由指向同一個組件。通過this.$route.query.id
區分編輯和新建。
$route和$router都可以通過 this 直接獲取。
給路由傳遞參數有三種形式:params、search、state(可參考 react 這裡)。這裡使用 search 方式
- 新建下載頁:,在CMS系統中配置菜單就可以訪問該頁面。系統已經將動態路由這部分做成配置。
註:新增下載頁時,CMS系統中配置菜單,但頁面找不到。重新登錄也不行,最後本地重啟服務、重新登錄即可,不需要添加其他代碼,或修改許可權 —— 暫時不深究。
核心知識點
- 最外層使用
<a-card>
卡片容器,可承載文字、列表、圖片、段落,常用於後臺概覽頁面。有點像div,但提供了一些更豐富的東西。 - 表單使用 FormModel 組件。從官網介紹來看,FormModel組件和 Form組件 一樣,但FormModel使用
v-model
,感覺更新,更簡單。4.x 版本不像 1.x 提供了 FormModel和Form 兩個組件,只有一個 Form 組件,並且使用的是 v-model,沒有 1.x 中 Form 中的 v-decorator(這個東西看起來有點難)。
Tip: v-bind="formItemLayout" 用法類似 v-bind=$attrs(可參考 爺孫傳遞數據),將屬性一次性傳給組件
<a-form-model v-bind="formItemLayout">
等價於
<a-form-model
:labelCol="{ span: 5 }"
:wrapperCol="{ span: 14 }"
>
Tip:更多知識點請看下麵代碼的註釋。
核心代碼
<template>
<a-card>
<!--
FormModel 表單 (支持 v-model 檢驗)(版本:1.5.0+):具有數據收集、校驗和提交功能的表單,包含覆選框、單選框、輸入框、下拉選擇框等元素。
layout - 表單佈局。水平佈局如果不配合labelCol、wrapperCol,則仍舊是垂直佈局。
ref - 用於取得表單組件,可用於校驗
model - 表單數據對象
rules - 表單驗證規則
-->
<a-form-model
layout="horizontal"
ref="ruleForm"
:model="form"
:rules="rules"
v-bind="formItemLayout"
>
<!--
prop 就是一個傳給組件(a-form-model-item)的屬性
官網:Form 組件提供了表單驗證的功能,只需要通過 rules 屬性傳入約定的驗證規則,並將 FormItem 的 prop 屬性設置為需校驗的欄位名即可
如果將 prop 屬性註釋掉,點擊保存或 blur 都不會觸發
-->
<a-form-model-item ref="title" label="標題" prop="title">
<a-input
placeholder="請輸入標題"
v-model="form.title"
@blur="() => {
$refs.title.onFieldBlur();
}
"
>
<!--
Input 輸入框中的`首碼和尾碼`:https://1x.antdv.com/components/input-cn/#components-input-demo-prefix-and-suffix
-->
<span slot="addonAfter">{{ form.title.length }}/50</span>
</a-input>
</a-form-model-item>
<a-form-model-item label="應用簡介" prop="description">
<!--
沒有一個單獨的 textarea,只有 input
4.x 有 maxlength 屬性,只能輸入約定的字元個數
-->
<a-input
v-model="form.description"
type="textarea"
:autoSize="{ minRows: 4, maxRows: 9 }"
/>
<div class="words-color">
當前字數{{ form.description.length ?
form.description.length : 0 }}/1000
</div>
</a-form-model-item>
<!-- 通過 required 增加一個必傳的樣式 *,不知道是否有其他副作用 -->
<a-form-model-item label="應用截圖" required>
<a-row>
<a-col :span="24">
<ApplicationPhone :phones="phones"/>
</a-col>
</a-row>
</a-form-model-item>
</a-form-model>
<fixedBar>
<div class="options-btn">
<!-- 氣泡確認框 -->
<a-popconfirm title="關閉頁面將丟失已輸入內容?" @confirm="cannoe">
<a-button class="btn">關閉</a-button>
</a-popconfirm>
<a-button type="primary" @click="saveform" class="btn">保存</a-button>
</div>
</fixedBar>
</a-card>
</template>
<script>
export default {
// name 組件屬性有作用,比如跳轉到其他頁面,回來要保持這個裝填。這裡不需要,直接去掉
data () {
return {
form: {
// 標題
title: '',
// 應用簡介
description: '',
// 應用名稱
appName: '',
// 安卓下載地址
androidUrl: '',
},
rules: {
title: [
{ required: true, message: '請輸入標題', trigger: 'blur' },
{ min: 1, max: 150, message: '最多輸入150個字元', trigger: 'blur' },
],
iosUrl: [
{ required: true, message: '請輸入iOS下載地址', trigger: 'blur' },
// 參考:https://github.com/yiminghe/async-validator
{ type: 'url', message: '請輸入url類型', trigger: 'blur' },
{ min: 1, max: 800, message: '最多輸入800個字元', trigger: 'blur' },
],
},
// 這個組件會通過路由跳轉至此,所以直接通過 $route 取得數據即可。就像這樣:/foo?user=1 通過 $route.query.user 取得 user 的值
id: this.$route.query.id,
}
},
computed: {
formItemLayout () {
return true
? {
labelCol: { span: 4 },
wrapperCol: { span: 14 },
}
: {};
},
isEdit () {
return !!this.id
},
},
created () {
if (this.id) {
this.getDataList()
}
},
methods: {
async getDataList () {
if (res.code === 0) {
this.form.title = res.data.title
const json = JSON.parse(res.data.contentTxt)
// 報錯 —— 一度懷疑不能這麼設置form 的值。其實這麼寫沒問題,出錯是 json 對象缺少 title 屬性,於是模板中就報錯了
// this.form = {...json};
this.form = { ...this.form, ...json }
json.phones.forEach((item, i) => this.phones[i].imageUrl = item)
}
},
cannoe () {
this.$router.back(-1)
},
saveform () {
/*
this.$refs.ruleForm.validate(valid => {
if (!valid) {
console.log('驗證失敗');
return false
}
});
*/
// promise 的寫法
this.$refs.ruleForm.validate().then(() => {
// 應用圖標必傳驗證
if (!this.form.logoUrl) {
// Message 全局提示,根據ui文檔寫即可
this.$message.error('請上傳xxx')
return false
}
this._saveform()
}).catch(error => {
// 驗證失敗
})
},
async _saveform () {
if (this.id) {
if (res.code === 0) {
this.$message.success(res.msg)
// 對於統一的編碼規範和標準化建議,推薦使用 this.$router.go(-1) 來進行路由的回退操作
this.$router.back(-1)
}
} else {
if (res.code === 0) {
this.$message.success(res.msg)
this.$router.back(-1)
}
}
},
}
}
</script>
page2
核心知識點
- table 中的具名插槽使用的是 slot-scope,自 vue 2.6.0 起被廢棄。
- a-row 屬於 Grid 柵格。支持 flex,比如垂直方向的對齊、水平方向的對齊。比原生的 flex(flex 佈局的基本概念) 簡單點
- 將搜索中的 a-form 改成 a-form-model(筆者感覺 FormModel 更簡單),雖然不需要校驗也可以用起來。其中如果需要 Reset 功能,需要設置 prop 屬性,否則不生效。
核心代碼
<template>
<a-card>
<!-- 為了實現現在效果,這麼寫感覺很怪 -->
<a-row type="flex" justify="space-between" align="middle">
<a-col></a-col>
<a-col>
<router-link to="/demo/add">
<a-button type="primary" icon="plus">添加</a-button>
</router-link>
</a-col>
</a-row>
<!-- 將搜索中的 a-form 改成 a-form-model... -->
<a-row type="flex" justify="space-between" align="middle">
<a-col :span="24">
<a-form-model ref="ruleForm" layout="inline" :model="listQuery">
<a-form-model-item prop="title">
<a-input placeholder="關鍵詞" v-model="listQuery.title" />
</a-form-model-item>
<a-form-model-item prop="state">
<a-select allowClear v-model="listQuery.state" placeholder="全部狀態" @change="stateChange">
<a-select-option
v-for="item in stateList"
:key="item.id"
:value="item.id"
>{{item.name}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item>
<a-button type="primary" @click="handleQuery()">查詢</a-button>
<a-button style="margin-left: 15px" @click="resetQueryForm()">清空</a-button>
</a-form-model-item>
</a-form-model>
</a-col>
</a-row>
<a-table
...
>
<span slot="index" slot-scope="text, record, index">
{{
(listQuery.current - 1) * listQuery.size + index + 1
}}
</span>
<template slot-scope="text, record" slot="state">
<span class="statustag s1" v-if="record.state === 1">啟用</span>
<span class="statustag s2" v-else>禁用</span>
</template>
<template slot="operation" slot-scope="text, record">
<a
href="javascript:;"
@click="copyLink(record.xx)"
title="複製鏈接"
>鏈接</a>
<a
href="javascript:void(0);"
@click="offlineFn(record.id)"
v-if="record.state === 1"
>禁用</a>
<a href="javascript:void(0);" @click="onlineFn(record.id)" v-else >啟用</a>
<router-link :to="'/demo/edit?id=' + record.id">編輯</router-link>
<a-divider type="vertical" />
<a href="javascript:void(0);" @click="logFn(record)">日誌</a>
<a-divider type="vertical" />
<a-popconfirm title="您確認要刪除嗎?" @confirm="() => deleteItem(record.id)">
<a href="javascript:void(0);">刪除</a>
</a-popconfirm>
</template>
</a-table>
<!-- main.js 中引入的組件,在 components 中,有 readme.md 說明文檔。 -->
<footer-tool-bar>
<a-pagination
...
/>
</footer-tool-bar>
</a-card>
</template>
page3
核心代碼
<template>
<a-card>
<a-form :form="form" :model="queryParams" layout="inline">
<!-- v-decorator 是 Ant Design Vue 提供的一個指令,用於表單校驗和數據綁定。 -->
<a-form-item>
<a-select
v-decorator="['type']"
allowClear
@change="typeChange"
placeholder="xx"
>
<a-select-option :value="item.value" v-for="(item, i) in typeList" :key="i">{{item.cnt}}</a-select-option>
</a-select>
</a-form-item>
<!--
DatePicker 組件
v-decorator="['rangeDate']" 改成 v-model="queryParams.rangeDate"
-->
<a-form-item>
<a-range-picker
v-model="queryParams.rangeDate"
@change="dateRangeChange"
:format="dateFormat"
:ranges="{ '今天': [$moment(), $moment()], '近3天': [$moment().subtract(2, 'days'), $moment()],'近一周': [$moment().subtract(6, 'days'), $moment()] }"
style="width:260px;"
>
<a-icon slot="suffixIcon" type="calendar" />
</a-range-picker>
</a-form-item>
<a-form-item>
<a-input v-model="queryParams.昵稱" @change="nickNameChange" placeholder="用戶名稱" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleQuery()">查詢</a-button>
</a-form-item>
</a-form>
<a-tabs
:animated="false"
>
<a-tab-pane v-for="tab in tabList" :key="tab.key">
<span slot="tab">
{{tab.value}}
<sup>0</sup>
<sup>{{cnt[tab['number']]}}</sup>
</span>
</a-tab-pane>
</a-tabs>
<!-- 多個 tab 對應一個table -->
<a-table
...
>
...
</a-table>
<footer-tool-bar>
<a-pagination
...
/>
</footer-tool-bar>
</a-card>
</template>
Tip:其中的表單、表格、審核等部分可以從 index.vue 中提取出去,作為一個單獨的文件引入,就像 spug 開源項目中一樣。
防抖和節流
防抖
- 1秒內,只要有新的觸發產生,則從0開始計時。
節流
- 1秒內,只要有新的觸發產生則無效,除非之前的操作執行完。
Tip:原生input事件和change 事件觸發時機感覺有些不好理解。可參考這裡
比如在 input 中增加 lazy,可以實現防抖效果:<input v-model.lazy="msg">
。當blur或回車時,msg的值就會更新。
但是 <a-input v-model.lazy="queryParams.昵稱"
失效,輸入一個字元 msg 就會同步更新。可能是 ant design vue 1.x 不支持lazy。筆者使用 lodash 實現防抖效果。請看代碼:
<script>
function debounce (func, delay) {
return _.debounce(func, delay);
}
export default {
created () {
this.getDataList()
// 提前創建一個防抖函數
this.debouncedHandleQuery = _.debounce(() => {
this._handleQuery();
}, 200); // 設置延遲時間為200毫秒
},
methods: {
handleQuery () {
this.debouncedHandleQuery()
},
_handleQuery () {
if (this.queryFlag) return
this.queryParams.current = 1
this.getDataList()
},
}
}
編輯按鈕的許可權
比如某頁面中的編輯按鈕,有許可權就顯示,否則隱藏。
許可權通過 this.$store.getters. 獲取,就像這樣:
/*
登錄成功後,後端返回的數據包括許可權,
"permissions": [
"demo:cms:push", // 推送許可權
...
*/
if (this.$store.getters.permisaction.includes('demo:cms:push')) {
this.isEditPerxx = true
}
擴展
this.$message
比如保存成功或失敗,項目中會通過 this.$message.xx
,這裡是 ant design vue 框架中提供的Message 全局提示
回退操作
編輯完成,需要返回之前的頁面。
按照官方文檔的建議,推薦使用 this.$router.go(-1) 來實現路由的回退操作,因為 this.$router.back(-1) 和 this.$router.back() 實際上是 this.$router.go(-1) 的簡寫形式
Tip: this.$router.push('/')
和 this.$router.push({ path: '/' })
等價
params 是否可以不通過 path 傳遞
params 是否可以不通過 path 傳遞?
比如項目中 path 不是/push/:id/:title,而是 /push/id/title。而編程導航卻出現如下代碼:
this.$router.push({ name: 'demo2', params: { title: record.title, id: record.id, address: record.address } })
跳轉到名為 demo2 的路由,通過 params 傳遞參數。
說這種方式刷新頁面後,就不能獲取參數了。
現存可復用組件
Tip:沒必要在這裡寫出來。
ant design pro 自帶的組件在 components 文件夾中
本地項目自己擴展的在 myComponents 文件夾中
通過vscode 搜索 @/myComponents/
發現常用的有上傳相關組件,其他常用的粗略感覺只有不到十個。稍微知曉一下這幾個常用的組件即可。
Tip: 其中 ant design vue 中 components 中有的組件配有 readme.md,介紹該組件,這個就很好。
佈局
ant design vue 中佈局有:Grid 柵格、layout 佈局、Space 間距
其中 Grid 柵格支持flex,layout 佈局可能更適合整體佈局,Space 間距是個不錯的東西,但本地項目沒有使用起來。
Tip:這幾個UI提供的佈局,感覺可以大範圍用起來,避免在代碼中寫很多零碎的css佈局相關代碼,不好維護。
本地項目常見 Bug
輸入超過50字後無法提交
有可能一個是50,一個是100,就像這樣:
<a-input
placeholder="請輸入標題(必填)"
maxLength="50"
@change="titleChange">
<span slot="addonAfter">{{queryParams.title ? queryParams.title.length : 0}}/100</span>
</a-input>
某一級導航切換到另一個頁面,瀏覽器崩潰
原因在於後端返回5000條數據,前端一次性渲染,導致性能受阻。在該頁面進行其他操作(比如編輯、刪除)也會很慢
優化手段:後端先返回一級列表數據,點擊某個數據後再去後端請求數據
保留上一次篩選條件
搜索中輸入關鍵字 pengjiali
,點擊“搜索”按鈕,在查詢出的數據中點擊編輯,編輯完該數據後點保存按鈕,然後返回到該頁面,未記住上一次搜索的信息 —— 也就是 pengjiali
沒有了。
解決方法是:在該頁面中的 beforeRouteLeave
(Vue Router 的導航守衛之一,用於在離開當前路由前執行邏輯) 中更新更新需要緩存的組件。這裡使用了<keep-alive
(是一個抽象組件,用於緩存動態組件)。
相關代碼如下:
export default {
// 組件名稱。非常重要,必須和 {1} 處保持一致
name: 'name001',
beforeRouteLeave (to, from, next) {
if (this.xxRouteNames.includes(to.name)) {
this.$store.commit('setKeepAlive', ['name001']) // {1}
} else {
this.$store.commit('setKeepAlive', [])
}
next()
}
setKeepAlive 是 mutations,用於更新需要緩存組件的變數 cacheArray:
const app = {
state: {
// 動態緩存需要的組件
cacheArray: []
},
mutations: {
setKeepAlive: (state, keepAlive) => {
state.cacheArray = keepAlive;
},
cacheArray 用在 <keep-alive
中:
<template>
<div id="app">
<keep-alive :include="cacheArray">
<router-view />
</keep-alive>
</div>
</template>
<script>
export default {
name: 'RouteView',
computed: {
cacheArray() {
return this.$store.state.app.cacheArray;
}
}
編輯某條數據後,返回到列表頁,列表信息未更新
Tip:編輯頁和列表頁屬於不同的路由,或者說是兩個頁面。
在 activated 中重新請求數據即可:
activated () {
this.getDataList()
},
在 Vue.js 中,activated 是一個生命周期鉤子函數,用於處理組件被激活時的邏輯。
當使用 Vue Router 進行頁面導航時,如果路由組件在之前已經被渲染過,並且現在再次被訪問,那麼它的 activated 鉤子函數將會被調用。
通常情況下,可以在 activated 鉤子中執行一些需要在組件被重新激活時處理的邏輯,比如數據刷新、重新載入資源、定時器的重新啟動等。
編輯操作取消勾選或者勾選後,點擊取消按鈕,再次點擊編輯,會保留上一次的編輯結果
Tip:編輯是一個彈框,裡面有很多 checkbox
在彈框的取消
事件中重新請求數據,用於初始化狀態:
+ onCancel() {
+ this.show = false
+ this.getDataList()
+ },
翻到第二頁,輸入關鍵字,點擊搜索,還是搜索的第二頁
點擊搜索時重置 current 為 1:
handleQuery () {
+ this.listQuery.current = 1
this.getDataList();
},
表格錯行顯示
使用 ant design vue 的 table 組件,有時會使用 fixed: left、fixed: right(例如將操作
固定在最右側)。
某些情況就會出現錯行顯示
在 1.x 中如果設置了 fixed: left、fixed: right,這個表就會變成三個表,在 4.x 中使用高級css屬性,只會生成一個表。
筆者將某一列的固定寬度去除,也就是留一列不設置即可。但某些數據下還是會有問題,比如某列很長,某列又太短,這時可以配合 scroll.x。
Tip: 若列頭與內容不對齊或出現列重覆,請指定固定列的寬度 width。如果指定 width 不生效或出現白色垂直空隙,請嘗試建議留一列不設寬度以適應彈性佈局,或者檢查是否有超長連續欄位破壞佈局。
建議指定 scroll.x 為大於表格寬度的固定值或百分比。註意,且非固定列寬度之和不要超過 scroll.x —— 官網 table-cn
{
title: 'desc',
dataIndex: 'desc',
- width: 240,
scopedSlots: { customRender: 'desc' }
})
編輯器中正文只添加引號或問號,保存後未生效
編輯器使用的是:一個“包裝”了 UEditor 的 Vue 組件 —— vue-ueditor-wrap
特殊字元都會保存不上,比如 $。改成 observer
模式即可。
Tip:listener 模式藉助 UEditor 的 contentChange 事件,優點在於依賴官方提供的事件 API,無需額外的性能消耗,瀏覽器相容性更好。但缺點在於監聽不准確,存在如“特殊字元(? ! $ #)輸入時不觸發”的 BUG —— 官網
表格子級序號錯誤
16
11
11
11
12
修複如下:
<span slot="index" slot-scope="text, record, index">
{{
- (listQu.current - 1) * listQu.size + index + 1
- }}
+ Object.is(record.pid, 0) ? ((listQu.current - 1) * listQu.size + index + 1) : (index + 1)
+ }}
</span>
出處:https://www.cnblogs.com/pengjiali/p/18023789
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。