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

注册页

页面设计 - 图1

登录页

页面设计 - 图2

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

页面设计 - 图3

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

页面设计 - 图4

发表文章页

页面设计 - 图5

编辑文章页

页面设计 - 图6

未登录时的文章页

页面设计 - 图7

登录后的文章页

页面设计 - 图8

通知

页面设计 - 图9
页面设计 - 图10
页面设计 - 图11

4.5.1 组件

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

主页

页面设计 - 图12

文章页

页面设计 - 图13

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

public/css/style.css

  1. /* ---------- 全局样式 ---------- */
  2. body {
  3. width: 1100px;
  4. height: 100%;
  5. margin: 0 auto;
  6. padding-top: 40px;
  7. }
  8. a:hover {
  9. border-bottom: 3px solid #4fc08d;
  10. }
  11. .button {
  12. background-color: #4fc08d !important;
  13. color: #fff !important;
  14. }
  15. .avatar {
  16. border-radius: 3px;
  17. width: 48px;
  18. height: 48px;
  19. float: right;
  20. }
  21. /* ---------- nav ---------- */
  22. .nav {
  23. margin-bottom: 20px;
  24. color: #999;
  25. text-align: center;
  26. }
  27. .nav h1 {
  28. color: #4fc08d;
  29. display: inline-block;
  30. margin: 10px 0;
  31. }
  32. /* ---------- nav-setting ---------- */
  33. .nav-setting {
  34. position: fixed;
  35. right: 30px;
  36. top: 35px;
  37. z-index: 999;
  38. }
  39. .nav-setting .ui.dropdown.button {
  40. padding: 10px 10px 0 10px;
  41. background-color: #fff !important;
  42. }
  43. .nav-setting .icon.bars {
  44. color: #000;
  45. font-size: 18px;
  46. }
  47. /* ---------- post-content ---------- */
  48. .post-content h3 a {
  49. color: #4fc08d !important;
  50. }
  51. .post-content .tag {
  52. font-size: 13px;
  53. margin-right: 5px;
  54. color: #999;
  55. }
  56. .post-content .tag.right {
  57. float: right;
  58. margin-right: 0;
  59. }
  60. .post-content .tag.right a {
  61. color: #999;
  62. }

views/header.ejs

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title><%= blog.title %></title>
  6. <link rel="stylesheet" href="//cdn.bootcss.com/semantic-ui/2.1.8/semantic.min.css">
  7. <link rel="stylesheet" href="/css/style.css">
  8. <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
  9. <script src="//cdn.bootcss.com/semantic-ui/2.1.8/semantic.min.js"></script>
  10. </head>
  11. <body>
  12. <%- include('components/nav') %>
  13. <%- include('components/nav-setting') %>
  14. <%- include('components/notification') %>

views/footer.ejs

  1. <script type="text/javascript">
  2. $(document).ready(function () {
  3. // 点击按钮弹出下拉框
  4. $('.ui.dropdown').dropdown();
  5. // 鼠标悬浮在头像上,弹出气泡提示框
  6. $('.post-content .avatar-link').popup({
  7. inline: true,
  8. position: 'bottom right',
  9. lastResort: 'bottom right'
  10. });
  11. })
  12. </script>
  13. </body>
  14. </html>

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

在 views 目录下新建 components 目录用来存放组件(即可以复用的模板片段),在该目录下创建以下文件:

views/components/nav.ejs

  1. <div class="nav">
  2. <div class="ui grid">
  3. <div class="four wide column"></div>
  4. <div class="eight wide column">
  5. <a href="/posts"><h1><%= blog.title %></h1></a>
  6. <p><%= blog.description %></p>
  7. </div>
  8. </div>
  9. </div>

views/components/nav-setting.ejs

  1. <div class="nav-setting">
  2. <div class="ui buttons">
  3. <div class="ui floating dropdown button">
  4. <i class="icon bars"></i>
  5. <div class="menu">
  6. <% if (user) { %>
  7. <a class="item" href="/posts?author=<%= user._id %>">个人主页</a>
  8. <div class="divider"></div>
  9. <a class="item" href="/posts/create">发表文章</a>
  10. <a class="item" href="/signout">登出</a>
  11. <% } else { %>
  12. <a class="item" href="/signin">登录</a>
  13. <a class="item" href="/signup">注册</a>
  14. <% } %>
  15. </div>
  16. </div>
  17. </div>
  18. </div>

views/components/notification.ejs

  1. <div class="ui grid">
  2. <div class="four wide column"></div>
  3. <div class="eight wide column">
  4. <% if (success) { %>
  5. <div class="ui success message">
  6. <p><%= success %></p>
  7. </div>
  8. <% } %>
  9. <% if (error) { %>
  10. <div class="ui error message">
  11. <p><%= error %></p>
  12. </div>
  13. <% } %>
  14. </div>
  15. </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

  1. app.render = function render(name, options, callback) {
  2. ...
  3. var opts = options;
  4. var renderOptions = {};
  5. ...
  6. // merge app.locals
  7. merge(renderOptions, this.locals);
  8. // merge options._locals
  9. if (opts._locals) {
  10. merge(renderOptions, opts._locals);
  11. }
  12. // merge options
  13. merge(renderOptions, opts);
  14. ...
  15. tryRender(view, renderOptions, done);
  16. };

express/lib/response.js

  1. res.render = function render(view, options, callback) {
  2. var app = this.req.app;
  3. var opts = options || {};
  4. ...
  5. // merge res.locals
  6. opts._locals = self.locals;
  7. ...
  8. // render
  9. app.render(view, opts, done);
  10. };

可以看出:在调用 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) 上一行添加如下代码:

  1. // 设置模板全局常量
  2. app.locals.blog = {
  3. title: pkg.name,
  4. description: pkg.description
  5. }
  6. // 添加模板必需的三个变量
  7. app.use(function (req, res, next) {
  8. res.locals.user = req.session.user
  9. res.locals.success = req.flash('success').toString()
  10. res.locals.error = req.flash('error').toString()
  11. next()
  12. })

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