博客详情页面
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,会重定向到最近的包:>
下面的地址是个固定地址,不会随着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,效果如下:
每刷新一次,浏览量会加1,创建时间也会相应变化。
作业
如果我们的博客里写了代码片段,你会发现博客并不能语法高亮。也就是不会像这样带颜色:
你可以在ejs文件中引入highlight.js来实现此功能。
下一步,我们需要把所有博客都呈现出来,现在首页http://localhost:8000/posts还是个空页面。