上节最后提到:
再思考下,上面的代码还有什么问题?还有什么能优化的吗?
问题
先说2个问题。
1. id是自增数字
- 每次生成,都有两次IO(先读一次,再写一次)。当然,性能问题暂时不必考虑。
- 可能会暴露一些额外的信息。比如用户看到url:http://localhost/user/1,可能会猜测http://localhost/user/2,http://localhost/user/1000等。
- JavaScript中数字是有上限的。假设你的服务要运行很久的话。
- 不利于分布式部署。这个暂时也不必考虑。
所以,推荐使用自动生成的唯一字符串来做id,常见的有uuid和nanoid,后者出现较晚,比uuid要短和高效,详见:mp.weixin.qq.com/s/CPSS7B7Ns…。
使用上也很简单:
import {nanoid} from "https://deno.land/x/nanoid/mod.ts"
nanoid() //=> "lQLTBJKVRCuc"
2. 代码复用
假如我们要新增一个功能,比如角色的接口,你需要写个role.service.ts,功能差不多,你该怎么办?可能需要做的就是把localStorage的存储空间从users_开头修改成roles_,总不能把所有代码复制粘贴一遍吧?
程序开发中,代码复用是很重要的,一是代码结构的精减,二是利于维护,因为复制一时爽,改时火葬场,有一处逻辑修改了,你就要动两处三处甚至...
另外,UserService还有个问题是业务代码和数据处理严重耦合在一起,如果我们想要再切换localStorage存储改用数据库,是不是还得改它的代码?
这时,我们可以再加一层DAO(Data Access Object,数据访问层),专门负责增删改查等细节的API,Service层要做什么,只需要组装这些API就可以了。
分层的作用
对于初学者,可能对这些分层不了解,又或者知道有这些分层,但为什么要这样分,每层应该写什么代码一头雾水。
这里以阿里编码规范中约束的分层为例:
对应到我们现在的代码里,user.router.ts就是请求处理层,也是一些框架中常见的控制层(Controller),业务逻辑层就是现在的user.service.ts,数据持久层就是下来我们要写的model.ts,通用处理层暂时没有用到。
说下每层的作用:
-
Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。也就是说,轻业务逻辑,参数校验,异常兜底。
-
Service 层:相对具体的业务逻辑服务层。简单理解就是对下层DAO的组装调用,业务复杂的还需要事务控制。
-
Manager 层:通用业务处理层,它有如下特征:
-
对第三方平台封装的层,预处理返回结果及转化异常信息
-
对Service层通用能力的下沉,如缓存方案、中间件通用处理
-
与DAO层交互,对多个DAO的组合复用
简言之就是为了代码复用性而剥离出来的中间层,有的团队把它和Service层混在一起。
-
-
DAO 层:数据访问层,与底层数据库如MySQL、Oracle、MongoDB等进行数据交互。
这个称不上是业内标准,每个团队有每个团队的规则,但分层说到底还是为了明确职责边界,便于后续维护。具体怎么做可以自行摸索。这里只是推荐上述规范。
优化
在src下,新建一个model.ts的文件,内容如下:
// deno-lint-ignore-file require-await
import { nanoid } from "https://deno.land/x/nanoid@v3.0.0/mod.ts";
function getData<T = string>(key: string) {
const str = localStorage.getItem(key);
if (str) {
return JSON.parse(str) as T;
}
return null;
}
function setData(key: string, val: unknown) {
localStorage.setItem(key, JSON.stringify(val));
}
export class Model<T> {
name: string;
constructor(name: string) {
this.name = name;
}
private getAllIds(): string[] {
return getData(`${this.name}_ids`) || [];
}
private addToIds(id: string) {
const ids = this.getAllIds();
ids.push(id);
setData(`${this.name}_ids`, ids);
return ids.length;
}
/** 增加一个文档 */
async insertOne(doc: Omit<T, "id">): Promise<string> {
const id = nanoid();
setData(id, { ...doc, id });
this.addToIds(id);
return id;
}
/** 查找所有 */
async findAll(): Promise<T[]> {
const docs = await Promise.all(
this.getAllIds().map((id) => this.findById(id)),
);
return docs.filter(Boolean) as T[];
}
/** 根据id查找文档 */
async findById(id: string): Promise<T | null> {
return getData<T>(id);
}
/** 根据id更新文档 */
async findByIdAndUpdate(
id: string,
doc: Partial<Omit<T, "id">>,
): Promise<{ modifiedCount: number }> {
const oldDoc = await this.findById(id);
const modifiedCount = 0;
if (oldDoc) {
for (const key in doc) {
if (doc[key] === undefined) {
Reflect.deleteProperty(doc, key);
}
}
Object.assign(oldDoc, doc);
setData(id, oldDoc);
}
return { modifiedCount };
}
/** 根据id删除文档 */
async findByIdAndDelete(id: string): Promise<number> {
localStorage.removeItem(id);
const ids = this.getAllIds();
const index = ids.indexOf(id);
if (index > -1) {
ids.splice(index, 1);
setData(`${this.name}_ids`, ids);
return 1;
} else {
console.warn(`${id} not found in ${this.name}_ids`);
return 0;
}
}
}
修改user.service.ts:
// deno-lint-ignore-file require-await
import { Model } from "./model.ts";
export interface User {
id: string;
author: string;
age: number;
}
class UserService {
userModel: Model<User>;
constructor() {
this.userModel = new Model("users");
}
async getAll(): Promise<User[]> {
return this.userModel.findAll();
}
async getUserById(id: string) {
return this.userModel.findById(id);
}
async addUser(user: Omit<User, "id">) {
return this.userModel.insertOne(user);
}
async removeUser(id: string) {
return this.userModel.findByIdAndDelete(id);
}
async updateUser(id: string, user: Partial<Omit<User, "id">>) {
return this.userModel.findByIdAndUpdate(id, user);
}
}
export const userService = new UserService();
整段代码是不是非常干净清爽?
如果我们要新增一个role.service.ts,只需要这样:
import { Model } from "./model.ts";
class RoleService {
roleModel: Model<Role>;
constructor() {
this.roleModel = new Model("roles");
}
async getAll(): Promise<Role[]> {
return this.roleModel.findAll();
}
}
注意:这时程序会报错,还需要把mod.ts中我们把id转换为数字的代码去掉。
作业
思考下,我们的文件越来越多,你该怎么调整现在的目录结构?