問大家一個問題,曾經的你是否也遇到過,一個項目中有好幾個頁面長得基本相同,但又差那么一點,想用 vue extends 繼承它又不能按需繼承html模板部分,恰好 B 頁面需要用的 A 頁面 80% 的模板,剩下的 20% 由 B 頁面自定義,舉個栗子:
我們假設這是兩個頁面,B頁面比A頁面多了個p標簽,剩余的東西都一樣,難道僅僅是因為這一個 p標簽就要重新寫一份模板嗎?相信大部分伙伴解決方式是把公共部分抽成一個組件來用,這是一個好的做法。沒錯,但是來了,老板讓你在 標題1、標題2下面分別插入一段內容,這會兒你是不是頭大了?難道只能重寫一份了嗎?當然不是,來開始我們的填坑之路~(當你的業務能用插槽或者組件抽離的方式固然更好,以下內容僅針對當你項目達到一定體量,vue老三套難以處理的情況下采用)
準備以下工具包:
npm install --save node-html-parser
<template extend="./xxx.vue">
</template>
<template extend="./xxx.vue">
<div>
<extend type="replace" target="#div_1">
<a>通過replace替換掉父頁面下id為div_1的元素 </a>
</extend>
</div>
</template>
最終它生成的應該是除了 id 為 div_1元素被<a>通過replace替換掉父頁面下id為div_1的元素 </a>替換掉之外,剩下的全部和xxx.vue一樣的頁面。
子頁面繼承父頁面既可以完全繼承,也可以通過某種方式以父頁面為基板,對其進行增、刪、改。方便理解,我們先定義一個自定義標簽 extend,子頁面通過該標簽對其繼承的頁面操刀動手術,為了實現一個比較完善的繼承拓展,extend 標簽需要具備以下屬性:
參數 | 說明 | 類型 | 可選值 |
type | 指定擴展類型 | string | insert(插入)、replace(替換)、remove(移除)、append(向子集追加) |
position | 指定插入的位置(僅在 type 取值 insert 時生效) | string | before(目標前)、after(目標后) |
指定插入的位置(僅在 type 取值 append 時生效,用于指定插入成為第幾個子節點) | number | - | |
target | 指定擴展的目標 | string |
新建一個vue2的項目,項目結構如下:
我們的繼承拓展通過自定義loader在編譯的時候實現,進入到src/loader/index.js
const extend = require('./extend');
module.exports = function (source) {
// 當前模塊目錄
const resourcePath = this.resourcePath;
// 合并
const result = new extend(source, resourcePath).mergePage();
// console.log('result :>> ', result);
// 返回合并后的內容
this.callback(null, result);
};
實現繼承拓展主要邏輯代碼:src/loader/extend.js
const parser = require('node-html-parser');
const fs = require('fs');
const pathFile = require('path');
/**
* 通過node-html-parser解析頁面文件重組模板
* @param {String} source 頁面內容
* @param {String} resourcePath 頁面目錄
* @returns {String} 重組后的文件內容
*/
class Extend {
constructor(source, resourcePath) {
this.source = source;
this.resourcePath = resourcePath;
}
// 合并頁面
mergePage() {
// 通過node-html-parser解析模板文件
const pageAst = parser.parse(this.source).removeWhitespace();
// 獲取template標簽extend屬性值
const extendPath = pageAst.querySelector('template').getAttribute('extend');
if (!extendPath) {
return pageAst.toString();
}
// extendPath文件內容
const extendContent = fs.readFileSync(pathFile.resolve(pathFile.dirname(this.resourcePath), extendPath), 'utf-8');
// extendContent文件解析
const extendAst = parser.parse(extendContent).removeWhitespace();
// 獲取頁面文件標簽為extend的元素
const extendElements = pageAst.querySelectorAll('extend');
extendElements.forEach((el) => {
// 獲取對應屬性值
const type = el.getAttribute('type');
const target = el.getAttribute('target');
const position = parseInt(el.getAttribute('position'));
// 匹配模板符合target的元素
let templateElements = extendAst.querySelectorAll(target);
// type屬性為insert
if (type === 'insert') {
templateElements.forEach((tel) => {
// 通過position屬性判斷插入位置 默認為after
if (position === 'before') {
el.childNodes.forEach((child) => {
tel.insertAdjacentHTML('beforebegin', child.toString());
});
} else {
el.childNodes.forEach((child) => {
tel.insertAdjacentHTML('afterend', child.toString());
});
}
});
}
// type屬性為append
if (type === 'append') {
templateElements.forEach((tel) => {
const elNodes = el.childNodes;
let tlNodes = tel.childNodes;
const len = tlNodes.filter((node) => node.nodeType === 1 || node.nodeType === 3).length;
// 未傳position屬性或不為數字、大于len、小于0時默認插入到最后
if(isNaN(position) || position > len || position <= 0){
elNodes.forEach((child) => {
tel.insertAdjacentHTML('beforeend', child.toString());
});
}else {
tlNodes = [...tlNodes.slice(0, position-1), ...elNodes, ...tlNodes.slice(position-1)]
tel.set_content(tlNodes);
}
});
}
// type屬性為replace
if (type === 'replace') {
templateElements.forEach((tel) => {
tel.replaceWith(...el.childNodes);
});
}
// type屬性為remove
if (type === 'remove') {
templateElements.forEach((tel) => {
tel.remove();
});
}
});
// 重組文件內容
const template = extendAst.querySelector('template').toString();
const script = pageAst.querySelector('script').toString();
const style = extendAst.querySelector('style').toString() + pageAst.querySelector('style').toString()
return`${template}${script}${style}`
}
}
module.exports = Extend;
好的,自定義loader已經編寫完成,在vue.config.js里面配置好我們的loader
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
configureWebpack: {
module: {
rules: [
{
test: /\.vue$/,
use: [
{
loader: require.resolve('./src/loader'),
},
],
},
],
},
},
})
接下來我們嘗試編寫A頁面和B頁面:
A.vue:
<template>
<div class="template">
<div id="div_1" class="div">父頁面的div_1</div>
<div id="div_2" class="div">父頁面的div_2</div>
<div id="div_3" class="div">父頁面的div_3</div>
<div id="div_4" class="div">父頁面的div_4</div>
<div id="div_5" class="div">父頁面的div_5</div>
<div id="div_6" class="div">父頁面的div_6</div>
<div id="div_7" class="div">父頁面的div_7</div>
<div id="div_8" class="div">父頁面的div_8</div>
</div>
</template>
<script>
export default {
name: 'COM_A',
props: {
msg: String
}
}
</script>
<style scoped>
.div {
color: #42b983;
font-size: 1.5em;
margin: 0.5em;
padding: 0.5em;
border: 2px solid #42b983;
border-radius: 0.2em;
}
</style>
B.vue:
<template extend="./A.vue">
<div>
<extend type="insert" target="#div_1" position="after">
<div id="div_child" class="div">子頁面的div_5</div>
</extend>
<extend type="append" target="#div_3" position="2">
<a> 子頁面通過append插入的超鏈接 </a>
</extend>
</div>
</template>
<script>
import A from './A.vue'
export default {
name: 'COM_B',
extends: A,//繼承業務邏輯代碼
props: {
msg: String
}
}
</script>
<style scoped>
#div_child {
color: #d68924;
font-size: 1.5em;
margin: 0.5em;
padding: 0.5em;
border: 2px solid #d68924;
}
a {
color: blue;
font-size: 0.7em;
}
</style>
我們在App.vue下引入B.vue
<template>
<div id="app">
<B/>
</div>
</template>
<script>
import B from './components/B.vue'
export default {
name: 'App',
components: {
B
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
當我們執行編譯的時候,實際上B.vue的編譯結果如下:
<template>
<div class="template">
<div id="div_1" class="div">父頁面的div_1</div>
<div id="div_child" class="div">子頁面的div_5</div>
<div id="div_2" class="div">父頁面的div_2</div>
<div id="div_3" class="div">
父頁面的div_3
<a> 子頁面通過append插入的超鏈接 </a>
</div>
<div id="div_4" class="div">父頁面的div_4</div>
<div id="div_5" class="div">父頁面的div_5</div>
<div id="div_6" class="div">父頁面的div_6</div>
<div id="div_7" class="div">父頁面的div_7</div>
<div id="div_8" class="div">父頁面的div_8</div>
</div>
</template>
<script>
import A from './A.vue'
export default {
name: 'COM_B',
extends: A,//繼承業務邏輯代碼
props: {
msg: String
}
}
</script>
<style scoped>
.div {
color: #42b983;
font-size: 1.5em;
margin: 0.5em;
padding: 0.5em;
border: 2px solid #42b983;
border-radius: 0.2em;
}
</style>
<style scoped>
#div_child {
color: #d68924;
font-size: 1.5em;
margin: 0.5em;
padding: 0.5em;
border: 2px solid #d68924;
}
a {
color: blue;
font-size: 0.7em;
}
</style>
注意我們在B.vue使用了extends繼承了組件A,這里是為了能復用業務邏輯代碼,最后我們運行代碼,頁面輸出為:
在真實的項目當中,我們遇到大量重復的頁面但是又有小區別的頁面,是可以通過這種方式減少我們的代碼量,當然也許有更好的辦法,也希望大伙能提出寶貴的建議。
最后引用一下 @XivLaw 老哥的評論:有很多人說通過cv就能解決,但是當你的業務有成千上萬個頁面是趨同,并且具有相同的基本功能,當界面需要統一調整或者需要進行ui統一管控的時候,cv就成了你的累贅了。 也有朋友說通過組件化和插槽解決,組件化是一個不錯的方案,但是當成千上萬個趨同的界面存在時,插槽并一定能覆蓋所有的業務定制化。 使不使用這種方式,主要看你的業務。
直白一點說就是:我現在有一千個頁面幾乎一樣,有的頁面是頭部多一點東西,有的是底部,有的是某個按鈕旁邊多一個按鈕,有的是輸入框之間多個輸入框,ui或者界面或者同時需要添加固定功能,需要調整的時候,這一千個頁面要怎么調?
作者:小小小小_柏
鏈接:https://juejin.cn/post/7347973138787467274
目開發中的函數抽離和復用
在實際的項目開發中,盡可能要做到讓開發者易于理解和后期維護,那么,其中一個最重要的就是必須將重復使用的相同代碼塊或者是差異不明顯的代碼塊抽離出來。在需要使用的地方傳參調用執行。
這樣做的好處其中一個是,盡可能減少代碼模塊的改動,當項目中有需要拓展或者修改的地方,那就不需要在一個大的函數體里面去修改,而是該對應的模塊,只要確定好輸入的參數和返回值就可以。另一個好處是易于項目的拓展,將通用邏輯抽離出來之后,如果有新增的方法,直接新命名一個新的函數體實現新的邏輯,舊的函數體也可以保留,保證目前線上代碼的兼容性。這就是面向對象編程的開發思想。
下面是一個簡單的例子:
這是一段數據上報相關的代碼,在上報之前合并所需參數,之后判斷當前環境再執行上報事件。
// index.vue
methods : {
pageReport(ags) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.report({ ...baseParams, ...args });
},
report(args) {
if (this.isApp) {
web.Connect.Report({
"data": args
});
} else {
web.report(args);
}
},
}
流程圖為:
這一段代碼本身是沒有什么問題,但是后來想了一下,如果這個時候做拓展增加上報的場景,比如微信情況下,qq情況下,或者在上報的時候增加了一些其他邏輯,那么,report 這個函數的代碼就會很臃腫,而且也不好理解。所以,可以先把客戶端上報和web上報的函數單獨給抽離出來。
所以,可以代碼修改成這樣:
// index.vue
methods : {
pageReport(args) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.reportDistribute({ ...baseParams, ...args });
},
reportDistribute(args) {
if (!this.isApp) return this.webReport(args);
this.appReport(args);
},
appReport(args) {
web.Connect.Report({
"data": args
});
},
webReport(ars) {
web.report(args)
},
}
流程圖為:
由中間的一個函數做判斷處理,這樣至少可以先保證客戶端上報和web上報這兩個地方不論在邏輯上怎么去改,只要是不涉及到上報本身的邏輯修改,那么就只要修改 reportDistribute 這個函數就可以,可以理解為 appReport 和 webReport 這兩個函數是底層執行代碼,而 reportDistribute 這個就是邏輯層代碼,負責判斷處理調用。
這樣抽離之后底層負責執行的模塊就會和邏輯層分開,需要拓展的之后只需要增加對應的模塊和定義好需要的數據,使用的時候再去調用既可以了,不會再改到邏輯層上的代碼,同時,任意一個模塊需要修改,那么只要修改對應模塊就可以。
上面的代碼目前只有兩種情況,客戶端內和Web情況下上報 。雖然是將底層代碼抽離了出來,但如果這個地方如果增加了其他場景的上報比如微信下的,或者是qq下的又或者是微博下的,按照目前的寫法,就是增加 if else,但是這樣并不好理解,代碼的圈復雜度也高,這個時候可以寫一個配置表進行映射 ,代碼如下:
// index.vue
methods : {
appReport(ars) {
// do something
},
webReport(ars) {
// do something
},
weixinReport(ars) {
// do something
},
getReportParams() {
let isApp = isApp ? '2' : '1';
if (+isApp === 1 && isWeixin === 1) {
isApp = 3
}
return isApp
},
reportDistribute(args, type) {
const config = {
1: this.webReport,
2: this.appReport,
3: this.weixinReport
}
config[type](args);
},
reportInit(args) {
return this.reportDistribute(args, this.getReportParams());
},
pageReport(args) {
let baseParams = {
'event_id': this.eventId,
'event_name': this.eventName,
'page_id': '123',
};
return this.reportInit({ ...baseParams, ...args });
},
}
流程圖為:
這樣不管再新增多少情況的上報,只要在 reportDistribute 里面增加一個配置,在 getReportParams 中多增加一個返回值,就可以完成配置,整一段代碼清晰可維護。
接著,這一段代碼負責底層邏輯的一些映射相關的配置,這些一般改動的是比較少的,那么可以在把這一些抽離出來單獨放在一個 config 文件里面,而不是全都擠在一個 vue 頁面的 methods 中。
首先,將映射配置相關和負責底層執行的函數抽離出來,放在一個page-config.js 文件中,再將接口export 出去。 代碼如下:
第一個文件 page-config.js ,定義基本配置和映射表。
// page-config.js
function appReport(ars) {
alert(JSON.stringify(ars));
}
function webReport(ars) {
alert(JSON.stringify(ars));
}
function weixinReport(ars) {
alert(JSON.stringify(ars));
}
const baseParams = {
eventId: '1',
eventName: 'test-page',
pageId: 'index',
};
const pageConfig = {
appReport: appReport,
webReport: webReport,
weixinReport: weixinReport,
};
export { pageConfig, baseParams };
之后如果需要做拓展,直接在這個配置文件里面加內容即可。
再是第二個文件 index.vue 文件
// index.vue
import { pageConfig, baseParams } from './page-config';
export default {
methods: {
getReportParams() {
let isApp = this.isApp() ? 'appReport' : 'webReport';
if (+isApp === 'webReport' && this.isWeixin === true) {
isApp = 'weixinReport'
}
return isApp
},
// add function
pageGo(args) {
return this.eventDistribute('link', 'https://www.google.com');
},
eventDistribute(type, args) {
return pageConfig[type](args);
},
pageReport(args) {
return this.eventDistribute(this.getReportParams(), { ...baseParams, ...args });
},
}
}
函數抽離之后,這里以后的拓展需要改到的只有 getReportParams 函數,這里面可能會返回新的數值。
這里注意一點是,之前的 reportDistribute 函數改成了 eventDistribute,這樣做的目的是更加的通用,因為一個項目不單單是只有上報事件,肯定還有其他功能邏輯,例如這時候需要再加一個網頁跳轉的事件,那么就可以向上面的代碼一樣 ,增加一個 pageGo調用 eventDistribute 。
eventDistribute 函數通用第一個參數區分執行的函數類型和第二個參數進行傳參執行對應函數。
整理的流程圖改為:
接下來,也可以再對 page-config 做一點優化,因為,上面舉例的事件類型也只有兩大類一個是頁面跳轉,另一個是上報事件。那么,要考慮再多其他類型的情況,也不太好記的請。所以,可以對底層的映射配置再做一點優化,比如,通過參數的名稱進行對應的函數執行,代碼如下:
methods : {
getReportParams() {
let isApp = this.isApp ? "report.appReport" : "report.webReport";
if (isApp === "report.webReport" && this.isWeixin) {
isApp = "report.weixinReport";
}
return isApp;
},
// add function
pageGo(args) {
return this.eventDistribute("go.link", "https://www.google.com");
},
eventDistribute(type, args) {
return pageConfig(type, args);
},
pageReport(args) {
return this.eventDistribute(this.getReportParams(), {
...baseParams,
...args,
});
},
},
把事件類型分類,再使用小數點隔開 report.appReport
// page-config.js
function appReport(ars) {
alert(JSON.stringify(ars));
}
function webReport(ars) {
alert(JSON.stringify(ars));
}
function weixinReport(ars) {
alert(JSON.stringify(ars));
}
function link(url) {
window.location.href = url;
}
const baseParams = {
eventId: '1',
eventName: 'test-page',
pageId: 'index',
};
const pageEvent = (name, args) => {
eval(name + "(" + JSON.stringify(args) + ")");
};
const pageFunConfig = {
'report': pageEvent,
'go': pageEvent
};
const pageConfig = (type, args) => {
const eventType = type.split('.')[0];
const eventName = type.split('.')[1];
pageFunConfig[eventType](eventName, args);
}
export { pageConfig, baseParams };
切割出函數名稱之后執行函數。流程圖如下:
上面的代碼只是利用了上報這個行為做了一個例子,并不是說一定就是要這樣寫,更多的是一種將代碼抽離達到多次服用和容易維護的目的。
如果小伙伴有更好的方法和建議,麻煩請在我的公眾號留言,謝謝!
-----
利寶閣確實有點東西。
歡迎關注各位小伙伴我的公眾號 @GavinUI
中1, 2, 3, 4 表示優先級
選擇器 | 格式 | 優先級權重 |
!important | 10000 | |
內聯樣式 | 1000 | |
id 選擇器 | #id | 100 |
類選擇器 | #classname | 10 |
屬性選擇器 | a[ref=“eee”] | 10 |
偽類選擇器 | li:last-child | 10 |
標簽選擇器 | div | 1 |
偽元素選擇器 | li::after | 1 |
相鄰兄弟選擇器 | h1+p | 0 |
子選擇器 | ul>li | 0 |
后代選擇器 | li a | 0 |
通配符選擇器 | * | 0 |
注意事項:
可繼承:
字體系列 font-family font-weight font-size
文本系列 color text-align line-height
可見系列 如 visibility
由于屬性太多,這里只列舉常見的可繼承的屬性
屬性值 | 作用 |
none | 元素不顯示,并且會從文檔流中移除。 |
block | 塊類型。默認寬度為父元素寬度,可設置寬高,換行顯示。 |
inline | 行內元素類型。默認寬度為內容寬度,不可設置寬高,同行顯示。 |
inline-block | 默認寬度為內容寬度,可以設置寬高,同行顯示。 |
list-item | 像塊類型元素一樣顯示,并添加樣式列表標記。 |
table | 此元素會作為塊級表格來顯示。 |
inherit | 規定應該從父元素繼承 display 屬性的值。 |
block:
*請認真填寫需求信息,我們會在24小時內與您取得聯系。