1、你在项目中的数据访问层是否还长这样?
import request from '@/utils/request'
export function login(data) {
return request({
url: '/user/login',
method: 'post',
data
})
}
export function getInfo(token) {
return request({
url: '/user/info',
method: 'get',
params: { token }
})
}
export function logout() {
return request({
url: '/user/logout',
method: 'post'
})
}
随着我年纪的增长,我思考了2个问题:
1、如果有同事因为不仔细,把元数据写错了怎么办?
2、能不能不用每次都去写这些元数据代码?
2、从Nest.js或者Spring MVC框架中得到的一些启示。
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
如果我们前端的数据访问层也能写成如nest.js的这种风格,那么,我们的代码看起来将会是相当的简洁。所以,从这个例子,我们已经确定了我们的设计目标,如果我们能得到一组装饰器,那么,我们的数据访问层将会改写为以下形式:
import { Controller, GET, POST } from '@/decorators/request';
@Controller('/user')
export class UserApi {
@POST()
login() {}
@GET('/info')
getInfo() {}
@POST()
logout() {}
}
3、回到原点,背上行囊,从装饰器出发。
这是一段摘录自阮一峰老师的《ES6入门》一书的话:
装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript。
装饰器是装饰模式在语法层面的提供,通过装饰器,我们可以在某个横切面完成我们的业务逻辑,如预判,如果不满足,直接终止该方法的执行,或执行方法之后,需要的一些补救行为。这些横切面跟我们的业务代码没有或者少有耦合,更容易帮助我们编写出优秀的软件系统。
装饰器其实就是一个普通的函数,写成@ + 函数名,放在类名或者类的成员方法(或属性)名之前,因为有变量的提升,普通函数不能被装饰。目前ES支持两种类型的装饰器。
3.1、类装饰器
// 定义装饰器
//如果不需要接收参数
function Controller(Target) {
//悄悄的做一些事儿
return Target;
}
//如果需要接收参数
function Controller(path) {
return function(Target){
//悄悄的做一些事儿
return Target;
}
}
//使用装饰器
//当不需要接收参数的时候
@Controller
class Demo{}
//当需要接收参数的时候
@Controller('/login')
class Demo {}
类的装饰器接收一个参数,target,这个target就是类本身。
3.2、 方法(或属性)装饰器
//如果不需要接收参数
function GET(target, prop, descriptor) {
//悄悄的做一些操作
return descriptor;
}
//如果需要接收参数
function GET(path){
return function(target,prop, descriptor) {
// 悄悄的做一些操作。
return descriptor;
}
}
//使用装饰器
//当不需要接收参数的时候
class Demo{
@GET
login() {}
}
//当需要接收参数的时候
class Demo{
@GET('/login')
login(){}
}
方法的装饰器有3个参数,顺序分别是target,prop,descriptor,第一个参数target是被装饰类的原形对象,第二个参数是被装饰方法或属性的字段名,第三个参数是一个descriptor,它的类型是PropertyDecorator。由于笔者在之前的博文有讲解过这个类型,因此,此处仅贴出它的类型定义代码 不再详细讲解,不太了解的读者可参考MDN或笔者早先的博文。
PropertyDescriptor的定义如下:
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
// 对于babel,还可能出现initializer,这是一个函数,用于描述属性初始化操作的代码块
}
4、编写装饰器
4.1、 前置知识,reflect-metadata库
GitHub地址:reflect-metadata
我们不应该修改语言本身的定义以外的属性(如给descriptor加一些奇奇怪怪的字段,或者直接在function定义一些奇奇怪怪的字段,或者在Object,或者自己去是现实一个记录元数据的对象等),否则可能会给我们的程序带来潜在的隐患。reflect-metadata可以给我们提供这些便利,使用这个库以后,可以方便的记录我们传递的元数据参数。
4.2 、装饰器的实现
根据笔者在3年项目开发中对axios的使用感受,提炼出了以下方法:
import "reflect-metadata";
//这就是我们正常的对axios的统一封装的实例。
import http from "@/utils";
function hasPrefix(str) {
return str.startsWith("/");
}
function normalize(str) {
var url = !hasPrefix(str) ? "/" + str : str;
return url;
}
/**
* 定义ResponseType
* @param responseType 响应的类型
*/
export function ResponseType(responseType) {
return function(target, prop, descriptor) {
Reflect.defineMetadata("responseType", responseType, target, prop);
return descriptor;
};
}
/**
* 需要对路由进行RESTful重写的路由
*/
export function RESTful(rewritePath) {
return function(target, prop, descriptor) {
Reflect.defineMetadata("rewritePath", rewritePath, target, prop);
return descriptor;
};
}
/**
* 定义Controller
* @param controller
*/
export function Controller(controller) {
return function(Target) {
//记住在controller部分的path
Reflect.defineMetadata("controller", controller, Target.prototype);
return Target;
};
}
/**
* 定义访问的附加Headers
* @param headers
*/
export function Headers(headers) {
return function(target, prop, descriptor) {
//记住要传递的headers
Reflect.defineMetadata("headers", headers, target, prop);
return descriptor;
};
}
/**
* 强制指定某些参数作为querystring传递
* @param {String[]} reserveAsParams
*/
export function Param(reserveAsParams) {
return function(target, prop, descriptor) {
//定义要强制作为querystring传递的参数键集合
Reflect.defineMetadata("params", reserveAsParams, target, prop);
return descriptor;
};
}
export function Request(requestPath, requestMethod) {
return function(target, prop, descriptor) {
var method = requestMethod || "GET";
var path = requestPath || prop;
descriptor.value = function() {
/**
*由于目前JS的尚未支持参数装饰器,因此,函数只能接收一个参数,且该参数必须是对象。
*/
//如果参数是多个的话,给出提醒
if (arguments.length > 1) {
throw `can not call this function with argument more than one`;
}
// 如果蚕食是一个,且是非对象类型的话,这个参数将会被忽略
if (arguments.length == 1 && Object.prototype.toString.call(arguments[0]) !== "[object Object]") {
console.warn("the param only one will be ignore if you call this function with a basic data-type ");
}
var url = "";
var controller = Reflect.getMetadata("controller", target);
var headers = Reflect.getMetadata("headers", target, prop);
var rewritePath = Reflect.getMetadata("rewritePath", target, prop);
var responseType = Reflect.getMetadata("responseType", target, prop);
var hasRequestBody = ["post", "put", "patch"].includes(method.toLowerCase());
//自动忽略非对象参数
var arg = Object.prototype.toString.call(arguments[0]) === "[object Object]" ? arguments[0] : {};
//用作放在请求体上的数据
var data = hasRequestBody ? arg : undefined;
//用作放在查询字符串的数据
var params = hasRequestBody ? {} : arg;
//对于post这类请求,强制通过params传递给后端的数据,
var reserveAsParams = Reflect.getMetadata("params", target, prop);
if (hasRequestBody && Array.isArray(reserveAsParams)) {
//将指定的key从data拷贝到params上去
reserveAsParams.forEach((key) => {
params[key] = data[key];
delete data[key];
});
}
controller && (url += `${normalize(controller)}`);
url += `${normalize(path)}`;
//如果需要对URL进行重写的话,处理需要进行重写的部分,剩余的部分
if (rewritePath) {
var eties = Object.entries(params);
eties.forEach(([prop, value]) => {
var regExp = new RegExp("\\$\\{" + prop + "\\}");
//如果能匹配到的话,说明此参数需要进行重写,需要从params里面移除
if (regExp.test(rewritePath)) {
delete params[prop];
}
rewritePath = rewritePath.replace(regExp, value);
});
//合并重写的path
url += `${normalize(rewritePath)}`;
}
return http({
url,
method,
data,
headers,
responseType,
//如果没有一个需要通过queryString发送给后台的数据,则不会处理查询字符串
params: Object.keys(params).length > 0 ? params : undefined,
});
};
return descriptor;
};
}
export function GET(path) {
return Request(path, "GET");
}
export function POST(path) {
return Request(path, "POST");
}
export function DELETE(path) {
return Request(path, "DELETE");
}
export function PUT(path) {
return Request(path, "PUT");
}
export function HEAD(path) {
return Request(path, "HEAD");
}
export function PATCH(path) {
return Request(path, "PATCH");
}
在数据访问类中使用:
import { Controller, POST, GET, ResponseType, Param, Headers, RESTful } from '@/decorators/request'
@Controller('/app')
export class HomeApi {
@GET()
detail(){}
/*
等价于
detail(params) {
return http({
url: '/app/detail',
method: 'get',
params,
})
}
*/
@GET('/list')
getDataList() {}
/*
等价于
getDataList(params) {
return http({
url: '/app/list',
method: 'get',
params
})
}
*/
@POST('/entity')
saveEntity() {}
/*
等价于
saveEntity(data) {
return http({
url: '/app/list',
method: 'post',
data
})
}
*/
@GET('/entity')
@RESTful('/${name}/${age}')
getEntity(){}
/*
等价于
getEntity(params = {}){
const { name, age, ...rest } = params;
return http({
url:`/app/entity/${name}/${age}`,
method:'get',
params: rest,
});
}
*/
@GET('/download')
@ResponseType('blob')
download(){}
/*
等价于
download() {
return http({
url: '/app/download',
method: 'get',
responseType: 'blob',
})
}
*/
@POST('/create')
@Headers({
"Content-Type":"application/json"
})
save(){}
/*
等价于
save() {
return http({
url: '/app/create',
method: 'post',
headers: {
"Content-Type": 'application/json'
}
})
}
*/
@POST('/submit')
@Param(['time'])
submit(){}
/*
等价于
submit(data = {}) {
const { time, ...rest } = data;
return http({
url: '/app/submit',
method: 'post',
data: rest,
params: {
time
}
})
}
*/
}
因为我们的数据访问层现在是按照装饰器这种风格写的,由于目前ES还不支持参数装饰器(TS支持),当我们传递多余一个参数或者传递一个基本类型的参数时候,无法识别要传递给后台的key,因此,我们在调用的时候必须写成仅含有一个参数,且这个参数必须是对象的这种统一格式,多少还是有一点儿遗憾。😣
5、总结
1、借助装饰器,我们可以少写很多代码,简化我们的开发。
2、在日常的开发中,我们绝大部分场景都在写url,可以避免因单词的拼写错误,导致数据传递出错的问题。
由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,你们的意见将会帮助我更好的进步。本文乃笔者原创,若转载,请联系作者本人,邮箱404189928@qq.com🥰