背景在過去對(duì)框架的設(shè)計(jì)中,我收到過的最有用的建議是:“不要一開始就根據(jù)現(xiàn)有的技術(shù)去整合和改進(jìn)。而是先搞清楚你覺得最理想的框架應(yīng)該是怎樣的,再根據(jù)現(xiàn)在的技術(shù)去評(píng)估,的確實(shí)現(xiàn)不了時(shí)再妥協(xié)。這樣才能做出真正有意義的框架。” 前端,還是從前端說起。前端目前的現(xiàn)狀是,隨著早期的 Backbone,近期的 Angular、React 等框架的興起,前端在 模塊化、組件化 兩個(gè)方向上已經(jīng)形成了一定的行業(yè)共識(shí)。在此基礎(chǔ)上,React 的 FLUX、Relay 則是進(jìn)一步的對(duì)前端應(yīng)用架構(gòu)的探索。這些技術(shù)在目前國內(nèi)的大公司、大團(tuán)隊(duì)內(nèi)部實(shí)際上都落地得非常好,因?yàn)楹苋菀缀凸緝?nèi)部已有的后端技術(shù)棧結(jié)合。而且這些純前端框架的配套技術(shù)方案一般比較成熟,例如在支付寶確定使用 React,其實(shí)有一部分原因是它兼容 IE8,并且有服務(wù)器端渲染方案來加速首屏。 相比之下,像 Meteor 這類從前到后包辦的框架就較難落地。雖然能極大地提高開發(fā)效率,整體架構(gòu)非常先進(jìn),但架構(gòu)的每一個(gè)層級(jí)往往不容易達(dá)到行業(yè)內(nèi)的頂尖標(biāo)準(zhǔn)。特別是在服務(wù)器端,對(duì)大公司來說,通常都有適合自己業(yè)務(wù)的服務(wù)器集群、數(shù)據(jù)庫方案,并且經(jīng)受過考驗(yàn)。因此當(dāng)一個(gè)團(tuán)隊(duì)一上手就要做面向十萬級(jí)、甚至百萬級(jí)用戶的產(chǎn)品時(shí),是不太愿意冒風(fēng)險(xiǎn)去嘗試的。反而是個(gè)人開發(fā)者、創(chuàng)業(yè)型的團(tuán)隊(duì)會(huì)愿意去用,因?yàn)榇_實(shí)能在短時(shí)間內(nèi)高效地開發(fā)出可用的產(chǎn)品出來。包括像 Leancloud 提出的這類型的服務(wù),也是非常受歡迎的。 這種現(xiàn)狀,就是理想和現(xiàn)實(shí)的一個(gè)爭論。Meteor 的方式能滿足我對(duì)開發(fā)效率的理想,而團(tuán)隊(duì)已有的技術(shù)方案能保障穩(wěn)定。能否整合其中的優(yōu)勢(shì),不妨讓我們進(jìn)一步來細(xì)化一下對(duì)框架的希望: - 有強(qiáng)大的前后端一致的數(shù)據(jù)模型層
var user = new User({id:1}); user.pull(); user.watch();
- 我不希望這個(gè)數(shù)據(jù)模型層去包含業(yè)務(wù)邏輯,也就是我創(chuàng)建的user對(duì)象,我不希望它提供 login、logout 等 api。 數(shù)據(jù)與邏輯我們以這樣一個(gè)問題開頭:任何一個(gè)應(yīng)用,我們的代碼最少能少到什么程度? 這算半個(gè)哲學(xué)問題。任何人想一想都會(huì)得到同一個(gè)答案:最少也就少到和應(yīng)用本身的描述一一對(duì)應(yīng)而已了。什么是應(yīng)用描述?或者說什么是應(yīng)用?我們會(huì)這樣描述一個(gè)博客:“用戶可以登錄、退出。用戶登錄后可以發(fā)表文章。發(fā)表文章時(shí)可以添加相應(yīng)的標(biāo)簽?!? 抽象一下描述,答案很簡單:數(shù)據(jù),和邏輯。 如果你在一個(gè)流程要求嚴(yán)格的公司,應(yīng)用描述就是prd或系分文檔。應(yīng)用的數(shù)據(jù)就是數(shù)據(jù)字典,應(yīng)用的邏輯就是流程圖的總和:
流程圖
那么代碼最少能怎么寫呢?數(shù)據(jù)很簡單,參照數(shù)據(jù)字典,我們來用一種即使是產(chǎn)品經(jīng)理都能掌握的偽碼來寫:
//描述字段 User : { name : string } Post : { title : string, content : text } Tag : { name : string } //描述關(guān)系 User -[created]-> Post Post -[has]-> Tag
這里為了進(jìn)一步幫助讀者從已有的技術(shù)思維中跳出來,我想指出這段偽碼和數(shù)據(jù)庫字段描述有一個(gè)很大的區(qū)別,那就是:我不關(guān)心 User 和 Post 中間的關(guān)聯(lián)關(guān)系到底是在兩者的字段中都創(chuàng)建一個(gè)字段來保存對(duì)方的id,還是建立一個(gè)中間表。我只關(guān)心我描述它時(shí)的邏輯就夠了。數(shù)據(jù)描述的代碼,最簡也就簡單到這個(gè)程度了。 那么邏輯呢?我們先用按常規(guī)方式試試?
class User{
createPost( content, tags=[] ){
var post = new Post({content:content})
post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) )
return post
}
}
好像還不錯(cuò),如果今天產(chǎn)品經(jīng)理說我們?cè)黾右粋€(gè) @ 功能,如果文章里 @ 某個(gè)用戶,那么我們就發(fā)個(gè)站內(nèi)信給他。 class User{
createPost( content, tags=[] ){
var post = new Post({content:content})
post.setTags( tags.map(tagName=>{ return new Tag(tagName)} ) )
if( at.scan(content) ){
at.getUser(content).forEach( atUser =>{
system.mail( atUser )
})
}
return post
}
}
你應(yīng)該意識(shí)到我要說什么了,像互聯(lián)網(wǎng)這種可以快到一天一個(gè)迭代的開發(fā)速度,如果沒有一個(gè)好的模式,可能用不了多久,新加的功能就把你的 createPost 搞成了800行。當(dāng)然,我也并不是要講設(shè)計(jì)模式。代碼中的設(shè)計(jì)模式,完全依賴于程序員本人,我們要思考的是從框架層面提供最簡單的寫法。 讓我們?cè)倩氐秸軐W(xué)角度去分析一下業(yè)務(wù)邏輯。 - 干什么 如果我們用左右表示會(huì)互相影響的順序,從上下表示互不相干的順序,把上面的最初的流程圖重畫一下:
這是一棵樹。如果我們?cè)偌觽€(gè)功能,添加的標(biāo)簽如果是某個(gè)熱門標(biāo)簽,那么我們就把這篇文章放到網(wǎng)站的熱門推薦里。這棵樹會(huì)變成什么樣子呢:
是的,事實(shí)上人類思維中的任何過程,都可以畫成一棵樹。有條件的循環(huán)可以拆解成遞歸,最終也是一棵樹。但重點(diǎn)并不是樹本身,重點(diǎn)是上面這個(gè)例子演化的過程,從一開始最簡單的需求,到加上一點(diǎn)新功能,再到加上一些惡心的特殊情況,這恰恰就是真實(shí)世界中 web 開發(fā)的縮影。真實(shí)世界中的變化更加頻繁可怕。其中最可怕的是,很多時(shí)候我們的程序結(jié)構(gòu)、用到的設(shè)計(jì)模式,都是適用于當(dāng)前的業(yè)務(wù)模型的。而某天業(yè)務(wù)模型變化了,代碼質(zhì)量又不夠好的話,就可能遇到牽一發(fā)動(dòng)全身,大廈將傾的噩夢(mèng)。幾乎每個(gè)大公司都有一個(gè)“運(yùn)行時(shí)間長,維護(hù)的工程師換了一批又一批”的項(xiàng)目。Amazon曾經(jīng)有個(gè)工程師描述維護(hù)這種項(xiàng)目的感覺:“climb the shit mountain”。 回到之前的話題,在邏輯處理上,我們的理想是寫出的代碼即短,又具有極高的可維護(hù)性和可擴(kuò)展性。 更具體一點(diǎn),可維護(hù)性,就是代碼和代碼結(jié)構(gòu),能最大程度地反映業(yè)務(wù)邏輯。最好我的代碼結(jié)構(gòu)在某種程度上看來和我們流程圖中的樹一樣。這樣我讀代碼,就幾乎能理解業(yè)務(wù)邏輯。而可擴(kuò)展性,就是當(dāng)出現(xiàn)變化時(shí),我能在完成變化時(shí),能盡量少地去修改之前的代碼。同樣的,如果我們能保障代碼和代碼結(jié)構(gòu)能和流程圖盡量一致,那么在修改時(shí),圖上怎么改,我們代碼就怎么改。這也就是理論上能達(dá)到的最小修改度了。綜上,我們用什么樣的系統(tǒng)模型能把代碼變得像樹形結(jié)構(gòu)一樣? 很簡單,事件系統(tǒng)就可以做到。我們把都一個(gè)業(yè)務(wù)邏輯當(dāng)做事件來觸發(fā),而具體需要執(zhí)行的操作單做監(jiān)聽器,那么上面的代碼就可以寫成:
// emitter 是事件中心 emitter.on("post.create", function savePost(){...}) emitter.on("post.create", function createTags(){...}, {before:"savePost"}) emitter.on("post.create", function scanSensitiveWords( post ){ if( system.scanSensitiveWords( post ) ){ return new Error("you have sensitive words in post.") } }, {block:all}) emitter.on("post.create", function scanPopTags(){...})
//執(zhí)行創(chuàng)建文章操作 emitter.fire("post.create", {...args})
這樣看來,每個(gè)操作的代碼變得職責(zé)單一,整體結(jié)構(gòu)也非常工整。值得注意的是,在這段偽碼里,我們用了 `{before:"savePost"}` 這樣的參數(shù)來表示操作的順序,看起來也和邏輯本身的描述一致。 讓我們回到可維護(hù)性和可擴(kuò)展性來檢查這種寫法。首先在可維護(hù)性上,代碼職責(zé)變得很清晰,并且與流程描述一致。不過也有一個(gè)問題,就是操作的執(zhí)行順序已經(jīng)無法給人宏觀上的印象,必須把每個(gè)監(jiān)聽器的順序參數(shù)拼起來,才能得到整體的順序。 在可擴(kuò)展性上,無路是新增還是刪除操作,對(duì)應(yīng)到代碼上都是刪除或新增相應(yīng)的一段,不會(huì)影響到其他操作代碼。我們甚至可以把這些代碼拆分到不同的文件中,當(dāng)做不同的模塊。這樣在增減功能時(shí),就能通過增刪文件來實(shí)現(xiàn),這也為實(shí)現(xiàn)一個(gè)文件級(jí)的模塊管理器提供了基礎(chǔ)技術(shù)。 至此,除了無法在執(zhí)行順序上有一個(gè)宏觀印象這個(gè)問題,似乎我們得到了理想的描述邏輯的方式。那我們現(xiàn)在來攻克這最后一個(gè)問題。拿目前的這段偽碼和之前的比較,不難發(fā)現(xiàn),之前代碼需要被執(zhí)行一遍才能較好地得到其中函數(shù)的執(zhí)行順序,才能拿到一個(gè)調(diào)用棧。而現(xiàn)在的這段代碼,我只要實(shí)現(xiàn)一個(gè)簡單的 emitter,將代碼執(zhí)行一遍,就已經(jīng)能得到所有的監(jiān)聽器信息了。這樣我就能通過簡單的工具來得到這個(gè)宏觀的執(zhí)行順序,甚至以圖形化的方式展現(xiàn)出來。得到的這張圖,不就是我們一模一樣的流程圖嗎?! 不知道你有沒有意識(shí)到,我們已經(jīng)打開了一扇之前不能打開的門!在之前的代碼中,我們是通過函數(shù)間的調(diào)用來組織邏輯的,這和我們現(xiàn)在的方式有一個(gè)很大的區(qū)別,那就是:用來封裝業(yè)務(wù)邏輯的函數(shù),和系統(tǒng)本身提供的其他函數(shù),沒有任何可以很好利用的區(qū)別,即使我們能得到函數(shù)的調(diào)用棧,這個(gè)調(diào)用棧用圖形化的方式打印出來也沒有意義,因?yàn)槠渲袝?huì)參雜太多的無用函數(shù)信息,特別是當(dāng)我們還用了一些第三方類庫時(shí)。打印的結(jié)果可能是這樣:
而現(xiàn)在,我們用來表述業(yè)務(wù)的某個(gè)邏輯,就是事件。而相應(yīng)的操作,就是監(jiān)聽器。監(jiān)聽器無論是觸發(fā)還是注冊(cè),都是通過 emitter 提供的函數(shù),那么我們只需要利用 emitter,就能打印出只有監(jiān)聽器的調(diào)用棧。而監(jiān)聽器的調(diào)用棧,就是我們的流程圖。
代碼結(jié)構(gòu)可圖形化,并且是有意義的可圖形化,這扇大門一旦打開,門后的財(cái)富是取之不盡的。我們從 開發(fā)、測試、監(jiān)控 三個(gè)方面來看我們能從中獲得什么。 在開發(fā)階段,我們可以通過調(diào)用棧生成圖,那通過圖來生成代碼還會(huì)難嗎?對(duì)于任何一份流程圖,我們都能輕易地直接生成代碼。然后填空就夠了。在調(diào)試時(shí)、我們可以制作工具實(shí)時(shí)地打印出調(diào)用棧,甚至可以將調(diào)用時(shí)保存的傳入傳出值拿出來直接查看。這樣一旦出現(xiàn)問題,你就可以直接根據(jù)當(dāng)前保存的調(diào)用棧信息排查問題,而再無需去重現(xiàn)它。同理,繁瑣的斷點(diǎn),四處打印的日志都可以告別了。 測試階段,既然能生成代碼,再自動(dòng)生成測試用例也非常容易。我們可以通過工具直接檢測調(diào)用棧是否正確,也可以更細(xì)致地給定輸入值,然后檢測各個(gè)監(jiān)聽器的傳入傳出值是否正確。 同樣很容想到監(jiān)控,我們可以默認(rèn)將調(diào)用棧的數(shù)據(jù)建構(gòu)作為日志保留,再用系統(tǒng)的工具去掃描、對(duì)邊,就能自動(dòng)實(shí)現(xiàn)對(duì)業(yè)務(wù)邏輯本身的監(jiān)控。 總結(jié)一下上述,用事件系統(tǒng)去描述邏輯、流程,使得我們代碼結(jié)構(gòu)和邏輯,能達(dá)到一個(gè)非常理想的對(duì)應(yīng)程度。這個(gè)對(duì)應(yīng)程度使得代碼里的調(diào)用棧信息就能表述邏輯。而這個(gè)調(diào)用棧所能產(chǎn)生的巨大價(jià)值,一方面在于可圖形化,另一方面則在于能實(shí)現(xiàn)測試、監(jiān)控等一系列工程領(lǐng)域的自動(dòng)化。 到這里,我們已經(jīng)得到了兩種理想的表達(dá)方式來分別表述數(shù)據(jù)和邏輯。下面真正激動(dòng)人心的時(shí)刻到了,我們來關(guān)注現(xiàn)實(shí)中的技術(shù),看是否真的能夠做出一個(gè)框架,讓我們能用一種革命性的方式來寫應(yīng)用? 理想到現(xiàn)實(shí)首先來看數(shù)據(jù)描述語言和和數(shù)據(jù)持久化。你可能早已一眼看出 `User -[create]-> Post` 這樣的偽碼是來自圖數(shù)據(jù)庫 Neo4j 的查詢語言 cypher 。在這里我對(duì)不熟悉的讀者科普一下。Neo4j 是用 java 寫的開源圖數(shù)據(jù)庫。圖數(shù)據(jù)本身是以圖的方式去存儲(chǔ)數(shù)據(jù)。 例如同樣對(duì)于 User 這樣一個(gè)模型,在 關(guān)系型數(shù)據(jù)庫中就是一張表,每一行是一個(gè) user 的數(shù)據(jù)。在圖數(shù)據(jù)庫中就是一堆節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)是一個(gè) user。當(dāng)我們又有了 Post 這個(gè)模型時(shí),如果要表示用戶創(chuàng)建了 Post 這樣一個(gè)關(guān)系的話,在關(guān)系型數(shù)據(jù)庫里通常會(huì)建立一個(gè)中間表,存上相應(yīng) user 和 post 的 id。也或者直接在 user 或 post 表里增加一個(gè)字段,存上相應(yīng)的id。不同的方案適用于不同的場景。而 在圖數(shù)據(jù)庫中要表達(dá) user 和 post 的關(guān)系,就只有一種方式,那就是創(chuàng)建一個(gè) user 到 post 的名為 CREATED 的 關(guān)系。這個(gè)關(guān)系還可以有屬性,比如 {createdAt:2016,client:"web"} 等。 你可以看出圖數(shù)據(jù)和關(guān)系型數(shù)據(jù)庫在使用上最大的區(qū)別是,它讓你完全根據(jù)真實(shí)的邏輯去關(guān)聯(lián)兩個(gè)數(shù)據(jù)。而關(guān)系型數(shù)據(jù)庫則通常在使用時(shí)就已經(jīng)要根據(jù)使用場景、性能等因素做出不同的選擇。 我們?cè)倏床樵冋Z言,在 SQL 中,我們是以`SELECT ... FROM` 這樣一種命令式地方式告訴數(shù)據(jù)怎樣給我我要的數(shù)據(jù)。語句的內(nèi)容和存數(shù)據(jù)的表結(jié)構(gòu)是耦合的。例如我要找出某個(gè) user 創(chuàng)建的所有 post。表結(jié)構(gòu)設(shè)計(jì)得不同,那么查詢語句就不同。而在 Neo4js 的查詢語句 cypher 中,是以 `(User) -[CREATED] ->(Post)` 這樣的 模式匹配 的語句來進(jìn)行查詢的。這意味著,只要你能以人類語言描述自己想要的數(shù)據(jù),你就能自己翻譯成 cypher 進(jìn)行查詢。 除此之外,圖數(shù)據(jù)當(dāng)然還有很多高級(jí)特性。但對(duì)開發(fā)者來說,模式匹配式的查詢語句,才是真正革命性的技術(shù)。熟悉數(shù)據(jù)庫的讀者肯定有這樣的疑問: 其實(shí)很多 ORM 就能實(shí)現(xiàn) cypher 現(xiàn)在這樣的表達(dá)形式,但在很多大公司里,你會(huì)發(fā)現(xiàn)研發(fā)團(tuán)隊(duì)仍然堅(jiān)持手寫 SQL 語句,而堅(jiān)決不用 ORM。理由是,手寫 SQL 無論在排查問題還是優(yōu)化性能時(shí),都是最快速的。特別是對(duì)于大產(chǎn)品來說,一個(gè) SQL 就有可能節(jié)約或者損失巨額資產(chǎn)。所以寧愿用 “多人力、低效率” 去換 “性能和穩(wěn)定”,也不考慮 ORM。那么 cypher 如何面對(duì)這個(gè)問題? 確實(shí),cypher 可以在某種程度上理解成數(shù)據(jù)庫自帶的 ORM。它很難通過優(yōu)化查詢語句來提升性能,但可以通過其他方式。例如對(duì)耗時(shí)長的大查詢做數(shù)據(jù)緩存。或者把存儲(chǔ)分層,圖數(shù)據(jù)庫變成最底層,中間針對(duì)某些應(yīng)用場景來使用其他的數(shù)據(jù)庫做中間層。對(duì)有實(shí)力的團(tuán)隊(duì)來說,這個(gè)中間層甚至可以用類似于智能數(shù)據(jù)庫的方式來對(duì)線上查詢自動(dòng)分析,自動(dòng)實(shí)現(xiàn)中間層。事實(shí)上,這些中間技術(shù)早就已經(jīng)成熟,結(jié)合上圖數(shù)據(jù)庫和cypher,是可以把傳統(tǒng)的“人力密集型開發(fā)”轉(zhuǎn)變?yōu)椤凹夹g(shù)密集型開發(fā)”的。 扯得略遠(yuǎn)了,我們重新回到模式匹配型的查詢語句上,為什么說它是革命性的,因?yàn)樗鼊偤脻M足了我們之前對(duì)數(shù)據(jù)描述的需求。任何一個(gè)開發(fā)者,只要把數(shù)據(jù)字典做出來。關(guān)于數(shù)據(jù)的工作就已經(jīng)完成了。或者換個(gè)角度來說,在任何一個(gè)已有數(shù)據(jù)的系統(tǒng)中,只要我能在前端或者移動(dòng)端中描述我想要的數(shù)據(jù),就能開發(fā)出應(yīng)用,不再需要寫任何服務(wù)器端數(shù)據(jù)接口。Facebook 在 React Conf 上放出的前端 Relay 框架和 GraphQL 幾乎就已經(jīng)是這樣的實(shí)現(xiàn)。 再來看邏輯部分,無論在瀏覽器端還是服務(wù)器端,用什么語言,實(shí)現(xiàn)一個(gè)事件系統(tǒng)都再簡單不過。這里我們倒是可以進(jìn)一步探索,除了之前所說的圖形界面調(diào)試,測試、監(jiān)控自動(dòng)化,我們還能做什么?對(duì)前端來說,如果前后端事件系統(tǒng)可以直接打通,并且出錯(cuò)時(shí)通過圖形化的調(diào)試工具能無需回滾直接排查,那就最好了。
//觸發(fā)前端的 post.create 事件 var post = {title: "test", content: "test"} emitter.fire("post.create").then(function(){ alert("創(chuàng)建成功") }).catch(function(){ alert("創(chuàng)建失敗") })
在處理邏輯的文件中: //可以增加前端專屬的邏輯 emitter.on("post.create", function checkTest(post){ if( post.title === "test"){ console.log("this is a test blog.") } }) //通過 server: 這樣的命名空間來觸發(fā)服務(wù)器端的事件 emitter.on("post.create", function communicateWithServer(post){ console.log("communicating with server") return emitter.fire("server:post.create", post) })
得到的事件棧
在瀏覽器端可以打通和服務(wù)器端的事件系統(tǒng),那么在服務(wù)器端呢?剛剛提到我們我們其實(shí)可以用任何自己熟悉的語言去實(shí)現(xiàn)事件系統(tǒng),那是不是也意味著,只要事件調(diào)用棧的數(shù)據(jù)格式一致,我們就可以做一個(gè)跨語言的架構(gòu)? 例如我們可以用nodejs的web框架作為服務(wù)器端入口,然后用python,用go去寫子系統(tǒng)。只要約定好系統(tǒng)間通信機(jī)制,以及事件調(diào)用棧的數(shù)據(jù)格式,那么就能實(shí)現(xiàn)跨語言的事件系統(tǒng)融合。這意味你未來看到的調(diào)用棧圖可能是:
跨語言的實(shí)現(xiàn),本身也是一筆巨大財(cái)富。例如當(dāng)我們未來想要找人一起協(xié)同完成某一個(gè)web應(yīng)用時(shí),再也不必局限于某一種語言的實(shí)現(xiàn)。甚至利用docker等容器技術(shù),執(zhí)行環(huán)境也不再是限制。再例如,當(dāng)系統(tǒng)負(fù)載增大,逐漸出現(xiàn)瓶頸時(shí)。我們可以輕松地使用更高效的語言或者執(zhí)行環(huán)境去替換掉某個(gè)業(yè)務(wù)邏輯的監(jiān)聽器實(shí)現(xiàn)。 更多的例子,舉再多也舉不完。當(dāng)你真正自己想清楚這套架構(gòu)之后,你會(huì)發(fā)現(xiàn)未來已經(jīng)在你眼前。 到這里,對(duì)“理想”的想象和對(duì)實(shí)現(xiàn)技術(shù)的思考終于可以劃上句號(hào)了。對(duì)熟悉架構(gòu)的人來說,其實(shí)已經(jīng)圓滿了。但我也不想放棄來“求干貨”的觀眾們。下面演示的,就是在框架原型下開發(fā)的簡單應(yīng)用。這是一個(gè)多人的todo應(yīng)用。
前端基于react,后端基于koa。
目錄結(jié)構(gòu)
前端數(shù)據(jù)(todo 列表) /public/data/todos.js
前端邏輯(todo 基本邏輯) /public/events/todo.js
前端邏輯(輸入@時(shí)展示用戶列表) /public/events/mention.js
后端邏輯(通知被@用戶) /modules/mention.js
通過調(diào)試工具得到的創(chuàng)建時(shí)的調(diào)用棧和輸入@符號(hào)時(shí)的調(diào)用棧
這只是一個(gè)引子,目的是為了讓你宏觀的感受將應(yīng)用拆解為“數(shù)據(jù)+邏輯”以后能有多簡單。目前這套框架已完成 50% ,實(shí)現(xiàn)了數(shù)據(jù)部分的設(shè)計(jì)、前后端事件融合,還有跨語言等方案正在開發(fā)中。未來將開源,期待讀者關(guān)注。
|
|
|