Author Archives: Phở Code

Rails – Kiểm tra dữ liệu gửi lên form

Trong bài này chúng ta sẽ tìm hiểu cách kiểm tra sự đúng đắn của dữ liệu.

Kiểm tra dữ liệu ở đây là kiểm tra xem dữ liệu được gửi lên có đúng với yêu cầu hay không, cũng có thể xem đây là câu lệnh catch trong quá trình kiểm tra lỗi exception vậy, chẳng hạn như chúng ta yêu cầu người dùng nhập vào email nhưng người dùng nhập sai cấu trúc, hay upload ảnh avatar mà lại gửi lên một file PDF, nguy hiểm hơn nữa là up file ảnh giả – tức file có đuôi .jpg, .png… nhưng lại không chứa dữ liệu ảnh mà chứa mã độc… vì thế chúng ta nên kiểm tra dữ liệu được gửi lên trước khi lưu chúng vào CSDL.

Validator

Trong ứng dụng depot mà chúng ta vừa làm trong các bài trước, chúng ta có tạo một model có tên Product, khi tạo model thì Rails sẽ tạo một lớp cũng có tên Product trong một file là product.rb, và đặt file này trong thư mục app/models, mặc định thì Rails không định nghĩa gì trong lớp này cả:

class Product < ActiveRecord::Base
end

Lớp Product được kế thừa từ lớp ActiveRecord::Base của Rails, trong lớp ActiveRecord::Base có một phương thức tên là validates dùng để kiểm tra dữ liệu. Ví dụ chúng ta sửa lại đoạn code trên như sau:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
end

Phương thức validates nhận vào danh sách các thuộc tính do chúng ta định nghĩa và các phương thức kiểm tra dữ liệu, ở đây title, descriptionimage_url là các thuộc tính do chúng ta định nghĩa, presence là một phương thức kiểm tra dữ liệu, ý nghĩa của phương thức validates này là kiểm tra xem các thuộc tính có khớp với điều kiện trong phương thức kiểm tra hay không. Ở đây presence nhận vào tham số true tức là bắt buộc các thuộc tính kia không được để trống.

Bạn có thể thử chạy server, tạo một product mới và không điền gì vào 3 trường title, description, image_url thì server sẽ thông báo lỗi:

capture

Chúng ta thử kiểm tra một điều kiện khác như sau:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
end

Ở đây phương thức numericality nhận vào giá trị là một đối tượng khác, đối tượng này là greater_than_or_equal_to có giá trị 1.0, ý nghĩa là giá trị phải là số và lớn hơn 1.0.

capture

Một phương thức kiểm tra khác là uniqueness, ví dụ:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
    validates :title, :uniqueness => true
end

Ý nghĩa của phương thức này là thuộc tính phải là duy nhất, tức là không có 2 đối tượng/bản ghi nào có thuộc tính này giống nhau.

capture

Ví dụ tiếp theo là phương thức format:

class Product < ActiveRecord::Base
    validates :title, :description, :image_url, :presence => true
    validates :price, :numericality => {:greater_than_or_equal_to => 1.0}
    validates :title, :uniqueness => true
    validates :image_url, :format => {
        :with => %r{\.(gif|jpg|png)\Z}i,
        :message => 'Chi nhan file GIF, JPG, PNG'
    }
end

Phương thức format này nhận vào tham số with có giá trị là một chuỗi Regex (Regular Expression – Biểu thức chính quy), chuỗi này sẽ so sánh chuỗi giá trị với một mẫu nào đó, ở đây mẫu Regex của chúng ta có nghĩa là đường dẫn file ảnh phải kết thúc bằng .jpg, .png hoặc .gif. Ngoài ra chúng ta có thể sửa lại thuộc tính message, thuộc tính này sẽ in thông báo lỗi ra theo ý chúng ta muốn, thuộc tính này không bắt buộc phải có.

capture

Ngoài ra còn có rất nhiều phương thức kiểm tra khác mà mình không đề cập ở đây, bạn có thể tham khảo tại:

http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html

Bạn cũng có thể tự viết phương thức kiểm tra riêng cho mình, nhưng mình sẽ đề cập ở bài khác.

Rails – Tùy chỉnh View

Chúng ta sẽ chỉnh sửa giao diện người dùng (View) với ứng dụng depot đã làm trong bài trước.

Tạo dữ liệu với seeds.rb

Trước tiên chúng ta sẽ cần đến một số dữ liệu mẫu để sử dụng. Nếu bạn còn nhớ thì trong bài trước chúng ta biết là Rails sẽ tạo sẵn một form để chúng ta thực hiện thêm bản ghi vào model rất dễ dàng. Tuy nhiên việc ngồi gõ từng dòng dữ liệu như thế rất chán và mệt mỏi, do đó Rails cung cấp cho chúng ta một cách tạo dữ liệu khác.

Ở trong thư mục db có một file có tên là seeds.rb, đây là file Rails dành cho chúng ta để ghi đoạn code tạo đối tượng từ model, và Rails sẽ đọc các dòng code đó rồi insert thành các bản ghi trong CSDL luôn. Ví dụ chúng ta mở file này ra rồi chèn vào đoạn code sau:

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
# Mayor.create(name: 'Emanuel', city: cities.first)
Product.delete_all
Product.create(:title => 'Learn Web Development with Rails',
    :description => 
    %{
        Ruby on Rails Tutorial book and screencast series 
        teach you how to develop and deploy real, 
        industrial-strength web applications with Ruby on Rails.
    },
    :image_url => 'rails_book.png',
    :price => 29.99)
 
Product.create(:title => 'The Ruby Programming Language',
    :description =>
    %{
        The Ruby Programming Language is the authoritative guide 
        to Ruby and provides comprehensive coverage 
        of versions 1.8 and 1.9 of the language.
    },
    :image_url => 'ruby_book.png',
    :price => 39.99)

Trong đoạn code trên chúng ta tạo 2 đối tượng Product. Dòng đầu tiên chúng ta gọi phương thức delete_all, phương thức này sẽ xóa toàn bộ bản ghi có trong CSDL, tiếp theo chúng ta gọi phương thức create và đưa vào các tham số dữ liệu để tạo mới đối tượng. Có một lưu ý là trong thuộc tính description chúng ta dùng cú pháp %{...}, cú pháp này cho phép chúng ta ghi chuỗi trên nhiều dòng, thay vì truyền vào trong dấu nháy kép thì phải nối chuỗi.

Sau đó để thực hiện việc tạo các bản ghi này trong CSDL thì chúng ta mở command prompt lên rồi gõ lệnh rake db:seed là được.

C:\Project\Rails\depot>rake db:seed

Bạn có thể chạy server rồi trỏ đến URL /products hoặc dùng trình sqlite3 để kiểm tra nội dung trong CSDL đã được chèn vào hay chưa, tất nhiên là chẳng có gì đặc biệt cả.

Tùy chỉnh View

Trước tiên chúng ta tìm hiểu sơ qua về khái niệm Layout. Layout ở đây có nghĩa là một đoạn code được dựng sẵn để dùng chung cho toàn bộ website, các trang View khác trong website sẽ copy code trong layout vào code của chính nó rồi chèn thêm các đoạn code đặc trưng của nó vào. Vậy đoạn code layout dùng chung đó nằm ở đâu? câu trả lời la file application.html.erb trong thư mục app/views/layouts, file này được Rails tạo ra tự động và có nội dung như sau:

<!DOCTYPE html>
<html>
<head>
    <title>Depot</title>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
    <%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>

Chúng ta sẽ tìm hiểu về layout sau, ở đây chúng ta chỉ cần để ý các đoạn nằm trong thẻ <%= %>. Các đoạn code nằm trong cặp thẻ này được gọi chung là template, chúng ta có thể gọi một số hàm, thực hiện một số câu lệnh đơn giản như if, vòng lặp… hoặc tham chiếu đến biến trong cặp thẻ này… và những gì nằm trong cặp thẻ này sẽ được trình biên dịch Rails dịch ra thành code HTML rồi chèn vào đó. Ở đây dòng:

<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>

có nghĩa là tạo các thẻ <link> trỏ đến toàn bộ file CSS có trong thư mục app/assets/stylesheets, bạn có thể mở thư mục này ra và thấy trong đó có sẵn một số file CSS do Rails tự tạo ra rồi, một số file có thể có đuôi .scss, nhưng cũng không quan trọng mấy.

Tương tự, các file Javascript và các file ảnh sẽ được đặt vào trong thư mục app/assets/javascriptsapp/assets/images.

Bây giờ chúng ta tạo một file có tên depot.css chứa nội dung như sau:

#product_list table tr td {
    padding: 5px;
    vertical-align: top;
}

#product_list .list_image {
    width: 60px;
    height: 70px;
}

#product_list .list_description {
    width: 60%;
}

#product_list .list_description dl {
    margin: 0;
}

#product_list .list_description dt {
    color: #244;
    font-weight: bold;
    font-size: larger;
}

#product_list .list_description dd {
    margin: 0;
}

#product_list .list_actions {
    font-size: x-small;
    text-align: right;
    padding-left: 1em;
}

#product_list .list_line_even {
    background: #e0f8f8;
}

#product_list .list_line_odd {
   background: #f8b0f8;
}

#store .entry {
    overflow: auto;
    margin-top: 1em;
    border-bottom: 1px dotted #77d;
}

#store .title {
    font-size: 120%;
    font-family: sans-serif;
}

#store .entry img {
    width: 80px;
    margin-right: 5px;
    margin-bottom: 5px;
    float: left;
}


#store .entry h3 {
    margin-top: 0;
    margin-bottom: 2px;
    color: #227;
}

#store .entry p {
    margin-top: 0.5em; 
    margin-bottom: 0.8em; 
}

#store .entry .price_line {
    clear: both;
    margin-bottom: 0.5em;
}

#store .entry .add_to_cart {
    position: relative;
}

#store .entry .price {
    color: #44a;
    font-weight: bold;
    margin-right: 2em;
}

Chúng ta định nghĩa vài class CSS đơn giản để hiển thị trang /products theo giao diện mới một tí. Và tất nhiên file này sẽ được file layout tham chiếu tới như đã nói ở trên.

Tiếp theo chúng ta sửa lại file index.html.erb trong thư mục app/views/products như sau:

<div id="product_list">
    <h1>Products</h1>
 
    <table>
    <% @products.each do |product| %>
        <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
 
            <td>
                <%= image_tag(product.image_url, :class => 'list_image') %>
            </td>
 
            <td class="list_description">
                <dl>
                    <dt><%= product.title %></dt>
                    <dd><%= truncate(strip_tags(product.description),
                            :length => 80) %></dd>
                </dl>
            </td>
 
            <td class="list_actions">
                <%= link_to 'Show', product %><br/>
                <%= link_to 'Edit', edit_product_path(product) %><br/>
                <%= link_to 'Destroy', product, 
                    data: { confirm: 'Are you sure?' },
                    :method => :delete %>
            </td>
        </tr>
    <% end %>
    </table>
</div>

<br />

<%= link_to 'New product', new_product_path %>

Có một số điểm lưu ý trong đoạn code trên như sau:

  • Hàm cycle() có tác dụng duyệt qua lần lượt từng phần tử trong danh sách, khi đến hết danh sách thì lại quay về phần tử đầu tiên, trong ví dụ trên thì class của thẻ tr đầu tiên sẽ là list_line_odd hoặc list_line_even tùy vào từng lần lặp.
  • Hàm truncate() có tác dụng lấy một lượng kí tự nhất định, ở đây chúng ta lấy 80 kí tự, ngoài ra chúng ta còn dùng hàm strip_tags() để loại bỏ các thẻ HTML trong chuỗi cho trước.
  • Các dòng link_to <tên phương thức>  chẳng hạn như link_to 'Destroy' có nghĩa là gọi đến phương thức destroy trong file controller, tức là file app/controllers/products_controller.rb, ngoài ra theo sau chúng ta còn có thể truyền vào các tham số, ví dụ như data: { confirm: 'Are you sure?'} tức là truyền vào đối tượng data có thuộc tính confirm có giá trị là chuỗi "Are you sure?" (dưới dạng JSON). Dòng này sẽ được Rails dịch thành thuộc tính data-confirm trong thẻ <a>, nếu bạn rành HTML5 thì bạn sẽ biết là thuộc tính này sẽ tạo một hộp thoại để xác nhận.

Bây giờ bạn có thể chạy server và trỏ đến localhost:3000/products để xem giao diện mới.

capture

Rails – Xây dựng ứng dụng MVC

Chúng ta sẽ xây dựng một website bán hàng đơn giản, sử dụng cơ sở dữ liệu là SQLite3

SQLite3

SQLite3 là một cơ sở dữ liệu SQL, đặc điểm của SQLite3 là rất nhỏ, nhẹ, dễ cài đặt, phù hợp với nhiều loại ứng dụng. Điểm đặc biệt của SQLite3 là chúng ta không cần một server, không cần các bước cài đặt phức tạp, rườm rà.

Nếu bạn đã có trình sqlite3.exe trên máy thì bạn có thể bỏ qua phần này và kéo xuống phần tạo cơ sở dữ liệu để đọc tiếp. Nếu chưa thì đầu tiên bạn tải công cụ của sqlite3 tại địa chỉ https://sqlite.org/download.html

Bạn tải các tool về và giải nén theo hệ điều hành mà bạn đang dùng, chẳng hạn như bạn dùng Windows thì tải file sqlite-tools-win32-x86-3140200.zip (1.52 MiB) về:

capture

Tiếp theo bạn nên (hoặc phải) đưa đường dẫn đến thư mục chứa các file vừa được giải nén này vào biến môi trường PATH để tiện sử dụng sau này:

Ví dụ với Windows 10:

Bạn bấm chuột phải vào My Computer → Properties → Advanced system settings → Environment Variables, sau đó click chọn biến Path trong phần System variables rồi chèn thêm đường dẫn đến thư mục sqlite3 vừa giải nén vào. Chẳng hạn ở đây mình giải nén vào thư mục F:\DevSoft\sqlite3 thì mình có hình như sau:

capture

Bây giờ chúng ta kiểm tra xem sqlite3 đã được đưa vào biến môi trường Path chưa thì bạn mở terminal lên (Cmd trong Windows) rồi gõ lệnh sqlite3 -version để xem phiên bản sqlite3.

capture

Nếu bạn ra được giống như hình trên thì đường dẫn đến thư mục chứa sqlite3 của bạn đã hoàn toàn nằm trong biến môi trường Path rồi. Ở đây mình dùng SQLite3 phiên bản 3.14.2 như trong hình.

Tạo project

Chúng ta tạo project với tên là depot (nghĩa là nhà kho).

C:\Projects\Rails>rails new depot
...

Tiếp theo chúng ta sẽ tạo CSDL. Chúng ta chuyển thư mục trong cmd vào thư mục của project (bằng lệnh cd) và gõ lệnh sau:

C:\Project\Rails\depot>rails generate scaffold Product title:string description:text 
image_url:string price:decimal
    invoke active_record
    create   db/migrate/20161106144824_create_products.rb
    create   app/models/product.rb
    invoke test_unit
    create   test/models/product_test.rb
    ...

Lệnh trên có cú pháp như sau:

rails generate scaffold <tên bảng> <tên trường 1:kiểu dữ liệu> <tên trường 2:kiểu dữ liệu>...<tên trường n:kiểu dữ liệu>

Lệnh này sẽ định nghĩa các model để tạo CSDL, ở đây chúng ta tạo model Product (sản phẩm) với các trường title, description, image_url, price. Tuy nhiên lệnh này chưa tạo bảng và model ngay mà chỉ tạo một thư mục có tên migrate trong thư mục db và một file có có tên tương tự như 20161106144824_create_products.rb trong thư mục đó, file này sẽ lưu thông tin của các bảng sẽ được tạo sau này:

class CreateProducts < ActiveRecord::Migration
    def change
        create_table :products do |t|
            t.string :title
            t.text :description
            t.string :image_url
            t.decimal :price

            t.timestamps null: false
        end
    end
end

Chúng ta sửa lại file này để quy định trường price có độ lớn 8 chữ số và số phần thập phân là 2 chữ số như sau:

class CreateProducts < ActiveRecord::Migration
    def change
        create_table :products do |t|
            t.string :title
            t.text :description
            t.string :image_url
            t.decimal :price, :precision => 8, :scale => 2

            t.timestamps null: false
        end
    end
end

Tiếp theo để thực sự tạo CSDL và tạo bảng thì chúng ta chạy lệnh rake db:migrate như sau:

C:\Project\Rails\depot>rake db:migrate
== 20161106144824 CreateProducts: migrating ===========================================
-- create_table(:products)
   -> 0.0041s
== 20161106144824 CreateProducts: migrated (0.0066s)===================================

Lệnh này sẽ tạo CSDL, mặc định Rails sử dụng cơ sở dữ liệu SQLite3 và file chứa CSDL sẽ nằm trong thư mục db và có tên là development.sqlite3. Bạn có thể cấu hình để Rails sử dụng CSDL khác hoặc đổi tên file CSDL mặc định thành tên khác bằng cách chỉnh sửa trong file database.yml trong thư mục config. Cách cấu hình thì mình sẽ nói ở bài khác.

Model mà chúng ta đã tạo bằng lệnh rails generate scaffold có tên là Product, tuy nhiên Rails sẽ tự động chuyển tên model thành viết thường và thêm ký tự ‘s‘ vào cuối tên và dùng tên đó để tạo bảng, tức là ở đây bảng của chúng ta sẽ có tên là products. Một điều khác nữa là Rails sẽ tự động thêm trường id có kiểu dữ liệu là INTEGER tự động tăng và dùng trường này làm khóa chính, và tạo 2 trường created_atupdated_at có kiểu dữ liệu DATETIME dùng để lưu thời điểm tạo bản ghi, thời điểm cập nhật bản ghi.

Ngoài việc tạo CSDL, Rails cũng sẽ tạo luôn model, controller và một số view mẫu để chúng ta sử dụng. Bây giờ chúng ta chạy server bằng lệnh rails server sau đó trỏ đến địa chỉ localhost:3000/products là sẽ được trang có hình như sau:

capture

Bạn có thể thực hiện các thao tác thêm (click vào link New Product), sửa, xóa, cập nhật sản phẩm một cách dễ dàng, tất cả các thao tác cơ bản này đều do Rails tạo ra tự động.

Khi click vào đường dẫn New Product, bạn sẽ được chuyển đến trang http://localhost:3000/products/new, trang này có giao diện được dựng từ file app/views/products/_form.html.erb, chúng ta có thể tùy ý chỉnh sửa để giao diện hiển thị theo ý chúng ta, ở đây chúng ta sửa file này để hiển thị số dòng trong trường description là 6 dòng như sau:

<%= form_for(@product) do |f| %>
    <% if @product.errors.any? %>
        <div id="error_explanation">
            <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2>

            <ul>
            <% @product.errors.full_messages.each do |message| %>
                <li><%= message %></li>
            <% end %>
            </ul>
        </div>
    <% end %>

    <div class="field">
        <%= f.label :title %><br>
        <%= f.text_field :title %>
    </div>
    <div class="field">
        <%= f.label :description %><br>
        <%= f.text_area :description, :rows => 6 %>
    </div>
    <div class="field">
        <%= f.label :image_url %><br>
        <%= f.text_field :image_url %>
    </div>
    <div class="field">
        <%= f.label :price %><br>
        <%= f.text_field :price %>
    </div>
    <div class="actions">
        <%= f.submit %>
    </div>
<% end %>

Save lại file này, sau đó refresh lại trang products/new, bạn có thể tự tạo một sản phẩm (hay 1 bản ghi) mẫu nào đó.

capture

Bạn sẽ nhận được thông báo sản phẩm được tạo thành công. Bây giờ bạn có thể quay lại trang /products và sản phẩm sẽ được hiển thị lên.

capture

Ngoài ra bạn sẽ thấy còn có 3 đường link là Show, Edit được route với các trang /products/<id>, /products/<id>/edit dùng để hiển thị thông tin chi tiết sản phẩm và chỉnh sửa sản phẩm. Đường link Destroy sẽ hiển thị một hộp thông báo hỏi chúng ta có muốn xóa sản phẩm không, nếu bấm OK thì sản phẩm đó sẽ bị xóa.

Các chức năng thêm, sửa, xóa, cập nhật được gọi chung là thao tác CRUD (Create – Read – Update – Delete), hầu hết mọi ứng dụng quản lý (như quản lý cửa hàng, quản lý khách sạn, quản lý sinh viên…v.v) đều phải code các chức năng này. Vậy nên Rails đã hỗ trợ chúng ta bằng cách tạo sẵn các thao tác này, do đó phần việc của chúng ta được tinh giảm đi rất nhiều. Việc cần làm còn lại chỉ là chỉnh sửa giao diện, thêm các chức năng đặc thù như phân quyền, xác thực, bảo mật…v.v

Rails – Kiến trúc của Rails

Một trong những đặc điểm của Rails là bạn bị buộc phải viết chương trình theo một loạt các quy tắc nhất định, tức là bạn phải theo mô hình kiến trúc mà Rails đã đề ra, tuy nhiên những quy tắc này lại khiến việc phát triển ứng dụng trở nên dễ dàng hơn rất nhiều.

Mô hình MVC – Model, View, Controller

Mô hình MVC được Trygve Reenskaug đề ra vào năm 1979, mô hình này chia ứng dụng làm 3 phần: model, viewcontroller.

Trong đó model sẽ chịu trách nhiệm duy trì dữ liệu của ứng dụng. Đôi khi dữ liệu này chỉ tồn tại trong một thời gian ngắn, đôi khi lại được lưu trữ lâu dài trong cơ sở dữ liệu. Ở đây model không đơn thuần chỉ là dữ liệu, ứng dụng sẽ buộc phải chạy theo các quy tắc đã áp đặt lên dữ liệu đó. Chẳng hạn như chúng ta định nghĩa phiếu giảm giá cho các đơn hàng không lớn hơn 200.000đ, đó là một quy tắc và ứng dụng sẽ phải tuân thủ quy tắc đó. Và nhờ vào các quy tắc này mà dữ liệu của chúng ta không bị biến đổi một cách bất hợp lệ.

View sẽ chịu trách nhiệm tạo ra giao diện người dùng, và giao diện này sẽ dựa trên model. Ví dụ chúng ta có một website bán hàng, trên trang “Sản phẩm” chúng ta liệt kê danh sách các sản phẩm, vậy thì danh sách này sẽ lấy dữ liệu là hàng hóa được định nghĩa bởi model, view sẽ lấy dữ liệu từ model và chuyển đổi thành giao diện hiển thị lên cho người dùng. Công việc của view là chỉ có hiển thị chứ không xử lý bất kỳ thao tác nào của người dùng. Một model có thể được truy cập bởi nhiều view, chẳng hạn như website bán hàng có 2 trang là “Sản phẩm” và “Chỉnh sửa sản phẩm”, trang “Sản phẩm” sẽ truy cập model và lấy danh sách các sản phẩm rồi hiển thị cho người dùng, trong khi đó trang “Chỉnh sửa sản phẩm” cũng lấy danh sách các sản phẩm nhưng hiển thị cho người quản lý website.

Controller sẽ chịu trách nhiệm vận hành ứng dụng. Controller sẽ nhận các sự kiện từ bên ngoài, thông thường là từ người dùng, sau đó tương tác với model và gọi view tương ứng để hiển thị.

capture

Bộ ba Model, ControllerView hợp thành thành mô hình MVC. Mô hình MVC cho phép chúng ta tách ứng dụng thành các bộ phận riêng biệt, nhờ đó chúng ta có thể dễ dàng phát triển và bảo trì ứng dụng. Ruby on Rails cũng được phát triển theo mô hình MVC.

Bất cứ ứng dụng Rails nào cũng có 3 phần model, view và controller. Việc kết nối giữa 3 thành phần này đã được Rails giải quyết tự động rồi, do đó bạn chỉ cần quan tâm đến việc phát triển từng thành phần thôi.

Trong một ứng dụng Rails, một gói tin HTTP được gửi từ trình duyệt sẽ được chuyển tới các router trước tiên, đây là nơi vận chuyển các gói tin HTTP đến các phương thức nhất định (trong Rails thì phương thức hay hàm còn có tên khác là action). Phương thức đó sẽ đọc các dữ liệu có trong gói tin và có thể sẽ tương tác với model, hoặc gọi thêm các phương thức/action khác. Cuối cùng thì phương thức đầu tiên được gọi đó sẽ tính toán ra các dữ liệu khác rồi gửi cho một view nào đó để hiển thị cho người dùng.

Tính năng Object Relational Mapping

Thông thường chúng ta sẽ lưu dữ liệu trong một cơ sở dữ liệu quan hệ. Chẳng hạn như một hệ thống đặt hàng sẽ gồm có các bảng đơn hàng, sản phẩm, khách hàng

Tuy nhiên cơ sở dữ liệu quan hệ lại thường không mấy liên quan tới lập trình hướng đối tượng, chẳng hạn như trong SQL bạn có thể thực hiện các câu truy vấn để trích xuất các tập dữ liệu có liên quan với nhau rất dễ dàng, nhưng để làm việc này trong hướng đối tượng rất khó. Chẳng hạn việc tìm một tập các sản phẩm có giá < 2.000.000đ thuộc danh mục máy tính được đăng vào năm 2016 rất dễ trong SQL, nhưng lại rất khó trong code Ruby.

Chính vì vậy mà nhóm phát triển đã cho ra đời một tính năng hỗ trợ việc chuyển đối dữ liệu quan hệ sang các đối tượng một cách dễ dàng đó là tính năng Object Relational Mapping (ORM)

Các thư viện ORM sẽ làm công việc chuyển đổi các bảng trong CSDL sang các lớp. Ví dụ như trong CSDL có bảng Order (đơn hàng) thì ORM sẽ tạo một lớp có tên Order trong Ruby, các dòng trong bảng sẽ là các đối tượng của lớp đó, các thuộc tính của lớp sẽ tương ứng với các cột trong bảng. Ngoài ra ORM sẽ định nghĩa các phương thức để chúng ta có thể đọc ghi dữ liệu một cách dễ dàng. Ví dụ chúng ta sẽ có các đoạn code dạng như sau:

order = Order.find(1)
...
Order.where(:name => 'phocode').each do |order|
    order.payment = "Paypal"
    order.save
end

Active Record

Active Record là bộ phận thực hiện chức năng ORM trong Rails, bao gồm việc tạo lớp từ các bảng, tạo đối tượng từ các dòng trong bảng, tạo thuộc tính từ các cột. Dưới đây là đoạn code ví dụ:

require 'active_record'

class Order < ActiveRecord::Base
end

order = Order.find(1)
order.pay_type = "Paypal"
order.save

Trong ví dụ trên, lớp Order có phương thức find() dùng để tìm một đối tượng có id1, ngoài ra chúng ta còn có thể chỉnh sửa thuộc tính pay_type, đoạn code trên chỉ là ví dụ nhỏ, trên thực tế chúng ta còn phải làm nhiều thứ nữa.

Action Pack

Như chúng ta đã biết, controller sẽ gửi dữ liệu cho view, nhận và xử lý sự kiện từ view, do đó view và controller khá “thân thiết” với nhau. Chính vì vậy mà bộ phận xử lý controller và view được gộp lại làm một và được gọi là Action Pack. Bạn đừng hiểu lầm là chúng ta sẽ viết  code cho view và controller chung một chỗ, ở đây chẳng qua chỉ là chúng được vận hành bởi cùng một thứ mà thôi.

View

Trong Rails thì view có nhiệm vụ tạo ra các đoạn code HTML để hiển thị lên trình duyệt của người dùng, thông thường code HTML này có kèm theo dữ liệu được tạo ra từ các phương thức trong controller.

Các dữ liệu được tạo ra sẽ được tạo ra từ các template, bạn cứ hình dung đây giống như là một trình thông dịch nhỏ khác là được. Hiện có 3 loại template trong Rails là Embedded Ruby (ERb), XML Builder và RJS.

Trong đó ERb là phổ biến nhất, code ERb sẽ được nhúng chung với code HTML, nếu bạn đã từng làm việc với PHP hay JSP (Java) thì bạn sẽ thấy quen thuộc. Mặc dù code kiểu này rất linh hoạt nhưng đa phần người ta nghĩ rằng việc trộn chung công việc giữa các thành phần khác nhau là không nên.

Controller

Phần controller trong Rails là bộ phận trung tâm, có nhiệm vụ xử lý tương tác giữa người dùng, view và model. Tuy nhiên chúng ta sẽ chỉ tập trung vào việc phát triển các tính năng của website, còn việc kết nối giữa các thành phần này sẽ được Rails giải quyết, chúng ta không cần quan tâm.

Cơ bản thì controller hỗ trợ các tính năng sau:

  • Định tuyến (routing) các URL tới các phương thức/hàm/action tương ứng. Ngoài ra controller còn định nghĩa các URL có cấu trúc thân thiện, dễ nhìn
  • Quản lý cache, giúp tăng hiệu năng của hệ thống
  • Quản lý các module hỗ trợ, giúp mở rộng các tính năng của template
  • Quản lý session, giúp theo dõi các hoạt động đang diễn ra trên ứng dụng

 

Rails – Tạo project Rails

Trong phần này chúng ta sẽ tìm hiểu cách tạo một project Rails.

Tạo project

Để tạo project thì bạn mở Command Prompt (cmd) lên rồi dùng lệnh cd để chuyển đến thư mục mà bạn muốn tạo, sau đó gõ lệnh rails new <tên project> là được, ví dụ:

C:\Project\Rails>rails new Example
    create 
    create READNE.rdoc
    create Rakefile
    create config.ru
    create .gitignore
    create Gemfile
    create app
    create app/assests/javascripts/application.js
    create app/assests/stylesheets/application.css
    create app/controllers/application_controller.rb
    create app/helpers/application_helper.rb
    ...
    ...
       run bundle install
Fetching gem metadata from http://rubygems.org/.........
Fetching version metadata from http://rubygems.org/........
Fetching dependency metadata from http://rubygems.org/........
Resolving dependencies......
Installing rake 11.3.0
...

Lệnh rails new Example sẽ tạo một thư mục có tên Example và tạo các file với thư mục con cần thiết trong đó, đây là các file và thư mục của một project Rails.

Nếu bạn chạy lệnh mà báo lỗi “The system cannot find the file specified” thì bạn vào thư mục Ruby2.2.0/bin trong thư mục cài đặt Rails, sau đó tìm trong tất cả các file có đuôi .bat dòng này:

@"C:\Users\emachnic\GitRepos\railsinstaller-windows\stage\Ruby2.2.0\bin\ruby.exe"

Và thay bằng dòng này:

@"%~dp0ruby.exe"

Sau đó bạn chạy lại lệnh rails new.

Ngoài ra còn có một lỗi khác cũng có thể xuất hiện, khi chạy lệnh rails new, bạn để ý các dòng đầu tiên là create <tên file> hoặc <tên thư mục>, sau đó sẽ là dòng run bundle install, dòng này có nghĩa là rails đang cài các thư viện cần thiết đi kèm, sau đó sẽ là các dòng Fetching…. nhưng nếu của bạn không có các dòng này mà thay bằng các dòng tương tự như:

Fetching source index from https://rubygems.org/
Retrying fetcher due to error (2/4): Bundler::Fetcher::CertificateFailureError Could 
not verify the  SSAL certificate for https://rubygems.org/
There is a chance you are experiencing a man-in-the-middle attack, but most likely 
your system doesn't have the CA certificates needed for verification. For information
about OpenSSL certificates, see http://bit.ly/ruby-ssl. To connect without using SSL, 
edit your Gemfile sources and change 'https' to 'http'

Đây là các dòng thông báo lỗi, bạn có thể sửa bằng một cách thủ công vào mở toàn bộ file có tên Gemfile (không có phần mở rộng) trong thư mục cài đặt Rails, rồi tìm dòng sources https://rubygems.org/ và đổi thành sources http://rubygems.org/ (bỏ kí tự ‘s’ sau ‘http’). Mặc dù cách này khá thủ công nhưng cách này chắc chắn 100% thành công.

Ngoài ra còn có một cách sửa khác nhưng sẽ không đảm bảo thành công ở thời điểm hiện tại, đó là bạn chạy lệnh gem update --system để cập nhật phần mềm gem là được, (gem là phần mềm quản lý gói của Rails), tuy nhiên khi chạy lệnh này bạn có thể gặp lỗi sau:

SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate 
verify failed

Bạn có thể sửa bằng cách lên trang https://rubygems.org/pages/download tải phiên bản gem mới nhất về, sau đó giải nén, bạn sẽ được một thư mục chứa bộ mã nguồn cài đặt gem, trong đó có một file tên là setup.rb, bạn mở Command Prompt lên trỏ đến thư mục đó (bằng lệnh cd) rồi chạy lệnh ruby setup.rb --help là xong.

Bây giờ bạn có thể chạy lại lệnh rails new để tạo project, tuy nhiên hiện tại một lần nữa nếu bạn vẫn còn thấy báo lỗi SSL thì lần này bạn chỉ còn có thể sửa bằng cách thay đổi đường dẫn https://rubygems.org/ thành http://rubygems.org/ như đã nói ở trên.

Chạy project

Trong thư mục của project có rất nhiều file, chúng ta sẽ tìm hiểu sau. Bây giờ để chạy một project thì bạn mở Command Prompt lên rồi chuyển đến thư mục đó, sau đó gõ lệnh rails server là được:

C:\Project\Rails\Example>rails server
=> Booting WEBrick
=> Rails 4.2.5.1 application starting in development on http://localhost:3000
=> Run 'rails server -h' for more startup options
=> Ctrl-C to shutdown server
[2016-10-25 09:58:49] INFO WEBrick 1.3.
[2016-10-25 09:58:49] INFO ruby 2.2.4 (2015-12-16) [i386-mingw32]
[2016-10-25 09:58:49] INFO WEBrick::HTTPServer#start: pid=14844 port=3000

Mỗi project rails sẽ tự chạy một web server cho riêng nó, khác với các công nghệ như PHP, ASP.NET, Java… là bạn sẽ phải cài một webserver riêng như Apache, NginX hay IIS…

Chúng ta đã chạy một web server trên cổng 3000, bạn có thể mở trình duyệt và trỏ đến URL http://localhost:3000 hoặc http://127.0.0.1:3000 để xem thông báo của server.

capture

Để tắt server thì bạn có thể dùng tổ hợp phím Ctrl+C hoặc đơn giản là tắt Command Prompt luôn 🙂

Tùy chỉnh project

Rails được xây dựng dựa trên mô hình MVC, về cơ bản thì khi chúng ta mở trình duyệt và nhập vào URL của server, thì trình duyệt sẽ gửi một yêu cầu HTTP đến server, server nhận được yêu cầu đó và sẽ gửi vào một Controller nào đó và gọi phương thức xử lý ở đó. Tiếp theo Controller sẽ gọi một View để trả về nội dung HTML cho trình duyệt. Chúng ta sẽ tìm hiểu thêm về kiến trúc này sau.

Bây giờ chúng ta sẽ tạo một Controller và một View mẫu và hiển thị dòng chữ Hello World.

Chúng ta mở một Command Prompt khác lên rồi chuyển đến thư mục của project, sau đó gõ lệnh rails generate controller để tạo một controller, lệnh này có cú pháp như sau:

rails generate controller <tên controller> <tên route 1> <tên route 2>....<tên route n>

Ví dụ:

C:\Project\Rails\Example>rails generate controller Say hello goodbye
    create app/controllers/say_controller.rb
     route get 'say/goodbye'
     route get 'say/hello'
    invoke erb
    create   app/views/say
    create   app/views/say/hello.html.erb
    create   app/views/say/goodbye.html.erb
    invoke test_unit
    create   test/controllers/say_controller_test.rb
    invoke helper
    create   app/helpers/say_helper.rb
    invoke   test_unit
    invoke assets
    invoke   coffee
    create     app/assets/javascripts/say.coffee
    invoke   scss
    create     app/assets/stylesheets/say.scss

Ở đây chúng ta tạo một controller có tên là Say, trong đó có 2 hàm route là hellogoodbye. Bạn có thể mở thư mục app\controllers trong thư mục gốc của project là sẽ thấy một file có tên say_controller.rb, trong file này có nội dung như sau:

class SayController < ApplicationController
    def hello
    end

   def goodbye
    end
end

Ở đây chúng ta có lớp SayController kế thừa từ lớp ApplicationController, lớp này có 2 phương thức là hellogoodbye. Nếu bạn chưa từng học qua mô hình MVC thì ở đây bạn có thể hiểu nôm na một Controller là một file .rb, trong đó có nhiều hàm route, hàm route là các hàm xử lý một đường dẫn URL cụ thể.

Nếu bạn đã từng làm việc với Django hay Node.js thì bạn sẽ cần phải gọi các hàm cấu hình tương ứng cho từng URL tới từng hàm route cụ thể. Trong Rails cũng vậy, mặc định khi tạo một controller thì Rails đã định nghĩa cách các URL được chuyển đến các hàm route rồi. Chẳng hạn ở đây chúng ta có controller là Say và hàm routing hellogoodbye trỏ đến các URL tương ứng là http://localhost:3000/say/hellohttp://localhost:3000/say/goodbye. Chúng ta sẽ tìm hiểu về cách routing sau.

capture

Hiện tại hàm hello trong lớp SayController chẳng làm gì cả, trang web được hiển thị như hình trên là nhờ vào file View, đó là file hello.html.erb  trong thư mục app/views/say.

<h1>Say#hello</h1>
Find me in app/views/say/hello.html.erb

Về bản chất thì các file này chỉ chứa code HTML, CSS… và một loại code đặc biệt nữa của riêng Rails dùng để kết nối giữa controller và view. Để ví dụ thì chúng ta sửa lại đoạn code trên như sau:

<h1>Hello world</h1>
<p>
    Current time: <%= Time.now %>
</p>

Loại code mà chúng ta đang nói đến ở đây được gọi chung là template, bạn để ý trong đoạn code trên, ngoài các thẻ h1, p của HTML thì có một thẻ khác là <%= %>, những gì nằm trong cặp thẻ này sẽ được dịch bằng trình thông dịch Ruby, Ruby sẽ dịch đoạn code trong thẻ này và chuyển đổi thành các chuỗi bình thường. Chẳng hạn như <%= Time.now %> sẽ được dịch thành chuỗi dạng như sau: 2016-10-25 10:43:12 +0700

Chúng ta lưu lại file này sau đó tải lại trang /say/hello sẽ thấy kết quả dạng như sau:

capture

Tuy nhiên trên thực tế thì chúng ta không gọi hàm trực tiếp như vậy, ma thay vào đó là ở trong hàm route sẽ tính toán một biến nào đó, rồi trong file view sẽ tham chiếu đến giá trị của biến đó. Ví dụ chúng ta sửa đoạn code trong file say_controller.rb như sau:

class SayController < ApplicationController
    def hello
        @time = Time.now
    end

    def goodbye
    end
end

Ở đây chúng ta lấy thời gian hiện tại của máy và lưu vào biến @time, nếu bạn chưa biết thì ký tự @ chỉ định biến time là biến instance, biến này chỉ thấy được trong từng đối tượng, nói cho dễ hiểu thì bạn có thể nghĩ biến này có phạm vi truy xuất tương tự như private trong các ngôn ngữ hướng đối tượng khác.

Tiếp theo chúng ta sửa lại file hello.html.erb như sau:

<h1>Hello world</h1>
<p>
    Current time: <%= @time %>
</p>

Lần này thay vì gọi trực tiếp hàm Time.now, chúng ta gọi đến biến @time trong lớp controller tương ứng.

Rails – Giới thiệu

Ruby on Rails (viết ngắn gọn là Rails) là một web framework được viết bằng ngôn ngữ Ruby. Rails được giới thiệu lần đầu vào năm 2004 bởi một lập trình viên người Đan Mạch là David Heinemeier Hansson, và sau đó đã nhanh chóng phát triển trở thành một trong những web framework phổ biến. Một số công ty lớn sử dụng Rails là Airbnb, Base-camp, Github, Kickstarter, Shopify…

Rails có những đặc điểm sau:

  • Là một framework theo mô hình MVC
  • Fullstack, bạn có thể làm tất cả mọi thứ với Rails
  • Rails cho phép lập trình viên viết code ít hơn
  • Có 3 môi trường lập trình là môi trường phát triển, thử nghiệm và môi trường triển khai

Cài đặt Rails trên Windows

Để có thể dùng Rails thì bạn phải cài sẵn trước Ruby trên máy đã, nếu bạn chưa có thì lên trang http://rubyinstaller.org để tải và cài đặt.

Sau đó để cài đặt Rails trên Windows thì bạn tải trình Installer về và cài đặt tại địa chỉ http://railsinstaller.org/. Bạn lưu ý tải đúng phiên bản Rails hỗ trợ với phiên bản Ruby mà mình đã cài.

Sau đó trong quá trình cài thì bạn nhớ check tùy chọn thêm đường dẫn đến thư mục cài đặt Rails vào biến môi trường PATH để tiện sử dụng sau này.

capture

Xem phiên bản Rails

Sau khi đã cài xong, bạn có thể xem phiên bản Rails mà mình vừa cài cũng như kiểm tra xem Rails đã được cài đặt thành công chưa bằng cách mở Command Prompt (cmd) lên và gõ lệnh rails -v hoặc rails --version.

rail_version

 

Ở đây mình sử dụng phiên bản 4.2.5.1.

NodeJS – Xây dựng tính năng nhắn tin với Socket.IO

Trong phần trước chúng ta đã tìm hiểu qua thư viện Socket.IO, trong phần này chúng ta sẽ sử dụng thư viện này để xây dựng tính năng nhắn tin thời gian thực.

Tạo model

Trong file models-sequelize/users.js chúng ta sửa lại như sau:

var events = require('events');
var async = require('async');
var emitter = module.exports.emitter = new events.EventEmitter();

var util = require('util');
var Sequelize = require('sequelize');
var sequelize = undefined;
var User = undefined;

module.exports.connect = function(params, callback) {
    sequelize = new Sequelize(params.dbname,
                              params.username,
                              params.password,
                              params.params);
    User = sequelize.define('User', {
        id: {
            type: Sequelize.INTEGER,
            primaryKey: true,
            unique: true
        },
        username: {
            type: Sequelize.STRING,
            unique: true
        },
        password: Sequelize.STRING,
        email: Sequelize.STRING
    });
    User.sync().then(function() {
        callback()
    }).error(function(err) {
        callback(err);
    });
 
    Messages = sequelize.define('Messages', {
        idTo: { type: Sequelize.INTEGER, unique: false},
        idFrom: { type: Sequelize.INTEGER, unique: false},
        message: { type: Sequelize.STRING, unique: false}
    });
    Messages.sync();
}

module.exports.findById = function(id, callback) {
    User.find({ where: { id: id} }).then(function(user) {
        if(!user) {
            callback('User ' + id + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
            });
         }
     });
}

module.exports.findByUsername = function(username, callback) {
    User.find({where: {username: username}}).then(function(user) {
        if(!user) {
            callback('user ' + username + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
            });
         } 
     });
}

module.exports.create = function(id, username, password, email, callback) {
    User.create({
        id: id,
        username: username,
        password: password,
        email: email
    }).then(function(user) {
        callback();
    }).error(function(err) {
        callback(err);
    });
}

module.exports.update = function(id, username, password, email, callback) {
    User.find({where: {id: id}}).then(function(user) {
        user.updateAttributes({
            id: id,
            username: username,
            password: password,
            email: email
        }).then(function() {
            callback();
        }).error(function(err) {
            callback(err);
        });
    });
}

module.exports.allUsers = function(callback) {
    User.findAll().then(function(users) {
        if(users) {
            var userList = [];
            users.forEach(function(user) {
                userList.push({
                    id: user.id,
                    name: user.username
                });
            });
            callback(null, userList);
        } else
            callback();        
    });
}

module.exports.sendMessage = function(id, from, message, callback) {
    Messages.create({
        idTo: id,
        idFrom: from,
        message: message
    }).then(function(user) {
        emitter.emit('newmessage', id);
        callback();
    }).error(function(err) {
        callback(err);
    });
}

module.exports.getMessages = function(id, callback) {
    Messages.findAll({
        where: { idTo: id}
    }).then(function(messages) {
        if(messages) {
            var messageList = [];
            async.eachSeries(messages, 
               function(msg, done) {
                   module.exports.findById(msg.idFrom,
                       function(err, userFrom) {
                           messageList.push({
                               idTo: msg.idTo,
                               idFrom: msg.idFrom,
                               fromName: userFrom.username,
                               message: msg.message
                            });
                            done();
                        });
                    },
                    function(err) {
                        if(err) 
                            callback(err);
                        else 
                            callback(null, messageList);
                        }
                    );
        } else {
            callback();
        }
    });
}

module.exports.delMessage = function(id, from, message, callback) {
    Messages.find({
        where: { idTo: id, idFrom: from, message: message}
    }).then(function(msg) {
        if(msg) {
            msg.destroy().then(function() {
                emitter.emit('delmessage');
                callback();
            }).error(function(err) {
                callback(err);
            });
        } else 
            callback();
    });
}

Lẽ ra các tin nhắn nên được lưu trong một file model riêng, nhưng ở đây chúng ta sẽ lưu trong file users.js luôn vì chúng ta chỉ làm đơn giản thôi.

module.exports.connect = function(params, callback) {
    sequelize = new Sequelize(params.dbname,
                              params.username,
                              params.password,
                              params.params);
    ...
 
    Messages = sequelize.define('Messages', {
        idTo: { type: Sequelize.INTEGER, unique: false},
        idFrom: { type: Sequelize.INTEGER, unique: false},
        message: { type: Sequelize.STRING, unique: false}
    });
    Messages.sync();
}

Đầu tiên chúng ta khai báo mô hình dữ liệu và gọi hàm sync() để tạo bảng trong CSDL. Ở đây chúng ta chỉ lưu các tin nhắn đơn giản thôi, chỉ cần id người gửi, id người nhận và nội dung tin nhắn, nếu bạn còn nhớ thì sequelize sẽ tạo thêm 2 trường nữa trong CSDL là createdAtupdatedAt như đã nói trong bài lưu trữ dữ liệu MySQL.

module.exports.allUsers = function(callback) {
    User.findAll().then(function(users) {
        if(users) {
            var userList = [];
            users.forEach(function(user) {
                userList.push({
                    id: user.id,
                    name: user.username
                });
            });
            callback(null, userList);
        } else
            callback();        
    });
}

Tiếp theo chúng ta định nghĩa hàm allUsers() có chức năng lấy danh sách user, đoạn code trên rất đơn giản, chúng ta lấy toàn bộ danh sách các user ra rồi trả về.

module.exports.sendMessage = function(id, from, message, callback) {
    Messages.create({
        idTo: id,
        idFrom: from,
        message: message
    }).then(function(user) {
        emitter.emit('newmessage', id);
        callback();
    }).error(function(err) {
        callback(err);
    });
}

Hàm sendMessage() có chức năng gửi tin nhắn, ở đây chúng ta tạo một đối tượng model Messages, sau đó giải phóng sự kiện newmessage, đây là sự kiện mới, chúng ta sẽ code đoạn bắt sự kiện trong file app.js.

module.exports.getMessages = function(id, callback) {
    Messages.findAll({
        where: { idTo: id}
    }).then(function(messages) {
        if(messages) {
            var messageList = [];
            async.eachSeries(messages, 
               function(msg, done) {
                   module.exports.findById(msg.idFrom,
                       function(err, userFrom) {
                           messageList.push({
                               idTo: msg.idTo,
                               idFrom: msg.idFrom,
                               fromName: userFrom.username,
                               message: msg.message
                            });
                            done();
                        });
                    },
                    function(err) {
                        if(err) 
                            callback(err);
                        else 
                            callback(null, messageList);
                        }
                    );
        } else {
            callback();
        }
    });
}

Hàm getMessages() sẽ lấy toàn bộ tin nhắn được gửi tới một user nhất định, đoạn code trên cũng rất đơn giản, giống hệt với hàm titles() trong file model models-sequelize/notes.js thôi.

module.exports.delMessage = function(id, from, message, callback) {
    Messages.find({
        where: { idTo: id, idFrom: from, message: message}
    }).then(function(msg) {
        if(msg) {
            msg.destroy().then(function() {
                emitter.emit('delmessage');
                callback();
            }).error(function(err) {
                callback(err);
            });
        } else 
            callback();
    });
}

Hàm delMessage() sẽ xóa một tin nhắn nhất định, có một lưu ý là ở đây trong bảng lưu các tin nhắn chúng ta không có một trường nào làm khóa chính cả, do đó việc tìm một tin nhắn rồi xóa sẽ dựa vào cả 3 trường id người nhận, id người gửi và nội dung, nhưng như thế cũng không đúng vì một người có thể nhắn nhiều tin nhắn có nội dung giống nhau đến một người khác, và do đó khi xóa một tin nhắn thì các tin nhắn khác giống nhau cũng sẽ bị xóa luôn, ở đây mình chỉ làm đơn giản nên không kiểm tra vấn đề này, nếu muốn bạn có thể tự cải tiến thêm, chẳng hạn như thêm một trường làm id… Ngoài ra ở đây hàm delMessage() cũng sẽ giải phóng sự kiện delmessage nếu xóa thành công, đây cũng là một sự kiện mới.

Cấu hình routing

Chúng ta sửa lại file app.js như sau:

...
app.use('/sendmessage', users.ensureAuthenticated, users.sendMessage);
app.post('/dosendmessage', users.ensureAuthenticated, users.doSendMessage);

...
 
io.sockets.on('connection', function(socket) {
    socket.on('notetitles', function(fn) { 
        models.titles(function(err, titles) {
            if(err) {
                util.log(err); 
            } else {
                fn(titles);
            }
        });
    });
 
    var broadcastUpdated = function(newnote) {
        socket.emit('noteupdated', newnote);
    }
    models.emitter.on('noteupdated', broadcastUpdated);
    socket.on('disconnect', function() {
        models.emitter.removeListener('noteupdated', broadcastUpdated);
    });
 
    var broadcastDeleted = function(notekey) {
        socket.emit('notedeleted', notekey);
    }
    models.emitter.on('notedeleted', broadcastDeleted);
    socket.on('disconnect', function() {
        models.emitter.removeListener('notedeleted', broadcastDeleted);
    });
 
    socket.on('getmessages', function(id, fn) {
        usersModels.getMessages(id, function(err, messages) {
            if(err) {
                util.log('getmessages ERROR ' + err);
            } else 
                fn(messages);
        });
    });
 
    var broadcastNewMessage = function(id) {
        socket.emit('newmessage', id);
    }
    usersModels.emitter.on('newmessage', broadcastNewMessage);
 
    var broadcastDelMessage = function() {
        socket.emit('delmessage');
    }
    usersModels.emitter.on('delmessage', broadcastDelMessage);
 
    socket.on('disconnect', function() {
        usersModels.emitter.removeListener('newmessage', broadcastNewMessage);
        usersModels.emitter.removeListener('delmessage', broadcastDelMessage);
    });
 
    socket.on('dodelmessage', function(id, from, message, fn) {
        // do nothing
    });
});

Ở đây chúng thêm routing 2 đường dẫn mới là /sendmessage/dosendmessage, và viết một số hàm bắt sự kiện mới.

Các hàm bắt sự kiện mới gồm có sự kiện getmessages, newmessagedelmessage, dodelmessage, các sự kiện này sẽ được phát ra từ trình duyệt của client giống như với đoạn code xử lý sự kiện tạo, sửa, xóa ghi chú vây. Chỉ có một lưu ý là hàm bắt sự kiện dodelmessage không làm gì cả vì ở đây mình chưa hoàn thiện phần xóa tin nhắn như đã nói ở trên (đây là bài tập cho bạn đó, bạn tự code sao cho có thể xóa được tin nhắn đi :))

Tiếp theo chúng ta viết thêm 2 hàm sau vào file routes/users.js để xử lý 2 URL /sendmessage /dosendmessage như sau:

...
module.exports.sendMessage = function(req, res) {
    users.allUsers(function(err, userList) {
       res.render('sendmessage', {
           title: "Send a message",
           user: req.user,
           users: userList,
           message: req.flash('error')
       });
    });
}

module.exports.doSendMessage = function(req, res) {
    users.sendMessage(req.body.seluserid, 
        req.user.id, req.body.message, function(err) {
            if(err) {
                res.render('showerror', {
                    user: req.user ? req.user : undefined,
                    title: "Could not send message",
                    error: err
                });
            } else {
                res.redirect('/');
            }
    });
}

Hàm sendMessage() sẽ trả về nội dung trong file sendmessage.ejs (chúng ta sẽ viết ở dưới), hàm doSendMessage() sẽ gọi hàm sendMessage() ở bên phía user để tạo và lưu một tin nhắn mới.

Tạo view

Chúng ta tạo một file có tên sendmessage.ejs trong thư mục views có nội dung như sau:

<% include top %>
<form method="POST" action="/dosendmessage">
    <p>To:
    <select name="seluserid">
        <% for(var i in users) { %>
            <option value="<%= users[i].id %>">
                <%= users[i].name %>
            </option>
        <% } %>
    </select>
    </p>
    <p>Message: <input type='text' name='message' /></p>
    <p><input type='submit' value="Submit" /></p>
</form>
<% include bottom %>

Ở đây chúng ta hiển thị một form để người dùng chọn người gửi và nhập nội dung tin nhắn, form này sẽ được gửi lên URL /dosendmessage.

Tiếp theo chúng ta sửa lại file top.ejs như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = undefined;
        var delMessage = function(idTo, idFrom, message) {
            socket.emit('dodelmessage', idTo, idFrom, message);
        }
        $(document).ready(function() {
            socket = io.connect('/'); 
            <% if(user) { %> 
                var getmessages = function() {
                    socket.emit('getmessages', <%= user.id %>,
                    function(msgs) {
                        $('#messageBox').empty();
                        if(msgs.length > 0) {
                            for(var i in msgs) {
                                $('#messageBox').append('<p>');
                                $('#messageBox').append('<button onclick="delMessage('+
                                                   msgs[i].idTo + ', ' +
                                                   msgs[i].idFrom + ', \'' + 
                                                   msgs[i].message + '\')">DEL</button> '); 
                                $('#messageBox').append(msgs[i].fromName + ": ");
                                $('#messageBox').append(msgs[i].message);
                                $('#messageBox').append('</p>');
                            }
                            $('#messageBox').show();
                        } else 
                            $('#messageBox').hide();
                    });
        };
        getmessages();
        socket.on('newmessage', function(id) {
            getmessages();
        });
        socket.on('delmessage', getmessages);
        <% } %>
    });
    </script>
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
    <p>
        <a href='/'>Home</a> 
        | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/sendmessage'>Send message</a>
        | <a href='/logout'>Log Out</a> 
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
    </div>
    <% if(user) { %>
         <div id='messageBox' style='display:none;'></div>
    <% } %>

Ở đây cái chính là chúng ta viết hàm getmessages(), và hàm này được gọi ngay trong $(document).ready(), tức là khi người dùng đăng nhập vào thì chúng ta sẽ lấy toàn bộ tin nhắn được gửi tới người dùng đó và hiển thị lên cùng với các ghi chú luôn.

socket.on('newmessage', function(id) {
    getmessages();
});
socket.on('delmessage', getmessages);

Ngoài ra ở đây chúng ta còn cho lắng nghe 2 sự kiện delmessagenewmessage, 2 sự kiện này sẽ được server phát đi khi có người gửi tin nhắn hoặc xóa tin nhắn, và trình duyệt sẽ tự động cập nhật lại luôn vì chúng ta xử lý 2 sự kiện này bằng hàm getmessages() vừa được định nghĩa ở trên.

| <a href='/sendmessage'>Send message</a>

Chúng ta thêm một đường link trỏ đến URL /sendmessage để khi người dùng click vào thì ra trang gửi tin nhắn.

 <% if(user) { %>
     <div id='messageBox' style='display:none;'></div>
 <% } %>

Đoạn code trên sẽ bỏ hiển thị thẻ <div> có id là messageBox khi người dùng không đăng nhập.

Nếu muốn bạn có thể chỉnh sửa giao diện hiển thị cho phần tin nhắn một tí trong file public/stylesheets/style.css như sau:

#messageBox {
    border: solid 2px red;
    background-color: #eeeeee;
    padding: 5px;
}

Bây giờ bạn có thể chạy project được rồi, trước khi chạy, bạn có thể mở file models-sequelize/setup.js và tạo một user mới để test.

capture

Bạn cũng có thể mở 2 trình duyệt khác nhau như Chrome và Firefox, mỗi bên đăng nhập một tài khoản, rồi một bên gửi tin nhắn cho bên kia và ghi gửi thành công thì bên nhận cũng sẽ tự động hiển thị tin nhắn đó lên luôn mà không cần tải lại trang web.

NodeJS – Cập nhật dữ liệu thời gian thực với Socket.IO

Công nghệ web thời sơ khai có một nhược điểm hiển thị dữ liệu không nhất quán, tức là sẽ có trường hợp 2 trình duyệt cùng trỏ đến một địa chỉ URL nhưng nội dung trả về lại khác nhau. Ví dụ như có 2 người cùng truy cập một trang wiki, sau đó một trong hai người chỉnh sửa lại trang này, thì sau khi người chỉnh sửa xong bấm lưu trang, chỉ có người đó mới thấy được sự thay đổi, người kia nếu muốn thấy sự thay đổi ngay lúc đó thì phải refresh lại trang đó mới thấy được. Với sự phát triển của công nghệ web ngày nay thì chúng ta đã có thể làm cho web thực hiện các công việc theo thời gian thực (real-time), tức là khi trang web của trình duyệt bên này có sự thay đổi thì lập tức trình duyệt bên kia bằng cách nào có cũng sẽ tự cập nhật thay đổi đó luôn. Ví dụ điển hình nhất là Facebook, mỗi khi có người bấm “like” hoặc bình luận là ngay lập tức bạn sẽ thấy ngay, hoặc chí ít là sẽ có thông báo đến cho bạn.

Trong phần này chúng ta sẽ tìm hiểu cách xây dựng các tính năng real-time như vậy trong ứng dụng Notes, thực ra ngay từ đầu thì mục đích phát minh ra Node là để hỗ trợ các tính năng real-time rồi.

Việc code tính năng real-time cũng sẽ đụng chạm rất nhiều đến phần giao thức mạng, vốn dĩ là thứ mà chúng ta không nên quan tâm, do đó chúng ta sẽ sử dụng thư viện cho nhanh 🙂 Ở đây chúng ta sẽ dùng thư viện Socket.IO, thư viện này đơn giản hóa quá trình tuyền thông giữa trình duyệt và server, hỗ trợ rất nhiều giao thức, ngoài ra còn hỗ trợ cả Internet Explorer tới cả phiên bản 5.5 nữa.

Một thư viện khác thường đi chung với Socket.IO là Backbone.js, đây là một thư viện hỗ trợ xây dựng model nhanh chóng. Tuy nhiên chúng ta sẽ không dùng đến thư viện này vì dữ liệu model của chúng ta rất đơn giản, không phức tạp.

Khởi tạo Socket.IO với Express

Trong các bài trước, chúng ta đã biết là đối tượng http.Server là đối tượng chính thực hiện phần mạng, Express được xây dựng dựa trên đối tượng http.Server, và Socket.IO cũng hoạt động tương tự như vậy.

Đầu tiên chúng ta khai báo trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "start": "node ./bin/www"
    },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*",
        "workforce": "*",
        "socket.io": "*"
    }
}

Sau đó chạy lệnh npm install để cài module socket.io.

Chỉnh sửa app.js

File app.js được sửa lại như sau

var http = require('http');

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
//var models = require('./models-mongoose/notes');
var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);
var app = express();

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

var server = http.Server(app);
var io = require('socket.io').listen(server);
app.set('port', 3000);

server.listen(app.get('port'), function() {
    console.log("Express server listening on port " + app.get('port'));
});

io.sockets.on('connection', function(socket) {
    socket.on('notetitles', function(fn) {        
        models.titles(function(err, titles) {
            if(err) {
                util.log(err); 
            } else {
                fn(titles);
            }
        });
     });
 
    var broadcastUpdated = function(newnote) {
        socket.emit('noteupdated', newnote);
    }
    models.emitter.on('noteupdated', broadcastUpdated);
    socket.on('disconnect', function() {
       models.emitter.removeListener('noteupdated', broadcastUpdated);
    });

    var broadcastDeleted = function(notekey) {
       socket.emit('notedeleted', notekey);
    }
    models.emitter.on('notedeleted', broadcastDeleted);
    socket.on('disconnect', function() {
        models.emitter.removeListener('notedeleted', broadcastDeleted);
    });

});

Chúng ta sẽ chạy trực tiếp server từ file app.js chứ không chạy theo file www trong thư mục bin do Express tự tạo nữa.

var http = require('http');

Chúng ta sẽ cần đến module http.

var server = http.Server(app);
var io = require('socket.io').listen(server);
app.set('port', 3000);
server.listen(app.get('port'), function() {
    console.log("Express server listening on port " + app.get('port'));
});

Tiếp theo chúng ta tạo đối tượn http.Server và đối tượng socket.io, về cơ bản thì đối tượng socket.io sẽ bọc lấy đối tượng server. Sau đó chúng ta thiết lập biến môi trường port là 3000 và cho server lắng nghe trên cổng port.

io.sockets.on('connection', function(socket) {
    socket.on('notetitles', function(fn) { 
        models.titles(function(err, titles) {
            if(err) {
                util.log(err); 
            } else {
                fn(titles);
            }
        });
    });
 
    var broadcastUpdated = function(newnote) {
        socket.emit('noteupdated', newnote);
    }
    models.emitter.on('noteupdated', broadcastUpdated);
    socket.on('disconnect', function() {
        models.emitter.removeListener('noteupdated', broadcastUpdated);
    });

    var broadcastDeleted = function(notekey) {
        socket.emit('notedeleted', notekey);
    }
    models.emitter.on('notedeleted', broadcastDeleted);
    socket.on('disconnect', function() {
        models.emitter.removeListener('notedeleted', broadcastDeleted);
    });

});

Tiếp theo chúng ta có đoạn code chính như trên, như đã nói, Socket.IO được xây dựng dựa trên lớp EventEmitter, tức là đối tượng này sẽ lắng nghe các sự kiện và trả lời các sự kiện đó. Bắt đầu từ đây mọi thứ sẽ hơi rối rắm một chút xíu.

Đầu tiên chúng ta cho socket lắng nghe sự kiện connection, sự kiện này sẽ được phát ra khi có trình duyệt trỏ tới website. Trong đó, chúng ta cho socket lắng nghe 2 sự kiện là notetitlesdisconnect, cả 2 sự kiện này đều được trình duyệt phát đi, tức là chúng ta sẽ code phần phát sự kiện này trong các file .ejs, nhưng chúng ta không dùng Node mà dùng jQuery. Ngoài ra khi người dùng chỉnh sửa một ghi chú nào đó hoặc xóa một ghi chú thì đối tượng models sẽ phát đi 2 sự kiện là noteupdatednotedeleted, chúng ta sẽ code trong file models-sequelize/notes.js. Cuối cùng sự kiện disconnect được phát đi khi người dùng thoát hẳn khỏi website.

Trong đối tượng models (ở file sequelize-models/notes.js) cũng sẽ có một đối tượng EventEmitter mà chúng ta sẽ định nghĩa sau, đối tượng này được dùng để phát đi sự kiện noteupdatednotedeleted đã nói ở trên, trong file app.js chúng ta lại dùng chính đối tượng emitter đó để lắng nghe 2 sự kiện này, việc xảy ra tiếp theo sẽ là gọi hàm broadcastUpdated() hoặc broadcastDeleted(), 2 hàm này sẽ lại phát sinh 2 sự kiện cùng tên là noteupdatednotedelete bằng đối tượng socket, tuy nhiên 2 sự kiện này sẽ không được bắt bởi đối tượng models mà sẽ được bắt ở trình duyệt vì chúng ta dùng đối tượng socket để gửi đi.

Phát sự kiện từ model

Trong file models-sequelize/notes.js chúng ta sửa lại như sau:

var events = require('events');
var emitter = module.exports.emitter = new events.EventEmitter();

var Sequelize = require('sequelize');
var Note = undefined;
module.exports.connect = function(params, callback) {
    var sequlz = new Sequelize(
        params.dbname, 
        params.username, 
        params.password,
        params.params
    );
    Note = sequlz.define('Note', {
        notekey: {
            type: Sequelize.STRING,
            primaryKey: true,
            unique: true
        },
        title: Sequelize.STRING,
        body: Sequelize.TEXT
    });
    Note.sync().then(function() {
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.disconnect = function(callback) {
    callback();
}

exports.create = function(key, title, body, callback) {
    Note.create({
        notekey: key,
        title: title,
        body: body
    }).then(function(note) {
        exports.emitter.emit('noteupdated', {
        key: key,
        title: title,
        body: body
    });
        callback();
    }).error(function(err) {
        callback(err);
    });
}
exports.update = function(key, title, body, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback(new Error("No note found for key " + key));
        } else {
            note.updateAttributes({
                title: title,
                body: body
            }).then(function() { 
                exports.emitter.emit('noteupdated', {
                    key: key,
                    title: title,
                    body: body
                });
                callback();
            }).error(function(err) {
                callback(err);
            });
        }
    }).error(function(err) {
        callback(err);
    });
}

exports.read = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        if(!note) {
            callback("Nothing found for " + key);
        } else {
            callback(null, {
                notekey: note.notekey,
                title: note.title,
                body: note.body
            });
        }
    });
}

exports.destroy = function(key, callback) {
    Note.find({ where:{ notekey: key} }).then(function(note) {
        note.destroy().then(function() {
            emitter.emit('notedeleted', key);
            callback();
        }).error(function(err) {
            callback(err);
        });
    });
}

exports.titles = function(callback) { 
    Note.findAll().then(function(notes) { 
        var noteList = []; 
        notes.forEach(function(note) { 
            noteList.push({
                key: note.notekey,
                title: note.title
            }); 
        });
        callback(null, noteList);
    });
}

Chúng ta chỉnh sửa lại để mỗi khi người dùng tạo mới, cập nhật hoặc xóa một ghi chú thì sẽ có các sự kiện phát ra tương ứng.

var events = require('events');
var emitter = module.exports.emitter = new events.EventEmitter();
...
exports.emitter.emit('noteupdated', {
    key: key,
    title: title,
    body: body
});
...
emitter.emit('notedeleted', key);

Việc code cũng khá đơn giản, chúng ta chỉ cần tạo đối tượng EventEmitter, rồi gọi phương thức emit() khi cần là được.

Khởi tạo Socket.IO và jQuery trên trình duyệt

Ở đây ứng dụng của chúng ta cần phải có sự tương tác giữa cả server và client, và Socket.IO đều được viết ra để chạy trên server và client.

Trong file views/top.ejs chúng ta sửa lại như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io.connect('/'); 
    </script>
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
    <p>
        <a href='/'>Home</a> | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/logout'>Log Out</a>
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
    </div>

Chủ yếu là thêm các thẻ <script> tham chiếu đến file socket.io.js, file này sẽ tự động tải về khi chúng ta gọi phương thức io.connect(). Ngoài ra ở đây chúng ta còn cần dùng thêm jQuery nữa để thực hiện việc thay đổi nội dung trên trang web.

Xử lý sự kiện trên client

Đầu tiên chúng ta sửa file views/index.ejs như sau:

<% include top %>
<div id='notetitles'>
<%
     if(notes) {
         for(var i in notes) {
             %><p><%= notes[i].key %>:
             <a href="/noteview?key=<%= notes[i].key %>"><%= notes[i].title %></a>
             </p><% 
         }
     }
%>
</div>

<script>
    $(document).ready(function() { 
        var getnotetitles = function() { 
            socket.emit('notetitles', function(notes) { 
                $('#notetitles').empty();
                for(var i in notes) {
                    var str = '<p>' + notes[i].key + ': <a href="noteview?key=' + notes[i].key + '">' + notes[i].title + '</a>' + '</p>';
                    $('#notetitles').append(str);
                }
            });
        }
 
        socket.on('noteupdated', getnotetitles);
        socket.on('notedeleted', getnotetitles);
    });
</script>
<% include bottom %>

Đoạn code trên sẽ thay đổi nội dung trên trình duyệt khi có thay đổi phát ra.

<div id='notetitles'>
...
</div>

Đầu tiên chúng ta bọc danh sách các ghi chú trong thẻ <div>idnotetitles để có thể dễ dàng tham chiếu tới từ jQuery.

$(document).ready(function() { 
        var getnotetitles = function() { 
            socket.emit('notetitles', function(notes) { 
                $('#notetitles').empty();
                for(var i in notes) {
                    var str = '<p>' + notes[i].key + ': <a href="noteview?key=' + notes[i].key + '">' + notes[i].title + '</a>' + '</p>';
                    $('#notetitles').append(str);
                }
            });
        }
        socket.on('noteupdated', getnotetitles);
        socket.on('notedeleted', getnotetitles);
     });

Tiếp theo chúng ta viết đoạn code lắng nghe sự kiện noteupdatednotedeleted từ server, và ở đây chúng ta xử lý 2 sự kiện này bằng cách phát ra sự kiện notetitles, và server sẽ bắt sự kiện này rồi trả về danh sách các ghi chú mới cho chúng ta, sau đó chúng ta tham chiếu đến thẻ <div> ở trên và cập nhật lại danh sách ghi chú mới này bằng jQuery.

Bây giờ chúng ta sửa lại file views/noteview.ejs như sau:

<% include top %>
<div id="noteview">
<h3 id="notetitle"><%= note ? note.title : "" %></h3>
<p id="notebody"><%- note ? note.body : "" %></p>
<p>Key: <%= notekey %></p>
<% if(user && notekey) { %>
    <hr/>
    <p><a href="/notedestroy?key=<%= notekey %>">Delete</a>
    | <a href="/noteedit?key=<%= notekey %>">Edit</a></p>
<% } %>
</div>

<script>
    $(document).ready(function() {
        var updatenote = function(newnote) {
            $('#notetitle').empty();
            $('#notetitle').append(newnote.title);
            $('#notebody').empty();
            $('#notebody').append(newnote.body);
        }
        socket.on('noteupdated', function(newnote) {
            if(newnote.key === "<%= notekey %>") {
                updatenote(newnote);
            }
        });
        socket.on('notedeleted', function(notekey) {
            if(notekey === "<%= notekey %>") {
                document.location.href = "/";
            }
        });
   });
</script>
<% include bottom %>

Trong ứng dụng Notes chúng ta không xây dựng chức năng phân quyền, tức là ở đây bất cứ ai đăng nhập vào cũng có thể chỉnh sửa một ghi chú bất kỳ, giả sử có 2 người đang cùng thực hiện chỉnh sửa một ghi chú, thì khi một người lưu lại ghi chú đó, chúng ta sẽ cập nhật lại nội dung mới đó trên trang web của người kia luôn. Và để làm việc này thì chúng ta cũng làm tương tự như trong file views/index.ejs

Vậy là xong, bây giờ chúng ta có thể chạy ứng dụng được rồi. Tuy nhiên chúng ta sẽ không dùng lệnh npm start để chạy server của Express mà chúng ta phải dùng lệnh node app.js để chạy server do chúng ta cấu hình.

capture

Để kiểm tra thì bạn có thể mở 2 trình duyệt khác nhau như Chrome và Firefox rồi cùng trỏ đến website, sau đó một bên chỉnh sửa một ghi chú, thì bên kia cũng sẽ tự động cập nhật lại ghi chú mới.

NodeJS – Tăng hiệu suất server – Phần 2

Chúng ta đều biết rằng engine V8 là một engine đơn luồng (single-thread), tức là ở đây không thể chạy song song nhiều công việc cùng một lúc được, cơ chế bất đồng bộ (Asynchronous) của Node chẳng qua cũng chỉ là để chống server bị blocked thôi chứ thực chất các công việc vấn theo thứ tự trước-sau. Trong bài Tăng hiệu suất server chúng ta cũng đã tìm hiểu một cách để chạy nhiều server đó là tạo nhiều instance chạy trên nhiều cổng khác nhau, tuy nhiên như thế sẽ rất bất tiện và đem lại trải nghiệm không tốt cho người dùng, chẳng ai lại muốn gõ địa chỉ website rồi thêm số cổng đằng sau cả.

Do đó trong phần này chúng ta sẽ tìm hiểu một cách khác để tăng hiệu suất của server là chạy nhiều tiến trình để tận dụng các core của CPU. Kể từ phiên bản Node 0.8 trở lên, Node cho ra đời module cluster cho phép lập trình viên “tạo nhiều tiến trình mạng sử dụng chung cổng”, các tiến trình này đều nằm trên một máy, module này không cho phép chúng ta thao tác với cùng tiến trình trên các máy khác.

Module cluster cung cấp các hàm API ở cấp độ rất thấp, do đó bạn có thể làm rất nhiều trò với CPU, và cũng vì thế mà module này rất khó dùng nếu bạn không có nhiều kiến thức về cách hệ điều hành quản lý tiến trình, bộ nhớ… Tuy nhiên trên thực tế thì chúng ta cũng không nên quan tâm đến các thứ cấp thấp đó mà chỉ nên tập trung phát triển các tính năng của website, do đó chúng ta sẽ sử dụng các module ở cấp cao hơn để đơn giản hóa việc sử dụng, ở đây chúng ta sẽ dùng module workforce .

Cài đặt

Đầu tiên chúng ta khai báo module này trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
 },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*",
        "workforce": "*"
    }
}

Sau đó chạy lệnh npm install để cài đặt.

Tiếp theo chúng ta tạo file workforce.js trong thư mục gốc của project với đoạn code sau:

var workforce = require('workforce');
var manager = workforce('./app.js');
manager.set('workers', 4);
manager.set('title', 'Notes');
manager.set('restart threshold', '10s');
manager.set('exit timeout', '5s');
manager.listen(process.env.PORT || 3000);

Trong đoạn code trên chúng ta tạo một đối tượng workforce và thiết lập một số thông số cần thiết. Trong đó:

  • workers là số tiến trình tối đa được mở, thường thì con số này nên bằng số core của CPU
  • title sẽ được thêm trước vào tên mỗi tiến trình được tạo ra
  • restart threshold là thời gian một tiến trình được phép tồn tại, sau khi hết thời gian đó thì workforce sẽ hủy tiến trình này và tạo lại một tiến trình mới thay thế
  • exit timeout là thời gian chờ sau khi có lệnh hủy một tiến trình

Trong file app.js, ở cuối file có dòng:

module.exports = app;

Dòng này sẽ cho phép đối tượng app có thể được gọi từ các module khác. Nếu của bạn không có thì bạn thêm dòng này vào.

Vậy là xong, bây giờ chúng ta có thể chạy nhiều tiến trình server được rồi, và chúng ta sẽ không dùng lệnh npm start nữa mà dùng lệnh:

C:\NodeJS\notes>node workforce.js

Bạn có thể mở Task Manager trên Windows để xác nhận.

capture

Sở dĩ ở đây có 5 tiến trình là vì tiến trình đầu tiên là tiến trình chạy file workfoce.js, từ tiến trình này 4 tiến trình app.js được tạo ra.

NodeJS – Xác thực người dùng với PassportJS

Trong phần này chúng ta sẽ xây dựng tính năng xác thực user.

HTTP là một giao thức vô trạng thái, nghĩa là chúng ta không thể biết user đang lướt web đó có đăng nhập hay không, hay thậm chí chúng ta cũng không biết có đúng là hành động lướt web đó có do con người làm hay không.

Do đó cách xác thực thông thường đối với các ứng dụng sử dụng giao thức HTTP là gửi một đoạn token (một chuỗi id) vào cookie của trình duyệt. Chuỗi token sẽ được dùng để xác định xem người dùng ở trình duyệt đó có đang đăng nhập hay không. Và cứ mỗi lần trình duyệt truy cập đến website thì ngoài các thông tin bình thường, trình duyệt sẽ phải gửi cả chuỗi token đó, và chúng ta sẽ biết được là user nào đang đăng nhập vào với trình duyệt đó.

Node có khá nhiều module hỗ trợ xác thực user thông qua cookie, trong đó 2 module Passport (http://passportjs.org) và Everyauth (http://everyauth.com) là phổ biến nhất. Ở đây chúng ta sẽ dùng module Passport.

Cài module

Ngoài module chính là passport thì chúng ta sẽ cần thêm một số module khác nữa bao gồm connect-flash, passport-local và express-session, đầu tiên chúng ta khai báo các module này trong file package.json:

{
    "name": "notes",
    "version": "0.0.0",
    "private": true,
    "scripts": {
    "start": "node ./bin/www"
    },
    "dependencies": {
        "body-parser": "~1.15.1",
        "cookie-parser": "~1.4.3",
        "debug": "~2.2.0",
        "ejs": "~2.4.1",
        "express": "~4.13.4",
        "morgan": "~1.7.0",
        "serve-favicon": "~2.3.0",
        "async": "*",
        "sqlite3": "*",
        "mongoose": "*",
        "sequelize": "*",
        "connect-flash": "*",
        "passport": "*",
        "passport-local": "*",
        "express-session": "*"
    }
}

Sau đó chạy lệnh npm install để cài các module này vào project.

Ở đây module connect-flash có chức năng hỗ trợ hiển thị thông báo, passport là module chính dùng để xác thực, passport-local là module con trong module passport có chức năng xác thực bằng dữ liệu cục bộ (khác với xác thực thông qua các dịch vụ trung gian như Facebook, Twitter…), express-session hỗ trợ lưu trữ các chuỗi token thông qua session. Chúng ta sẽ lần lượt tìm hiểu các module này kỹ hơn.

Cấu hình app.js

Chúng ta sửa lại file app.js như sau:

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');
var notes = require('./routes/notes');
//var models = require('./models-fs/notes');
//var models = require('./models-sqlite3/notes');
//var models = require('./models-mongoose/notes');
var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);
var app = express();

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});


module.exports = app;

Chúng ta sẽ thêm và sửa khá nhiều thứ.

var flash = require('connect-flash');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var expressSession = require('express-session');

Đầu tiên là 4 dòng require() mới.

passport.serializeUser(users.serialize);
passport.deserializeUser(users.deserialize);
passport.use(users.strategy);

Ba dòng trên dùng để cấu hình module passport, trong đó serializeUser là thiết lập token cho user, deserializeUser là hủy token của user, passport.use(users.strategy) là thiết lập Strategy cho passport. Module passport gọi các cơ chế xác thực là các strategy, chẳng hạn trong phần này chúng ta dùng cách xác thực dữ liệu cục bộ thì gọi là “local strategy”. Cả 3 phương thức trên đều nhận vào tham số là một hàm, chúng ta sẽ định nghĩa các hàm tương ứng đó sau.

app.use(expressSession({secret: 'keyboard cat'}));
app.use(flash());
app.use(passport.initialize());
app.use(passport.session());

Passport sẽ lưu các token vào các biến session và dữ liệu session sẽ được lưu vào cookie. Ở đây chúng ta dùng module express-session. Hàm flash() sẽ khởi tạo module connect-flash, module này chỉ đơn giản là một module hỗ trợ hiển thị các câu thông báo. Phương thức passport.initialize() sẽ khởi tạo module passport, phương thức passport.session() sẽ bật tính năng sử dụng session, lưu ý là chúng ta phải khởi tạo express-session trước rồi mới sử dụng session trong passport.

var models = require('./models-sequelize/notes');
var usersModels = require('./models-sequelize/users');

models.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
usersModels.connect(require('./sequelize-params'),
    function(err) {
        if(err)
            throw err;
    });
users.configure({
    users: usersModels,
    passport: passport
});
notes.configure(models);
routes.configure(models);

Bắt đầu từ đây chúng ta không truyền các thông tin về cơ sở dữ liệu trực tiếp nữa mà thay vào đó sẽ lưu trong một module riêng, và khi cần thì chúng ta tham chiếu đến module đó, ở đây là file sequelize-params.js được tạo trong thư mục gốc của project, chúng ta sẽ tạo file này sau. Ngoài ra chúng ta cũng sẽ tạo một file users.js dùng để lưu thông tin của user, file này cũng chứa các thông tin và hàm dùng để kết nối CSDL như file model notes.js, chúng ta cũng sẽ tạo file này sau.

app.use('/', routes.index);
app.use('/noteview', notes.view);
app.use('/noteadd', users.ensureAuthenticated, notes.add);
app.use('/noteedit', users.ensureAuthenticated, notes.edit);
app.use('/notedestroy', users.ensureAuthenticated, notes.destroy);
app.post('/notedodestroy', users.ensureAuthenticated, notes.dodestroy);
app.post('/notesave', users.ensureAuthenticated, notes.save);
app.use('/account', users.ensureAuthenticated, users.doAccount);
app.use('/login', users.doLogin);
app.post('/doLogin', passport.authenticate('local', {
    failureRedirect: '/login',
    failureFlash: true
}), users.postLogin);
app.use('/logout', users.doLogout);

Cuối cùng chúng ta thêm và sửa lại phần routing, trong đó có một số đường dẫn routing sẽ cần thêm một hàm dùng để xác nhận xem người dùng có đang đăng nhập hay không bằng hàm users.ensureAuthenticated. Đường dẫn /login sẽ được dùng để thực hiện xác thực người dùng, trong đó chúng ta gọi phương thức passport.authenticate('local'...), phương thức này sẽ gọi đến phương thức kiểm tra mà chúng ta sẽ viết thêm ở dưới.

Như đã nói ở trên, chúng ta tạo file sequelize-params.js dùng để lưu các thông tin về cơ sở dữ liệu như sau:

module.exports = {
    dbname: "notes",
        username: "<username>",
        password: "<mật khẩu>",
        params: {
            host: "127.0.0.1",
            dialect: "mysql"
        }
};

Bạn thay username và mật khẩu tương ứng với CSDL của mình.

Tạo users model

Trong thư mục models-sequelize, chúng ta tạo file users.js có nội dung như sau:

var util = require('util');
var Sequelize = require('sequelize');
var sequelize = undefined;
var User = undefined;

module.exports.connect = function(params, callback) {
    sequelize = new Sequelize(params.dbname,
        params.username,
        params.password,
        params.params);
        User = sequelize.define('User', {
            id: {
                type: Sequelize.INTEGER,
                primaryKey: true,
                unique: true
            },
            username: {
                type: Sequelize.STRING,
                unique: true
            },
            password: Sequelize.STRING,
            email: Sequelize.STRING
        });
        User.sync().then(function() {
        callback()
    }).error(function(err) {
        callback(err);
    });
}

module.exports.findById = function(id, callback) {
    User.find({ where: { id: id} }).then(function(user) {
        if(!user) {
            callback('User ' + id + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
            });
        }
    });
}

module.exports.findByUsername = function(username, callback) {
    User.find({where: {username: username}}).then(function(user) {
        if(!user) {
            callback('user ' + username + ' does not exist');
        } else {
            callback(null, {
                id: user.id,
                username: user.username,
                password: user.password,
                email: user.email
           });
        } 
    });
}

module.exports.create = function(id, username, password, email, callback) {
    User.create({
        id: id,
        username: username,
        password: password,
        email: email
    }).then(function(user) {
        callback();
    }).error(function(err) {
        callback(err);
    });
}

module.exports.update = function(id, username, password, email, callback) {
    User.find({where: {id: id}}).then(function(user) {
        user.updateAttributes({
            id: id,
            username: username,
            password: password,
            email: email
        }).then(function() {
            callback();
        }).error(function(err) {
            callback(err);
        });
    });
}

File này đại diện cho phần model của user, tất cả đều tương tự như file notes.js trong cùng thư mục. Hàm connect() dùng để tạo bảng, các hàm read(), create(), update(), destroy(), titles() được dùng để xem, sửa, xóa, tạo mới một bản ghi người dùng trong cơ sở dữ liệu.

Routing

Trong thư mục routes, chúng ta tạo một file có tên users.js, khi tạo project thì express cũng có tạo một file như vậy nhưng cũng không có gì nhiều trong đó, nếu bạn có file đó thì khỏi tạo, trong file này chúng ta có thêm code như sau:

var LocalStrategy = require('passport-local').Strategy;
var users = undefined;
var passport = undefined;

exports.configure = function(params) {
    users = params.users;
    passport = params.passport;
}

module.exports.serialize = function(user, done) {
    done(null, user.id);
}

module.exports.deserialize = function(id, done) {
    users.findById(id, function(err, user) {
        done(err, user);
    });
}

module.exports.strategy = new LocalStrategy(
    function(username, password, done) {
        process.nextTick(function() {
            users.findByUsername(username, function(err, user) {
                if(err)
                    return done(err);
                if(!user) {
                    return done(null, false, {
                        message: 'Unknown user ' + username
                    });
                }
                if(user.password !== password) {
                    return done(null, false, {
                        message: 'Invalid password'
                    });
                }
                return done(null, user);
            });
        });
    }
);

module.exports.ensureAuthenticated = function(req, res, next) {
    if(req.isAuthenticated())
        return next();
    return res.redirect('/login');
}

module.exports.doAccount = function(req, res) {
    res.render('account', {
        title: 'Account information for ' + req.user.username,
        user: req.user
     });
}

module.exports.doLogin = function(req, res) {
    res.render('login', {
        title: 'Login to Note',
        user: req.user,
        message: req.flash('error')
    });
}

module.exports.postLogin = function(req, res) {
     res.redirect('/');
}

module.exports.doLogout = function(req, res) {
     req.logout();
     res.redirect('/');
}

Đoạn code trên xử lý phần định tuyến cho các url mới như /login, /account, /doLogin.

var LocalStrategy = require('passport-local').Strategy;
var users = undefined;
var passport = undefined;

exports.configure = function(params) {
    users = params.users;
    passport = params.passport;
}

Hàm configure() có chức năng tương tự như trong file routes/notes.js, đó là nhận thông tin về user hiện tại, và module passport hiện được dùng để xác thực.

module.exports.serialize = function(user, done) {
    done(null, user.id);
}

module.exports.deserialize = function(id, done) {
    users.findById(id, function(err, user) {
        done(err, user);
    });
}

Như đã nói ở trên, hàm serialize() sẽ tạo chuỗi token, ở đây chúng ta chỉ đơn giản là dùng chính id của người dùng để làm token. Hàm deserialize() sẽ hủy chuỗi token đó.

module.exports.strategy = new LocalStrategy(
    function(username, password, done) {
        process.nextTick(function() {
            users.findByUsername(username, function(err, user) {
                if(err)
                    return done(err);
                if(!user) {
                    return done(null, false, {
                        message: 'Unknown user ' + username
                    });
                }
                if(user.password !== password) {
                    return done(null, false, {
                        message: 'Invalid password'
                    });
                }
                return done(null, user);
            });
        });
    }
);

Tiếp theo chúng ta khởi tạo một đối tượng lớp LocalStrategy, khi xác thực bằng cách gọi hàm passport.authenticate() trong file app.js thì hàm trong đối tượng LocalStrategy này sẽ được gọi để thực hiện kiểm tra username và mật khẩu, ở đây chúng ta cũng kiểm tra đơn giản, chỉ là xem username có tồn tại hay không, nếu có thì kiểm tra xem password có trùng hay không, nếu tất cả đều hợp lệ thì trả về dữ liệu của user đó, nếu có vấn đề gì thì chúng ta trả về lỗi. Trên thực tế chúng ta sẽ cần làm nhiều thứ hơn như mã hóa mật khẩu, dùng salt…v.v Ngoài ra ở đây hàm process.nextTick() sẽ làm các công việc trên theo hướng bất đồng bộ.

module.exports.ensureAuthenticated = function(req, res, next) {
    if(req.isAuthenticated())
        return next();
    return res.redirect('/login');
}

Tiếp theo hàm ensureAuthenticated() là hàm kiểm tra xem người dùng có đang đăng nhập hay không khi lướt qua các trang khác, chúng ta chỉ cần gọi hàm req.isAuthenticated() là đủ, đây là hàm do module passport cung cấp.

module.exports.doAccount = function(req, res) {
    res.render('account', {
        title: 'Account information for ' + req.user.username,
        user: req.user
     });
}

Hàm doAccount() sẽ xử lý đường dẫn /account và hiển thị thông tin về user.

module.exports.doLogin = function(req, res) {
    res.render('login', {
        title: 'Login to Note',
        user: req.user,
        message: req.flash('error')
    });
}

module.exports.postLogin = function(req, res) {
     res.redirect('/');
}

Hàm doLogin() sẽ hiển thị form đăng nhập cho người dùng, hàm req.flash() sẽ hiển thị thông báo nếu người dùng đưang nhập sai. Hàm postLogin() sẽ chuyển hướng về trang '/' khi người dùng đăng nhập thành công.

module.exports.doLogout = function(req, res) {
     req.logout();
     res.redirect('/');
}

Hàm doLogout() sẽ xóa thông tin đăng nhập của người dùng, hàm req.logout() sẽ thực hiện xóa các thông tin đó, hàm này do passport thêm vào.

Sửa view

Chúng ta sửa lại file top.ejs như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
        <p>
        <a href='/'>Home</a> | <a href='/noteadd'>ADD Note</a>
        <% if(user) { %>
        | <a href='/logout'>Log Out</a>
        | logged in as <a href='/account'><%= user.username %></a>
        <% } else { %>
        | <a href='/login'>Log in</a>
        <% } %>
    </p>
 </div>

Bây giờ website sẽ hiển thị thêm đường dẫn để đăng nhập, hoặc thông tin người dùng đã đăng nhập.

Tiếp theo chúng ta tạo file account.ejs trong thư mục views như sau:

<% include top %>
<p>Name: <%= user.username %> (<%= user.id %>)</p>
<p>E-Mail: <%= user.email %></p>
<% include bottom %>

File này sẽ được dùng để hiển thị thông tin chi tiết về người dùng.

Cuối cùng chúng ta tạo file login.ejs trong thư mục views như sau:

<% include top %>
<form method="POST" action="/doLogin">
    <p>User name: <input type='text' name='username' /></p>
    <p>Password: <input type='text' name='password' /></p>
    <p><input type='submit' value='Submit' /></p>
</form>
<% include bottom %>

File này sẽ hiển thị form đăng nhập cho người dùng. Khi người dùng đăng nhập thì form sẽ gửi một yêu cầu đến đường dẫn /doLogin với phương thức là POST.

Sửa routing

Chúng ta sửa lại file notes.js trong thư mục routes như sau:

var notes = undefined;
exports.configure = function(params) {
    notes = params;
}

var readNote = function(key, user, res, done) {
    notes.read(key, function(err, data) {
        if(err) {
            res.render('showerror', {
                title: "Could not read note " + key,
                error: err,
                user: user ? user : undefined
            });
            done(err);
        } else
            done(null, data);
    });
}

exports.view = function(req, res, next) { 
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('noteview', {
                    title: data.title, 
                    notekey: req.query.key,
                    note: data,
                    user: req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note", 
            error: "Must provide a Key to view a Note",
            user: req.user ? req.user : undefined
        });
    }
}

exports.save = function(req, res, next) { 
    ((req.body.docreate === "create") ? notes.create : notes.update)
    (req.body.notekey, req.body.title, req.body.body,
        function(err) { 
            if(err) {
                res.render('showerror', {
                    title: "Could not update file",
                    error: err,
                    user: req.user ? req.user : undefined
            });
        } else {
            res.redirect('/noteview?key='+req.body.notekey);
        }
    });
}
exports.add = function(req, res, next) {
    res.render('noteedit', {
        title: "Add a Note",
        docreate: true,
        notekey: "",
        note: undefined,
        user: req.user ? req.user : undefined
    });
}

exports.edit = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('noteedit', {
                    title: data ? ("Edit " + data.title) : "Add a Note", 
                    docreate: false,
                    notekey: req.query.key,
                    note: data,
                    user : req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
             title: "No key given for Note",
             error: "Must provide a Key to view a Note",
             user : req.user ? req.user : undefined
         });
     }
}

exports.destroy = function(req, res, next) {
    if(req.query.key) {
        readNote(req.query.key, req.user, res, function(err, data) {
            if(!err) {
                res.render('notedestroy', {
                    title: data.title,
                    notekey: req.query.key,
                    note: data,
                    user: req.user ? req.user : undefined
                });
            }
        });
    } else {
        res.render('showerror', {
            title: "No key given for Note", 
            error: "Must provide a Key to view a note",
            user: req.user ? req.user : undefined
        });
    }
}

exports.dodestroy = function(req, res, next) {
    notes.destroy(req.body.notekey, function(err) {
        if(err) {
            res.render('showerror', {
                title: "Could not delete Note " + req.body.notekey,
                error: err
            });
        } else {
           res.redirect('/');
        }
    });
}

Ở đây chúng ta thêm vào đối tượng user để mỗi lần hiển thị lên trình duyệt thì không bị lỗi undefined.

Tạo user

Vậy là mọi thứ đã hoàn tất, bây giờ trước khi chạy thử thì chúng ta phải có tài khoản để sử dụng, do ở đây chúng ta không thực hiện chức năng đăng ký tài khoản nên chúng ta phải làm bằng tay. Để tiện thì trong thư mục models-sequelize, chúng ta tạo một file có tên setup.js với nội dung như sau:

var users = require('./users');
users.connect(require('../sequelize-params'),
    function(err) {
        if(err)
            throw err;
        else {
             users.create('1', 
                         'phocode', 
                         '123', 
                         'admin@phocode.com',
                         function(err) {
                             if(err)
                                 throw err; 
                         });
        }
    });

Đoạn code trên sẽ tạo tài khoản và lưu vào CSDL. Để chạy thì bạn mở cmd lên trong thư mục models-sequelize rồi chạy lệnh node setup.js là được.

Vậy là xong, bây giờ chúng ta có thể chạy ứng dụng và sử dụng chức năng đăng nhập được rồi.

capture