这篇文章主要通过一个小例子介绍使用Umajs和scf框架做项目的基本流程,后端接口数据是如何被web页面接收的整个过程。
对于Umajs初学者来说,肯定和我一样有这些疑问:
- 为什么要使用Umajs框架?
- scf到底是什么,在整个项目中扮演什么角色,为什么不用http接口?
- 通过scf具体是如何实现接口调用的?
下面就这几个问题一一来看。
Umajs:
Umajs是一个基于 Koa2 使用 TypeScript 开发、通过装饰器使用大部分功能的轻量级的Node.js Web 框架。
Umajs的核心优势是:
- 功能强大:内置丰富的装饰器,诸如IOC 装饰器、AOP 装饰器、路由装饰器等等,也可以自定义装饰器;
- 扩展灵活:通过插件机制扩展框架,提供多种方式兼容 Koa 中间件;
- 快捷返回:框架内置了常用的返回类型,同时支持扩展;结合切面及装饰器能够覆盖绝大多数业务场景;
- 开发效率:Umajs内置了大量功能能够提升开发效率;同时 Koa 项目可以平滑的迁移到 Umajs。
详情可以去这里查看: Umajs文档。
SCF:
SCF:是58 自主研发的支持跨平台具有高并发、高性能、高可靠性,并提供异步、多协议、事件驱动的中间层服务框架,也就是RPC远程过程调用(Remote Procedure Call) 框架。更多详情请移步scf文档: scf文档(1) , scf文档(2)。
先说一下我在做优信拍买家网站需求时,是如何用到这两个框架的,在这个需求中,接口调用采用的是scf的接口调用方式。
首先为什么要采用scf调用接口?以前基本都是前端去调后端的http接口,现在我们在umajs中使用scf的调用模式,前端去调后端的tcp接口,这样做有什么好处呢?
请看下面的模型图:
OSI模型-TCP/IP模型
从上图中可以看到,http位于应用层,而tcp位于传输层,应用层位于传输层之上,也就是说要到达应用层需要先到达传输层,所以使用tcp接口在调用过程中速度明显快于调用http接口,这一点来说用户体验会更好。
例子:
下面就通过一个例子来介绍一下前端页面到底是如何获取到后端接口数据的。
下面例子中,主要涉及Umajs中的Router路由、controller控制器、service服务、Result返回、依赖注入这五个功能。
路由装饰器:@Path
依赖注入装饰器:@Service
1.cli 工具在初始化会自动生成目录:
![](https://img-blog.csdnimg.cn/img_convert/0fa5dc565a067d759b899f2b7da4d907.png)
umajs项目目录结构如上图所示,其中特别强调两个文件夹,controller和service文件夹中都是以".controller.ts"和".service.ts"为后缀的文件,也是下方例子中会出现的文件。
2.首先在scf文件夹下的pom.xml文件引入相关依赖jar包
一般后端会提供此段代码,我们直接粘贴到文件中即可。
<dependencies>
<dependency>
<groupId>com.bj58.che.yxp</groupId>
<artifactId>yxpbaseservice.contract</artifactId>
<version>1.0.8</version>
</dependency>
</dependencies>
3.在scf.config文件中增加服务端配置信息
其中,name属性是对应的服务名,host是对应的ip地址,由后端提供。
<?xml version="1.0" encoding="utf-8" ?>
<SCF>
<Service name="yxpbaseservice" id="4" maxThreadCount="100">
<Commmunication>
<SocketPool bufferSize="4096" minPoolSize="3" maxPoolSize="100"
nagle="true" autoShrink="00:00:20" sendTimeout="00:00:05"
receiveTimeout="00:00:05" waitTimeout="00:00:05" />
<Protocol serialize="SCFV3" encoder="UTF-8" compressType="UnCompress" />
</Commmunication>
<Loadbalance>
<Server deadTimeout="00:00:20">
<add name="yxpbaseservice" host="10.167.25.105" port="18819" maxCurrentUser="100" />
</Server>
</Loadbalance>
</Service>
</SCF>
4.**.service.ts文件
a.在对应的**.service.ts文件中通过后端提供的tcp地址(下面代码中"tcp://yxpbaseservice/AreaServiceImpl"就是由后端提供的地址),与后端接口建立连接;
b.后端将接口声明为一个函数,我们去调用这个函数即可,通过baseService去调用后端接口函数;
c.自定义一个getProvinceList方法暂存数据,下方调用的是一个获取省份的接口listProvinceCity,resData就是接口返回的数据(省份列表)。
/* 文件名:trade.service.ts */
import { BaseService } from "@umajs/core"; //编写service时需继承默认 BaseService
import { ScfFactory } from "scf-node";
import * as IAreaService from "../../scf-build/yxpbaseservice.contract/IAreaService";
const baseService = ScfFactory(IAreaService, "tcp://yxpbaseservice/AreaServiceImpl");
export default class Trade extends BaseService {
/* 获取省份列表接口数据 */
async getProvinceList() {
const resData = await baseService.listProvinceCity();
return resData;
}
}
5.**.controller.ts文件
拿到数据之后,将获取到的数据传给对应路由下的页面,以便在页面文件中能获取到数据。
下方代码中,依赖注入装饰器 Service,@Service 装饰器仅提供 Controller 使用,
a.在controller控制器文件中引入trade.service.ts文件;
b.通过@Path修饰器修饰类,设置页面路由,类中的方法都会基于这个路由;
c.通过依赖注入调用service,其中@Service(" trade "),括号中的trade与引入的trade.service.ts文件名对应,将数据存在变量trade中;注入之后可直接使用无需再进行实例,直接通过this.trade调用trade.service.ts文件中我们定义的方法getProvinceList,拿到省份数据;
d.通过Result.viewExpress(templatePath,data)统一返回用来渲染模板的数据,此时在模版文件中取body就能取到数据(例子中是将数据传到模版文件中,也可传入其他需要该数据的文件,详情参见Result统一返回)。
/* 文件名:trade.controller.ts */
import { BaseController, Path, Service } from "@umajs/core";
import tradeService from "../service/trade.service";
import { Result } from "../plugins/expressCompatible/index";
@Path("/trade")
export default class Trade extends BaseController {
@Service("trade")
trade:tradeService;
@Path()
async index() {
try {
const provincelistData = await this.trade.getProvinceList();
return Result.viewExpress("home/page/trade.tpl", {
tag: "trade",
body: provincelistData,
});
} catch (error) {
console.log(error);
...
}
}
}
umajs中,前端开发人员在controller中调用后端接口,这样的好处是,前端开发人员可以自行选择需要的字段参数。以前当后端对接口字段进行更改之后,前端开发人员不能正确的拿到数据,就需要询问后端人员确认,现在就可以在controller中获取接口的所有数据,可根据需求拿到想要的字段,不用再多次与后端确认,达到提升开发效率的目的。
以上就是在umajs项目中通过scf调用模式调用后端接口的基本流程。
6.补充
1.注意:上面代码中用到了@service修饰器,@service修饰器仅提供Controller使用,为了方便使用,Service 注入中内置了 ctx上下文对象,如果要在非 Controller 中使用 Service ,必须传入 ctx 进行实例化才能使用。
上面代码中用到了依赖注入,下面就总结一下依赖注入相关的修饰器:
有三个关于依赖注入的修饰器,分别是:@service、@Resource、@Inject 修饰器。
注入文件 | 依赖注入方式 |
---|---|
controller文件中 | 使用@service修饰器,Service 注入中内置了 ctx上下文对象,不需要再进行实例化; a):Service中可以传service所在的文件名,且需要以字符串的形式; b):Service中也可以直接传service所在的class类名(被其他文件引入后可能起了别名,此时就传该别名)。 |
非controller文件中 | 方法一:使用@Resource、@Inject 修饰器搭配: 1.先使用@Resource修饰ICO容器中的class; 2.再使用@Inject 修饰器将被@Resource修饰过的class的实例注入到指定变量中。 a):Inject中可以传Resource修饰过的class所在的文件名,且需要以字符串的形式; b):Inject中也可以直接传Resource修饰过的class类名(被其他文件引入后可能起了别名,此时就传该别名)。 方法二:使用@Service修饰器,但必须传入 ctx 进行实例化才能使用; |
2.上面例子中,用到了@Path修饰器修饰路由,下面总结一下被修饰的各种情况下,访问地址规则:
默认路由:没有被任何修饰器修饰的情况下,访问的地址为"127.0.0.1:端口号/控制器名/方法名",我们对这种路由称之为默认路由。
修饰情况 | 访问规则 |
---|---|
class和method都没有被@Path修饰 | 通过默认路由访问(不推荐使用,未来会取消该功能) |
只有class被@Path修饰 | 1.一个class只允许使用一个@Path修饰器且只接收一个参数; 2.只装饰 class 不生效(必须和 method 装饰配合使用); 3.class中的方法还是可以通过默认路由被访问; |
只有method被@Path修饰 | 1.作为根路由; 2.该方法不能再通过默认路由的方式访问; 3.同一个方法上允许设置多个 Path 路径; 4.也可以通过多个@Path 修饰器修饰的方式来指定多个路径; |
class和method都被@Path修饰 | 1.class上的路由作为根路由; 2.一个class只允许使用一个@Path修饰器且只接收一个参数; 3.该方法的访问路径将和 class 上的 @Path 指定路径合并使用; 4.方法上的修饰器中为空也表示被修饰了【即@Path( )】; 5.也遵从上一种情况的2,3,4点; |
3.另外,如果需要获取请求中的参数,可以使用@Param 和@Query 两种修饰器。
@Param 和@Query 修饰器可以快捷的获取到请求中的 param 和 query 参数,@Param 传入的名称要和@Path 中设置的名称一致,@Query 传入的名称要和请求的 query 参数名称一致,才能正确获取到。
例如:调用城市模糊查询接口时,请求".../getCityByName?city=北"获取所有城市名中含有“北”的接口数据:
在controller.ts文件中定义方法:
@Path("/getCityByName")
async getCityByName(@Param("getCityByName") title: string,@Query("city") name:string) {
const resData = await this.trade.getCityByName(name); //传入请求的参数"北"
return Result.json({
data: resData,
});
}
同时在service.ts文件中的也要给对应接口函数也要传参,这样后端的接口函数getCitiesByName拿到参数去返回对应的数据:
/* 模糊查询获取城市接口数据 */
async getCityByName(cityName: string) {
const resData = await baseService.getCitiesByName(cityName);
return resData;
}
4.在使用scf调用接口函数时,要严格遵守参数类型。当要给函数传递枚举类型参数时,该如何传呢?需要先引用后端声明的参数类,再通过这个参数类来new一个实体对象,并把枚举数据传给参数,最后把这个实体对象作为接口函数的参数,这样我们就把枚举类型数据传给了接口函数。请看下面例子:
如下:假设这是我们的枚举类型数据,如何传给后端接口函数呢?
export enum ENUM_CLIENT {
PC = 1,
APP_IOS,
APP_ANDROID,
}
这是后端声明的一个参数类,里面规定了参数的类型:
/*DisplayReportReq.ts文件*/
export = DisplayReportReq;
declare class DisplayReportReq {
constructor(params?: { [key: string]: any });
clientCode: any;
reportId: number;
}
在下方文件方法中,new一个实体对象req,并传枚举数据(注:代码中省略了部分相关引用):
/*.service.ts文件*/
import * as DisplayReportReq from "../../scf-build/chechakecontract/DisplayReportReq";
const reportService = ScfFactory(IReportDisplayService, "tcp://chake/ReportDisplayServiceImpl");
export default class ReportService extends BaseService {
async queryNissanReport(reportId: string) {
const req = new DisplayReportReq({
clientCode: ENUM_CLIENT.PC,
reportId: parseInt(reportId),
});
const basicInfo= await reportService.displayReportBasicInfo(req); //调接口函数时直接传req
}
}
想了解更多相关内容,请查看下方链接: