2016年7月31日 星期日

Node.js筆記:Node.js with Socket.io

WebSocket:

為一個從HTML5中開始的一個協定, 瀏覽器和伺服器之間只需要一個handshaking動作就形成了一條快速通道. 兩者之間可以直接互相傳送資料.
這種方式可以避免無謂的浪費伺服器頻寬和資源(舊式的是一段時間後再發一次request.)

Socket.io

一般的http協定不支援長久的連接, 需要不斷地發送request給server更新資料. 而Socket.io可以達到 server 和 client 的雙向溝通, 只要一建立連線 server 可以一直傳訊息給 client.

ChatRoom Example

1.新增Chatroom資料夾並新增package.json
首先新增一個Chatroom的資料夾, 並執行以下指令安裝相關套件.

    {
      "name": "socket-chat-example",
      "version": "0.0.1",
      "description": "my first socket.io app",
      "dependencies": {}
    }

2.安裝express & Socket.io

    $npm install --save express
    $npm install --save socket.io

3.新增index.js
第一行: 建立一個express module的app function.
第二行: 建立 http 物件
第三行(app.get…): 處理 / (根目錄) 的 URL GET 要求

sendFile為傳輸檔案, 在這裡傳的是index.js所在資料夾底下的index.html
__dirname: 返回被執行的.js檔所在資料夾的絕對路徑
最後則是監聽PORT:3000

    var app = require('express')();
    var http = require('http').Server(app);

    app.get('/', function(req, res){
      res.sendFile(__dirname + '/index.html');
    });

    http.listen(3000, function(){
      console.log('listening on *:3000');
    });

4.建立Socket.io
首先導入Socket.io模組, 因 Socket.io 依賴 HTTP 所以這裡採用一併引入的方式.

socket.io 的核心函數:

emit :用來發射一個事件或者說觸發一個事件, 第一個參數為事件名,第二個參數為要發送的數據, 第三個參數為callback function.
on :用來監聽一個emit 發射的事件, 第一個參數為要監聽的事件名,第二個參數為一個callback function用來接收對方發來的數據.

如上述的介紹這裡的io.on就可以解釋為使用 on() 函數將特定的事件連接到指定的匿名函數(用來定義當伺服器接收到請求時,該做什麼事情,以及該如何回應), 藉此定義整個資料傳輸過程要如何運作。

而connection/connect事件則為socket.io默認的事件之一.
在這邊socket.io提供的interface是基於事件的,server端監聽”connectione”事件, 如果接收到client的連結請求, 就會進入callback function,而連上的socket就會開始監聽事件,事件的名稱就是’chat message’,當socket發出’chat message’事件時就會執行所寫的程式內容,以下面為例,當觸發事件後會針對io所有的client socket發出’chat message’事件以及其後發所帶的msg參數。

io.on為監聽跟client是否達成連線, 內圈的socket.on則是監聽client的事作而回覆對應的function, 通常這裡的事件也需要在client端建立同樣的事件名稱才行.

    var io = require('socket.io')(http);

    io.on('connection', function(socket){
        socket.on('chat message', function(msg){
          io.emit('chat message', msg);
        });
    }); 

5.撰寫index.html
在使用 Socket.io 時, 伺服器端與客戶端都必須要有它的 JS 檔案才有辦法運作, 而在上一步已經為server端裝上 Socket.io module了, 接下來就是為client端, 也就是在html中裝上模組.

裝法為在 HTML 頁面中加入以下這段程式碼.

    <script src="https://cdn.socket.io/socket.io.js"></script>

在Client端一樣要先註冊一個socket, 並建立監聽Server的’chat message’事件。
完整如下:

    <!doctype html>
    <html>
      <head>
        <title>Socket.IO chat</title>
      </head>
      <body>
        <ul id="messages"></ul>
        <form action="">
          <input id="m" autocomplete="off" /><button>Send</button>
        </form>
        <script src="/socket.io/socket.io.js"></script>
        <script src="http://code.jquery.com/jquery-1.12.4.js"></script>
        <script>
          var socket = io.connect();
          $('form').submit(function(){
            socket.emit('chat message', $('#m').val());
            $('#m').val('');
            return false;
          });
          socket.on('chat message', function(msg){
            $('#messages').append($('<li>').text(msg));
          });
        </script>
      </body>
    </html>

參考:
1. http://socket.io/get-started/chat/
2. https://blog.allenchou.cc/socket-io-nodejs/
3. http://socket.io/docs/server-api/

2016年7月3日 星期日

Node.js筆記:Blog system(3)

新增資料庫功能

先在專案中新增一個”lib”的資料夾, 並新建一個檔案”db.js”. 在這裡我們要使用mongoose來存取MongoDB.

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var Blog = new Schema({
    Username: String,
    Article: String,
    CreateDate: Date
});

var Comment = new Schema({
    Visitor: String,
    Comment: String,
    MessageID: Schema.Types.ObjectId,
    CreateDate: Date
});

mongoose.model( 'Blog', Blog );
mongoose.model( 'Comment', Comment );
mongoose.connect('mongodb://localhost/blog');

在上面我們新增的兩個model, 一個叫Blog, 一個是Comment. 最後一行則是連線到本機端的MongoDB伺服器.

接著修改apis.js初始化資料庫
主要是利用require把資料庫設定載入, 再把定義好的model也加入.

require('../lib/db');

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Blog = mongoose.model('Blog');
var Comment = mongoose.model('Comment');

...

支線任務:
1.brew update error.
問題簡述在此:https://github.com/Homebrew/legacy-homebrew/issues/49879
解決方式就是利用下面兩組command就可以讓brew正常運作.

    $sudo chown -R $(whoami):admin /usr/local
    $cd $(brew --prefix) && git fetch origin && git reset --hard origin/master

2.MongoDB使用.
安裝:

//Step 1 - Installation ( Don't follow this step if you have already installed MongoDB ):
brew update
brew install mongodb

//Step 2 - Run Mongo Daemon:
mkdir -p /data/db
sudo mongod

//Step 3 - Run Mongo Shell Interface:
mongo

新增功能

1.首頁列出所有文章
修改routes/index.js阿

利用find()找出資料庫中所有的資料, 並把資料存在blogs物件中, 最後把blogs物件回傳給頁面.

require('../lib/db');

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Blog = mongoose.model('Blog');

router.get('/', function(req, res, next) {
    res.locals.username = req.session.name ;
    res.locals.authenticated = req.session.logined;
    Blog.find(function (err, blogs, count) {
      res.render('index', {
        title : 'Blog System';
        blogs : blogs;
      });
    });
});

module.exports = router;

修改index.jade, 利用forloop把blogs物件的資料都列印在頁面上.

...
div(style='padding:50px;')
        h1= title

        for article in blogs
                div
                    div(style='float:left;margin-top:10px;')= article.Username
                    div(style='float:right;margin-top:10px;')= article.CreateDate
                    br
                    div(style='clear:both')= article.Article
                        a(href='/users/message/'+article.id, style='float:right;margin-top:105px;margin-right:20px;') Leave a message
                    hr(style='margin-top:110px;')

2.新增文章
views/users/add_article.jade

doctype html
html
    head
        title= Login
    body
        div(style='float:left;margin:10px;')
            a(href='/') Home

        div(style='float:right')
            if (username && authenticated)
                span #{username}
                a(href='/users/profile', style='margin:10px;') Profile
                a(href='/users/signout', style='margin:10px;') Sign Out
            else
                a(href='/users/signin', style='margin:10px;') Log In
                a(href='/users/register', style='margin:10px;') Register
                a(href='/users/forget', style='margin:10px;') Forgot password

        div(style='padding:50px;')
            form(name="add", action="/apis/add", method="post")
                h1 Add Article
                div
                    p
                        span Writer:
                        label(style='padding-right:50px') #{username}
                    p
                        textarea(cols="50", rows="25", style='resize : none;', name="Content")
                    div
                        span(style='padding-right:377px')
                        input(type="submit", name="submit", value="Save")

後半段為一個POST方法, 當按下送出按鈕後會執行 /apis/add 的網址.

修改users.js跟apis.js
下面先判斷使用者是否有登入, 如果有登入才能進入新增文章.

#/users/js

router.get('/add_article', function(req, res, next) {
  if((!req.session.name) || (!req.session.logined)) {
    res.redirect('/');
    return;
  }
  res.locals.username = req.session.name;
  res.locals.authenticated = req.session.logined;
  res.render('users/add_article');
});

...
#/apis.js

router.post('/add', function(req, res, next) {
  if (!req.session.name) {
      res.redirect('/');
      return;
  }
  new Blog({
      Username: req.session.name,
      Article: req.body.Content,
      CreateDate: Date.now()
  }).save( function( err ){
      if (err) {
          console.log('Fail to save to DB.');
          return;
      }
      console.log('Save to DB.');
  });
  res.redirect('/');
});

enter image description here

2016年7月1日 星期五

Node.js筆記:Blog system(2)

Express generation 專案架構

當用express generation指令後會產生一個node project的資料夾.
enter image description here

  • app.js: 開機檔案, 或是說入口網站
  • bin: 存放執行檔
  • node_modules: 存放package.json中安裝的模組, 當你在package.json中新增相依的模組並安裝後, 該模組會存放在這個資料夾下.
  • package.json: 儲存著專案的資訊以及模組相依(dependencies), 當在dependencies中新增相依的模組時, 執行npm install, npm會檢查目前的資料下的package.json, 並自動安裝所有指定的模組
  • public: 存放image, css, js等文件
  • routes: 存放路由文件
  • views: 存放視圖檔, 或是說範本檔案

配置Routing

規劃網站各個頁面的路由路徑. 首先再次修改app.js: 新增apis路由, 用來負責POST方法的處理.

    ...
    var routes = require('./routes/index');
    var users = require('./routes/users');
    var apis = require('./routes/apis');

    ...
    app.use('/', routes);
    app.use('/users', users);
    app.use('/apis', apis);

設定user路由

在我們的設定中, user路由負責處理所有來自/user網址的要求, 例如新增文章, 會員註冊等. user路由會視用戶的請求而回應不同的資料.

新增首頁

首先在views資料夾中建立一個users資料夾. 裡面會放置所有user路由會用到的頁面.

首頁

1.更新index.jade檔
中間的if判斷是會根據使用者是否登入而顯示不一定的連結給使用者.

    doctype html
html
    head
        title= 'Blog System'
    body
        div(style='float:left;margin:10px;')
            a(href='/') Home

        div(style='float:right')
            if (username && authenticated)
                a(href='/users/signout', style='margin:10px;') Sign Out
                a(href='/users/add_article', style='margin:10px;') Add Article
                a(href='/users/profile', style='margin:10px;') Profile
                span #{username}
            else
                a(href='/users/signin', style='margin:10px;') Log In
                a(href='/users/register', style='margin:10px;') Register
                a(href='/users/forget', style='margin:10px;') Forgot password

    div(style='padding:50px;')
        h1= title

2.修改routes/index.js
把session所存的變數所存的變數傳回頁面.

    var express = require('express');
    var router = express.Router();

    /* GET home page. */
    router.get('/', function(req, res, next) {
        res.locals.username = req.session.name ;
        res.locals.authenticated = req.session.logined;
        res.render( 'index', {title : 'Blog System'});
    });
    module.exports = router;

使用者註冊頁面

1.新增views/users/register.jade
這邊的重點是寫了一個form的POST表單來處理使用者設定帳密

doctype html
html
    head
        title= Register
    body
        div(style='float:left;margin:10px;')
            a(href='/') Home

        div(style='float:right')
                a(href='/users/signin', style='margin:10px;') Log In
                a(href='/users/forget', style='margin:10px;') Forgot password

        div(style='padding:50px;')
            form(name="login", action="/apis/login", method="post")
                h1 Register
                div
                    p
                        label(style='padding-right:50px') Username or e-mail
                        input(type="text", name="user", placeholder='username@example.com')
                    p
                        label(style='padding-right:110px')  password
                        input(type="password", name="passwd")
                p
                    span(style='padding-right:260px')
                    input(type="submit", name="submit", value="Sing Up")

2.修改routes/user.js
新增一個判斷式, 如果logined是true, 代表使用者已經是登入狀態, 就會直接回到首頁.
如果尚未登入則會顯示register.jade的頁面.

var express = require('express');
var router = express.Router();

router.get('/register', function(req, res, next) {
    if (req.session.logined) {
        res.redirect('/');
        return;
    }
    res.render('users/register');
});

    ....

3.修改routes/apis.js
新增login函數, 處理註冊頁面傳來的POST方法.
先利用if來判斷使用者是否有輸入帳密, 當其中少一個的話則返回註冊畫面.
當輸入正確就把帳密給儲存到session中. 並把login設定為true代表使用者登入了.

ar express = require('express');
var router = express.Router();

router.post('/login', function(req, res, next) {
    if ((!req.body.user) || (!req.body.passwd)) {
        res.redirect('register');
        return;
    }
    req.session.name = req.body.user;
    req.session.passwd = req.body.passwd;
    req.session.logined = true;
    res.redirect('/');
});

...

使用者登入畫面

1.使用者登入頁面跟註冊的register.jade比, 只有差在字改成”Login”.

doctype html
html
    head
        title= Login
    body
        div(style='float:left;margin:10px;')
            a(href='/') Home

        div(style='float:right')
                a(href='/users/register', style='margin:10px;') Register
                a(href='/users/forget', style='margin:10px;') Forgot password

        div(style='padding:50px;')
            form(name="login", action="/apis/login", method="post")
                h1 Login
                div
                    p
                        label(style='padding-right:50px') Username or e-mail
                        input(type="text", name="user", placeholder='username@example.com')
                    p
                        label(style='padding-right:110px')  password
                        input(type="password", name="passwd")
                p
                    span
                        a(href='/users/forget', style='padding-right:175px') Forgot password
                    input(type="submit", name="submit", value="login")

2.修改routes/user.js

...

router.get('/signin', function(req, res, next) {
    if (req.session.logined) {
        res.redirect('/');
        return;
    }
    res.render('users/signin');
});

使用者登出頁面

1.修改routes/user.js
當登出之後會回到首頁.

...
router.get('/signout', function(req, res, next) {
    req.session.logined = false;
    res.redirect('/');
    res.end();
});

忘記密碼頁面

1.views/users/forget/jade

doctype html
html
    head
        title= 'Forget Password'
    body
        div(style='float:left;margin:10px;')
            a(href='/') Home

        div(style='float:right')
                a(href='/users/register', style='margin:10px;') Register
                a(href='/users/signin', style='margin:10px;') Log In

        div(style='padding:50px;')
            form(name="login", action="/apis/login", method="post")
                h1 Forget Password
                div
                    p
                        label(style='padding-right:50px') Username or e-mail
                        input(type="text", name="user", placeholder='username@example.com')
                p
                    span(style='padding-right:210px')
                    input(type="submit", name="submit", value="Forget Password")

2.修改routes/user.js

...

router.get('/forget', function(req, res, next) {
    if (req.session.logined) {
        res.redirect('/');
        return;
    }
    res.render('users/forget');
});