这是百度百科的答案
《我在一个小公司,我把我们公司前端给架构了》
, (我当时还看成《我把我们公司架构师给上了》
)BATJ
),最大的问题在于,觉得自己不是leader
,就没有想过如何去提升、优化项目,而是去研究一些花里胡哨的东西,却没有真正使用在项目中。(自然很少会有深度)前端架构师
SpaceX-API
SpaceX-API
是什么?SpaceX-API
是一个用于火箭、核心舱、太空舱、发射台和发射数据的开源 REST API
(并且是使用Node.js
编写,我们用这个项目借鉴无可厚非)为了阅读的舒适度,我把下面的正文尽量口语化一点
git clone https://github.com/r-spacex/SpaceX-API.git
package.json
文件)package.json
文件的几个关键点:main
字段(项目入口)scripts
字段(执行命令脚本)dependencies
和devDependencies
字段(项目的依赖,区分线上依赖和开发依赖,我本人是非常看中这个点,SpaceX-API
也符合我的观念,严格的区分依赖按照) "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\""
},
server.js
npm run start
"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(代码质量检查)这里强调一点,如果你的代码需要两人及以上维护,我就强烈建议你不要使用任何黑魔法,以及不使用非主流的库,除非你编写核心底层逻辑时候非用不可(这个时候应该只有你维护)
REST API
,严格分层 "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"
},
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);
});
SERVER.listen
的host参数也会传入,这里是为了避免产生不必要的麻烦。至于这个麻烦,我这就不解释了(一定要有能看到的默认值,而不是去靠猜)process
进程退出,防止出现僵死线程、端口占用等(因为node部署时候可能会用pm2等方式,在 Worker 线程中,process.exit()将停止当前线程而不是当前进程)koa
提供基础服务monggose
负责连接mongoDB
数据库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;
koa
路由提供api服务(代码编写顺序,即代码运行后的业务逻辑,我们写前端的react
等的时候,也提倡由生命周期运行顺序去编写组件代码,而不是先编写unmount
生命周期,再编写mount
),例如应该这样://组件挂载
componentDidmount(){
}
//组件需要更新时
shouldComponentUpdate(){
}
//组件将要卸载
componentWillUnmount(){
}
...
render(){}
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;
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;
/admin/cache
接口,请求方式为delete
,请求这个接口,首先要经过auth
和authz
两个中间件处理401
。但是登录后,你只能做你权限内的事情,例如你只是一个打工人,你说你要关闭这个公司,那么对不起,你的状态码此时应该是403
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';
};
authz
. (所以redux的中间件源码是多么重要.它可以说贯穿了我们整个前端生涯,我以前些过它的分析,有兴趣的可以翻一翻公众号)/**
* 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;
};
authz
这里会根据你传入的操作类型(这里是'cache:clear'),看你的对应所有权限roles
里面是否包含传入的操作类型role
.如果没有,就返回403,如果有,就继续下一个中间件 - 即真正的/admin/cache
接口// 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);
}
});
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
层内部出现异常,只要抛出,就会被error
中间件处理,直接返回状态码和错误信息. 如果没有传入状态码,那么默认是500(所以我之前说过,代码要稳定,一定要有显示的指定默认值,要关注代码异常的逻辑,例如前端setLoading,请求失败也要取消loading,不然用户就没法重试了,有可能这一瞬间只是用户网络出错呢)补一张koa洋葱圈的图
// 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);
}
});
a.b.c
这种代码(如果a.b
为undefined
那么就会报错了)C++
)再者是要多阅读优秀的开源项目源码,不用太多,但是一定要精
以上是我的感悟,后面我会在评论中补充,也欢迎大家在评论中补充探讨!