我们需要怎样的 Service

标签: 前端 接口设计 插件 | 发表时间:2022-01-10 09:57 | 作者:ES2049
出处:https://segmentfault.com/blogs

14 世纪,英格兰的逻辑学家奥卡姆在他的《箴言书注》中说「不要浪费过多的东西,去做那些用较少的东西同样可以做好的事情」。后来这句话被简化为「奥卡姆剃刀原理」,即:如无必要,勿增实体。奥卡姆剃刀在各个领域都有他的运用,他不是一个公理,没有严谨的推导过程,但他却是一个在实践中被证明非常有效的解决问题的手段。

在编程世界里,有太多我们习以为常的东西,我相信存在即合理,同时我也相信存在都有前提,而前提会随着时间变化甚至消失。下面我想跟大家探讨下,我们前端项目中那些应该被剃刀剃掉的东西。

前端项目里的 service 层

在一个前端项目中,一般包含以下文件目录:

  • containers:页面
  • components:组件
  • utils:工具方法
  • routes:路由
  • services:数据服务
  • index.js 入口文件

我们的业务代码基本都在 containers components 里,utils 和 routes 也是必不可少的,但仔细思考我们就会发现,这里有个 services 文件夹,他被称为数据服务层,是我们跟后端打交道的。这一层真的需要吗?
我们来看看大家是怎么使用 service 的。

  // services  文件夹下的 accoutService.js

import { post } from '@/utils/request';

// 获取账号列表
export const getAccountsList = params => post('/api/accounts.json', params);
// 新增账号
export const insertAccount = params => post('/api/insertAccount.json', params);
// 更新状态
export const updateAccount = params => post('/api/updateAccount.json', params);
// 校验账号查询
export const checkAccount = params => post('/api/checkAccount.json', params);

-------- 使用 ---------

import { getAccountsList } from '@/services/accountService'

const App = () => {
  const [name, setName] = useState('');
    useEffect(() => {
      getAccountsList().then((res) => {
       setName(res.name);
    });
  }, [])
  return <div>{name}</div>;
};

从上面的代码我们可以看出,services 文件下基本是一些模板代码,偶尔有少见的一些数据转换。这些内容对于我们的业务代码来说,都是非业务相关的,写这些模板性的控制代码真的有必要吗?

service 里包含什么?

  • 数据转换逻辑 converHandler
  • 数据请求工具 request
  • 请求地址定义 url
  • 全局拦截器 interceptor
  • 附加功能 openApi

数据转换逻辑 converHandler:并不通用,有的一个请求在不同的页面需要走不同的转换逻辑,这些转换逻辑一般会写在调用位置的代码里,我也建议这么做,因为数据转换也是这块某个 container 的功能,而且为了方便测试,建议添加 handler.js 将转换逻辑抽离出来。

数据请求工具 request:主要是封装各种请求,这部分需要统一。非业务相关,可以提出来。

请求地址定义 url:这部分是强业务相关的,不应该放到 service 里,而是作为 service 的一个配置,由外部输入。

全局拦截器 interceptor:处理一些通用的业务状态码,比如编辑成功 10001,这部分也是强业务相关的,而且相对比较复杂,但是可以通过配置 schame 来描述,后面再讲。

附加功能 openAPI:如果你系统的接口想让别的系统复用,比如 MTEE 基础平台的接口需要复用给运营平台,那么前端需要提供领域物料,领域物料里会发请求,发请求要解决跨域、登陆、授权的问题,openAPI 应运而生。

综上可以看出,service 层只需要一些统一的逻辑处理和配置文件就能描述清楚,甚至我们可以把 Service 层简化为

$$service = request + config$$

我的 service 包

由此,我希望能设计这样一个 service 包,他需要包含下面的功能:

请求

支持常见的 get post jsonp 请求,以及对于这些请求的附加方法,比如 debounce、throttle、缓存、loading 等功能。也可以提供大家比较喜欢的 hooks API。

接口配置

一个接口包含域名 domain,地址路径 path,请求方法 method,参数 params,一些常见功能的开关,比如开启防抖 { debounce:true } 。参数的配置里,可以添加该参数的基本属性,比如是否必选 { require: true } ,这样包内可以对参数做必要的校验,这样可以保证非法数据传入后台。

环境切换

环境切换是一个非业务相关的功能,他不应该硬编码到代码里,带到线上。他应该只是一个配置,尽量与代码脱离,因此是用浏览器插件来切换,就是一个很好的方法。可以设计 service 包接收一个 domainMap,这个 domainMap 来自 window.GlobalConfig 下的某个变量,浏览器插件可以动态改变这个变量,就可以做到环境的切换了。
image.png

网关转发

我们写代码追求复用,从代码块的复用到组件复用,再到业务能力的复用,而业务能力复用的一个载体就是领域物料。一个领域物料里很有多个接口请求,如果我们把原来在业务代码里的组件拆出来作为领域物料的话,就不得不把项目里的 service 层也要打包进去,这样才能发送请求和处理一些统一的异常。上面的我提到的把是service 层做成一个包,别人在使用的时候,只需要传配置进来,也是出于领域物料这个场景。
这之后,我们还要解决一个问题:领域物料在不同站点使用带来的接口跨域问题。我们现在的解决办法是,前端搭建一套基于 node 的网关,用于做接口转发和鉴权。service 包里会集成这个过程,外部使用者只需要配置开不开启网关就可以了。他完全不需要知道网关是如何转发的,就像在自己的站点下写组件一样。

接口文档

我们在接手别的项目的时候,总是不容易找到他的接口文档,因为文档和代码是割裂的,文档的维护也有滞后性,甚至慢慢文档的链接也找不到了。因此,代码和文档应该在一起,最好是代码即文档。大家可能觉得用注释就可以了,但程序员总是要求别人写注释,但自己却不爱写。写注释如果可以像写代码一样,或许能规范这部分的行为。例如:

  {
    name: '获取账号',
    domain: DOMAIN.TAOBAO,
    url: '/api/getAccount.json',
    method: METHOD.GET,
    params: {
      userId: {
        name: '策略包id',
        type: PARAM_TYPE.STRING,
        required: true,
      },
    },
    response: {
        name: '账户名字'
    },
  },

这里用配置文件的方式规范了文档的形式,还可以与浏览器插件相结合,通过插件来查看当前用的接口文档。

异常拦截

异常分为服务器异常和业务异常,服务器异常一般是用 http 状态码,400、500等;业务异常则需要是用 body 里的 code 来表示。在真实的业务实践中,我们发现对于服务器异常我们是很容易写出通用的拦截器做一些处理的,但是对于业务异常,就相对比较复杂了,这里面存在几个问题:

  • 很多后端不习惯使用 code 返回相应的业务编码来表示不同的状态。
  • 前端直接使用后端返回的 message 展示给用户,这里有两个问题,① 后端的需要引入第三方库对 message 做国际化 ② 后端定义的 message 不是用户语言,用户一般是看不懂的。因此这里就需要一个第三方系统的参与,他提供业务 code 和前端动作的映射关系表,比如:后端返回 code:10000,前端应该弹窗并展示 message,定义的 json 如下:
  {
  code: 10000,
  message: '编辑失败',
  debug: '后端数据库读写异常,堆栈信息:',
  showType: 'openDialog'
}

这里的 message 是可以根据不同语言环境返回不同语言文字的,showType 表示了前端的动作类型,这个是可枚举的,其中肯定有一种动作是,不做动作,直接透传。这个第三方系统,就可以配置不同编码的动作,有利于精细化的管理异常,给用户更好的体验。

落地

实践是检验真理的唯一标准,基于上面的理想,我的 service 包也已经成型,使用他非常简单。只需要两步:
① 配置文件
② 引入包
③ 业务代码里调用

配置

  // 配置文件 account.js

import { METHOD, PARAM_TYPE } from '@ali/hulu-service';

export const DOMAIN = {
   TAOBAO: '//taobao.com',
   ALIPAY: '//alipay.com',
};

export default {
  getAccount: {
    name: '获取账号',
    domain: DOMAIN.TAOBAO,
    url: '/api/getAccount.json',
    method: METHOD.GET,
    params: {
      userId: {
        name: '策略包id',
        type: PARAM_TYPE.STRING,
        required: true,
      },
    },
    response: {
        name: '账户名字'
    },
  },
};

引入包

  import HService from '@ali/hulu-service';
import account from './account';

// 初始化service
const service = HService.init({
  urls: [
      account,
  ]
});

export default service;

调用 API

  import Service from './service';

const App = () => {
  const [name, setName] = useState('');
    useEffect(() => {
      Service.getAccount().then((res) => {
       setName(res.name);
    });
  }, []);
  return <div>{name}</div>;
};

export default App;

同时基于浏览器插件,可以快速的切换环境,查看接口文档等。

想想边界

开头,我们说到奥卡姆剃刀,如无必要,勿增实体,这个的前提是,有清晰独立的实体,如果我们的实体之间相互勾连耦合,那又如何剃掉不必要的实体呢。
其实,无论做任何软件构架,都要分清楚边界,也就是一个模块他的定位是什么,哪些功能是他该做的,哪些不是。这里面一个非常重要的依据就是是否易于变更。哪些是业务的、常变化的,哪些是非业务的、一般不变的。我们的代码常常,坏就坏在边界不清晰,或者是边界原则没有一以贯之。工程代码里耦合了业务,业务代码里掺杂着工程(比如环境判断)。代码的坏味道是一点一点积累而成的,而这个坏的开始,就是初始的架构设计边界不清晰,没有用代码定义规范。
抵抗代码的腐败,这是一个漫漫长路,没有银弹,但确实可以精进一个人的系统思维。

作者:ES2049 / 黑石
文章可随意转载,但请保留此原文链接。
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 [email protected]

相关 [需要 service] 推荐:

我们需要怎样的 Service

- - SegmentFault 最新的文章
14 世纪,英格兰的逻辑学家奥卡姆在他的《箴言书注》中说「不要浪费过多的东西,去做那些用较少的东西同样可以做好的事情」. 后来这句话被简化为「奥卡姆剃刀原理」,即:如无必要,勿增实体. 奥卡姆剃刀在各个领域都有他的运用,他不是一个公理,没有严谨的推导过程,但他却是一个在实践中被证明非常有效的解决问题的手段.

SPRING BOOT OAUTH2 + KEYCLOAK - service to service call

- - BlogJava-首页技术区
employee-service调用department-service,如果要按OAUTH2.0流程,只需要提供client-id和client-secrect即可. 在KEYCLOAK中引入service-account,即配置该employee-service时,取消standard-flow,同时激活service-account.

Web Service入门

- - 博客 - 伯乐在线
本文来自文章作者 @Jeremy黄国华 的投稿. 伯乐在线也欢迎其他朋友投稿,投稿时记得留下您的新浪微博账号哦~. 目前对Web Service没有统一的定义,定义一:Web Service是自包含的、模块化的应用程序,它可以在Web中被描述、发布、查找以及调用. 定义二:Web Service是基于网络的、分布式的模块化组件,它执行特定的任务,遵守具体的技术规范,这些规范使得Web Service能与其他兼任的组件进行操作.

Android Service 详解

- - CSDN博客移动开发推荐文章
一个Service也是一种应用程序组件,它运行在后台以提供某种服务,通常不具有可见的用户界面. 其它的应用程序组件可以启动一个Service,即使在用户切换到另外一个应用程序后,这个Service还是一直会在后台运行. 此外,一个应用程序也可以绑定到一个Service然后使用进程间通信(IPC)方式与Service之间发生交互.

【转】 Service Manager

- - 移动开发 - ITeye博客
android2.3 ---  Service Manager分析. Android系统Binder机制的总管是Service Manager,所有的Server(System Server)都需要向他注册,应用程序需要向其查询相应的服务. 可见其作用是多么的重要,那么我们这里就要重点介绍一下这个.

Web Service的研究

- - CSDN博客系统运维推荐文章
SOA和Web Service. 首先明白SOA和Web Service的关系:. * SOA面向服务架构,用于大型分布式系统的一个概念;. * Web Service是实现SOA的方式之一,不是所有的SOA都是基于Web service的;. * 但Webservice确实为最主流的SOA实现方式,有的人甚至把SOA等同于Webservice.

Android学习之路——7.Service

- - ITeye博客
这两天又学习了Android四大组件之一的Service. (1)Service不是一个单独的Process,除非特别指派了,也不是一个Thread,但也不是运行在Main Thread中. (3)Service的生命周期:. 调用的Context.startService()   oncreate() --> onStartCommand ()--> Service is running --> The service is stopped by its or a client--> onDestroy() --> Service is shut down .

Android Activity与Service通信

- - CSDN博客移动开发推荐文章
一、当Acitivity和Service处于同一个Application和进程时,通过继承Binder类来实现.      当一个Activity绑定到一个Service上时,它负责维护Service实例的引用,允许你对正在运行的Service进行一些方法调用. 比如你后台有一个播放背景音乐的Service,这时就可以用这种方式来进行通信.

Restful 和 Jersey介绍(Web Service )

- - CSDN博客架构设计推荐文章
REST 2000 年由 Roy Fielding 在博士论文中提出,他是 HTTP 规范 1.0 和 1.1 版的首席作者之一. REST 中最重要的概念是资源(resources) ,使用全球 ID(通常使用 URI)标识. 客户端应用程序使用 HTTP 方法(GET/ POST/ PUT/ DELETE )操作资源或资源集.

service层异常的处理

- - Web前端 - ITeye博客
1、在service方法里面如果对异常进行了捕获的话,该事务是不会进行回滚的.        默认spring事务只在发生未被捕获的 runtimeexcetpion时才回滚.   方案1.例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeException()语句,以便让aop捕获异常再去回滚,并且在controller层要继续捕获这个异常并处理.