掘金從上線到現在,網站的前端重構了 3 次,後端也陸陸續續修改了整個網站的結構 2 次,但是隨著業務不斷推演複雜,團隊人手增加,有需要一波進一步的優化!
這周,我們會根據當下掘金的情況和接下里的主要業務,整理程式碼。
掘金技術整理系列文章:
- 掘金技術整理(一)掘金的後端架構
- 掘金技術整理(二)掘金前端 Vue 結構及最佳實踐 - 正在寫
後端架構梳理
在架構開發之前,我們首先要梳理現有的網站狀態和需求,然後再做優化
- 後端語言、框架、功能架構狀態
- 主要業務
- 程式碼結構
- 一些最佳實踐
- 下一步的展望
後端語言、框架、功能架構狀態
後端語言 Node.js
由於網站的早期開發是我來負責的,雖然我曾經寫過 PHP
、Ruby on Rails
和 Python
的後臺,但因為自己同時寫很多前端程式碼因此對 JavaScript
最熟。與此同時,我們選擇了 LeanCloud 作為我們的雲端儲存、推送、託管平臺,因而就繼續使用了 Node.js
為開發框架。
- 當前版本:
v4.4.5
框架 Express.js
因為選擇了 LeanCloud 的緣故,其後端你託管要用 Express
加上本身對這個框架的使用還算熟練,便沿用了下來。
- 當前版本:
4.13.3
功能架構狀態
上面已經講了我們選擇了 LeanCloud,具體來講我們使用瞭如下功能:
LeanStorage
:資料儲存LeanMessage
:移動應用推送Leanengine
:雲引擎- Web 伺服器
- 網頁渲染
- 簡單的 API 介面
- 雲函式
- 資料繫結指令碼
- 定時指令碼
- Web 伺服器
LeanAnalytics
:資料統計工具- 當前版本:
1.0.0-beta
由於掘金網站的主頁面是一個純前端應用,其部分業務會與後端資料介面直接關聯。下圖主要展示了我們後端的整體狀態:
其中黑色的箭頭表示業務需求,而黃色箭頭代表資料更新需求。
主要業務模組
Web Server
網站伺服器是整個應用最基礎的部分,它處理網頁端的頁面、API 請求,外聯使用者資訊和 LeanStorage 資料庫。
config.js // 後端配置檔案
server.js // 伺服器啟動指令碼
app.js // 後端業務,被 server.js 引用
cloud.js // 雲函式定義,被 app.js 引用
webpack.config.js // webpack 打包配置檔案複製程式碼
根據不同的環境(生產環境、前端開發、後端開發),config.js
定義了各個開發環境需要的配置檔案資訊。例如在 package.json
的 npm scripts
裡會定義:
"dev": "cross-env NODE_ENV=devFrontend supervisor -i vue,node_modules server"複製程式碼
這樣,npm run dev
就會設定當下的 NODE_ENV
是 devFrontend
,也就是前端開發環境。
接下來 server.js
就會根據當下環境讀取的配置檔案開啟服務端業務,如 app.js
定義的後端業務程式碼或 cloud.js
裡的雲函式。當然,在不同環境下 webpack
也會做不同的操作。
app.js
app.js
援引了配置檔案後,執行 Express.js
框架,開啟業務程式碼。除了基本的 middleware(頁面 template engine jade
, cookie, session 等等),其最主要的業務包含:
- 頁面:
routes
- 使用者:
auth
- 錯誤處理
由於網站大量使用前端 router,因而在 routes
定義中也要格外小心,掘金的開發方式是這樣的。例如 Styleguide 頁面,可能是這樣的定義的:
// app.js 檔案
// routes/page.js 裡定義了網頁相關的路由及後端渲染
var page = require('./routes/page');
// app 繫結了 styleguide 頁面的路由,及幾個前端路由統一
app.get('/styleguide', page.styleguide);
app.get('/styleguide/base', page.styleguide);
app.get('/styleguide/components', page.styleguide);複製程式碼
cloud.js
雲函式
LeanCloud 很好地支援了雲函式,可以幫助你完成後端的資料觸發性 Hook 指令碼及定時指令碼。
例如每一條評論儲存在 Comment
Table 裡,那麼對於這條評論,我們可以捕捉到
beforeSave // 儲存之前
afterSave // 儲存之後
beforeUpdate // 更新之前
afterUpdate // 更新之後
beforeDelete // 刪除之前
afterDelete // 刪除之後複製程式碼
加入,我們想要實現一個功能,就是增加一個 comment
後,更新相應的文章的評論數加一,而刪除後則減一。
// cloud.js 檔案
// cloud/comment.js 定義了關於 Comment 資料的 Hook 函式
var comment = require('./cloud/comment');
AV.Cloud.afterSave('Comment', comment.afterSave);
AV.Cloud.afterDelete('Comment', comment.afterDelete);複製程式碼
// cloud/comment.js
exports.afterSave = function(request, response) {
... // update 相關文章的資料
if (ok) {
response.success();
} else {
response.error('...');
}
})複製程式碼
webpack.config.js
webpack 是一個模組打包工具,隨著它的外掛、業務越來越強大,它也像是之前的 grunt
和 gulp
一樣分攤了一部分指令碼自動化的功能。
webpack.config.base.js
:基本的打包配置檔案,主要用於開發環境熱更新webpack.config.prod.js
:生產環境的配置檔案,引用了base
,定義打包需求,生成 build 好的檔案
這裡我就不展開關於 webpack 本身的配置優化的部分。
程式碼結構
除了上面說的最基本的伺服器開啟檔案,整個專案的程式碼結構如下:
config.js
server.js
app.js
/routes // 各個路由的後端業務邏輯
/views // 網頁渲染的 jade 檔案
/vue // 各個頁面的 vue 業務邏輯
/redis // 快取定義
/public // 外部訪問的靜態檔案
/assets // 後端靜態檔案
/data // 後端靜態資料
/scss // SCSS 樣式檔案
cloud.js
/cloud // 雲函式相關定義檔案
webpack.config.js // webpack 打包配置檔案
webpack.config.base.js
webpack.config.prod.js複製程式碼
當我們要增加一個頁面的時候
我們再以 Styleguide 為例,如果我們要新增這樣的一個網頁程式碼:
- 我們確認它應該在
app.js
的路由的哪個模組下,/styleguide
是一個獨立頁面,因而它應該被定義在/routes/page.js
裡,並定義到:// page.js 檔案 exports.styleguide = function(req, res) { res.render('styleguide', { title: '掘金前端 Style Guide' }) }複製程式碼
- 路由繫結:
// app.js app.get('/styleguide', page.styleguide);複製程式碼
- 由於它是是一個網頁,因而我們還要在
/views
裡面定義/views/styleguide.jade
- 這個時候我們會看這個頁面是否會是一個前端網頁:
- 是:在
/vue
裡定義,並要在webpack.config.base.js
裡定義打包邏輯 - 否:則在
page.js
裡後端渲染頁面是傳入資料
- 是:在
- 基於網頁的複雜度來測試是否需要獨立的樣式,則需要定義
/assets/scss/styleguide.scss
(更多 CSS 結構我們會在另外一篇文章中詳細描述)
這樣,整個 Styleguide 頁面會影響到的後端程式碼是:
app.js // 路由繫結到 /styleguide
/routes
page.js // 定義了 styleguide 後端業務
/views
styleguide.jade
/vue
/styleguide // styleguide 相關 vue 前端業務
main.js
app.vue
/assets
/scss
/pages/styleguide // [optional] styleguide 內的複雜元件樣式
__style.scss
layout.scss
...
styleguide.scss // styleguide 相關獨立樣式
webpack.config.base.js // 定義新的 styleguide 相對應的 entry複製程式碼
當我們要增加 Hook 函式的時候
舉例,我們要開發一個資料的 Hook 函式到 LeanCloud,比如說每當一個新的 Comment 生成的時候,我們要更新對應文章的評論數及最新的評論:
cloud.js // AV.Cloud.afterSave('Comment', comment.afterSave)
/cloud
comment.js // exports.afterSave = function(request, response) {}複製程式碼
當我們要增加一個定時指令碼的時候
- 定義指令碼在
/cloud/____.js
檔案裡 - 更新
cloud.js
檔案註冊指令碼,如:AV.Cloud.define('cloudFunctionName', functionName)
- 部署後,在 LeanCloud 的定時指令碼控制檯定義執行的週期及時間
一些最佳實踐
Node.js
- 能用環境變數卻別開的資料,都放到
config.js
裡,不要用if
和else
語句區分 - 善用
npm scripts
繫結執行函式,如:npm run dev // 開發,測試資料 npm run dev-backend // 後端開發,測試資料 npm run dev-build // 測試資料,打包 npm run prod // 開發,生產資料 npm run prod-backend // 後端開發,生產資料 npm run prod-build // 生產資料,部署前的打包 npm run test // 測試 npm start // 開啟伺服器複製程式碼
- 函式名竟可能簡單易懂,類似於
getPopEntries
可以明確到getPopularEntries
- 每當安裝、刪除庫的時候,記得用
npm install/uninstall PACKAGE --save/--save-dev
,隨時更新庫 - 命名規範
- 常數:
I_LOVE_YOU
用下劃線加大寫字母 - 變數:
iLoveYou
駝峰,無論是普通變數還是函式名 - Class:
ILoveYou
首字母大寫的駝峰,包括 LeanCloud 自己的資料 Table 名 - 當內嵌的回撥函式用到了類似變數名則,使用
_iLoveYou
加前置下劃線
- 常數:
LeanCloud
- 不要重複 LeanCloud 定義好的資料類名,如
AV.User
,不要使用'_User'
- 善用
Promise
,將複雜的業務改寫為清晰的非同步處理流:start() .then(step1) .then(step2) .then(step3) .then(step4) .catch(errorHandler) .finally(callback)複製程式碼
- 但小心,LeanCloud 修改了幾個 keyword 函式名,如:
always
替換了原有的finally
AV.Promise.error
替換了Promise.reject
AV.Promise.when([promise1, promise2, promise3])
可以在三個 promise 都完成的情況下做非同步操作
- 但小心,LeanCloud 修改了幾個 keyword 函式名,如:
- LeanStorage 資料庫查詢的技巧:
- 多用自帶的一些語句,如
exists
,startsWith
,matches
等 - 利用
select
拿去部分資料 - 查詢一個資料時,善用
query.first()
和query.get(id)
,但是注意:first()
後then(function(obj) {})
中的obj
可能是null
get()
後如果得不到資料會直接引發error
- 使用
AV.Object.createWithoutData(TABLE_NAME, ID)
來實現指標查詢,無需取一遍資料 - 關聯查詢:
query.include('reply.user')
,一個文章查詢可以用這類語句直接查出來一個評論的回覆的使用者資料,也就是說拿出來的一個comment
可以訪問到comment.get('reply')
和comment.get('reply').get('user')
。 - 內嵌查詢:
matchesQuery
- 多用自帶的一些語句,如
Git 管理
origin/
master // 線上版本
|- hotfix-login // 熱修復,如登入異常
release // 最新的要部署的版本
develop // 開發分支
|- feature-homepage-v2 // 正在開發的業務,如第二版的首頁
|- feature-timeline-api // 正在開發的業務,如 Timeline 的 API
developer-ming
master
release
develop
|- feature-timeline-api // 我正在開發這個 feature,不斷和 origin 同步複製程式碼
新的業務
- 任何的一個新的業務開發都要在本地從
develop
fork 出來一個新的 branchfeature-name
- 業務開發完成後,提交 Pull Request,
feature-name -> develop
,記得打 label 到feature
- Code Review,如果有錯誤,在
feature-name
裡修復 - 相關負責人 Merge Pull Request,假刪除這個分支
部署新的業務
develop
上不斷 merge 新的 review 過的業務功能- 部署前,發 Pull Request 到
develop -> release
- 相關負責人 Code Review,合併程式碼
npm run build
打包業務程式碼,準備部署- 部署前的 commit,打 label 到
publish
- 發 PR 到
release -> master
,標註版本號 - 部署,如果出錯,回滾或者新建
hotfix
分支
小技巧
develop
和release
的同步,用git rebase
develop
及feature
分支不做build
操作- 多人負責一個 feature 的時候,可以就一個功能再分拆到各個 branches
下一步的展望
根據產品接下來的發展路徑,有幾個重要的功能需要優化。
- 後端渲染頁面,SEO 優化
- 文章頁面的收斂,利用 Browser Agent
- 分享文章:Web Desktop 的詳情頁
- 分享文章:Web Mobile 的閱讀頁
- 原創文章:Web Desktop/Mobile 的閱讀頁
- 沸點活動:Web Desktop/Mobile 的詳情頁
- 所有文章:App 內的閱讀頁 / JSBridge
- 不同頁面的前後端打包,根據不同頁面的需求,載入相應的程式元件
- 脫離手動打包、配合部署
- 元件如:
- 使用者
- Vue 通用樣式元件
- 頁面本身的業務程式碼
- 網頁間的跳轉邏輯
- API 伺服器獨立,並配合移動端也無需求,通用 API 實現
- 推送邏輯重構