摘要:雖然不支持抽象類的自動(dòng)注入,我們依舊可以進(jìn)一步靈活運(yùn)用模板方法模式中的鉤子方法思想,將類中所需要的屬性,創(chuàng)建好方法作為鉤子,這樣就不再局限與自身的限制了。
前言
在《重構(gòu)》這本書中,提到了很多種的代碼的壞味道,有一種就是重復(fù)的代碼,以及各種各樣的Switch 與 if/else 判斷,面對(duì)這種情況,可以利用 java 的多態(tài)來進(jìn)行替換。
今天要講的模板方法就是其中一種利用多態(tài)減少重復(fù)代碼的手段~
注:文中代碼片段在實(shí)際項(xiàng)目中均已廢棄,不過畢竟與業(yè)務(wù)需求相關(guān),因此代碼片段僅保留與模板方法相關(guān)的部分,不保證代碼片段的實(shí)際運(yùn)行
業(yè)務(wù)場景以往我們的修改資源屬性和路由時(shí),都是實(shí)時(shí)生效的,改了就是改了。
那現(xiàn)在用戶有了這么一種需求,我的路由修改時(shí),不及時(shí)生效,當(dāng)用戶確認(rèn)修改時(shí)再生效,過程中不滿意還可以回滾到屬性與路由關(guān)系最開始的狀態(tài)。
我們將這種操作稱為流程中電路(其實(shí)這里比較類似于Oracle自身的回滾操作實(shí)現(xiàn))
那么這種需求該怎么實(shí)現(xiàn)呢?
以電路資源為例,電路的這種路由關(guān)聯(lián)關(guān)系存儲(chǔ)于 電路路由表中,我們再搞個(gè)歷史路由表,專門用來存放最初始的路由關(guān)系狀態(tài)。只要確保每次修改資源時(shí),都肯定已將最初始狀態(tài)緩存入歷史路由表中即可。
這樣即可確保路由資源在修改時(shí),其原始信息不會(huì)丟失。
一句話總結(jié):確保每次修改路由 / 屬性時(shí),都已將相關(guān)信息備份。
最初的需求:圍繞上述業(yè)務(wù)場景,我們來看看最開始的需求:
最開始僅僅要求了電路資源的流程需求,在電路資源的路由實(shí)現(xiàn)角度而言,分為這么幾個(gè)步驟:
將電路路由關(guān)系寫入回滾表
將當(dāng)前路由關(guān)系表中記錄刪除
將各個(gè)路由資源的屬性寫入回滾表
將路由表中涉及的資源狀態(tài)都設(shè)置為流程中的修改狀態(tài)
解決方案:來讓我們看看代碼:
public int saveRouteHistory(ResIdentify identify, Listroutes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); // 根據(jù)工單與事務(wù)狀態(tài)決定是否走流程中處理邏輯 if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 1、將電路路由表的數(shù)據(jù)寫入到回滾表 List
一開始這樣寫其實(shí)沒什么問題,代碼一共不到 40 行,同時(shí)相對(duì)清晰的實(shí)現(xiàn)了功能需求。
如果需求就是這樣,后期維護(hù)成本不怎么高,其實(shí)沒什么改的必要(我比較懶。。。)
進(jìn)階需求:然而生活與工作中卻總是 “驚喜” 多過平淡,挫折多過順風(fēng)。那該怎么辦?日子還是要過,積極面對(duì)唄
這不,客戶又來了個(gè)需求時(shí),不僅電路要這樣做,電路的路由——通道也需要支持這種流程中操作。
由于通道自身也有路由,所以其實(shí)上述相同的代碼邏輯通道也需要實(shí)現(xiàn)一份。
解決方案1——常規(guī)模式:先來看看常規(guī)的代碼是寫的呢?
ChannelDataOperation.java public int saveRouteHistory(ResIdentify identify, Listroutes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); // 根據(jù)工單與事務(wù)狀態(tài)決定是否走流程中處理邏輯 if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 1、將電路路由表的數(shù)據(jù)寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CHANNEL_ID); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CHANNEL_ROUTE, woId); // 2、帶工單刪除只需要更新路由的工單和事物狀態(tài) Map params = new HashMap (); params.put(ResAttrConst.CHANNEL_ID, identify.getResId()); params.put(ResAttrConst.WO_ID, woId); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); channelDao.updateTrsChannelRoute(params); // 3、路由表中資源實(shí)例寫入到回滾表中并更新路由狀態(tài) for (OperationResEntity route : routes) { result++; intelligentWriteResHis(woId, route); route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } return result; }
乍一看是不是都一樣呢?其實(shí)在第16行,19行,26行還是能發(fā)現(xiàn)少許不同的。
小結(jié)這種開發(fā)方式,其實(shí)就是我們最常見的 Ctrl C/V 開發(fā)法。
這種開發(fā)辦法有什么弊端呢?
傳輸段也有路由關(guān)系,那么如果傳輸段也要支持流程中操作了,那么是不是又得賦值一套?
以后我判斷是否流程中資源的校驗(yàn)邏輯更改了,那么是不是兩處我都得改一遍?
我不告訴你傳輸通道也支持了流程操作,那么是不是還需要完整的看一遍通道的代碼才能知道哪些資源已經(jīng)支持了流程操作呢?
復(fù)制容易出錯(cuò)
解決方案二——父類的使用拋去剛才說的第16行,19行,26行不論,既然其他的代碼都是一樣的,那我們就先把能抽取的重復(fù)代碼抽取出來唄~
那么問題來了,抽到哪里?
對(duì)于一個(gè)類內(nèi)部的重復(fù)代碼,我們可以將重復(fù)代碼抽取到這個(gè)類內(nèi)部的一個(gè)獨(dú)立方法中(ps: IDEA 中抽取方法的快捷鍵是 ctrl + alt + M)
但是這個(gè)例子中,重復(fù)代碼分散在了不同的類中。所以,我們只能新建一個(gè)新類,將重復(fù)的方法都放在這個(gè)新類中。
HisRouteResDataOperation.java protected int logRouteAndUpdateState(String woId, Listroutes) { int result = ResCommConst.ZERO; for (OperationResEntity route : routes) { result++; // 1、路由表中資源實(shí)例寫入到回滾表中 intelligentWriteResHis(woId, route); route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } return result; }
如上述代碼,我們將電路和通道中完全重復(fù)的一段代碼抽取成了方法,放在了 HisRouteResDataOperation 中,接下來使電路和通道的操作類繼承這個(gè)類就可以正常使用了。
接下來看看這時(shí) CircuitDataOperation.java 是怎樣的:
public int saveRouteHistory(ResIdentify identify, Listroutes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 1.0、將電路路由表的數(shù)據(jù)寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CIRCUIT_ID); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CIRCUIT_ROUTE, woId); // 1.1、帶工單刪除只需要更新路由的工單和事物狀態(tài) Map params = new HashMap (); params.put(ResAttrConst.CIRCUIT_ID, identify.getResId()); params.put(ResAttrConst.WO_ID, woId); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); circuitDao.updateTrsCirRoute(params); result += logRouteAndUpdateState(woId, routes); } return result; }
是不是簡化了一點(diǎn)?
這樣,我們就將通道和電路的其中兩塊重復(fù)代碼提取出來了。
不過同時(shí)也可以看到,在第3行,第5行,還有第8行,我們用了 Spring 的注解,留心記一下,這會(huì)在后面導(dǎo)致一個(gè)小問題。
拓展思考雖然我們已經(jīng)將代碼中的兩部分重復(fù)代碼移植入父類中,看起來清晰了一點(diǎn)。但是還沒有結(jié)束,我們發(fā)現(xiàn)其實(shí)電路和通道在寫回滾的邏輯上其實(shí)挺相似的,
1. 先判斷下是否在流程中, 2. 將路由關(guān)系寫回滾表并更新路由狀態(tài), 3. 將路由資源狀態(tài)寫回滾表并更新狀態(tài)。
解決方案三——模板方法登場禪師:那么我們?nèi)绻雽⑦@種流程上的先后順序進(jìn)行復(fù)用,又該怎么辦呢?
王小黑:既然大家這么相似,那么將這部分代碼直接放入父類中不好嗎?
禪師:嗯,小黑你再好好考慮考慮,還記得我們在進(jìn)階需求中的常規(guī)解決辦法中說的嗎?
王小黑:我知道了,通道與電路的保存邏輯,在第16行,19行,26行有一些區(qū)別!因?yàn)檫@少許的不同(其實(shí)就是我們常說的硬編碼),所以我們不能直接將方法抽取到父類中。
禪師:嗯,很好,那你知道該怎么解決嗎?
前文講到,由于存在硬編碼,我們沒有辦法直接將代碼邏輯移植入父類中。
而模板方法模式專門為此而生,讓我們看看該怎么寫吧~
版本一TrsHisRouteResDataOperation.java ······ public int saveRouteHistory(ResIdentify identify, Listroutes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); if (StringUtil.hasText(woId) && (ResDictValueConst.MNT_ADD.equals(recordOprType) || ResDictValueConst.MNT_DELETE.equals(recordOprType) || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) { // 1、將路由關(guān)系表的數(shù)據(jù)寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 2、更新當(dāng)前路由關(guān)系表中路由記錄的工單和事物狀態(tài)為刪除態(tài) Map params = new HashMap (); params.put(ResAttrConst.WO_ID, woId); params.put(getResId(), identify.getResId()); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); updateRouteRecord(params); for (OperationResEntity route : routes) { result++; // 1、路由表中資源實(shí)例寫入到回滾表中 trsRouteOperationService.logPropertiesToHis(route.getIdentify(), woId); // 2、更新路由狀態(tài) route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } // TODO wang.xi 解決正常的路由保存歷史表邏輯 return result; } /** * 鉤子方法,更新路由的工單與事務(wù)狀態(tài) * @param params 工單與事務(wù)狀態(tài)信息 */ protected void updateRouteRecord(Map params){}; protected String getRouteTableName(){return ""}; protected String getResId(){return ""};
單純的看這種飽含業(yè)務(wù)規(guī)則的代碼肯定是看不進(jìn)去的,所以這里我們著重看下第16行,19行,26行。
版本二禪師:前文講了,這幾行里面因?yàn)榇嬖谟簿幋a,如果簡單的將電路的代碼上移至父類中,那么通道資源使用這套代碼就會(huì)有問題了,小黑,你有什么好辦法嗎?
王小黑:這個(gè)我知道,有個(gè)最簡單的解決方案,反正電路和通道類內(nèi)都有類似的方法需求,針對(duì)第 26 行,我們在 TrsHisRouteResDataOperation 中編寫一個(gè)空的 updateRouteRecord() 方法使他能找到這個(gè)方法,不報(bào)錯(cuò)不就好了嗎?子類利用 java 的多態(tài)機(jī)制,實(shí)現(xiàn)一下這個(gè)方法就好了。(其他部分雷同)
禪師:嗯,你說的確實(shí)有用,上面這幾行代碼也確實(shí)是按照你說的做的。但是這樣有個(gè)缺點(diǎn),還是之前說的,如果以后傳輸段也要拓展呢?采取這種方案,即便傳輸段沒有實(shí)現(xiàn)這個(gè)方法,方法編譯時(shí)期也不會(huì)報(bào)錯(cuò)啊!
王小黑:那我們退一步,還有個(gè)解決方案,將這個(gè) updateRouteRecord() 方法定義為抽象方法,這不就解決你說的拓展問題了嗎?
禪師:根據(jù) java 的語法,如果你將一個(gè)方法定義為抽象方法,那么這個(gè)類也必須是抽象類了。
王小黑:抽象類就抽象類,又有什么所謂?
禪師:小黑,too young 了吧~ 你仔細(xì)看看第 5 行與第 32 行代碼,是不是有個(gè) context 與 trsRouteOperationService 對(duì)象? 這兩個(gè)對(duì)象都是 Spring 中動(dòng)態(tài)注入的對(duì)象,你可以查查 Spring 動(dòng)態(tài)注入與 java 抽象類的關(guān)系,就會(huì)絕望的發(fā)現(xiàn),Spring 居然不支持抽象類的自動(dòng)注入。。。(個(gè)中原因,等有機(jī)會(huì)再介紹 Spring 原理的時(shí)候再介紹給大家吧)
王小黑:唉,這也是坑那也是坑,橫豎都有問題,那么我們還玩不玩了?
再仔細(xì)思考下剛才示例中的 updateRouteRecord() 方法,我們在父類引入這個(gè)鉤子方法,就是為了利用 java 的多態(tài)機(jī)制,使父類能夠只關(guān)心方法的存在與否,而不用再關(guān)心具體的實(shí)現(xiàn)。
雖然 Spring 不支持抽象類的自動(dòng)注入,我們依舊可以進(jìn)一步靈活運(yùn)用模板方法模式中的鉤子方法思想,將類中所需要的屬性,創(chuàng)建好getter 方法作為鉤子,這樣就不再局限與 Spring 自身的限制了。
新的代碼如下:
TrsHisRouteResDataOperation.java public int saveRouteHistory(ResIdentify identify, Listroutes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.RECORD_OPR_TYPE); if (StringUtil.hasText(woId) && (ResDictValueConst.MNT_ADD.equals(recordOprType) || ResDictValueConst.MNT_DELETE.equals(recordOprType) || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) { // 1、將路由關(guān)系表的數(shù)據(jù)寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 2、更新當(dāng)前路由關(guān)系表中路由記錄的工單和事物狀態(tài)為刪除態(tài) Map params = new HashMap (); params.put(ResAttrConst.WO_ID, woId); params.put(getResId(), identify.getResId()); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); updateRouteRecord(params); for (OperationResEntity route : routes) { result++; // 1、路由表中資源實(shí)例寫入到回滾表中 getTrsRouteOperationService().logPropertiesToHis(route.getIdentify(), woId); // 2、更新路由狀態(tài) route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } return result; } /** * 鉤子方法,更新路由的工單與事務(wù)狀態(tài) * @param params 工單與事務(wù)狀態(tài)信息 */ protected abstract void updateRouteRecord(Map params); public abstract String getRouteTableName(); public abstract String getResId(); public abstract ResContext getContext() ; public abstract TrsRouteOperationService getTrsRouteOperationService() ;
以上就是模板方法的全部思想了,希望對(duì)大家有所幫助 ^_^
小結(jié)在設(shè)計(jì)模式中,模板方法應(yīng)該算是比較簡單易懂的了,這是理論上而言。
在實(shí)際項(xiàng)目中,我們總會(huì)因?yàn)楦鞣N各樣的困難,比如懶惰(別笑,這真的是個(gè)很充分的理由),比如對(duì)象類型不同,比如某一步方法名不同等等的原因,而無法將其抽象為一個(gè)模板方法。
但是不管是因?yàn)槭裁丛颍瑓s終究是造成了代碼中各種雷同邏輯的冗余。比如更早以前的傳輸帶路由資源(通道,電路等)的保存邏輯。因?yàn)閺牧鞒躺蟻碚f,就那么幾個(gè):
準(zhǔn)備對(duì)象 —>
刪除路由 —>
驗(yàn)證路由狀態(tài)并計(jì)算序號(hào) —>
保存路由 —>
設(shè)置路由狀態(tài)為占用 —>
刷新 A/Z 端屬性信息 —>
刷新文本路由信息 —>
記錄日志
試想,這么 8 個(gè)流程,換做是你,每個(gè)方法得用多少行來實(shí)現(xiàn)?同時(shí)具有這 8 個(gè)流程的資源還有 傳輸通道,傳輸電路,傳輸段三種。
算算開發(fā)的復(fù)雜度是幾乘幾呢?后期維護(hù)時(shí),流程有變換時(shí),又需要該多少行代碼呢?
不過雖然說了這么多,但是傳輸路由保存的代碼并沒有使用模板方法,同時(shí)也依舊很清晰,至于是怎么做到的,先賣個(gè)關(guān)子,我們下回再聊。
對(duì)了,大家可以圍繞今天講的模板方法先思考一下自己模塊的代碼中是否也有應(yīng)該使用模板方法的場景呢~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/70661.html
摘要:簡介官網(wǎng)上對(duì)它的定位是一個(gè)微開發(fā)框架。另外一個(gè)必須理解的概念是,簡單來說就是一套和框架應(yīng)用之間的協(xié)議。功能比較豐富,支持解析自動(dòng)防止攻擊繼承變量過濾器流程邏輯支持代碼邏輯集成等等。那么,從下一篇文章,我們就正式開始源碼之旅了 文章屬于作者原創(chuàng),原文發(fā)布在個(gè)人博客。 flask 簡介 Flask 官網(wǎng)上對(duì)它的定位是一個(gè)微 python web 開發(fā)框架。 Flask is a micro...
摘要:面向?qū)ο笕筇卣骼^承性多態(tài)性封裝性接口。第五階段封裝一個(gè)屬于自己的框架框架封裝基礎(chǔ)事件流冒泡捕獲事件對(duì)象事件框架選擇框架。核心模塊和對(duì)象全局對(duì)象,,,事件驅(qū)動(dòng),事件發(fā)射器加密解密,路徑操作,序列化和反序列化文件流操作服務(wù)端與客戶端。 第一階段: HTML+CSS:HTML進(jìn)階、CSS進(jìn)階、div+css布局、HTML+css整站開發(fā)、 JavaScript基礎(chǔ):Js基礎(chǔ)教程、js內(nèi)置對(duì)...
摘要:面向?qū)ο笕筇卣骼^承性多態(tài)性封裝性接口。第五階段封裝一個(gè)屬于自己的框架框架封裝基礎(chǔ)事件流冒泡捕獲事件對(duì)象事件框架選擇框架。核心模塊和對(duì)象全局對(duì)象,,,事件驅(qū)動(dòng),事件發(fā)射器加密解密,路徑操作,序列化和反序列化文件流操作服務(wù)端與客戶端。 第一階段: HTML+CSS:HTML進(jìn)階、CSS進(jìn)階、div+css布局、HTML+css整站開發(fā)、 JavaScript基礎(chǔ):Js基礎(chǔ)教程、js內(nèi)置對(duì)...
摘要:簡介安裝完成后輸入開始初始化,生成默認(rèn)的配置文件命令的實(shí)現(xiàn)在文件中目錄則包含了初始化相關(guān)的模板命令類類繼承了的類,實(shí)現(xiàn)為一個(gè)命令行的命令構(gòu)造函數(shù)構(gòu)造函數(shù)主要初始化了的和兩個(gè)變量是一個(gè)包含了多個(gè)模板的初始化器具體實(shí)現(xiàn)就是下面將要分析的是初始化 0 簡介 Deployer安裝完成后輸入dep init開始初始化,生成默認(rèn)的配置文件deploy.phpinit命令的實(shí)現(xiàn)在srcConsole...
摘要:最近在寫一個(gè)微信編輯器,然后已經(jīng)在編輯器那一塊選定了,想想覺得雖然不錯(cuò),但是似乎已經(jīng)很不更新了。補(bǔ)充一句,這個(gè)框架比誕生早了一個(gè)月,還是以為核心。自稱是一個(gè)模板驅(qū)動(dòng)的庫,在上說是下一代的操作。下面是一個(gè)簡單的,。 最近在寫一個(gè)微信編輯器,然后已經(jīng)在編輯器那一塊選定了CKEditor,想想覺得UEditor雖然不錯(cuò),但是似乎已經(jīng)很不更新了。 想想覺得編輯器這種東西,對(duì)于一般人來說還算挺常...
閱讀 2007·2021-11-15 18:09
閱讀 899·2021-09-06 15:13
閱讀 2643·2021-08-23 09:43
閱讀 2024·2019-08-30 15:54
閱讀 2218·2019-08-30 13:56
閱讀 2484·2019-08-26 11:31
閱讀 3078·2019-08-26 10:56
閱讀 700·2019-08-26 10:28