NodeJs03 express框架 Todo商城

前言

因爲NodeJs自己的異步非阻塞特性和對http的自然支持,因此使用NodeJs編寫高性能,可伸縮的Web服務器很是簡單。開發完整的Web服務器還須要路由,錯誤處理,請求攔截,請求和響應的解析,模板引擎等功能,因此直接使用NodeJs的http模塊開發起來仍是挺痛苦的。html

目前有不少的Web框架都是基於http模塊封裝而成,最流行的當屬Express框架。前端

學習資源:node

快速開始

npm install express --save

快速開啓服務器:git

const express = require('express');
const app = express();

app.get('/', function(req, res){
  res.send('hello world');
});

app.listen(3000);

靜態文件

express提供了託管靜態文件的功能,好比html,圖片,CSS,JavaScript等文件。github

app.use(express.static('public'));

支持指定掛載路徑:web

app.use('/static', express.static('public'));

通常框架提供的靜態文件託管功能,只是在開發環境使用。在生產環境,推薦交給反向代理服務器來管理,好比Nginx;最佳的作法是花點錢交給CDN提供商來服務。mongodb

路由

路由就是根據用戶請求的url路徑,交由對應的響應方法處理。數據庫

請求方法

app.get('/', function (req, res) {
  res.send('GET request to the homepage');
});

// POST method route
app.post('/', function (req, res) {
  res.send('POST request to the homepage');
});

路徑匹配

// 匹配根路徑
app.get('/', function (req, res) {
  res.send('root');
});

// 匹配 /user 路徑的請求
app.get('/user', function (req, res) {
  res.send('user');
});

// 匹配 /xxx 路徑的請求
app.get('/xxx', function (req, res) {
  res.send('xxx');
});

支持簡單的字符串匹配:express

// 匹配 acd 和 abcd
app.get('/ab?cd', function(req, res) {
  res.send('ab?cd');
});

// 匹配 abcd、abbcd、abbbcd等
app.get('/ab+cd', function(req, res) {
  res.send('ab+cd');
});

// 匹配 abcd、abxcd、abRABDOMcd、ab123cd等
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});

也支持徹底的正則方式,可是不能用字符串了。npm

// 匹配以abc開頭的全部字符
app.get(/abc[0-9a-zA-Z]*/, function(req, res) {
  res.send('ab?cd');
});

模塊化路由

模塊化路由是指咱們能夠將一組具備業務相關性的路由封裝爲一個router對象,而後掛載到指定路徑。

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

// 該路由使用的中間件
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// 定義模塊的根路由
router.get('/', function(req, res) {
  res.send('auth模塊根路由');
});
// 定義登陸
router.get('/login', function(req, res) {
  res.send('login');
});

module.exports = router;

將router掛載到指定路徑:

const auth = require('./auth');
...
app.use('/auth', auth);

中間件

中間件就是整個請求——>響應流程中的一系列處理函數。路由也是屬於中間件,是掛載了路徑的中間件。

使用中間件有兩點注意:

  1. next() 方法和 res.send() 必須調用一個,不然請求就會掛起,客戶端等待響應。
  2. 中間件的順序很重要,它是按照你註冊的順序執行的。

應用程序級別中間件

const app = express();

// 沒有掛載路徑的中間件,應用的每一個請求都會執行該中間件
app.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// 掛載至 /user/:id 的中間件,任何指向 /user/:id 的請求都會執行它
app.use('/user/:id', function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

路由級別中間件

路由級中間件和應用級中間件同樣,只是它綁定的對象爲 express.Router()

const router = express.Router();

// 沒有掛載路徑的中間件,經過該路由的每一個請求都會執行該中間件
router.use(function (req, res, next) {
  console.log('Time:', Date.now());
  next();
});

// 一箇中間件棧,顯示任何指向 /user/:id 的 HTTP 請求的信息
router.use('/user/:id', function(req, res, next) {
  console.log('Request URL:', req.originalUrl);
  next();
}, function (req, res, next) {
  console.log('Request Type:', req.method);
  next();
});

// 一箇中間件棧,處理指向 /user/:id 的 GET 請求
router.get('/user/:id', function (req, res, next) {
  // 若是 user id 爲 0, 跳到下一個路由
  if (req.params.id == 0) next('route');
  // 負責將控制權交給棧中下一個中間件
  else next(); //
}, function (req, res, next) {
  // 渲染常規頁面
  res.render('regular');
});

module.exports = router;

掛載router

const app = express();
app.use('/', router);

錯誤處理

程序總會遇到錯誤,好比:傳參錯誤,咱們不當心調用了undifined,或者咱們本身也可能拋出異常。當發生這些錯誤的時候咱們須要給用戶一個友好的迴應,這就是錯誤處理。

定義錯誤處理中間件和定義其餘中間件同樣,除了須要 4 個參數,而不是 3 個,其格式以下 (err, req, res, next)。例如:

app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('出錯了!');
});

錯誤中間件返回的響應是隨意的,能夠響應一個 HTML 錯誤頁面、一句簡單的話、一個 JSON 字符串,或者其餘任何您想要的東西。

在其餘 app.use() 和路由調用後,最後定義錯誤處理中間件,好比:

app.use(bodyParser());
app.use(router);
app.use(function(err, req, res, next) {
  // ...
});

模板引擎

Html頁面自己是靜態的,不含動態數據的。模板引擎的做用就是將從數據庫讀出來的,處理好的數據填充到html頁面,而後返回給用戶。

express支持不少的模板引擎,每一個模板引擎都有本身的語法。這裏以jade爲例。

須要安裝相應的模板引擎 npm 軟件包。

npm install jade --save

設置模板的目錄,和當前使用的模板引擎

app.set('views', './views') //設置模板存放的目錄
app.set('view engine', 'jade'); //設置使用的引擎

views 目錄下生成名爲 index.jade 的 Jade 模板文件,內容以下:

html
  head
    title=title
  body
    h1=message

而後建立一個路由渲染 index.jade 文件。

app.get('/', function (req, res) {
  res.render('index', { title: 'Hey', message: 'Hello there!'});
});

index.jade 文件會被引擎渲染爲html文件,而後發送給用戶。jade引擎的更多使用請參考它的官方文檔,這裏只作簡單演示,緣由以下:

現代Web開發的模式是逐漸趨向於先後端分離,前端的人負責寫Html頁面,後臺的人寫API服務,前端數據經過AJAX獲得。這樣的好處是開發效率高,溝通成本下降。加上Vue和React等具備高性能和高效率的前端渲染引擎的出現,後端的模板引擎的幾乎沒有用武之地了。但不排除某些公司的技術人員仍然頑固地選擇後端渲染。

WebApp進程管理

在生產環境咱們不會使用node app.js的方式來開啓程序,由於咱們有這樣的一些需求:

  • 若是進程崩潰了,須要重啓
  • 監控當前進程的CPU使用率和內存佔用
  • 搭建集羣

進程管理器就能提供上面的功能,讓咱們的NodeJs進程永不退出。最流行的進程管理器有3個:

其中PM2除了提供上面的功能外,還能夠監視程序的日誌,錯誤提醒,而且內置了負載均衡器,界面還友好。因此咱們選擇使用PM2。

首先,安裝。

npm install pm2 -g

啓動程序:

pm2 start app.js

其餘經常使用命令:

pm2 list
pm2 stop app
pm2 reload app
pm2 start xxx.js -i 4

學習資源

第三方中間件:http://www.expressjs.com.cn/resources/middleware.html

API查詢:http://www.expressjs.com.cn/4x/api.html

TODO 後臺項目

項目介紹

完成對TODO事項的增刪改查功能,數據庫使用mongodb。

安裝依賴

建立項目,新建package.json文件。

npm i express -S
npm i mongoose -S
npm i morgan -S  // 打印log的類庫
npm i express-async-errors -S  // 用來捕獲promise錯誤

因爲express 4x爲了精簡框架,移除了不少內置的中間件,好比body-parser。因此爲了可以解析body參數,須要額外安裝這個模塊。

npm i body-parser -S

項目結構搭建

整個項目結構,仍然是基於MVC架構來分層。

  • service包:存放全部的業務邏輯類,至關於service層

  • router包:存放全部的路由,至關於controller層
  • util包:存放全部的幫助類
  • model包:存放全部的mongodb模型類,至關於dao層
  • config.js:配置文件
  • db.js:數據庫入口文件
  • app.js:app入口文件

編寫項目邏輯

依次編寫:

  1. app.jsconfig.js

    'use strict'
    // 鏈接數據庫
    require('./db')
    
    const config = require('./config')
    const bodyParser = require('body-parser');
    const morgan = require('morgan');
    const express = require('express')
    // 引入express異步異常捕獲模塊
    require('express-async-errors');
    const app = express();
    
    // 註冊log中間件
    app.use(morgan('combined'));
    
    // 註冊body解析中間件
    // parse application/x-www-form-urlencoded
    app.use(bodyParser.urlencoded({ extended: false }));
    // parse application/json
    app.use(bodyParser.json());
    
    // 註冊路由
    app.use('/todos', require('./router/todo'));
    
    // 註冊錯誤處理中間件
    app.use(function (err, req, res, next) {
        console.log(err);
        res.send({
            code: -1,
            msg: err.toString()
        })
    });
    
    app.listen(config.PORT);
    'use strict'
    const config = {
        PORT: 3000,
        DB: 'TODO',
    }
    
    module.exports = config
  2. router

    'use strict'
    
    const router = require('express').Router()
    const todoController = require('../controller/todo')
    
    module.exports = router
    
    router.get('/', async (req, res)=>{
        let todos = await todoController.getAllTodo();
        res.send({
            code: 0,
            data: todos
        })
    });
    
    router.post('/', async (req, res)=>{
        await todoController.addTodo(req.body)
        res.send({code: 0})
    });
    
    router.put('/:id', async (req, res)=>{
        await todoController.updateTodo(req.params.id, req.body)
        res.send({code: 0})
    });
    
    // 刪除某個todo
    router.delete('/:id', async (req, res)=>{
        await todoController.deleteTodo(req.params.id)
        res.send({code: 0})
    });

  3. db.js 和 model

    'use strict'
    
    const config = require('./config')
    const mongoose = require('mongoose')
    
    mongoose.connect(`mongodb://127.0.0.1/${config.DB}`)
    
    const db = mongoose.connection
    
    db.on('error', err=>{
        console.log(err);
    });
    db.on('open', ()=>{
        console.log('db connect successful!');
    });
    'use strict'
    // To-do: 模型類, 內容,是否完成,建立日期
    const mongoose = require('mongoose')
    
    const schema = new mongoose.Schema({
        content: String,
        isDone: {
            type:Boolean,
            default: false
        },
        created: {
            type: Date,
            default: Date.now()
        }
    });
    
    module.exports = mongoose.model('todo', schema);

  4. controller

    'use strict'
    
    const Todo = require('../model/todo')
    
    // 取出數據庫中的全部todo,而後返回
    async function getAllTodo() {
        return await Todo.find({})
    }
    
    async function updateTodo(id, update) {
        await isIdExist(id)
    
        // { n: 1, nModified: 1, ok: 1 }
        let res = await Todo.updateOne({_id:id}, update)
        if(!res || res.n<1){
            throw `${update}更新失敗`
        }
    }
    
    async function addTodo(todo) {
        // 先查詢名字是否存在
        let t = await Todo.findOne({content: todo.content})
        if(t){
            //說明已經存在,則拋出異常,由異常處理中間件負責處理
            throw `${todo.content} 已經存在`
        }
        await Todo.create(todo)
    }
    
    async function isIdExist(id) {
        // 先判斷傳入的id是否存在
        let t = await Todo.findOne({_id: id})
        if(!t){
            throw `${id}不存在`
        }
    }
    
    async function deleteTodo(id) {
        await isIdExist(id)
    
        // { n: 1, ok: 1 }
        let res = await Todo.deleteOne({_id:id})
        console.log(res);
        if(!res || res.n < 1){
            throw `${id}刪除失敗`
        }
    }
    
    module.exports = {
        getAllTodo, updateTodo, addTodo, deleteTodo
    }

API 測試

使用postman進行測試每一個接口。

相關文章
相關標籤/搜索