6. todolist: 使用依赖注入重写持久化 Service
Opened this issue · 0 comments
Lellansin commented
MVC 模型
我们这几节课所写的例子已经完成了一个基本的 MVC 模型:
- model 模型
- DAO (Data Access Object) 数据访问对象,常用来操作数据库请求
- Service 公共代码逻辑(包括通过 DAO 操作数据)
- view 视图
- cotroller 控制器
其中我们所写的部分与 MVC 对应关系如下:
View | Controller | Model |
---|---|---|
HTML直出 | HomeController | fileDB |
请求时序:
由于业内流行的 “约定大约配置原则” 我们需要把我们上节课所写的 fileDB.ts 改写成 Midway.js 框架所熟悉的 Service 形式。
使用依赖注入编写一个 Midway Service
Midway 依赖注入的官方文档参见:https://www.yuque.com/midwayjs/midway_v2/container
Midway 的依赖注入由两个装饰器实现,分别是:
- @provide 直译:vt. 提供。Provide 装饰器可以修饰其随后的 class,将该 class 放入 Midway 的 IoC(依赖注入)容器中。
- @Inject 直译:vt. 注入。Inject 装饰器可以修饰其随后的 class 身上的属性,在 class 被 new 出来之后会自动从 IoC 容器中取到对应要注入(Inject)的 class 并 new 好实例化赋值在当前 class 的属性上。
原本的 fileDB.ts 代码为:
import { writeFileSync, readFileSync, existsSync } from 'fs';
export let todoList = [];
export function list() {
if (existsSync('./cache')) {
const buffer = readFileSync('./cache');
todoList = JSON.parse(buffer.toString());
}
return todoList;
}
export function add(text) {
todoList.push(text);
writeFileSync('./cache', JSON.stringify(todoList));
}
export function del(text) {
const idx = todoList.findIndex((item) => item === text);
todoList.splice(idx, 1)
writeFileSync('./cache', JSON.stringify(todoList));
}
重写之后为:
import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';
export const todoList = [];
// ↓ 通过 Provide 装饰器将 TodolistService 放入 IoC 容器中
@Provide('TodolistService')
export class TodolistService {
list() {
if (existsSync('./cache')) {
const buffer = readFileSync('./cache');
this.todoList = JSON.parse(buffer.toString());
}
return this.todoList;
}
add(text) {
this.todoList.push(text);
writeFileSync('./cache', JSON.stringify(this.todoList));
}
del(text) {
const idx = this.todoList.findIndex((item) => item === text);
this.todoList.splice(idx, 1)
writeFileSync('./cache', JSON.stringify(this.todoList));
}
}
在 Controller 中通过 Inject 注入 TodolistService:
import { Controller, Get, Provide, Inject } from '@midwayjs/decorator';
import { Context } from 'egg'; // egg 中 ctx 的定义
// import * as DB from '../service/fileDB';
@Provide()
@Controller('/')
export class HomeController {
@Inject('ctx') // 将 ctx 注入到当前 controller 类中
ctx: Context;
// Inject 装饰器根据 TodolistService 这个 key 标记当前属性
// 当 HomeControoler 被实例化的时候,Midway 会从 IoC 容器中
// 找到 TodolistService 对应的 class 并 new 出来赋值给 this.db
@Inject('TodolistService')
db;
// GET /
@Get('/')
async home() {
// 告诉浏览器,当前返回的是 HTML 页面(而不是纯文本)
this.ctx.type = 'html';
const todoList = this.db.list();
return `
动态渲染
<form action="/api/todo" method="POST">
<input name="text" /> <button>确定</button>
</form>
<ul>
${todoList.map((item) => `
<li>
<form action="/api/todo/delete" method="get">
${item}
<input name="text" type="hidden" value=${item} />
<button>删除</button>
</form>
</li>
`).join('')}
</ul>`;
}
}
单例作用域
默认的未指定或者未声明的情况下,所有的 @Provide
出来的 Class 的作用域都为 请求作用域。这意味着这些 Class ,会在每一次请求第一次调用时被实例化(new),请求结束后实例销毁。
所以如果我们将 todolist 数组放在 TodolistService 的 class 上变成一个 class 成员属性的时候:
import { Provide } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';
@Provide('TodolistService')
export class TodolistService {
todoList = [];
list() {
// ...
}
add(text) {
// ...
}
del(text) {
// ...
}
}
由于 IoC 的 Class 默认会是请求作用域,也就是在使用的过程中:
// ...
@Provide()
@Controller('/api')
export class APIController {
@Inject('ctx')
ctx: Context;
// 由于默认 IoC 的 Class 默认是请求作用域
// 每次请求进入时都会 new 一个新的 TodolistService
// 也就是每次请求进来都会碰到 this.db.todolist = [] 这样重置了内部数组
@Inject('TodolistService')
db;
// POST /api/todo
@Post('/todo')
async addTodo() {
const { text } = this.ctx.request.body;
// 于是再执行这里时,就会把 [ text ] 写入到 ./cache 文件中
// 那么不论之前 this.db.todolist 里面有多少内容都会被这个覆盖
this.db.add(text);
// 跳转到直出的 HTML 页面
this.ctx.redirect('/');
return 'ok';
}
}
解决方案是使用单例作用域。在 Midway 的依赖注入体系中,有三种作用域。
- Singleton 单例,全局唯一(进程级别)
- Request 默认,请求作用域,生命周期绑定请求链路,实例在请求链路上唯一,请求结束立即销毁
- Prototype 原型作用域,每次调用都会重复创建一个新的对象
不同的作用域有不同的作用,单例 可以用来做进程级别的数据缓存,或者数据库连接等只需要执行一次的工作,同时单例由于全局唯一,只初始化一次,所以调用的时候速度比较快。而 请求作用域 则是大部分需要获取请求参数和数据的服务的选择,原型作用域 使用比较少,在一些特殊的场景下也有它独特的作用。(更多参见 Midway 文档)
修改后的代码:
import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import { writeFileSync, readFileSync, existsSync } from 'fs';
// ↓ 通过 Scope 装饰器修饰 Provide 出来的 Class
// ↓ 告诉 Midway 框架这个类主需要初始化一次,之后都使用初始化好的
@Scope(ScopeEnum.Singleton)
@Provide('TodolistService')
export class TodolistService {
private todoList = [];
list() {
if (existsSync('./cache')) {
const buffer = readFileSync('./cache');
this.todoList = JSON.parse(buffer.toString());
}
return this.todoList;
}
add(text) {
this.todoList.push(text);
writeFileSync('./cache', JSON.stringify(this.todoList));
}
del(text) {
const idx = this.todoList.findIndex((item) => item === text);
this.todoList.splice(idx, 1)
writeFileSync('./cache', JSON.stringify(this.todoList));
}
}
使用 Class 写法的优点
- 可以将关键变量私有化(private),避免用户直接操作源数据
- 规范用户通过 class 上的成员方法来增删改查数据
- 由于隐藏了实现细节,方便重写重构
视频中的代码修改:ac108cd