当前位置:首页 > 通信资讯 > 正文

从一个优质开源项目来看前端架构的优点(开源的前端项目)

何为系统架构师?

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

这是百度百科的答案

大多数人的问题

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

  • 其实,前端架构师不应该是一个头衔,而应该是一个过程。我记得掘金上有人写过一篇文章:《我在一个小公司,我把我们公司前端给架构了》, (我当时还看成《我把我们公司架构师给上了》)
  • 我面试过很多人,从小公司出来(我也是从一个很小很小的公司出来,现在也没在什么BATJ),最大的问题在于,觉得自己不是leader,就没有想过如何去提升、优化项目,而是去研究一些花里胡哨的东西,却没有真正使用在项目中。(自然很少会有深度)
  • 在一个两至三人的前端团队小公司,你去不断优化、提升项目体验,更新迭代替换技术栈,那么你就是前端架构师

正式开始

我们从一个比较不错的项目入手,谈谈一个前端架构师要做什么

  • SpaceX-API
  • SpaceX-API是什么?
  • SpaceX-API 是一个用于火箭、核心舱、太空舱、发射台和发射数据的开源 REST API(并且是使用Node.js编写,我们用这个项目借鉴无可厚非)

为了阅读的舒适度,我把下面的正文尽量口语化一点

先把代码搞下来

  1. gitclonehttps://github.com/r-spacex/SpaceX-API.git
  • 一个优秀的开源项目搞下来以后,怎么分析它?大部分时候,你应该先看它的目录结构以及依赖的第三方库(package.json文件)

找到package.json文件的几个关键点:

  • main字段(项目入口)
  • scripts字段(执行命令脚本)
  • dependencies和devDependencies字段(项目的依赖,区分线上依赖和开发依赖,我本人是非常看中这个点,SpaceX-API也符合我的观念,严格的区分依赖按照)
  1. "main":"server.js",
  2. "scripts":{
  3. "test":"npmrunlint&&npmruncheck-dependencies&&jest--silent--verbose",
  4. "start":"nodeserver.js",
  5. "worker":"nodejobs/worker.js",
  6. "lint":"eslint.",
  7. "check-dependencies":"npxdepcheck--ignores=\"pino-pretty\""
  8. },
  • 通过上面可以看到,项目入口为server.js
  • 项目启动命令为npm run start
  • 几个主要的依赖:

  1. "koa":"^2.13.0",
  2. "koa-bodyparser":"^4.3.0",
  3. "koa-conditional-get":"^3.0.0",
  4. "koa-etag":"^4.0.0",
  5. "koa-helmet":"^6.0.0",
  6. "koa-pino-logger":"^3.0.0",
  7. "koa-router":"^10.0.0",
  8. "koa2-cors":"^2.0.6",
  9. "lodash":"^4.17.20",
  10. "moment-range":"^4.0.2",
  11. "moment-timezone":"^0.5.32",
  12. "mongoose":"^5.11.8",
  13. "mongoose-id":"^0.1.3",
  14. "mongoose-paginate-v2":"^1.3.12",
  15. "eslint":"^7.16.0",
  16. "eslint-config-airbnb-base":"^14.2.1",
  17. "eslint-plugin-import":"^2.22.1",
  18. "eslint-plugin-jest":"^24.1.3",
  19. "eslint-plugin-mongodb":"^1.0.0",
  20. "eslint-plugin-no-secrets":"^0.6.8",
  21. "eslint-plugin-security":"^1.4.0",
  22. "jest":"^26.6.3",
  23. "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服务的代码,里面都是准备的一些参数然后直接调服务

逐个分析

从项目依赖安装说起

  • 安装环境严格区分开发依赖和线上依赖,让阅读代码者一目了然哪些依赖是线上需要的

  1. "dependencies":{
  2. "blake3":"^2.1.4",
  3. "cheerio":"^1.0.0-rc.3",
  4. "cron":"^1.8.2",
  5. "fuzzball":"^1.3.0",
  6. "got":"^11.8.1",
  7. "ioredis":"^4.19.4",
  8. "koa":"^2.13.0",
  9. "koa-bodyparser":"^4.3.0",
  10. "koa-conditional-get":"^3.0.0",
  11. "koa-etag":"^4.0.0",
  12. "koa-helmet":"^6.0.0",
  13. "koa-pino-logger":"^3.0.0",
  14. "koa-router":"^10.0.0",
  15. "koa2-cors":"^2.0.6",
  16. "lodash":"^4.17.20",
  17. "moment-range":"^4.0.2",
  18. "moment-timezone":"^0.5.32",
  19. "mongoose":"^5.11.8",
  20. "mongoose-id":"^0.1.3",
  21. "mongoose-paginate-v2":"^1.3.12",
  22. "pino":"^6.8.0",
  23. "tle.js":"^4.2.8",
  24. "tough-cookie":"^4.0.0"
  25. },
  26. "devDependencies":{
  27. "eslint":"^7.16.0",
  28. "eslint-config-airbnb-base":"^14.2.1",
  29. "eslint-plugin-import":"^2.22.1",
  30. "eslint-plugin-jest":"^24.1.3",
  31. "eslint-plugin-mongodb":"^1.0.0",
  32. "eslint-plugin-no-secrets":"^0.6.8",
  33. "eslint-plugin-security":"^1.4.0",
  34. "jest":"^26.6.3",
  35. "pino-pretty":"^4.3.0"
  36. },

项目目录划分

  • 目录划分,严格分层

  • 通用,清晰简介明了,让人一看就懂

正式开始看代码

  • 入口文件,server.js开始
    1. consthttp=require('http');
    2. constmongoose=require('mongoose');
    3. const{logger}=require('./middleware/logger');
    4. constapp=require('./app');
    5. constPORT=process.env.PORT||6673;
    6. constSERVER=http.createServer(app.callback());
    7. //GracefullycloseMongoconnection
    8. constgracefulShutdown=()=>{
    9. mongoose.connection.close(false,()=>{
    10. logger.info('Mongoclosed');
    11. SERVER.close(()=>{
    12. logger.info('Shuttingdown...');
    13. process.exit();
    14. });
    15. });
    16. };
    17. //Serverstart
    18. SERVER.listen(PORT,'0.0.0.0',()=>{
    19. logger.info(`Runningonport:${PORT}`);
    20. //Handlekillcommands
    21. process.on('SIGTERM',gracefulShutdown);
    22. //Preventdirtyexitoncode-faultcrashes:
    23. process.on('uncaughtException',gracefulShutdown);
    24. //Preventpromiserejectionexits
    25. process.on('unhandledRejection',gracefulShutdown);
    26. });
  • 几个优秀的地方

    • 每个回调函数都会有声明功能注释

    • SERVER.listen的host参数也会传入,这里是为了避免产生不必要的麻烦。至于这个麻烦,我这就不解释了(一定要有能看到的默认值,而不是去靠猜)
    • 对于监听端口启动服务以后一些异常统一捕获,并且统一日志记录,process进程退出,防止出现僵死线程、端口占用等(因为node部署时候可能会用pm2等方式,在 Worker 线程中,process.exit()将停止当前线程而不是当前进程)

app.js入口文件

  • 这里是由koa提供基础服务
  • monggose负责连接mongoDB数据库
  • 若干中间件负责跨域、日志、错误、数据处理等

    1. constconditional=require('koa-conditional-get');
    2. constetag=require('koa-etag');
    3. constcors=require('koa2-cors');
    4. consthelmet=require('koa-helmet');
    5. constKoa=require('koa');
    6. constbodyParser=require('koa-bodyparser');
    7. constmongoose=require('mongoose');
    8. const{requestLogger,logger}=require('./middleware/logger');
    9. const{responseTime,errors}=require('./middleware');
    10. const{v4}=require('./services');
    11. constapp=newKoa();
    12. mongoose.connect(process.env.SPACEX_MONGO,{
    13. useFindAndModify:false,
    14. useNewUrlParser:true,
    15. useUnifiedTopology:true,
    16. useCreateIndex:true,
    17. });
    18. constdb=mongoose.connection;
    19. db.on('error',(err)=>{
    20. logger.error(err);
    21. });
    22. db.once('connected',()=>{
    23. logger.info('Mongoconnected');
    24. app.emit('ready');
    25. });
    26. db.on('reconnected',()=>{
    27. logger.info('Mongore-connected');
    28. });
    29. db.on('disconnected',()=>{
    30. logger.info('Mongodisconnected');
    31. });
    32. //disableconsole.errorsforpino
    33. app.silent=true;
    34. //Errorhandler
    35. app.use(errors);
    36. app.use(conditional());
    37. app.use(etag());
    38. app.use(bodyParser());
    39. //HTTPheadersecurity
    40. app.use(helmet());
    41. //EnableCORSforallroutes
    42. app.use(cors({
    43. origin:'*',
    44. allowMethods:['GET','POST','PATCH','DELETE'],
    45. allowHeaders:['Content-Type','Accept'],
    46. exposeHeaders:['spacex-api-cache','spacex-api-response-time'],
    47. }));
    48. //SetheaderwithAPIresponsetime
    49. app.use(responseTime);
    50. //Requestlogging
    51. app.use(requestLogger);
    52. //V4routes
    53. app.use(v4.routes());
    54. module.exports=app;
  • 逻辑清晰,自上而下,首先连接db数据库,挂载各种事件后,经由koa各种中间件,而后真正使用koa路由提供api服务(代码编写顺序,即代码运行后的业务逻辑,我们写前端的react等的时候,也提倡由生命周期运行顺序去编写组件代码,而不是先编写unmount生命周期,再编写mount),例如应该这样:
  1. //组件挂载
  2. componentDidmount(){
  3. }
  4. //组件需要更新时
  5. shouldComponentUpdate(){
  6. }
  7. //组件将要卸载
  8. componentWillUnmount(){
  9. }
  10. ...
  11. render(){}

router的代码,简介明了

  1. constRouter=require('koa-router');
  2. constadmin=require('./admin/routes');
  3. constcapsules=require('./capsules/routes');
  4. constcores=require('./cores/routes');
  5. constcrew=require('./crew/routes');
  6. constdragons=require('./dragons/routes');
  7. constlandpads=require('./landpads/routes');
  8. constlaunches=require('./launches/routes');
  9. constlaunchpads=require('./launchpads/routes');
  10. constpayloads=require('./payloads/routes');
  11. constrockets=require('./rockets/routes');
  12. constships=require('./ships/routes');
  13. constusers=require('./users/routes');
  14. constcompany=require('./company/routes');
  15. constroadster=require('./roadster/routes');
  16. conststarlink=require('./starlink/routes');
  17. consthistory=require('./history/routes');
  18. constfairings=require('./fairings/routes');
  19. constv4=newRouter({
  20. prefix:'/v4',
  21. });
  22. v4.use(admin.routes());
  23. v4.use(capsules.routes());
  24. v4.use(cores.routes());
  25. v4.use(crew.routes());
  26. v4.use(dragons.routes());
  27. v4.use(landpads.routes());
  28. v4.use(launches.routes());
  29. v4.use(launchpads.routes());
  30. v4.use(payloads.routes());
  31. v4.use(rockets.routes());
  32. v4.use(ships.routes());
  33. v4.use(users.routes());
  34. v4.use(company.routes());
  35. v4.use(roadster.routes());
  36. v4.use(starlink.routes());
  37. v4.use(history.routes());
  38. v4.use(fairings.routes());
  39. module.exports=v4;

模块众多,找几个代表性的模块

  • admin模块
  1. constRouter=require('koa-router');
  2. const{auth,authz,cache}=require('../../../middleware');
  3. constrouter=newRouter({
  4. prefix:'/admin',
  5. });
  6. //Clearrediscache
  7. router.delete('/cache',auth,authz('cache:clear'),async(ctx)=>{
  8. try{
  9. awaitcache.redis.flushall();
  10. ctx.status=200;
  11. }catch(error){
  12. ctx.throw(400,error.message);
  13. }
  14. });
  15. //Healthcheck
  16. router.get('/health',async(ctx)=>{
  17. ctx.status=200;
  18. });
  19. module.exports=router;
  • 分析代码

  • 这是一套标准的restful API ,提供的/admin/cache接口,请求方式为delete,请求这个接口,首先要经过auth和authz两个中间件处理

这里补充一个小细节

  • 一个用户访问一套系统,有两种状态,未登陆和已登陆,如果你未登陆去执行一些操作,后端应该返回401。但是登录后,你只能做你权限内的事情,例如你只是一个打工人,你说你要关闭这个公司,那么对不起,你的状态码此时应该是403

回到admin

  • 此刻的你,想要清空这个缓存,调用/admin/cache接口,那么首先要经过auth中间件判断你是否有登录
  1. /**
  2. *Authenticationmiddleware
  3. */
  4. module.exports=async(ctx,next)=>{
  5. constkey=ctx.request.headers['spacex-key'];
  6. if(key){
  7. constuser=awaitdb.collection('users').findOne({key});
  8. if(user?.key===key){
  9. ctx.state.roles=user.roles;
  10. awaitnext();
  11. return;
  12. }
  13. }
  14. ctx.status=401;
  15. ctx.body='https://youtu.be/RfiQYRn7fBg';
  16. };
  • 如果没有登录过,那么意味着你没有权限,此时为401状态码,你应该去登录.如果登录过,那么应该前往下一个中间件authz。(所以redux的中间件源码是多么重要。它可以说贯穿了我们整个前端生涯,我以前些过它的分析,有兴趣的可以翻一翻公众号)
    1. /**
    2. *Authorizationmiddleware
    3. *
    4. *@param{String}roleRoleforprotectedroute
    5. *@returns{void}
    6. */
    7. module.exports=(role)=>async(ctx,next)=>{
    8. const{roles}=ctx.state;
    9. constallowed=roles.includes(role);
    10. if(allowed){
    11. awaitnext();
    12. return;
    13. }
    14. ctx.status=403;
    15. };
  • 在authz这里会根据你传入的操作类型(这里是'cache:clear'),看你的对应所有权限roles里面是否包含传入的操作类型role 。如果没有,就返回403,如果有,就继续下一个中间件 - 即真正的/admin/cache接口
  1. //Clearrediscache
  2. router.delete('/cache',auth,authz('cache:clear'),async(ctx)=>{
  3. try{
  4. awaitcache.redis.flushall();
  5. ctx.status=200;
  6. }catch(error){
  7. ctx.throw(400,error.message);
  8. }
  9. });
  • 此时此刻,使用try catch包裹逻辑代码,当redis清除所有缓存成功即会返回状态码400,如果报错,就会抛出错误码和原因。接由洋葱圈外层的error中间件处理
  1. /**
  2. *Errorhandlermiddleware
  3. *
  4. *@param{Object}ctxKoacontext
  5. *@param{function}nextKoanextfunction
  6. *@returns{void}
  7. */
  8. module.exports=async(ctx,next)=>{
  9. try{
  10. awaitnext();
  11. }catch(err){
  12. if(err?.kind==='ObjectId'){
  13. err.status=404;
  14. }else{
  15. ctx.status=err.status||500;
  16. ctx.body=err.message;
  17. }
  18. }
  19. };
  • 这样只要任意的server层内部出现异常,只要抛出,就会被error中间件处理,直接返回状态码和错误信息. 如果没有传入状态码,那么默认是500(所以我之前说过,代码要稳定,一定要有显示的指定默认值,要关注代码异常的逻辑,例如前端setLoading,请求失败也要取消loading,不然用户就没法重试了,有可能这一瞬间只是用户网络出错呢)

补一张koa洋葱圈的图

从一个优质开源项目来看前端架构的优点(开源的前端项目)

再接下来看其他的services

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

  1. //Getonehistoryevent
  2. router.get('/:id',cache(300),async(ctx)=>{
  3. constresult=awaitHistory.findById(ctx.params.id);
  4. if(!result){
  5. ctx.throw(404);
  6. }
  7. ctx.status=200;
  8. ctx.body=result;
  9. });
  10. //Queryhistoryevents
  11. router.post('/query',cache(300),async(ctx)=>{
  12. const{query={},options={}}=ctx.request.body;
  13. try{
  14. constresult=awaitHistory.paginate(query,options);
  15. ctx.status=200;
  16. ctx.body=result;
  17. }catch(error){
  18. ctx.throw(400,error.message);
  19. }
  20. });


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

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

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

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

  • 多考虑错误异常的处理,前端也是如此,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一般少不了

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

原文链接:https://mp.weixin.qq.com/s?__biz=MzA4NTU1OTMwMQ==&mid=2650301728&idx=1&sn=492e6f144772636c6c093497f04543e8&chksm=87dad44ab0ad5d5c6cb02230769f4228082d9b6ec6075628e051a2a51474d7c983bf630617d9&token=1011

如果您对该产品感兴趣,请填写办理(客服微信:xiaoxiongyidong)

为您推荐:

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。