页面设计

我们使用 jQuery + Semantic-UI 实现前端页面的设计,最终效果图如下:

注册页

登录页

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

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

发表文章页

编辑文章页

未登录时的文章页

登录后的文章页

通知

4.5.1 组件

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

主页

文章页

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

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;
}

views/header.ejs

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title><%= blog.title %></title>
    <link rel="stylesheet" href="//cdn.bootcss.com/semantic-ui/2.1.8/semantic.min.css">
    <link rel="stylesheet" href="/css/style.css">
    <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
    <script src="//cdn.bootcss.com/semantic-ui/2.1.8/semantic.min.js"></script>
  </head>
  <body>
  <%- include('components/nav') %>
  <%- include('components/nav-setting') %>
  <%- include('components/notification') %>

views/footer.ejs

  <script type="text/javascript">
   $(document).ready(function () {
      // 点击按钮弹出下拉框
      $('.ui.dropdown').dropdown();

      // 鼠标悬浮在头像上,弹出气泡提示框
      $('.post-content .avatar-link').popup({
        inline: true,
        position: 'bottom right',
        lastResort: 'bottom right'
      });
    })
  </script>
  </body>
</html>

注意:上面 <script></script> 是 semantic-ui 操控页面控件的代码,一定要放到 footer.ejs 的 </body> 的前面,因为只有页面加载完后才能通过 JQuery 获取 DOM 元素。

在 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><%= blog.title %></h1></a>
      <p><%= blog.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?author=<%= 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>

4.5.2 app.locals 和 res.locals

上面的 ejs 模板中我们用到了 blog、user、success、error 变量,我们将 blog 变量挂载到 app.locals 下,将 user、success、error 挂载到 res.locals 下。为什么要这么做呢?app.localsres.locals 是什么?它们有什么区别?

express 中有两个对象可用于模板的渲染:app.localsres.locals。我们从 express 源码一探究竟:

express/lib/application.js

app.render = function render(name, options, callback) {
  ...
  var opts = options;
  var renderOptions = {};
  ...
  // merge app.locals
  merge(renderOptions, this.locals);

  // merge options._locals
  if (opts._locals) {
    merge(renderOptions, opts._locals);
  }

  // merge options
  merge(renderOptions, opts);
  ...
  tryRender(view, renderOptions, done);
};

express/lib/response.js

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var opts = options || {};
  ...
  // merge res.locals
  opts._locals = self.locals;
  ...
  // render
  app.render(view, opts, done);
};

可以看出:在调用 res.render 的时候,express 合并(merge)了 3 处的结果后传入要渲染的模板,优先级:res.render 传入的对象> res.locals 对象 > app.locals 对象,所以 app.localsres.locals 几乎没有区别,都用来渲染模板,使用上的区别在于:app.locals 上通常挂载常量信息(如博客名、描述、作者这种不会变的信息),res.locals 上通常挂载变量信息,即每次请求可能的值都不一样(如请求者信息,res.locals.user = req.session.user)。

修改 index.js,在 routes(app) 上一行添加如下代码:

// 设置模板全局常量
app.locals.blog = {
  title: pkg.name,
  description: pkg.description
}

// 添加模板必需的三个变量
app.use(function (req, res, next) {
  res.locals.user = req.session.user
  res.locals.success = req.flash('success').toString()
  res.locals.error = req.flash('error').toString()
  next()
})

这样在调用 res.render 的时候就不用传入这四个变量了,express 为我们自动 merge 并传入了模板,所以我们可以在模板中直接使用这四个变量。