在浏览器打开http://localhost:8000/user,看下现在的数据。
我先把数据清掉。
找段代码插入一句:localStorage.clear()就能清了。服务启动一次后记得删了。也可以加个接口来做清理工作。
打开浏览器F12,执行以下代码:
fetch("/user", {
method: "post",
headers: { "content-type": "application/json" },
body: JSON.stringify({ aa: "haha" }),
});
刷新页面:
是不是插入了一条不应该插入的数据?这就属于程序漏洞。严重的可能会搞垮你的服务器。
优化思路
怎么解决这个问题呢?其实接口流动的每个环节都可以处理,上层控制层(user.route.ts)、中间业务处理层(user.service.ts)、底层数据访问层(model.ts)都能做,只是在哪个环节合适的问题。
针对用户页面多传递的参数这种情况,底层model.ts比较合适,因为多传的参数可能在前面环节有特殊的作用(比如只是一个判断分支的标志),如果不想每个service都辛苦,就在底层处理好。
但model中怎么校验呢?上文看到,我们Model类的结构是这样的:
export class Model<T> {}
这个T,只是TypeScript中的一个泛型,只是开发阶段TypeScript的一个约束而已。
再次重申,一定要注意Deno并不是真正的TypeScript运行时,本质仍是依赖V8引擎运行的JavaScript。 所以TypeScript的特性(type、interface、泛型、private声明)等,在编译成JavaScript后就都没了,只有enum枚举会编译为一个对象。
在model中,我们获取不到这个泛型T的任何信息,因为它就是个虚拟的东西,假的。
这个泛型T,传递的是什么,你还有印象吗?是个接口:
export interface User {
id: string;
author: string;
age: number;
}
userModel: Model<User>;
为了让Model知道User的属性信息,必须把它改造成class:
export class User {
id: string;
author: string;
age: number;
}
再把它当作参数传递给Model:
this.userModel = new Model("users", User);
但你会悲哀地发现,class是真实存在的,但它的属性id、author、age一个个都是假的,class编译后只剩下光杆司令了。
export class User {}
类属性initializer
甚至当你敲了刚才的代码后,会发现编辑器也开始欺负你,居然报错了:
你鼠标移上去看报错信息:
Property 'id' has no initializer and is not definitely assigned in the constructor.deno-ts(2564)
这是一条deno默认配置的TypeScript的规则,意思是你不能定义一个空类,你得为这些属性负责,得初始化。这个当然没毛病。但对我们这个需求来说就不太友好了,这时你需要修改deno.jsonc文件,添加一段:
{
"compilerOptions": {
"strictPropertyInitialization": false
}
}
再看下编辑器,是不是波浪线没有了?(如果还不行,可能需要重启下vscode)。
开启装饰器
少年,不要高兴太早,你只是解决了编辑器对你的恶意,怎么才能拿到这几个定义的属性呢? 如果你好好看了《开发前准备》的前两个章节,对ES6中的装饰器应该有些印象(还不记得去看es6.ruanyifeng.com/#docs/decor…,www.typescriptlang.org/docs/handbo…)。
它与Java中的注解比较相似,能极大地简化我们的代码。
要使用装饰器和元数据,还得在刚才的deno.jsonc再添加两个属性emitDecoratorMetadata和experimentalDecorators(Deno 1.25版本开始内置了后者,不需要再添加了):
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"strictPropertyInitialization": false
}
}
再在deps.ts中,引用:
export { Reflect } from "https://deno.land/x/deno_reflect@v0.2.1/mod.ts";
对Deno如何使用,详见deno_reflect文档。
Prop装饰器
我们要实现一个Prop装饰器,作用在具体属性上,用法如下:
export class User {
@Prop()
id: string;
@Prop()
author: string;
@Prop()
age: number;
}
意思是通过装饰器,注入给User这个class一些信息。
给个简单的例子,可以放在src/main.ts上面自己看下:
const map: Record<string, any> = {};
function Prop() {
return function (target: any, propertyKey: string) {
console.log(target, target.constructor === User, propertyKey);
// target是类的实例,propertyKey就是author/age
map[propertyKey] = true;
return target;
};
}
class User {
@Prop()
author: string;
@Prop()
age: string;
}
console.log(map); // { author: true, age: true }
思路就是存储这个User构造函数与这些属性值的关系,这样Model在拿到User之后就能取到这些属性信息了,再对应地进行下过滤。
测试完上面代码就可以删了。
真正开发时,我们可以使用deno.land/x/deno_refl…的反射来简化我们的代码。
优化代码
schema.ts
新建一个src/schema.ts,内容如下:
// deno-lint-ignore-file no-explicit-any
import { Reflect } from "../deps.ts";
export type TargetInstance = any;
export type Constructor = new (...args: any[]) => any;
export type Target = Constructor & Record<string, any>;
const PROP_META_KEY = Symbol("design:prop");
export function Prop(props: any = {}) {
return function (target: TargetInstance, propertyKey: string) {
addSchemaMetadata(target, propertyKey, props);
return target;
};
}
export function addSchemaMetadata(
target: TargetInstance,
propertyKey: string,
props: any = {},
) {
Reflect.defineMetadata(PROP_META_KEY, props, target, propertyKey);
}
const schemaPropsCaches = new Map();
const instanceCache = new Map();
export function getInstance(cls: Target) {
if (instanceCache.has(cls)) {
return instanceCache.get(cls);
}
const instance = new cls();
instanceCache.set(cls, instance);
return instance;
}
export function getSchemaMetadata(target: Target): Record<string, any>;
export function getSchemaMetadata<T = any>(
target: Target,
propertyKey?: string,
): T;
export function getSchemaMetadata(
target: Target,
propertyKey?: string,
) {
const instance = getInstance(target);
if (propertyKey) {
return Reflect.getMetadata(PROP_META_KEY, instance, propertyKey);
}
let map: Record<string, any> = schemaPropsCaches.get(target);
if (!map) {
map = {};
Object.keys(instance).forEach((key) => {
const meta = Reflect.getMetadata(PROP_META_KEY, instance, key);
if (meta !== undefined) {
map[key] = meta;
}
});
schemaPropsCaches.set(target, map);
}
return map;
}
getSchemaMetadata写的比较复杂,原因是可以存储Prop的具体配置,比如{required:true}之类信息,以备后用。
user.schema.ts
新建src/user/user.schema.ts:
import { Prop } from "../schema.ts";
export class User {
@Prop()
author: string;
@Prop()
age: number;
}
看得出来,我刻意去掉了id属性。为什么呢?因为id是个最基本的属性,但它的类型是字符串、数字或者是某种具体的类,上层开发者不需要知道。所以id的属性应该由model返回给service。
model.ts
对Model进行修改,这里只写变化的部分:
import { Constructor, getSchemaMetadata } from "./schema.ts";
export type ModelWithId<T> = T & { id: string };
export class Model<T extends object, U = ModelWithId<T>> {
name: string;
schema: Constructor | undefined;
constructor(name: string, schema?: Constructor) {
this.name = name;
this.schema = schema;
}
/** 增加一个文档 */
async insertOne(doc: T): Promise<string> {
const id = nanoid();
const newdoc = { id, ...doc };
if (this.schema) {
const meta = getSchemaMetadata(this.schema);
Object.keys(newdoc).forEach((key) => {
if (!meta[key] && key !== "id") {
Reflect.deleteProperty(newdoc, key);
}
});
}
setData(id, newdoc);
this.addToIds(id);
return id;
}
}
user.service.ts
仍是只列修改的部分:
import { User } from "./user.schema.ts";
class UserService {
userModel: Model<User>;
constructor() {
this.userModel = new Model("users", User); // 把User当参数传递过去
}
async getAll() { // 去掉了返回类型,靠编辑器推断就可以
return this.userModel.findAll();
}
async addUser(user: User) { // User不需要再Omit去掉id
return this.userModel.insertOne(user);
}
async updateUser(id: string, user: Partial<User>) { // 与addUser一样
return this.userModel.findByIdAndUpdate(id, user);
}
}
验证
重新在浏览器F12里插入一条数据
fetch("/user", {
method: "post",
headers: { "content-type": "application/json" },
body: JSON.stringify({ aa: "haha" }),
});
看到是不是只增加了一条只有id的数据?
作业
你可能对上面的结果有些疑惑,这种情况是不是应该添加失败啊?
是的。
我们可以给Prop加个参数:
export interface SchemaType {
/**
* Adds a required validator to this SchemaType
*/
required?: boolean | [required: boolean, errorMsg: string];
}
export function Prop(props: SchemaType = {}) {
return function (target: TargetInstance, propertyKey: string) {
addSchemaMetadata(target, propertyKey, props);
return target;
};
}
怎么校验就交给你了!(下面章节我不准备处理了)