前端如何修改組件庫源碼來封裝符合自己需求的組件?
前端開發的同學們或許會遇到這樣的問題:產品中需要實現某項功能,常用的 elementui、antd 等組件庫中確實有差不多功能的組件。但實際上這些組件可能并不能滿足你的功能,或多或少都需要你去看看如何修改它才能滿足你的需求。
比如我曾遇到過 element-ui 中的「樹形控件」暴露出的參數沒有我需要的(獲取參數);或者是「對話框」組件我需要給它的 body 加上上下兩條 border 等(樣式修改);還有「級聯選擇器」的多選可搜索功能:需要修改級聯看板使它保持展開,且當子節點全部選中時,不展示全部子節點 tag 而只展示它的父節點 tag(源碼無此功能)。
例如,我需要的功能是左二(因為我不想選項過多時 tag 占得位置太大),而原組件如左一。我截了兩張圖對比:
關于組件庫可能要修改的地方,我將它們分為以下五類可供參考:
- 樣式問題
- 組件暴露的參數和方法不充分(源碼中存在)
- 可利用部分功能,其余功能要自己開發封裝
- 2+ 個組件之間的聯動,多合一
- 完全沒有符合的組件
下面詳細說說這些問題,以及如何解決這些問題。如果有不滿足或更好的建議,歡迎指出。
1、組件樣式問題
當修改單個文件的樣式時,以 less 為例,如果你想要修改組件的樣式,可以使用 /deep/ 或 >>> 來深度選擇到你要修改的樣式(這能夠幫你省去一大串的類名)。
- .dialog-wrapper {
- /deep/.el-dialog__body{
- border: solid 1px #999;
- }
- }
如果你要修改全局的樣式,第一種方法,你可以在全局樣式文件中寫樣式覆蓋,引入到 main.js 中即可全局生效。如下:
- import "./assets/css/index.css";
第二,跟著組件庫提供的『自定義主題』教程修改,一般組件庫都會給出相關的教程。
2、組件暴露的參數和方法不充分
首先提出一個問題,你如何知道組件暴露的參數和方法不充分?其實答案很簡單:因為我看了組件庫的源碼。
當我們想要獲取組件的一個參數,首先是看文檔中提供了哪些 Attributes、Events、Methods。如果符合需求,直接拿來用就好。如果沒有你要的屬性和方法,請你先去看看源碼中提供了哪些東西沒有向外暴露出來的,但是我們能拿來用的。
舉個🌰,上述的「Cascader 級聯選擇器」,我想要在選中一個搜索的選項后不關閉看板。我在組件的 Events、Methods中沒有找到相關的方法控制看板展開,如下:
img
但當我去 github 上看該組件的源碼時,我發現 toggleDropDownVisible() 方法是控制看板展開的。于是我在外部用 $refs 直接調用組件里的這個方法就好了。圖片
具體調用方法如下,這樣即使方法沒有暴露出來,也可以調用它內部的方法:
- <el-cascader
- ref="cascader" // ref獲取組件
- placeholder="試試搜索:指南"
- :options="options"
- :props="{ multiple: true }"
- filterable></el-cascader
- @visible-change="$refs.cascader.toggleDropDownVisible(true)"> // 調用組件及其方法
3、可利用部分功能,其余功能要自己開發封裝
第三類其實我們用到的已經比較少了,畢竟現在的組件庫已經非常豐富了。但是這一步引起的思考確是很重要的,多看別人的源碼有助于提高自己封裝組件的水平。
當要用到一個組件,但從頭開發這個組件既復雜又耗時,而組件庫中這個組件需要再往上加一些功能就能為你所用時,你可以考慮把組件庫的代碼拿到自己本地,修改它。
第一步你需要將組件代碼瀏覽一遍,了解它的邏輯。看看你需要加什么代碼,如果在 vue 中,使用 computed、watch,或是修改 created、mounted、methods 就能完成你的功能,那么就大膽地嘗試。
舉個🌰
在 element-ui 中,它提供的多選可搜索級聯組件有一個問題:當用戶選中全部子節點時不會合并為顯示父節點。要想完成這個功能,在經歷過上述步驟一番探索后發現還是要修改源碼才能完成。于是我基于原本多選可搜索的級聯選擇器,進行以下優化:
- 默認看到級聯看板展開,不會收起
- @visible-change="blurCascader(true)" // 可觸發展開
- mounted() {
- this.blurCascader(true)
- }
- // 失焦后觸發展開級聯看板(默認失焦后關閉看板)
- blurCascader() {
- this.$nextTick(() => {
- this.$refs.cascader.toggleDropDownVisible(true) // 調用組件內部未暴露的方法
- })
- },
- 搜索選中后展示級聯看板,并勾選搜索選中的節點
- // 響應選中的節點,選中節點后關閉選擇看板,展示級聯看板
- changecascader(e) {
- this.$refs.cascader.handleDropdownLeave()
- },
- 當子級節點全部選中后,tag只展示一個父級節點,而不是全部子節點
- // 獲取所有勾選的節點
- getPresetTags() {
- const tree = this.panel.menus[0]
- const result = []
- loop(tree)
- // 遞歸查找選中的節點
- function loop(tree = []) {
- for (let i = 0; i < tree.length; i++) {
- const child = tree[i]
- if (child.checked) { // checked 狀態表示選中
- result.push({ ...child, closable: true })
- } else if (child.indeterminate) { // indeterminate 狀態表示待定,是半選
- child.children && loop(child.children)
- }
- }
- }
- this.presentFormatTags = result // 得到可顯示的 tag
- },
- 刪除節點
由于我修改了 tag 的展示,所以它的 deleteTag 事件也要重寫。
- deleteTag(index, tag) {
- let _ = this
- if (tag && tag.hasChildren) {
- // 當刪除的節點是父節點時
- loop(tag.children)
- function loop(list) {
- for (let i = 0; i < list.length; i++) {
- if (list[i].hasChildren) {
- loop(list[i].children)
- } else {
- __.checkedValue = _.checkedValue.filter(n => n !== list[i].path)
- _.$emit('remove-tag', tag)
- }
- }
- }
- } else if (tag) {
- // 當刪除的是子節點時
- thisthis.checkedValue = this.checkedValue.filter((n, i) => n !== tag.path)
- this.$emit('remove-tag', tag)
- } else {
- // 當以回車鍵刪除時
- const temp = this.presentFormatTags[this.presentFormatTags.length - 1]
- temp && this.deleteTag(null, temp)
- }
- // 原本這個方法的代碼如下
- // const { checkedValue } = this
- // const val = checkedValue[index]
- // this.checkedValue = checkedValue.filter((n, i) => i !== index)
- // this.$emit('remove-tag', val)
- }
其實修改組件庫代碼的過程并不難,主要是看懂它的邏輯,以及其中哪些東西是你可以用的。
上述組件的源碼可以在 GitHub[1] 查看。
4、2+ 個組件之間的聯動,多合一
到這一步,其實你已經翻越最難的大山了!而這里要說的多個組件之間的聯動其實已經處于優化用戶體驗的道路上了。思考一下,什么時候會用到多個組件之間的聯動呢?
其實場景有很多,例如將「Form 表單」、「Table 表格」和「Pagination 分頁」結合起來,封裝成一個組件,這樣在多表格的項目中直接使用就好了;
將 table 和 pagination 放到一個組件中:
- <template lang="pug">
- div
- .el-table
- template(v-for="(item, index) in columns")
- el-table-column(
- :prop="item.prop"
- :key="index"
- :label="item.label")
- el-pagination.pg-wrapper(
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange"
- :current-page="currentPage"
- :page-sizes="[10, 20, 50, 100]"
- :page-size="pagesize"
- :total="total")
- </template>
需要傳入的參數如下:列信息 columns、單頁數據量 pagesize、當前頁碼 currentPage、表格數據 tableData、數據總數 total、表單查詢的參數 query 等。
- props: {
- // 列信息
- columns: {
- type: Array,
- default: [],
- }
- // 單頁數據量
- pagesize: {
- type: Number,
- default: 10,
- },
- // 當前頁碼
- currentPage: {
- type: Number,
- default: 1,
- },
- // 表格數據
- tableData: {
- type: Array,
- default: [],
- },
- // 數據總數
- total: {
- type: Number,
- default: 1000,
- },
- // 獲取數據的接口
- fetch:{
- type: function,
- default:() => {}
- },
- // 表單查詢的參數
- query:{
- type: Object,
- default: () => {}
- }
- },
- methods: {
- // 改變當前頁碼 currentPage 時觸發
- handleCurrentChange: function (currentPage) {
- this.$emit('handleChange', this.pagesize, currentPage)
- this.fetch(this.query)
- },
- // 改變當前頁 pageSize 時觸發
- handleSizeChange: function (pageSize) {
- this.$emit('handleChange', pageSize, this.currentPage)
- this.fetch(this.query)
- }
- }
使用時我們只需要傳以上的參數就可以直接調用這兩個組件了。
還有「表格中行選中狀態數據」,與「展示數據」之間的聯動等等,可發揮之處有很多。將他們封裝后可以大大減輕重復的工作量,特別是像后臺管理類的項目,頁面間相似度很高的,尤其適合這種方法。
5、完全沒有符合的組件
如果你要的組件,外部的組件庫中都沒有提供,那就自己動手封裝一個。盡可能將你的組件變得通用,兼容。嘗試想一想你的組件是否在其他情況下也能用。另外也可以多看看別人是如何封裝組件的,這有助于你自己開發。
如果你可以將你在前端開發道路上自己封裝的組件一個個收集起來,大概率可以方便你以后相同場景下直接復用,也有助于你的代碼解耦。
總結
如果你遇到了組件庫中的組件不合適的,先考慮看看是否能利用它的方法或屬性達到效果,再看看能否修改它的代碼達成目的。如果最后實在不行,那么就自己動手造輪子吧!自己造的輪子記得記下來,沒準以后就能用上!
最后,Vue Demo Collection[2] 這個項目,是我在開發過程中遇到的通用 Vue 組件的 demo 收集,包含了 Vue/CSS/Echarts 等一些可以復用的組件 ❤️,基本上我認為可以復用的組件和代碼片段我都會記錄在這,方便自己的回顧和使用,也算是個人成長的記錄。