您现在的位置是:亿华云 > 系统运维

从一个优质开源项目来看前端架构

亿华云2025-10-02 18:56:44【系统运维】8人已围观

简介何为系统架构师?系统架构师是一个最终确认和评估系统需求,给出开发规范,搭建系统实现的核心构架,并澄清技术细节、扫清主要难点的技术人员。主要着眼于系统的“技术实现”。因此他/她应该是特定的开发平台、语言

 

何为系统架构师?优质

系统架构师是一个最终确认和评估系统需求,给出开发规范,开源看前搭建系统实现的项目核心构架,并澄清技术细节、端架扫清主要难点的优质技术人员。主要着眼于系统的开源看前“技术实现”。因此他/她应该是项目特定的开发平台、语言、端架工具的优质大师,对常见应用场景能给出最恰当的开源看前解决方案,同时要对所属的项目开发团队有足够的了解,能够评估自己的端架团队实现特定的功能需求需要的代价。系统架构师负责设计系统整体架构,优质从需求到设计的开源看前每个细节都要考虑到,把握整个项目,项目使设计的项目尽量效率高,开发容易,维护方便,升级简单等

这是高防服务器百度百科的答案

大多数人的问题

如何成为一名前端架构师? 其实,前端架构师不应该是一个头衔,而应该是一个过程。我记得掘金上有人写过一篇文章: 《我在一个小公司,我把我们公司前端给架构了》 , (我当时还看成 《我把我们公司架构师给上了》 ) 我面试过很多人,从小公司出来(我也是从一个很小很小的公司出来,现在也没在什么 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, 严格分层

几个重点目录 :

server.js 项目入口

app.js 入口文件

services 文件夹 => 项目提供服务层

scripts 文件夹 => 项目脚本

middleware 文件夹 => 中间件

docs 文件夹 =>文档存放

tests 文件夹 => 单元测试代码存放

.dockerignore docker的忽略文件

Dockerfile 执行docker build命令读取配置的源码下载文件

.eslintrc eslint配置文件

jobs 文件夹 => 我想应该是对应检查他们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()将停止当前线程而不是当前进程) app.js入口文件 这里是由 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;  逻辑清晰,自上而下,首先连接db数据库,挂载各种事件后,经由koa各种中间件,而后真正使用koa路由提供api服务(代码编写顺序,即代码运行后的业务逻辑,我们写前端的react等的时候,也提倡由生命周期运行顺序去编写组件代码,而不是先编写unmount生命周期,再编写mount),例如应该这样: //组件挂载 componentDidmount(){  } //组件需要更新时 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;  模块众多,找几个代表性的模块 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; 

分析代码

 这是一套标准的restful API ,提供的/admin/cache接口,请求方式为delete,请求这个接口,首先要经过auth和authz两个中间件处理 这里补充一个小细节 一个用户访问一套系统,有两种状态,未登陆和已登陆,如果你未登陆去执行一些操作,后端应该返回 401 。但是登录后,你只能做你权限内的事情,例如你只是一个打工人,你说你要关闭这个公司,那么对不起,你的状态码此时应该是 403 回到admin 此刻的你,想要清空这个缓存,调用/admin/cache接口,那么首先要经过 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; };  如果没有登录过,那么意味着你没有权限,此时为401状态码,你应该去登录.如果登录过,那么应该前往下一个中间件 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);   } });  此时此刻,使用try catch包裹逻辑代码,当redis清除所有缓存成功即会返回状态码400,如果报错,就会抛出错误码和原因。接由洋葱圈外层的 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洋葱圈的图

再接下来看其他的services

现在,都非常轻松就能理解了

// 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);   } }); 

通过这个项目,我们能学到什么

一个能上天的项目,必然是非常稳定、高可用的,我们首先要学习它的优秀点:用最简单的技术加上最简单的实现方式,让人一眼就能看懂它的代码和分层

再者:简洁的注释是必要的

从业务角度去抽象公共层,例如鉴权、错误处理、日志等为公共模块(中间件,前端可能是一个工具函数或组件)

多考虑错误异常的处理,前端也是如此,js大多错误发生来源于a.b.c这种代码(如果a.b为undefined那么就会报错了)

显示的指定默认值,不让代码阅读者去猜测

目录分区必定要简洁明了,分层清晰,易于维护和拓展

成为一个优秀前端架构师的几个技能点

原生JavaScript、CSS、HTML基础扎实(系统学习过)

原生Node.js基础扎实(系统学习过),Node.js不仅提供服务,更多的是用于制作工具,以及现在serverless场景也会用到,还有SSR

熟悉框架和类库原理,能手写简易的常用类库,例如promise redux 等

数据结构基础扎实,了解常用、常见算法

linux基础扎实(做工具,搭环境,编写构建脚本等有会用到)

熟悉TCP和http等通信协议

熟悉操作系统linux Mac windows iOS 安卓等(在跨平台产品时候会遇到)

会使用docker(部署相关)

会一些c++最佳(在addon场景等,再者Node.js和JavaScript本质上是基于 C++ )

懂基本数据库、redis、nginxs操作,像跨平台产品,基本前端都会有个sqlite之类的,像如果是node自身提供服务,数据库和redis一般少不了

再者是要多阅读优秀的开源项目源码,不用太多,但是一定要精

很赞哦!(3678)