2.10 博客详情页

82 阅读2分钟

博客详情页面

post-content.ejs

新建views/components/post-content.ejs,这是博客内容页,如果当前用户正是博客作者,则能看到编辑和删除两个按钮。

<div class="post-content">
  <div class="ui grid">
    <div class="four wide column">
      <a class="avatar" href="/posts?userId=<%= post.author.id %>" data-title="<%= post.author.name %> | <%= ({m: '男', f: '女', x: '保密'})[post.author.gender] %>" data-content="<%= post.author.bio %>">
        <img class="avatar" src="/static/img/<%= post.author.avatar %>">
      </a>
    </div>

    <div class="eight wide column">
      <div class="ui segment">
        <h3><a href="/posts/<%= post.id %>">
            <%= post.title %>
          </a></h3>
        <pre><%- post.contentHtml %></pre>
        <div>
          <span class="tag">
            <%= post.createdAt %>
          </span>
          <span class="tag right">
            <span>浏览(<%= post.pv %>)</span>
          </span>
        </div>
      </div>
    </div>
  </div>
</div>

post.ejs

新建views/post.ejs:

<%- include('header') %>
<%- include('components/post-content') %>
<%- include('footer') %>

posts.schema.ts

修改src/posts/posts.schema.ts,新增一个属性author,但不需要加Prop:

import type { UserInfo } from "../user/user.schema.ts";

export class Post {
  ... 其它字段

  author?: UserInfo | null;
}

import_map.json

修改import_map.json,新增两个包:

"timeago": "<https://esm.sh/v87/timeago.js@4.0.2/es2022/timeago.js>",
"markdown": "<https://deno.land/x/markdown@v2.0.0/mod.ts>"

下面我们将用它们来转换创建时间和markdown为html。

在2.1中提过,但这里再啰嗦一下npm包的利用。

timeago.js(注意带.js)是npm上一个比较有名的时间库,像这种工具库,因为没有对Node.js的强依赖,通常使用CDN就很容易转换成ESM模块(早期的npm上的包资源基本上都是CommonJS语法)。

推荐使用esm.sh来转换,在浏览器输入esm.sh/timeago.js,会重定向到最近的包:> image.png

下面的地址是个固定地址,不会随着esm.sh服务器的升级而重建,也就是不会引发lock.json中的hash变化,所以最好使用这个地址。

Deno v1.26已经支持npm的方式直接导入,比如npm:timeago.js@4.0.2,有兴趣的同学也可以选择试用。

posts.service.ts

src/posts/posts.service.ts增加findById方法,根据配置参数查找相应的用户信息,或者将浏览量增加1。

import { Injectable } from "oak_nest";
import { InjectModel, Model } from "../model.ts";
import { Logger } from "../tools/log.ts";
import { UserService } from "../user/user.service.ts";
import { CreatePostDto } from "./posts.dto.ts";
import { Post } from "./posts.schema.ts";
import { format } from "timeago";
import { Marked } from "markdown";

interface PopulateOptions {
  isWithUserInfo?: boolean;
  isIncrementPv?: boolean;
}

@Injectable()
export class PostsService {
  constructor(
    @InjectModel(Post) private readonly model: Model<Post>,
    private readonly userService: UserService,
    private readonly logger: Logger,
  ) {}

   async findById(id: string, options: PopulateOptions = {}) {
    const post = await this.model.findById(id);
    if (!post) {
      return;
    }
    if (options.isWithUserInfo) {
      post.author = await this.userService.getUserById(post.userId);
    }
    // 增加浏览次数
    if (options.isIncrementPv) {
      this.model.findByIdAndUpdate(id, {
        pv: post.pv + 1,
      }).catch(this.logger.error);
    }
    this.format(post);
    return post;
  }

  private format(post: Post) {
    post.createdAt = format(post.createTime, "zh_CN");
    const html = Marked.parse(post.content).content;
    post.contentHtml = html;
  }
}

posts.controller.ts

src/posts/posts.controller.ts增加findPostById方法:


@Controller("/posts")
export class PostsController {
 
  @Get("/:id")
  async findPostById(@Params("id") id: string, @Render() render: Render) {
    const post = await this.postsService.findById(id, {
      isWithUserInfo: true,
      isIncrementPv: true,
    });
    if (!post) {
      throw new NotFoundException(`未找到id为${id}的文章`);
    }
    return render("post", {
      post,
    });
  }
}

验证

重新在页面上创建博客,成功后会跳转到http://localhost:8000/posts/:id,效果如下:

image.png

每刷新一次,浏览量会加1,创建时间也会相应变化。

image.png

作业

如果我们的博客里写了代码片段,你会发现博客并不能语法高亮。也就是不会像这样带颜色:

image.png

你可以在ejs文件中引入highlight.js来实现此功能。

下一步,我们需要把所有博客都呈现出来,现在首页http://localhost:8000/posts还是个空页面。