前端工程化開發方案app-proto
什么是前端工程化?根據具體的業務特點,將前端的開發流程、技術、工具、經驗等規范化、標準化就是前端工程化。它的目的是讓前端開發能夠“自成體系”,最大程度地提高前端工程師的開發效率,降低技術選型、前后端聯調等帶來的協調溝通成本。
美團點評廈門智能住宿前端研發團隊通過多個前端項目開發的探索和實踐,基于“約定優于配置”( Convention Over Configuration )的原則制定了一套前端工程化開發方案app-proto。本文將簡要介紹其中的一些設計細節和約定。
面臨的業務特點
智能住宿前端團隊承擔的前端業務主要面向B端項目,用戶主要是商家、銷售、運營、產品經理以及研發人員。
諸如工單管理、信息管理、門鎖運營、PMS( Property management system )、CRM( Customer relationship management )及AMS( Asset management system )等項目都是單頁面工具類應用,特點是功能交互繁多、復雜表單,非展示類、無SEO( Search engine optimization )需求。
如果這些項目脫離瀏覽器這個“外殼”,與傳統的原生桌面GUI軟件無異。換言之,這些項目就是一種運行于瀏覽器的工具軟件。
實際上,部分項目我們也確實利用CEF( Chromium Embedded Framework )等技術給其套個“外殼”,當作傳統的桌面GUI應用提供給用戶使用。
同時,部分服務需要從智能門鎖、控制盒Wifi等硬件設備收錄狀態數據,限于硬件環境測試的不穩定性,后端的開發測試周期遠比前端開發周期長。大部分場景下,前后端需并行開發,后端工程師并不能在第一時間兼顧到前端所需的API接口等服務,給前端開發造成沒有必要的“等待期”,影響開發進度。
此外,項目多、敏捷需求多、開發周期短以及面向多后端服務(多個后端團隊)等也是我們前端研發團隊面臨的挑戰。
一些前端經驗總結
針對多個項目的開發實踐和探索,我們在對前端工程化設計中得到如下一些經驗總結:
- 前端開發應該“自成體系”(包括構建、部署及前端運維),不應該和后端項目耦合在一起。
- 避免“大而全”的重量級框架,一個框架真的滿足不了所有的業務場景。項目多了,我們又不想為每個新項目重新造一遍技術“輪子”。
- 新的前端技術( React 、 Vue 、 Angular2 等)和工具( Grunt / gulp 、 webpack 、 Babel 等)不斷涌現、迭代,新技術選型應避免“改頭換面”式重構。
- 工程化設計要合理分層且相互獨立,隨時應對新需求和技術的變化,任何一層能夠低成本被替換、淘汰。
設計概覽
目前,app-proto將前端工程化項目拆分成三大模塊:Node服務(負責數據代理、url路由和服務端渲染)、Web應用開發(專注Web交互體驗)以及前端運維(構建、測試、部署及監控等)。整體的結構設計如圖1所示。
app-proto 結構設計圖
- Node服務:用于實現前后端分離,核心功能是實現數據代理中轉,附帶url路由分發和服務端渲染功能。
- Web應用開發:純粹的前端模塊,給予前端工程師極大的自由度進行技術選型,專注于Web交互體驗的開發。
- 前端運維:主要指前端項目構建和部署、工程質量(源碼質量檢查和測試等)及監控服務(日志、性能等)等工作。
前后端分離
正如前文所強調的,前端模塊開發應該“自成體系”,而不是后端項目的一部分(Controller或View層)。比如說,前端工程師要在本地跑通完整的項目,就必須配置好后端所需開發環境和各種服務,如果后端涉及的服務多、變化頻繁,配置開發聯調環境工作往往是耗時耗力的。為了實現徹底的前后端分離,我們在前端開發體系中引入了Node服務層。
在最初的開發中,為了降低Node端的開發和運營成本,我們極力避免在Node服務中“摻合”過多的業務邏輯。經過幾個項目的實踐,最后“約定”在Node服務中我們僅僅做三件事:數據代理、路由分發和服務端渲染。
數據代理
首先,前端數據從何而來?通過Ajax的形式直接從后端服務中獲取數據是傳統的方式,但是在應對多后端服務時,還是面臨著諸如請求認證、CORS( Cross-origin resource sharing )等困擾。常見的解決方案是通過 http-proxy ,即在Node端通過HTTP請求得到數據后,Web端再通過Ajax的方式從Node端間接獲取后端數據,Node服務起到“橋梁”的作用。
方案 http-proxy 對已經成熟的后端服務是具備實用價值的,但是在后端服務并沒有完成開發(或前后端并行開發)的場景下時,開發階段前端的數據來源依舊是個問題。同時,前端還面臨諸多請求合并、緩存等需求,解決這些困擾,前端工程師需要和后端技術人員做大量的溝通、約定。
在這里,我們基于原有的 http-proxy 基礎上在Node服務中添加 datasources 模塊,嘗試在數據的處理上給予前端工程師很大的自由度,并實現“按照約定寫代碼”。
舉例說明,開發某一前端業務時涉及到 pms 和 upm 兩個后端服務,且提供的API內容如下:
# pms API pms/api/v2.01/login pms/api/v2.01/inn/create pms/api/v2.01/inn/get # upm API upm/api/v3.15/menu
面對這些接口,理想情況下前端直接通過 ajax.post('pms/api/v2.01/login', params) 方式獲取即可。但是, pms 接口服務尚處在開發階段,面臨跨域或不可用問題。 upm 接口服務雖穩定,但是該服務由第三方團隊維護,請求需要權限認證。傳統的Ajax方式在這類場景下并不適用。而 datasources 模塊是通過怎樣的設計來優化這些問題的呢?首先,我們將前端需要的API映射到前端源碼倉庫,映射的目錄結構如下:
# server/datasources/{后端系統}/{接口目錄} ── datasources ├── pms │ ├── login.js │ ├── login.json │ └── inn │ ├── create.js │ └── get.js └── upm ├── menu.js └── menu.json
其中,每個 **.js 后綴的文件的內容是將原本Web端Ajax操作轉移到Node端的HTTP請求,以 pms/login.js 為例:
/* async 函數 */ export default async function (params) { const http = this.http const pms = this.config.api.pms try { const apiUri = `${pms.prefix}/login` // http 請求:http.post() 方法封裝了權限認證 const result = await http.post(apiUri, params) // 簡單的數據格式校驗 if (Number(result.status) === 0 && ('data' in result) && ('bid' in result.data)) { // 將bid值記錄至session this.session.bid = result.data.bid } return result } catch (e) { // 后端API出現異常 (實時通知 or 記錄日志) } return null }
當然,對于那些已經成熟穩定的API服務直接通過 http-proxy 方式實現數據中轉即可。但由于需求變更頻繁,后端API服務始終處在不斷迭代中,前端在進行數據處理過程中總會面臨如下的幾種情況:
- 接口校驗或數據二次加工:面臨多后端服務,API的格式可能不一致;或者對數據列表排序加工等。
- 合并請求:可以發多個http請求,避免Web端同時發送多個Ajax請求。
- 前端運維的數據:比如城市字典、陰陽歷轉換表等固定數據。
- 緩存數據:如請求的用戶信息,短期內不會有大變動,可以采用 Half-life cache 等算法實現簡單緩存。
- 需權限認證的接口: HTTP Authentication 。
這些場景下都建議使用 datasources 模塊進行數據中轉,將原本需由前后端溝通協調才能實現的功能全部交給前端自行處理,給予前端工程師處理數據提供自由度的同時也降低了后端API的開發維度。
那該如何快捷地調用 datasources 目錄下的 async 函數呢?這里我們做了簡單封裝,將該目錄下的所有 **.js 文件解析到Koa的上下文環境中以 this.ds 對象進行存儲,并按照目錄結構進行駝峰式( Camel-Case )命名,轉換過程見圖2。
datasources 目錄解析轉換過程
在Koa中間件中通過 this.ds 對象調用,比如 src/datasources/pms/login.js 函數映射至 this.ds.PmsLogin() :
// Koa Middlewares app.use(async (ctx, next) => { // ..`. // 最后一個參數為是否使用mock const loginData = await this.ds.PmsLogin(params, false) // ... })
在Web端可以統一封裝 ds() 方法,無需關注Ajax請求 Headers 、是否跨域等問題:
// Web (Browser) ds('PmsLogin', { username, password }, true) .then(success) .catch(error)
Mock支持
正如前文所提到的,后端研發進度一般滯后于前端,在后端API服務可用之前,前端僅有一份API文檔供參考。在規范中, **.json 后綴的文件就起到Mock作用,同樣以 pms/login.json 舉例:
{ "status": 0, "message": "成功", "data": { "bid": "@string(32)", "innCount": 1 } }
簡言之,當API服務可用時則執行 **.js 后綴文件中的 async 函數來獲取數據,不可用時則解析 **.json 后綴Mock文件,并不需要單獨開啟一個Mock服務。
路由分發
對url路由的處理和數據代理的做法類似,按照目錄結構來管理。url路由配置在 server/pages 目錄下,目錄下的文件會自動映射成為路由。
比如url為 http://example.com/pms 頁面,映射到 server/pages/pms.js 文件的寫法如下:
export default { urls: ['/pms', '/pms/error'], // 多種正則如:['/pms', ['/pms/v1'], ['/pms/v**']] methods: ['GET'], // 多種method:['GET', 'POST'] js: ['http://code.jquery.com/jquery-1.12.0.min.js'], css: ['http://yui.yahooapis.com/pure/0.6.0/pure-min.css'], template: 'default', // 服務端渲染模板 middlewares: [], // 針對本頁面的中間件 controller: async function(next) { // Koa中間件最后一環 // 可以從this.ds對象中拿數據 const loginData = await this.ds.PmsLogin(params) return {foo: '來自服務端數據', loginData} } }
由于 urls 支持多種正則,原則上每個根url映射 server/pages/ 目錄下一個 **.js 文件,映射關系如圖3所示。
pages目錄文件與url映射關系
如果對 js 、 css 、 template 沒有特殊設置(采用默認設置)的情況下,可精簡如下:
export default { urls: ['/pms', '/pms/error'], controller: async function (next) { const loginData = await this.ds.PmsLogin(params) return {foo: '來自服務端數據', loginData} } }
需要注意的是, controller 項是Koa中間件的最后一環,要求其返回值是可序列化的對象用于模板渲染的服務端參數,在此處也可以進行權限校驗、從 this.ds 對象中拿數據等操作。
服務端渲染
Node服務端最后一個核心功能是渲染:輸出 HTML Shell和 JSON。輸出JSON字符串的用途是為了瀏覽器端能以Ajax形式動態獲取數據,而輸出的HTML內容則是我們Web應用的所需的HTML“殼子”。
正如前文提到我們的業務特點是“一種運行于瀏覽器的工具軟件”,重操作交互、無SEO需求。因此,同構( Isomorphic JavaScript )不是強需求,不是每次都要依賴服務器來重復處理邏輯和數據。服務端只需要渲染簡單完善的HTML結構即可,具體的頁面內容則由客戶端JavaScript實現。簡言之,不鼓勵將前端JavaScript腳本再在Node服務端重復執行一遍。
渲染最簡單的HTML“殼子”如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"/> <title>app-proto</title> <script>window.serveData={foo: '來自服務端數據'}</script> </head> <body> <div id="app"></div> <script src="http://cdn/file-5917b08e4c7569d461b1.js"></script> </body> </html>
提供簡單的服務端數據 window.serveData 供客戶端使用,更多渲染則由 //cdn/file-5917b08e4c7569d461b1.js 進行增量控制。
靜態資源與Node端銜接
那Web端構建的靜態資源是如何Node服務端做銜接的呢?前端靜態資源構建工作與Node服務相互分離,Node服務在開啟的過程中會讀取前端構建生成的靜態資源映射表。前端的構建過程如圖4所示,在構建工作完成之后會生成 assets.json 靜態資源映射表。
靜態資源映射文件assets.json構建
前端構建工具基本都提供靜態資源映射表生成插件,比如構建工具Webpack就存在插件 assets-webpack-plugin 來實現該功能。
生成的 assets.json 映射表內容參考如下:
{ "index": // 對應的頁面(url: example.com/index) { "js":"http://s0.example.net/pms/index-2abb99.js" }, // 涉及到的靜態資源列表(帶版本號) "login": { "js":"http://s0.example.net/pms/login-5917b0.js" } }
比如在渲染頁面 example.com/index 時,Node服務會以 index 作為鍵值,讀取 assets.json 中帶版本號的靜態資源CDN地址列表,用于在“殼子”中與前端資源的銜接工作。
Web端的一些“約定”
Web端的技術選項是沒有強制性限制的,無論你采用何種構建工具、前端庫,只要生成符合約定供Node端使用的 assets.json 文件即可。
前端工程師可以根據具體的業務特點、團隊技術喜好來選取合理的開發方案,無論是React、Vue還是Angular2并不做強限制。盡管給予Web前端開發很大的自由度,但是鼓勵遵循下面幾條“約定”:
- Ajax請求從Node端代理,而非具體后端服務。
- 鼓勵將JavaScript、CSS、HTML視為前端領域的“匯編”。
- 重視前端頁面狀態管理,推薦的方案有 Redux 、 vuex 及 MobX 等。
- 強調組件化,面向組件集開發。
這里重點強調下面向組件集的前端開發。在項目初期我們一般不會馬上投入到業務開發,而是針對設計師和產品經理提供的設計稿、產品原型圖實現一套組件集或選擇合適的開源組件集,積累好基礎組件集后再投入到具體業務開發。
在進行前端技術調研時,該技術是否有配套的開源組件集往往是我們考慮的重點。比如基于React實現的開源組件集 ant.design 、 Material-UI 等,我們部分前端項目都直接或間接的使用到了,極大地減少了研發成本。
當然,美團點評內部也提供一個組件中心平臺,鼓勵大家將各自項目中的有價值組件分享出來,實現組件跨項目復用。
工程化支持
項目腳手架
項目腳手架的作用是在啟動一個新項目時,通過幾個簡單命令就能快速搭建好項目的開發環境。我們基于 Yeoman 構建了一個完整的項目腳手架。
# 安裝腳手架 $ npm install -g yo $ npm install -g @ia/generator-app-proto@latest # 初始化新項目(進行簡單選擇) $ yo @ia/app-proto
工程質量保障
我們重視項目的每次 commit ,同個項目要求遵循同一套編碼規范,并采用 ESLint 等工具進行約束,對于一些復用性高的核心組件也強制要求寫測試。
為保障項目質量,每個項目都要求接入美團點評基于 Stash 實現的Castle CI系統,每次的源碼提交都會自動執行一遍ESLint、測試和構建,并生成構建日志通過公司內部溝通工具大象進行實時消息推送。
標準化測試環境管理
美團點評內部提供了基于Docker實現的測試環境管理服務Cargo,用于提升測試和聯調測試效率,促進DevOps開發模式。將項目接入到Cargo服務后,只需在倉庫中提供簡單的配置文件 cargo.yml (配置參考如下),就會自動生成一套測試環境。
# 依賴的鏡像 image: registry.cargo.example.com/node:v4.2.1 # 容器占用的端口 ports: - '8998' # 環境變量 env: - COMMON_VARIABLE = 'true' - NODE_ENV = 'cargo' - DEBUG = 'app-proto,datasource.*' # 收集的日志文件 logs: - error = /var/path/logs/app-proto/error.log - out = /var/path/logs/app-proto/out.log # 構建腳本 build_script: bin/pre-deploy-staging # 運行腳本 run_script: bin/cargo-start
總結
前端工程化體系的引入,讓前端開發能和原生App應用項目開發一樣“自成體系”,脫離了對后端項目的依賴。基于“約定優于配置”、“按照約定寫代碼”的原則對Node層功能的設定能夠降低溝通協調成本,構建、部署等工作的規范化,使前端技術人員的開發重點回歸到Web應用的交互體驗本身,回歸到“純粹”的前端研發。
不想錯過技術博客更新?想給文章評論、和作者互動?第一時間獲取技術沙龍信息?
來自:http://tech.meituan.com/tech-salon-13-app-proto.html