创建页面
新建views/posts/create.ejs,内容如下:
<%- include('../header') %>
<div class="ui grid">
<div class="four wide column">
<a class="avatar" href="/posts?userId=<%= user.id %>" data-title="<%= user.name %> | <%= ({m: '男', f: '女', x: '保密'})[user.gender] %>" data-content="<%= user.bio %>">
<img class="avatar" src="/static/img/<%= user.avatar %>">
</a>
</div>
<div class="eight wide column">
<form class="ui form segment" method="post" action="/posts">
<div class="field required">
<label>标题</label>
<input type="text" name="title" required maxlength="100">
</div>
<div class="field required">
<label>内容</label>
<textarea name="content" rows="15" maxlength="1000" required></textarea>
</div>
<input type="submit" class="ui button" value="发布">
</form>
</div>
</div>
<%- include('../footer') %>
修改src/posts/posts.controller.ts,添加一个create的GET页面接口。
import { Controller, Get, UseGuards } from "oak_nest";
import { SSOGuard } from "../guards/sso.guard.ts";
import { Render } from "../tools/ejs.ts";
@Controller("/posts")
export class PostsController {
@Get("/create")
@UseGuards(SSOGuard)
createPage(@Render() render: Render) {
return render("posts/create", {});
}
}
在浏览器先登陆,再访问http://localhost:8000/posts/create,能看到创建页面如下:
创建接口
posts.schema.ts
新建src/posts/posts.schema.ts
import { Prop } from "../schema.ts";
export class Post {
@Prop({
required: true,
})
userId: string;
@Prop({
required: true,
})
title: string;
@Prop({
required: true,
})
content: string;
@Prop({
required: true,
})
pv: number;
@Prop()
createTime: Date;
@Prop()
updateTime: Date;
createdAt?: string;
contentHtml?: string;
}
这里的pv指的是浏览量。而最后两个字段createdAt和contentHtml并不会存储,而是给页面渲染时要使用。这些额外的字段放Schema中其实不是很合适,你可以考虑换个地方,在service里使用时引用。
posts.dto.ts
创建src/posts/posts.dto.ts,限定下标题、内容必须传递,字数长度也与ejs中保持一致。
import { IsOptional, IsString, MaxLength } from "deno_class_validator";
export class CreatePostDto {
userId: string;
@IsString()
@MaxLength(100)
title: string;
@IsString()
@MaxLength(1000)
content: string;
}
posts.service.ts
src/posts/posts.service.ts
import { Injectable } from "oak_nest";
import { InjectModel, Model } from "../model.ts";
import { CreatePostDto } from "./posts.dto.ts";
import { Post } from "./posts.schema.ts";
@Injectable()
export class PostsService {
constructor(@InjectModel(Post) private readonly model: Model<Post>) {}
save(params: CreatePostDto): Promise<string> {
const now = new Date();
return this.model.insertOne({
...params,
pv: 0,
createTime: now,
updateTime: now,
});
}
}
user.schema.ts
修改src/user/user.schema.ts,新增一个带id的类型UserInfo:
import type { ModelWithId } from "../model.ts";
export type UserInfo = ModelWithId<User>;
session.decorator.ts
修改src/session/session.decorator.ts,新增一个用户信息的装饰器:
export const UserParam = createParamDecorator(
(context: Context) => {
return context.state.session?.user;
},
);
posts.controller.ts
src/posts/posts.controller.ts,添加创建的POST接口:
import {
Controller,
Form,
Get,
Post,
Res,
Response,
UseGuards,
} from "oak_nest";
import { Flash, UserParam } from "../session/session.decorator.ts";
import type { UserInfo } from "../user/user.schema.ts";
import { CreatePostDto } from "./posts.dto.ts";
import { PostsService } from "./posts.service.ts";
@Controller("/posts")
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@UseGuards(SSOGuard)
@Post("/")
async createBlog(
@Form() params: CreatePostDto,
@Res() res: Response,
@UserParam() userInfo: UserInfo,
@Flash() flash: Flash,
) {
params.userId = userInfo.id;
const id = await this.postsService.save(params);
flash("success", "发表成功");
// 发表成功后跳转到该文章页
res.redirect(`/posts/${id}`);
}
}
注意这里UserInfo的import type的用法,Deno推荐引用typescript的类型type或interface时使用type标识,在上层文件二次导出时也是一样的。否则在低版本可能正常运行的代码,在高版本可能会失败,最好遵循规范。
目前Deno的最新版本v1.23.1主要限定使用了装饰器的参数类型的引用必须标注。
验证
重新访问http://localhost:8000/posts/create,点击发布,成功后就会跳转到文章页,不过目前这个页面我们还没有实现。
作业
我们的博客内容支持Markdown语法。其实要做的更好些,可以把上面的文本框替换成一个开源的Markdown插件,这个还是比较成熟的,读者可以自行尝试。
下一步,我们将开发文章详情页。