從一個(gè)優(yōu)質(zhì)開源項(xiàng)目來看前端架構(gòu)
何為系統(tǒng)架構(gòu)師?
-
系統(tǒng)架構(gòu)師是一個(gè)最終確認(rèn)和評(píng)估系統(tǒng)需求,給出開發(fā)規(guī)范,搭建系統(tǒng)實(shí)現(xiàn)的核心構(gòu)架,并澄清技術(shù)細(xì)節(jié)、掃清主要難點(diǎn)的技術(shù)人員。主要著眼于系統(tǒng)的“技術(shù)實(shí)現(xiàn)”。因此他/她應(yīng)該是特定的開發(fā)平臺(tái)、語言、工具的大師,對(duì)常見應(yīng)用場景能給出最恰當(dāng)?shù)慕鉀Q方案,同時(shí)要對(duì)所屬的開發(fā)團(tuán)隊(duì)有足夠的了解,能夠評(píng)估自己的團(tuán)隊(duì)實(shí)現(xiàn)特定的功能需求需要的代價(jià)。系統(tǒng)架構(gòu)師負(fù)責(zé)設(shè)計(jì)系統(tǒng)整體架構(gòu),從需求到設(shè)計(jì)的每個(gè)細(xì)節(jié)都要考慮到,把握整個(gè)項(xiàng)目,使設(shè)計(jì)的項(xiàng)目盡量效率高,開發(fā)容易,維護(hù)方便,升級(jí)簡單等
這是百度百科的答案
大多數(shù)人的問題
如何成為一名前端架構(gòu)師?
-
其實(shí),前端架構(gòu)師不應(yīng)該是一個(gè)頭銜,而應(yīng)該是一個(gè)過程。我記得掘金上有人寫過一篇文章:
《我在一個(gè)小公司,我把我們公司前端給架構(gòu)了》
, (我當(dāng)時(shí)還看成《我把我們公司架構(gòu)師給上了》
) -
我面試過很多人,從小公司出來(我也是從一個(gè)很小很小的公司出來,現(xiàn)在也沒在什么
BATJ
),最大的問題在于,覺得自己不是leader
,就沒有想過如何去提升、優(yōu)化項(xiàng)目,而是去研究一些花里胡哨的東西,卻沒有真正使用在項(xiàng)目中。(自然很少會(huì)有深度) -
在一個(gè)兩至三人的前端團(tuán)隊(duì)小公司,你去不斷優(yōu)化、提升項(xiàng)目體驗(yàn),更新迭代替換技術(shù)棧,那么你就是
前端架構(gòu)師
正式開始
我們從一個(gè)比較不錯(cuò)的項(xiàng)目入手,談?wù)勔粋€(gè)前端架構(gòu)師要做什么
-
SpaceX-API
-
SpaceX-API
是什么? -
SpaceX-API 是一個(gè)用于火箭、核心艙、太空艙、發(fā)射臺(tái)和發(fā)射數(shù)據(jù)的開源 REST API(并且是使用Node.js編寫,我們用這個(gè)項(xiàng)目借鑒無可厚非)
為了閱讀的舒適度,我把下面的正文盡量口語化一點(diǎn)
先把代碼搞下來
- git clone https://github.com/r-spacex/SpaceX-API.git
-
一個(gè)優(yōu)秀的開源項(xiàng)目搞下來以后,怎么分析它?大部分時(shí)候,你應(yīng)該先看它的目錄結(jié)構(gòu)以及依賴的第三方庫(
package.json
文件)
找到 package.json
文件的幾個(gè)關(guān)鍵點(diǎn):
-
main
字段(項(xiàng)目入口) -
scripts
字段(執(zhí)行命令腳本) -
dependencies和devDependencies字段(項(xiàng)目的依賴,區(qū)分線上依賴和開發(fā)依賴,我本人是非??粗羞@個(gè)點(diǎn),SpaceX-API也符合我的觀念,嚴(yán)格的區(qū)分依賴按照)
- "main": "server.js",
- "scripts": {
- "test": "npm run lint && npm run check-dependencies && jest --silent --verbose",
- "start": "node server.js",
- "worker": "node jobs/worker.js",
- "lint": "eslint .",
- "check-dependencies": "npx depcheck --ignores=\"pino-pretty\""
- },
-
通過上面可以看到,項(xiàng)目入口為
server.js
-
項(xiàng)目啟動(dòng)命令為
npm run start
-
幾個(gè)主要的依賴:
- "koa": "^2.13.0",
- "koa-bodyparser": "^4.3.0",
- "koa-conditional-get": "^3.0.0",
- "koa-etag": "^4.0.0",
- "koa-helmet": "^6.0.0",
- "koa-pino-logger": "^3.0.0",
- "koa-router": "^10.0.0",
- "koa2-cors": "^2.0.6",
- "lodash": "^4.17.20",
- "moment-range": "^4.0.2",
- "moment-timezone": "^0.5.32",
- "mongoose": "^5.11.8",
- "mongoose-id": "^0.1.3",
- "mongoose-paginate-v2": "^1.3.12",
- "eslint": "^7.16.0",
- "eslint-config-airbnb-base": "^14.2.1",
- "eslint-plugin-import": "^2.22.1",
- "eslint-plugin-jest": "^24.1.3",
- "eslint-plugin-mongodb": "^1.0.0",
- "eslint-plugin-no-secrets": "^0.6.8",
- "eslint-plugin-security": "^1.4.0",
- "jest": "^26.6.3",
- "pino-pretty": "^4.3.0"
都是一些通用主流庫: 主要是koa框架,以及一些koa的一些中間件,monggose(連接使用mongoDB),eslint(代碼質(zhì)量檢查)
這里強(qiáng)調(diào)一點(diǎn),如果你的代碼需要兩人及以上維護(hù),我就強(qiáng)烈建議你不要使用任何黑魔法,以及不使用非主流的庫,除非你編寫核心底層邏輯時(shí)候非用不可(這個(gè)時(shí)候應(yīng)該只有你維護(hù))
項(xiàng)目目錄
-
這是一套標(biāo)準(zhǔn)的
REST API,
嚴(yán)格分層 -
幾個(gè)重點(diǎn)目錄 :
-
server.js 項(xiàng)目入口
-
app.js 入口文件
-
services 文件夾 => 項(xiàng)目提供服務(wù)層
-
scripts 文件夾 => 項(xiàng)目腳本
-
middleware 文件夾 => 中間件
-
docs 文件夾 =>文檔存放
-
tests 文件夾 => 單元測試代碼存放
-
.dockerignore docker的忽略文件
-
Dockerfile 執(zhí)行docker build命令讀取配置的文件
-
.eslintrc eslint配置文件
-
jobs 文件夾 => 我想應(yīng)該是對(duì)應(yīng)檢查他們api服務(wù)的代碼,里面都是準(zhǔn)備的一些參數(shù)然后直接調(diào)服務(wù)
-
逐個(gè)分析
從項(xiàng)目依賴安裝說起
-
安裝環(huán)境嚴(yán)格區(qū)分開發(fā)依賴和線上依賴,讓閱讀代碼者一目了然哪些依賴是線上需要的
- "dependencies": {
- "blake3": "^2.1.4",
- "cheerio": "^1.0.0-rc.3",
- "cron": "^1.8.2",
- "fuzzball": "^1.3.0",
- "got": "^11.8.1",
- "ioredis": "^4.19.4",
- "koa": "^2.13.0",
- "koa-bodyparser": "^4.3.0",
- "koa-conditional-get": "^3.0.0",
- "koa-etag": "^4.0.0",
- "koa-helmet": "^6.0.0",
- "koa-pino-logger": "^3.0.0",
- "koa-router": "^10.0.0",
- "koa2-cors": "^2.0.6",
- "lodash": "^4.17.20",
- "moment-range": "^4.0.2",
- "moment-timezone": "^0.5.32",
- "mongoose": "^5.11.8",
- "mongoose-id": "^0.1.3",
- "mongoose-paginate-v2": "^1.3.12",
- "pino": "^6.8.0",
- "tle.js": "^4.2.8",
- "tough-cookie": "^4.0.0"
- },
- "devDependencies": {
- "eslint": "^7.16.0",
- "eslint-config-airbnb-base": "^14.2.1",
- "eslint-plugin-import": "^2.22.1",
- "eslint-plugin-jest": "^24.1.3",
- "eslint-plugin-mongodb": "^1.0.0",
- "eslint-plugin-no-secrets": "^0.6.8",
- "eslint-plugin-security": "^1.4.0",
- "jest": "^26.6.3",
- "pino-pretty": "^4.3.0"
- },
項(xiàng)目目錄劃分
-
目錄劃分,嚴(yán)格分層
-
通用,清晰簡介明了,讓人一看就懂
正式開始看代碼
-
入口文件,
server.js
開始
- const http = require('http');
- const mongoose = require('mongoose');
- const { logger } = require('./middleware/logger');
- const app = require('./app');
- const PORT = process.env.PORT || 6673;
- const SERVER = http.createServer(app.callback());
- // Gracefully close Mongo connection
- const gracefulShutdown = () => {
- mongoose.connection.close(false, () => {
- logger.info('Mongo closed');
- SERVER.close(() => {
- logger.info('Shutting down...');
- process.exit();
- });
- });
- };
- // Server start
- SERVER.listen(PORT, '0.0.0.0', () => {
- logger.info(`Running on port: ${PORT}`);
- // Handle kill commands
- process.on('SIGTERM', gracefulShutdown);
- // Prevent dirty exit on code-fault crashes:
- process.on('uncaughtException', gracefulShutdown);
- // Prevent promise rejection exits
- process.on('unhandledRejection', gracefulShutdown);
- });
-
幾個(gè)優(yōu)秀的地方
-
每個(gè)回調(diào)函數(shù)都會(huì)有聲明功能注釋
-
像
SERVER.listen
的host參數(shù)也會(huì)傳入,這里是為了避免產(chǎn)生不必要的麻煩。至于這個(gè)麻煩,我這就不解釋了(一定要有能看到的默認(rèn)值,而不是去靠猜) -
對(duì)于監(jiān)聽端口啟動(dòng)服務(wù)以后一些異常統(tǒng)一捕獲,并且統(tǒng)一日志記錄,
process
進(jìn)程退出,防止出現(xiàn)僵死線程、端口占用等(因?yàn)閚ode部署時(shí)候可能會(huì)用pm2等方式,在 Worker 線程中,process.exit()將停止當(dāng)前線程而不是當(dāng)前進(jìn)程)
-
app.js入口文件
-
這里是由
koa
提供基礎(chǔ)服務(wù) -
monggose
負(fù)責(zé)連接mongoDB
數(shù)據(jù)庫 -
若干中間件負(fù)責(zé)跨域、日志、錯(cuò)誤、數(shù)據(jù)處理等
- const conditional = require('koa-conditional-get');
- const etag = require('koa-etag');
- const cors = require('koa2-cors');
- const helmet = require('koa-helmet');
- const Koa = require('koa');
- const bodyParser = require('koa-bodyparser');
- const mongoose = require('mongoose');
- const { requestLogger, logger } = require('./middleware/logger');
- const { responseTime, errors } = require('./middleware');
- const { v4 } = require('./services');
- const app = new Koa();
- mongoose.connect(process.env.SPACEX_MONGO, {
- useFindAndModify: false,
- useNewUrlParser: true,
- useUnifiedTopology: true,
- useCreateIndex: true,
- });
- const db = mongoose.connection;
- db.on('error', (err) => {
- logger.error(err);
- });
- db.once('connected', () => {
- logger.info('Mongo connected');
- app.emit('ready');
- });
- db.on('reconnected', () => {
- logger.info('Mongo re-connected');
- });
- db.on('disconnected', () => {
- logger.info('Mongo disconnected');
- });
- // disable console.errors for pino
- app.silent = true;
- // Error handler
- app.use(errors);
- app.use(conditional());
- app.use(etag());
- app.use(bodyParser());
- // HTTP header security
- app.use(helmet());
- // Enable CORS for all routes
- app.use(cors({
- origin: '*',
- allowMethods: ['GET', 'POST', 'PATCH', 'DELETE'],
- allowHeaders: ['Content-Type', 'Accept'],
- exposeHeaders: ['spacex-api-cache', 'spacex-api-response-time'],
- }));
- // Set header with API response time
- app.use(responseTime);
- // Request logging
- app.use(requestLogger);
- // V4 routes
- app.use(v4.routes());
- module.exports = app;
- //組件掛載
- componentDidmount(){
- }
- //組件需要更新時(shí)
- shouldComponentUpdate(){
- }
- //組件將要卸載
- componentWillUnmount(){
- }
- ...
- render(){}
router的代碼,簡介明了
- const Router = require('koa-router');
- const admin = require('./admin/routes');
- const capsules = require('./capsules/routes');
- const cores = require('./cores/routes');
- const crew = require('./crew/routes');
- const dragons = require('./dragons/routes');
- const landpads = require('./landpads/routes');
- const launches = require('./launches/routes');
- const launchpads = require('./launchpads/routes');
- const payloads = require('./payloads/routes');
- const rockets = require('./rockets/routes');
- const ships = require('./ships/routes');
- const users = require('./users/routes');
- const company = require('./company/routes');
- const roadster = require('./roadster/routes');
- const starlink = require('./starlink/routes');
- const history = require('./history/routes');
- const fairings = require('./fairings/routes');
- const v4 = new Router({
- prefix: '/v4',
- });
- v4.use(admin.routes());
- v4.use(capsules.routes());
- v4.use(cores.routes());
- v4.use(crew.routes());
- v4.use(dragons.routes());
- v4.use(landpads.routes());
- v4.use(launches.routes());
- v4.use(launchpads.routes());
- v4.use(payloads.routes());
- v4.use(rockets.routes());
- v4.use(ships.routes());
- v4.use(users.routes());
- v4.use(company.routes());
- v4.use(roadster.routes());
- v4.use(starlink.routes());
- v4.use(history.routes());
- v4.use(fairings.routes());
- module.exports = v4;
模塊眾多,找?guī)讉€(gè)代表性的模塊
-
admin
模塊
- const Router = require('koa-router');
- const { auth, authz, cache } = require('../../../middleware');
- const router = new Router({
- prefix: '/admin',
- });
- // Clear redis cache
- router.delete('/cache', auth, authz('cache:clear'), async (ctx) => {
- try {
- await cache.redis.flushall();
- ctx.status = 200;
- } catch (error) {
- ctx.throw(400, error.message);
- }
- });
- // Healthcheck
- router.get('/health', async (ctx) => {
- ctx.status = 200;
- });
- module.exports = router;
-
分析代碼
- 這是一套標(biāo)準(zhǔn)的restful API ,提供的/admin/cache接口,請(qǐng)求方式為delete,請(qǐng)求這個(gè)接口,首先要經(jīng)過auth和authz兩個(gè)中間件處理
這里補(bǔ)充一個(gè)小細(xì)節(jié)
-
一個(gè)用戶訪問一套系統(tǒng),有兩種狀態(tài),未登陸和已登陸,如果你未登陸去執(zhí)行一些操作,后端應(yīng)該返回
401
。但是登錄后,你只能做你權(quán)限內(nèi)的事情,例如你只是一個(gè)打工人,你說你要關(guān)閉這個(gè)公司,那么對(duì)不起,你的狀態(tài)碼此時(shí)應(yīng)該是403
回到admin
-
此刻的你,想要清空這個(gè)緩存,調(diào)用/admin/cache接口,那么首先要經(jīng)過
auth
中間件判斷你是否有登錄
- /**
- * Authentication middleware
- */
- module.exports = async (ctx, next) => {
- const key = ctx.request.headers['spacex-key'];
- if (key) {
- const user = await db.collection('users').findOne({ key });
- if (user?.key === key) {
- ctx.state.roles = user.roles;
- await next();
- return;
- }
- }
- ctx.status = 401;
- ctx.body = 'https://youtu.be/RfiQYRn7fBg';
- };
-
如果沒有登錄過,那么意味著你沒有權(quán)限,此時(shí)為401狀態(tài)碼,你應(yīng)該去登錄.如果登錄過,那么應(yīng)該前往下一個(gè)中間件
authz
。 (所以redux的中間件源碼是多么重要。它可以說貫穿了我們整個(gè)前端生涯,我以前些過它的分析,有興趣的可以翻一翻公眾號(hào))
- /**
- * Authorization middleware
- *
- * @param {String} role Role for protected route
- * @returns {void}
- */
- module.exports = (role) => async (ctx, next) => {
- const { roles } = ctx.state;
- const allowed = roles.includes(role);
- if (allowed) {
- await next();
- return;
- }
- ctx.status = 403;
- };
- // Clear redis cache
- router.delete('/cache', auth, authz('cache:clear'), async (ctx) => {
- try {
- await cache.redis.flushall();
- ctx.status = 200;
- } catch (error) {
- ctx.throw(400, error.message);
- }
- });
-
此時(shí)此刻,使用try catch包裹邏輯代碼,當(dāng)redis清除所有緩存成功即會(huì)返回狀態(tài)碼400,如果報(bào)錯(cuò),就會(huì)拋出錯(cuò)誤碼和原因。接由洋蔥圈外層的
error
中間件處理
- /**
- * Error handler middleware
- *
- * @param {Object} ctx Koa context
- * @param {function} next Koa next function
- * @returns {void}
- */
- module.exports = async (ctx, next) => {
- try {
- await next();
- } catch (err) {
- if (err?.kind === 'ObjectId') {
- err.status = 404;
- } else {
- ctx.status = err.status || 500;
- ctx.body = err.message;
- }
- }
- };
-
這樣只要任意的
server
層內(nèi)部出現(xiàn)異常,只要拋出,就會(huì)被error
中間件處理,直接返回狀態(tài)碼和錯(cuò)誤信息. 如果沒有傳入狀態(tài)碼,那么默認(rèn)是500(所以我之前說過,代碼要穩(wěn)定,一定要有顯示的指定默認(rèn)值,要關(guān)注代碼異常的邏輯,例如前端setLoading,請(qǐng)求失敗也要取消loading,不然用戶就沒法重試了,有可能這一瞬間只是用戶網(wǎng)絡(luò)出錯(cuò)呢)
補(bǔ)一張koa洋蔥圈的圖
再接下來看其他的services
-
現(xiàn)在,都非常輕松就能理解了
- // Get one history event
- router.get('/:id', cache(300), async (ctx) => {
- const result = await History.findById(ctx.params.id);
- if (!result) {
- ctx.throw(404);
- }
- ctx.status = 200;
- ctx.body = result;
- });
- // Query history events
- router.post('/query', cache(300), async (ctx) => {
- const { query = {}, options = {} } = ctx.request.body;
- try {
- const result = await History.paginate(query, options);
- ctx.status = 200;
- ctx.body = result;
- } catch (error) {
- ctx.throw(400, error.message);
- }
- });
通過這個(gè)項(xiàng)目,我們能學(xué)到什么
-
一個(gè)能上天的項(xiàng)目,必然是非常穩(wěn)定、高可用的,我們首先要學(xué)習(xí)它的優(yōu)秀點(diǎn):用最簡單的技術(shù)加上最簡單的實(shí)現(xiàn)方式,讓人一眼就能看懂它的代碼和分層
-
再者:簡潔的注釋是必要的
-
從業(yè)務(wù)角度去抽象公共層,例如鑒權(quán)、錯(cuò)誤處理、日志等為公共模塊(中間件,前端可能是一個(gè)工具函數(shù)或組件)
- 多考慮錯(cuò)誤異常的處理,前端也是如此,js大多錯(cuò)誤發(fā)生來源于a.b.c這種代碼(如果a.b為undefined那么就會(huì)報(bào)錯(cuò)了)
-
顯示的指定默認(rèn)值,不讓代碼閱讀者去猜測
-
目錄分區(qū)必定要簡潔明了,分層清晰,易于維護(hù)和拓展
成為一個(gè)優(yōu)秀前端架構(gòu)師的幾個(gè)技能點(diǎn)
-
原生JavaScript、CSS、HTML基礎(chǔ)扎實(shí)(系統(tǒng)學(xué)習(xí)過)
-
原生Node.js基礎(chǔ)扎實(shí)(系統(tǒng)學(xué)習(xí)過),Node.js不僅提供服務(wù),更多的是用于制作工具,以及現(xiàn)在serverless場景也會(huì)用到,還有SSR
-
熟悉框架和類庫原理,能手寫簡易的常用類庫,例如promise redux 等
-
數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)扎實(shí),了解常用、常見算法
-
linux基礎(chǔ)扎實(shí)(做工具,搭環(huán)境,編寫構(gòu)建腳本等有會(huì)用到)
-
熟悉TCP和http等通信協(xié)議
-
熟悉操作系統(tǒng)linux Mac windows iOS 安卓等(在跨平臺(tái)產(chǎn)品時(shí)候會(huì)遇到)
-
會(huì)使用docker(部署相關(guān))
-
會(huì)一些c++最佳(在addon場景等,再者Node.js和JavaScript本質(zhì)上是基于
C++
) -
懂基本數(shù)據(jù)庫、redis、nginxs操作,像跨平臺(tái)產(chǎn)品,基本前端都會(huì)有個(gè)sqlite之類的,像如果是node自身提供服務(wù),數(shù)據(jù)庫和redis一般少不了
-
再者是要多閱讀優(yōu)秀的開源項(xiàng)目源碼,不用太多,但是一定要精