2.2 功能设计与页面设计

131 阅读4分钟

在开发前,先明确我们博客需要哪些功能。由于本教程面向初学者,所以只实现了博客最基本的功能,其余的功能(如用户管理、角色设置、归档、标签、分页等)读者可自行实现。

功能设计

功能及路由设计如下:

  1. 注册
    1. 注册页:GET /signup
    2. 注册(包含上传头像):POST /signup
  2. 登录
    1. 登录页:GET /signin
    2. 登录:POST /signin
  3. 登出:GET /signout
  4. 查看文章
    1. 主页:GET /posts
    2. 个人主页:GET /posts?author=xxx
    3. 查看一篇文章(包含留言):GET /posts/:postId
  5. 发表文章
    1. 发表文章页:GET /posts/create
    2. 发表文章:POST /posts
  6. 修改文章
    1. 修改文章页:GET /posts/:postId/edit
    2. 修改文章:POST /posts/:postId/edit
  7. 删除文章:GET /posts/:postId
  8. 留言
    1. 创建留言:POST /posts/:postId/comment
    2. 删除留言:GET /posts/:postId/comment/:commentId

说明:以上接口与《一起学Node.js》保持一致。

登出、删除操作其实应该用RESTful风格的DELETE操作,或者使用POST。但本书我们采用服务端渲染,不使用ajax或fetch交互,为图方便选用了GET接口。在真实生产环境应该避免这样使用,思考下为什么。

页面设计

页面效果

因为《一起学Node.js》作于2016年,当时jQuery 和Semantic-UI还有不少拥趸,所以使用它们来实现前端页面的设计。最终效果图如下:

注册页

4.5.1.png

登录页

4.5.2.png

未登录时的主页(或用户页)

4.5.3.png

登录后的主页(或用户页)

4.5.4.png

发表文章页

4.5.5.png

编辑文章页

4.5.6.png

未登录时的文章页

4.5.7.png

登录后的文章页

4.5.8.png

通知

4.5.9.png 4.5.10.png 4.5.11.png

组件

前面提到过,我们可以将模板拆分成一些组件,然后使用 ejs 的 include 方法将组件组合起来进行渲染。我们将页面切分成以下组件:

4.5.12.png

文章页:

4.5.13.png

根据上面的组件切分图,我们创建以下样式及模板文件:

public/css/style.css

/* ---------- 全局样式 ---------- */

body {
  width: 1100px;
  height: 100%;
  margin: 0 auto;
  padding-top: 40px;
}

a:hover {
  border-bottom: 3px solid #4fc08d;
}

.button {
  background-color: #4fc08d !important;
  color: #fff !important;
}

.avatar {
  border-radius: 3px;
  width: 48px;
  height: 48px;
  float: right;
}

/* ---------- nav ---------- */

.nav {
  margin-bottom: 20px;
  color: #999;
  text-align: center;
}

.nav h1 {
  color: #4fc08d;
  display: inline-block;
  margin: 10px 0;
}

/* ---------- nav-setting ---------- */

.nav-setting {
  position: fixed;
  right: 30px;
  top: 35px;
  z-index: 999;
}

.nav-setting .ui.dropdown.button {
  padding: 10px 10px 0 10px;
  background-color: #fff !important;
}

.nav-setting .icon.bars {
  color: #000;
  font-size: 18px;
}

/* ---------- post-content ---------- */

.post-content h3 a {
  color: #4fc08d !important;
}

.post-content .tag {
  font-size: 13px;
  margin-right: 5px;
  color: #999;
}

.post-content .tag.right {
  float: right;
  margin-right: 0;
}

.post-content .tag.right a {
  color: #999;
}

public/js/footer.js

$(function () {
  // 点击按钮弹出下拉框
  $('.ui.dropdown').dropdown();
  // 鼠标悬浮在头像上,弹出气泡提示框
  $('.post-content .avatar').popup({
    inline: true,
    position: 'bottom right',
    lastResort: 'bottom right'
  });
});

views/header.ejs

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>
    <%= title %>
  </title>
  <link rel="stylesheet" href="//cdn.uino.cn/deno/semantic.css">
  <link rel="stylesheet" href="/static/css/style.css">
  <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js" defer></script>
  <script src="//cdn.bootcss.com/semantic-ui/2.1.8/semantic.min.js" defer></script>
  <script src="/static/js/footer.js" defer></script>
</head>

<body>
  <%- include('components/nav') %>
  <%- include('components/nav-setting') %>
  <%- include('components/notification') %>

views/footer.ejs

</body>
</html>

views/components

在 views 目录下新建 components 目录用来存放组件,在该目录下创建以下文件views/components/nav.ejs

<div class="nav">
  <div class="ui grid">
    <div class="four wide column"></div>

    <div class="eight wide column">
      <a href="/posts">
        <h1>
          <%= title %>
        </h1>
      </a>
      <p>
        <%= description %>
      </p>
    </div>
  </div>
</div>

views/components/nav-setting.ejs

<div class="nav-setting">
  <div class="ui buttons">
    <div class="ui floating dropdown button">
      <i class="icon bars"></i>
      <div class="menu">
        <% if (user) { %>
        <a class="item" href="/posts?userId=<%= user.id %>">个人主页</a>
        <div class="divider"></div>
        <a class="item" href="/posts/create">发表文章</a>
        <a class="item" href="/signout">退出登陆</a>
        <% } else { %>
        <a class="item" href="/signin">登陆</a>
        <a class="item" href="/signup">注册</a>
        <% } %>
      </div>
    </div>
  </div>
</div>

views/components/notification.ejs

<div class="ui grid">
  <div class="four wide column"></div>
  <div class="eight wide column">

    <% if (success) { %>
    <div class="ui success message">
      <p><%= success %></p>
    </div>
    <% } %>

    <% if (error) { %>
    <div class="ui error message">
      <p><%= error %></p>
    </div>
    <% } %>

  </div>
</div>

views/posts.ejs

为了方便观察效果,我们先创建主页的模板:

<%- include('header') %>
<%- include('footer') %>

ejs.ts

修改src/tools/ejs.ts,修改render函数,添加第三个参数locals:

export function render(
  path: string,
  data: Record<string, any> = {},
  locals: Record<string, any> = {},
): Promise<string> {
  return renderFile("views/" + path + ".ejs", {
    user: null,
    success: null,
    error: null,
    ...globals.meta,
    ...locals,
    ...data,
  }, {
    cache: true,
    filename: path,
  });
}

为什么要这么做呢?

因为我们组装ejs模板的数据应该有3个来源:全局配置、中间件、参数。这三者优先级不一样。

为了简化代码,最好我们在中间件操作context上下文,这样就省得每处调用时自己组装这3种数据了。

于是我们再添加一个装饰器Render:

// deno-lint-ignore-file no-explicit-any
import { Context, createParamDecorator } from "oak_nest";

export type Render = (
  path: string,
  data: Record<string, any>,
) => Promise<string>;

export const Render = createParamDecorator(
  (context: Context): Render => {
    return (path: string, data: Record<string, any>) => {
      return render(path, data, context.state.locals);
    };
  },
);

posts

新建src/posts目录,新建posts.controller.ts:

import { Controller, Get } from "oak_nest";
import { Render } from "../tools/ejs.ts";

@Controller("/posts")
export class PostsController {
  @Get("/")
  async getAll(@Render() render: Render) {
    return await render("posts", {});
  }
}

新建posts.module.ts:

import { Module } from "oak_nest";
import { PostsController } from "./posts.controller.ts";

@Module({
  controllers: [
    PostsController,
  ],
})
export class PostsModule {
}

app

修改src/app.module.ts,把PostsModule引入:

image.png

修改src/app.controller.ts,把刚才的Render装饰器用上。

async version(@Render() render: Render) {}

main.ts

修改src/main.ts,创建app之后:

app.useStaticAssets("./public", {
  prefix: "static",
});

意思是将根目录下public文件夹代理为http://localhost:8000/static/

比如我们刚才创建的public/css/style.css,就用http://localhost:8000/static/css/style.css就能访问到。

验证

访问http://localhost:8000/posts,看到页面如下:

image.png

以及

image.png

作业

下一步,我们就要实现注册、登陆接口了。你可以尝试做下。