系統重構Part 2

AwesomeJerry
Feb 25, 2017 · 8 min read

重構來到了尾聲,整理一下後半段


程式結構關係
  • Hybrid Database Schema Design

這次設計DB的方式強制規範了內部一律使用每個table的auto-increment serial id作為primary key和其他table的foreign key,外部則一律採用uuid來作為資料的id。對外可以透過GraphQL resolver直接把uuid map成id使用。對內在做join時便很頭痛了。以往設計DB常常使用semantic key作為primary key,如身分證字號、學號。這種做法非常直覺,前端做query時也常常是帶著semantic key過來,很容易就能join其他table。現在這種做法有時候就必須要先透過uuid找回原本在DB裡面的serial id然後再做join,常常搞得頭腦打結。之前請教David學長得知這種做法因為uuid -> id是不會改變的,所以常見的做法是開一個cache server存map,減少DB query的次數。

  • dotenv

docker-compose使用.env檔案來定義環境變數,在非docker環境時開發則用dotenv來讀取.env檔案,部署不同instances時則透過不同的.env檔案來切割credentials、secrets、連線主機等不同資訊。

// .env.exampleAPP_ID=
PORT=
HOST=
DATA_URL=
WS_HOST=
WS_PORT=
JWT_SECRET=MAILGUN_API_KEY=
MAILGUN_DOMAIN=
  • WebSocket

這次使用WebSocket來達到即時App與Web和Web Server的互動,透過middleware的方式把WebSocket串進redux裡,並定義了contract來限制資料傳遞的格式。

// ws/middleware.jsimport { get, set } from './contract';export default ({ dispatch, getState }) => (next) => (action) => {
if (!process.env.BROWSER) {
next(action);
} else if (action.type === WS_START && !getState().ws) {
const ws = createWebSocketClient();
ws.onopen = () => {
dispatch(onOpen(ws));
};
ws.onclose = () => {
dispatch(onClose(ws));
};
ws.onerror = (e) => {
dispatch(onError(e));
};
ws.onmessage = (e) => {
dispatch(onMessage(get(e.data)));
};
} else if (action.type === WS_SEND_MESSAGE) {
const ws = getState().ws;
if (ws) {
ws.send(set(action.payload));
}
}
next(action);
};

透過新增Handler個別處理來自WebSocket的訊息

// ws/createWebSocketServer.jsimport WebSocket from 'ws';
import { set } from './contract';
import Student from './handlers/Student';
export default () => {
const wss = new WebSocket.Server({ port: process.env.WS_PORT || 8080 });
wss.broadcast = (data) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(set({ type: 'BROADCAST', payload: data }));
}
});
};
const student = new Student(wss); wss.on('connection', (ws) => {
student.addConnection(ws);
});
return wss;
};
  • Apollo Client

透過Apollo Client來把GraphQL query mapping到Presentational Components的props上,如此一來可以少寫很多boilerplate的code。因應我們有做server-side rendering,會需要透過web server和data server互動,所以必須把瀏覽器來的cookie帶進request header並送至data server(GraphQL)。

// apolloClient/server.jsimport ApolloClient, { createBatchingNetworkInterface } from 'apollo-client';
import config, { networkInterfaceConfig } from './config';
export default ({ cookie, dataUrl }) => {
const networkInterface = createBatchingNetworkInterface({
...networkInterfaceConfig,
uri: `${dataUrl}/graphql`,
});
networkInterface.use([{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {};
}
req.options.headers.cookie = cookie;
next();
},
}]);
return new ApolloClient({
...config,
networkInterface,
});
};
// apolloClient/client.jsimport ApolloClient, { createBatchingNetworkInterface } from 'apollo-client';
import config, { networkInterfaceConfig } from './config';
export default ({ dataUrl }) => {
const networkInterface = createBatchingNetworkInterface({
...networkInterfaceConfig,
uri: `${dataUrl}/graphql`,
});
return new ApolloClient({
...config,
initialState: window.APOLLO_STATE,
networkInterface,
});
};
// apolloClient/config.jsexport const networkInterfaceConfig = {
batchInterval: 100,
opts: {
credentials: 'include',
},
};
export default {
ssrMode: true,
queryDeduplication: true,
dataIdFromObject: o => o.id,
};

這階段的重構主要針對架構上改動,除了前端之外,最大的調整在於後端的安全性、擴充性設計。未來還有很多可以做得更好的地方:

  • Ducks

採用Ducks架構來管理檔案,把同一domain的邏輯放在一起:actions, reducers, sagas, epics, types。方便管理之外,在新加邏輯時也不用到處新增檔案。

  • Cache Server

蠻常需要從uuid對回DB的id,所以有一個cache server能大大地減低敲DB的次數,目前預計是用docker-compose掛一個redis container。


每次重構都又學到很多新知,變成下一次的重構目標,就像是輪迴一樣一直發生。漸漸地,能夠掌握的工具與知識愈多,工作起來的效率也愈快,在思索下一步時,能考慮到的面相也愈廣。有時候會蠻明顯感覺到自己變強了,只好繼續加油、繼續變強,Fineighbor也能在這些輪迴中一直往前進。

AwesomeJerry

Written by

Node.Tent Developer https://www.node.us.com/crew.html

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade