雖然2月初就回來了,可 CoreCRM 一直到5月才開始恢復開發,期間是各種生活中的意外和不方便。 1. 為什麼要重構 首先是一件很值得高興的事情:CoreCRM 有了第一位 contributor! "Larry" 是我原來的一位實習生,現在在某公司做前端開發。因為 Larry 的加入,我就不再是 ...
雖然2月初就回來了,可 CoreCRM 一直到5月才開始恢復開發,期間是各種生活中的意外和不方便。
1. 為什麼要重構
首先是一件很值得高興的事情:CoreCRM 有了第一位 contributor!Larry 是我原來的一位實習生,現在在某公司做前端開發。因為 Larry 的加入,我就不再是一個人戰鬥了。當然,也就得考慮怎麼進行合作的事情了。
年前的開發計劃是:使用 Bootstrap 按照悟空CRM的樣子先弄出一個可以用的版本來。然後再使用一些比較好的後臺前端框架來代替 Bootstrap。在和 Larry 討論之後,他覺得 Bootstrap 已經有點跟不上時代,以後擴展 UI 都會有一些困難。加上 Bootstrap 是 jQuery 作為交互基礎的,和 VueJS 這樣的框架也是不太配合。正好 Larry 的公司正在使用螞蟻金服出品的 Ant Design 開發後臺程式,他感覺這個框架設計的不錯:組件豐富、設計合理、社區活躍、文檔豐富(還是中文)。Ant Design 基於 React Component,也是當前非常流行的前端框架,經過多年的發展,據說其組件的豐富程度已經與 jQuery 不遑多讓。
鑒於之前我使用 VueJS 做全頁渲染的經驗,我認為 React 這東西,如果不做 Server-Side Rendering(SSR),用戶體驗不會太好。如何集成 SSR,其實有兩種方案:
- 完全使用 nodejs 來做前端伺服器,ASP.NET Core 做 API 伺服器
- 集成 node 到 ASP.NET Core 里,通過 ASP.NET Core 提供一些 Web 的服務
去年我也曾經用了一周的時間去研究幾個 ASP.NET Core 的 React Server-side Rendering 方案,可一個人的精力畢竟有限,要同時使用兩種語言開發,腦子的轉換效率是一個問題。最後我放棄了 React,轉而使用 ASP.NET Core 的 Razor 來做頁面渲染了。只對其中一些動態的部分做了 VueJS 組件。不過這次情況不一樣了,有 Larry 做前端的開發,我可以把更多的精力放到後面的API 開發和架構的優化上。而且,前後分離之後,還可以在對方工作滯後的情況下繼續開發。所以我決定對整個項目進行重構。
2. 重構的嘗試
近些年前端技術發展迅速,各種 hot reload 橫行,在開發的時候要方便和高效的多。那麼應該怎麼來實現 SSR 呢?我進行了三次嘗試:
2.1 Microsoft.AspNetCore.SpaServices
做為 ASP.NET Core 團隊的作品,感覺上是比 Facebook 做的 ReactJS.NET 更好用一些。特別是和 ASP.NET Core 的互通性上,應該有一些優勢。不過,ReactJS.NET 的 SSR 已經內置,寫起來要容易一些。
一開始,我嘗試使用 aspnetcore-spa 這個 yeoman generator,可這貨居然還是使用的 typescript 做為主語言。雖然我之前也學了一點 typescript,但與別人合作的時候,就不能只考慮自己的技術棧了。為了不增加 Larry 的學習成本,以使得項目能夠儘快進入開發,我決定還是自己搞一個基於 es6 的 ClientApp。方法當然也很簡單:用 dva-cli 創建一下就 OK 了。
在完成了 ClientApp 的創建之後,需要添加一個 boot-server.js 來實現 SSR:
var { match } = require('react-router');
var { createServerRenderer } = require('aspnet-prerendering');
module.exports = createServerRenderer(function(params) {
return new Promise(function (resolve, reject) {
var re = /^\/([^\/]*)(/[^\/]*)?/;
var matched = params.location.path.match(re);
var controller = matched[1];
var codeFile = './dist/' + controller;
var App = require(codeFile); // eslint-disable-line
var path = matched[2] === '' ? '/' : matched[2]; // this line is buggy.
match({
routes: App.routes,
location: path
}, function (err, redirectLocation, renderProps) {
if (err) throw new Error("Route match failed: " + err);
if (redirectLocation) new Error("I don't know how to redirect.");
var initialState = {};
resolve({ html: App.renderHTML(initialState, renderProps)});
})
});
});
params
里包含了一些從 Razor 傳進來的數據,比如訪問的路由、初始化數據等。這裡本來應該直接使用 params.location.path
來匹配路由,進行渲染的,可是我並沒有使用完全的 SPA (Single Page Application) 架構,而是有所分離,所以就需要使用正則表達式分離 controller 和 action,然後再進行頁內的匹配。現在 repo 里的代碼只是實現了單頁的測試載入,還沒有正式的使用起來。
2.2 koa + Web API Server
本來以為上面這個方案就已經可以了。前端使用了 dva + antd + roadhog,可以直接運行一個 webpack-dev-server 直接進行開發。然後我再轉到 Razor 里做為一個 Controller 的 View。不過,Larry 希望能完全脫離 ASP.NET Core 運行前端代碼。我也考慮了使用
SpaServices 可能會有一些限制,比如:需要處理路由、不能使用 ES6,同時運行的時候也還是需要安裝 Node,其實和一個獨立的前端 Server 並沒有什麼區別。所以我又嘗試把前端的 Server 完全分離出來。
這一步可能做的有點太激進了,我嘗試使用 Web API 的模板重新創建了 CoreCRM 這個 project。結果就悲劇了:Web API 是非常輕量的框架,裡面什麼也沒有。加上如果要分離前後端 Server,比較好的許可權驗證方案是使用 JSON-Web-Token,不然還需要在兩個 Server 之前同步 session,也是比較麻煩的事情。而搞一套 JWT 的驗證機制,也不是很容易。已經有的解決方案不是太簡單,就是太複雜……
回歸 SpaServices
上面這些困難加起來,讓我覺得這裡面的學習成本現在不可接受。所以就先放棄了這個方案。在經過一點設計和開發之後,我發現這其實和使用 koa 並沒有太大的差別。問題總是可以通過一些服務間的交互來解決的。只是現在這個節點,使用 SpaServices 更容易上手一點。待到項目有一個可用的版本,後面可以嘗試以其它的方式進行重構,也不是不可能的事情。畢竟這個項目至少還有2.0版。
3. 經驗
“選擇”總是一件很困難的事情。特別是每個選項都各有利弊的時候,選擇就更加困難。每一種組合都是一種可能,每一種組合有都有它的局限。差別可能就在團隊成員之間是不是能順暢的合作。如果合作出現問題,能不能及時調整。
希望這次調整能給項目帶來更多的活力。