Blogging cùng Keystone.js (2) - Development

Bạc xỉu

Sau phần 1 giới thiệu và cài đặt, phần 2 sẽ giải thích sơ qua về kiến trúc và data flow của Keystone.js. source code ăn liền (deployable) cho một blog hoàn chỉnh xây dựng bằng keystone ở cuối bài viết.

Keystone.js design pattern

Keystone.js được thiết kế dựa trên mô hình MVT (Model-View-Template), dưới đây sẽ là cách định nghĩa chính xác của từng phần M,V,T trong Keystone.js:

  • M - Data access layer: Có nhiệm vụ định nghĩa dữ liệu, quan hệ giữa các bảng, cách tương tác và validate chúng.
  • V - Business logic layer: Xử lý việc truy cập dữ liệu từ model, thực hiện các tính toán và đưa kết quả đến template cần thiết. Là thành phần kết nối giữa M và T, có thể nói gần giống với Controller trong mô hình MVC.
  • T - Presentation layer: Chịu trách nhiệm thể hiện các thành phần nhận được từ View, về cơ bản thì giống với View trong mô hình MVC.
Mô hình MVT

Keystone generator và source code demo đều chưa có trường tag dành cho bài viết nên trong bài này mình sẽ sử dụng luôn việc tạo trường này để có thể trình bày được data flow trong keystone một cách rõ ràng hơn.

Cấu trúc của một project cho dễ tưởng tượng =)

|--lib
|  Custom libraries and other code
|--models
|  Your application's database models
|--public
|  Static files (css, js, images, etc.) that are publicly available
|--routes
|  |--api
|  |  Your application's api controllers
|  |--views
|  |  Your application's view controllers
|  |--index.js
|  |  Initialises your application's routes and views
|  |--middleware.js
|  |  Custom middleware for your routes
|--templates
|  |--includes
|  |  Common .jade includes go in here
|  |--layouts
|  |  Base .jade layouts go in here
|  |--mixins
|  |  Common .jade mixins go in here
|  |--views
|  |  Your application's view templates
|--updates
|  Data population and migration scripts
|--package.json
|  Project configuration for npm
|--web.js
|  Main script that starts your application

Theo đúng thứ tự của mô hình, đầu tiên chúng ta cần tạo model PostTag tại folder models. Keystone sử dụng package Mongoose để kết nối tới mongodb. Document về các data types

models/PostTag.js

var keystone = require('keystone');

// Tạo key
var PostTag = new keystone.List('PostTag', {
autokey: { from: 'name', path: 'key', unique: true },
});

// Định nghĩa các trường của model
PostTag.add({
name: { type: String, required: true },
});

// Định nghĩa quan hệ với các model khác
PostTag.relationship({ ref: 'Post', path: 'posts', refPath: 'tags' });

PostTag.register();

models/Post.js

...
tags: { type: Types.Relationship, ref: 'PostTag', many: true },
...

Tiếp theo sẽ đến phần View, thực hiện việc lấy giữ liệu trả về cho Template tương ứng với mỗi route mà người dùng request tới. Keystone.js sử dụng express.js để thực hiện các tác vụ liên quan đến route và middleware, chúng ta cần khai báo route và middleware cho việc route binding của tag.

routes/index.js

...
app.get('/', routes.views.index);
app.get('/tag/:tag?', routes.views.tag);
...

views/tag.js

var keystone = require('keystone');
var async = require('async');

exports = module.exports = function (req, res) {

var view = new keystone.View(req, res);
var locals = res.locals;

// Init locals
locals.section = 'tag';
locals.filters = {
  tag: req.params.tag,
};
locals.data = {
  tags: [],
  posts: [],
};

// Load all tags
view.on('init', function (next) {

  keystone.list('PostTag').model.find().sort('name').exec(function (err, results) {

      if (err || !results.length) {
          return next(err);
      }

      locals.data.tags = results;

      // Load the counts for each category
      async.each(locals.data.categories, function (tag, next) {

          keystone.list('Post').model.count().where('tags').in([tag.id]).exec(function (err, count) {
              tag.postCount = count;
              next(err);
          });

      }, function (err) {
          next(err);
      });
  });
});

// Load the current tag filter
view.on('init', function (next) {

  if (req.params.tag) {
      keystone.list('PostTag').model.findOne({ key: locals.filters.tag }).exec(function (err, result) {
          locals.data.tag = result;
          next(err);
      });
  } else {
      next();
  }
});

// Load the posts
view.on('init', function (next) {

  var q = keystone.list('Post').paginate({
      page: req.query.page || 1,
      perPage: 10,
      maxPages: 10,
      filters: {
          state: 'published',
      },
  })
      .sort('-publishedDate')
      .populate('author categories tags');

  if (locals.data.tag) {
      q.where('tags').in([locals.data.tag]);
  }

  q.exec(function (err, results) {
      locals.data.posts = results;
      next(err);
  });
});

// Render the view
view.render('tag');
};

Sau khi lấy các post có trường tag theo param, dữ liệu sẽ được render ra tại template tag.

Keystone.js sử dụng pug thay cho html để viết template, về cơ bản thì việc này sẽ giúp code ngắn và dễ đọc hơn, tuy nhiên với các anh em chưa quen thì có thể sử dụng tool html2pug để có thể viết code dễ hơn.

template/views/tag.pug

extends ../layouts/default
include ../mixins/post.pug

block intro
.container
  h1= data.category ? data.category.name : 'Blog'

block content
.container: .row
  .col-sm-8.col-md-9
      if filters.category && !data.category
          h3.text-muted Invalid Category.
      else
          if data.posts.results.length
              if data.posts.totalPages > 1
                  h4.text-weight-normal Showing
                      strong #{data.posts.first}
                      |  to
                      strong #{data.posts.last}
                      |  of
                      strong #{data.posts.total}
                      |  posts.
              else
                  h4.text-weight-normal Showing #{utils.plural(data.posts.results.length, '* post')}.
              .blog
                  each post in data.posts.results
                      +post(post)

              if data.posts.totalPages > 1
                  ul.pagination
                      if data.posts.previous
                          li: a(href='?page=' + data.posts.previous): span.glyphicon.glyphicon-chevron-left
                      else
                          li.disabled: a(href='?page=' + 1): span.glyphicon.glyphicon-chevron-left
                      each p, i in data.posts.pages
                          li(class=data.posts.currentPage == p ? 'active' : null)
                              a(href='?page=' + (p == '...' ? (i ? data.posts.totalPages : 1) : p))= p
                      if data.posts.next
                          li: a(href='?page=' + data.posts.next): span.glyphicon.glyphicon-chevron-right
                      else
                          li.disabled: a(href='?page=' + data.posts.totalPages): span.entypo.glyphicon.glyphicon-chevron-right
          else
              if data.category
                  h3.text-muted There are no posts in the category #{data.category.name}.
              else
                  h3.text-muted There are no posts yet.

  if data.tags.length
      .col-sm-4.col-md-3
          h2 Tags
          .list-group(style='margin-top: 70px;')
              a(href='/tag', class=!data.tag ? 'active' : false).list-group-item All Tags
              each tag in data.tags
                  a(href='/tag/' + tag.key, class=data.tag && data.tag.id == tag.id ? 'active' : false).list-group-item= tag.name

Thay đổi giao diện admin trong keystone.js tại root folder

...
posts: ['posts', 'post-categories', 'post-tags'],
...

Như vậy là chúng ta đã thực hiện toàn bộ từ việc tạo model, lấy bản ghi từ database và truyền ra view. Mô hình của Keystone.js khiến mọi thứ trở nên đơn giản và code cũng rất trong sáng, dễ đọc =)

Node.js debug

Nếu chưa có kinh nghiệm lập trình bằng javascript (như mình) khi gặp những chức năng yêu cầu xử lý phức tạp hơn thì việc có thể debug code chắc chắn sẽ giúp ích rất nhiều. Với keystone nói riêng và các ứng dụng node.js nói chung thì có 2 cách debug đơn giản nhất:

  • Webstorm IDE (được dùng thử 1 tháng) bạn nào có mail sinh viên có thể đăng ký gói 1 năm để sử dụng, không cần terminal, không cần browser chúng ta vẫn có thể debug hoặc chạy code bình thường. Hướng dẫn cụ thể của Jetbrain
  • Chrome Devtools, bạn có thể sử dụng bất cứ text editor nào và chạy ứng dụng node bằng lệnh
node --inspect keystone.js

Hướng dẫn cụ thể tại đây

Source code của một project hoàn chỉnh sau cả 2 phần có tại đây

Các bạn hoàn toàn có thể clone về và sỡ hữu blog chính chủ sau khi cài đặt môi trường (MIT licence =)))

Tại bloging với keystone.js phần 3 và là phần cuối cùng mình sẽ nói về việc deploy Keystone blog lên server.

Hôm nay nhìn mấy cậu U23 đá dưới trời tuyết mà thích quá =)) Lâu lắm rồi mới lại thấy nhiệt đến như vậy, thức đêm cố viết xong post này cho đỡ hổ then với quốc gia, dân tộc vậy =)) Dù đắng nhưng vẫn thấy tự hào kinh khủng...

Tags: #blog #blogwithkeystones #javascript #nodejs #mongodb