2.9 创建博客

107 阅读2分钟

创建页面

新建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,能看到创建页面如下:

image.png

创建接口

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插件,这个还是比较成熟的,读者可以自行尝试。

下一步,我们将开发文章详情页。