系統重構Part 2
重構來到了尾聲,整理一下後半段

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