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

2860元腾讯云代金券免费领取,付款直接抵现金,立即领取>>>

腾讯云服务器1折限时抢购,2核4G云主机698元/3年,立即抢购>>>

何为系统架构师?

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

这是百度百科的答案

大多数人的问题

如何成为一名前端架构师?

  • 其实,前端架构师不应该是一个头衔,而应该是一个过程。我记得掘金上有人写过一篇文章:?《我在一个小公司,我把我们公司前端给架构了》?, (我当时还看成?《我把我们公司架构师给上了》?)
  • 我面试过很多人,从小公司出来(我也是从一个很小很小的公司出来,现在也没在什么?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的中间件源码是多么重要。它可以说贯穿了我们整个前端生涯,我以前些过它的分析,有兴趣的可以翻一翻公众号)
[email protected][email protected]?{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?中间件处理
[email protected][email protected][email protected]?{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一般少不了
  • 再者是要多阅读优秀的开源项目源码,不用太多,但是一定要精
  • 发表于:
  • 原文链接http://news.51cto.com/art/202106/668019.htm
  • 如有侵权,请联系 [email protected] 删除。

扫码关注云+社区

领取腾讯云代金券

http://www.vxiaotou.com