前端學習 node 快速入門 系列 —— 報名系統 - [express]

彭加李發表於2021-04-03

其他章節請看:

前端學習 node 快速入門 系列

報名系統 - [express]

最簡單的報名系統:

  • 只有兩個頁面
  • 人員資訊列表頁:展示已報名的人員資訊列表。裡面有一個報名按鈕,點選按鈕則會跳轉到報名頁
  • 報名頁:用於報名。裡面是一個表單,可以輸入姓名和年齡,點選儲存,成功後會跳轉到人員資訊列表頁

本文主要分 3 部分:

  1. 使用 node 實現這個專案
  2. 介紹 express 相關知識
  3. 使用 express 重寫這個專案

Tip: 有將本文分成兩篇的打算,因為篇幅有點長;但最後還是決定寫在一起,因為更加緊湊。

node 實現

目錄如下:

- demo
  - public          // 存放靜態資源
    - css
      - global.css
  - views           // 存放模板
    - add.html      // 報名頁
    - list.html     // 列表頁
  - index.js        // 入口檔案
  - package.json    // PS: 自己安裝依賴包

global.css:

body{color:red;}

add.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <form action="/submit" method='get'>
        <p><input type="text" name='name'></p>
        <p><input type="text" name='age'></p>
        <p><input type="submit" value='儲存'></p>
    </form>
</body>
</html>

list.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="/public/css/global.css">
</head>
<body>
    <p><a href="/add">報名</a></p>
    <section>
      {{each rows}}
      <li>{{$value.name}} {{$value.age}}</li>
      {{/each}}
    </section>
</body>
</html>

index.js:

// 模擬資料庫
const DB = [
    {name: 'ph', age: '18'},
    {name: 'lj', age: '19'}
]
const http = require('http')
const fs = require('fs')
const template = require('art-template')

http.createServer(function(req, res){
    const url = req.url
    // URL模組的 WHATWG API。Constructor: new URL(input[, base])
    // api: https://nodejs.org/dist/v12.18.1/docs/api/url.html#url_constructor_new_url_input_base
    const urlObj = new URL(url, `https://${req.headers.host}`)
    // 列表頁面 list.html
    if(url === '/'){
        fs.readFile('./views/list.html', (err, data) => {
            if (err) throw err;
            const ret = template.render(data.toString(), {
                rows: DB
            });
            res.end(ret)
        })
    // 留言頁面 add.html
    }else if(url.indexOf('/add') === 0){
        fs.readFile('./views/add.html', (err, data) => {
            if (err) throw err;
            res.end(data)
        })
    // 提交留言
    }else if(urlObj.pathname === '/submit'){
        // 插入資料
        const row = {}
        row.name = urlObj.searchParams.get('name')
        row.age = urlObj.searchParams.get('age')
        DB.unshift(row);
        // 臨時重定向
        res.statusCode = '302'
        res.setHeader('Location', '/');
        res.end()
    }else if(urlObj.pathname.endsWith('.css')){
        fs.readFile('./' + url, (err, data) => {
            if (err) throw err;
            res.end(data)
        })
    }else{
        res.end('404')
    }
        
}).listen(3000)

package.json:

  • 可以先在 demo 路徑下執行 npm init -y 來幫助我們生成 package.json 檔案
  • 接著執行 npm install art-template 安裝外掛即可
{
    ...
    "dependencies": {
        "art-template": "^4.13.2"
    }
}

執行程式:

$ cd demo

// 自行安裝依賴包: npm install

// 啟動服務 - 前文已介紹筆者使用 nodemon 來代替 node 啟動服務
$ nodemon index

瀏覽器訪問 http://localhost:3000/,進入列表頁(list.html),頁面顯示:

報名

ph 18
lj 19

:如果 node 控制檯報錯,則需要你根據錯誤提示修改一下,比如你把資料夾 views 一不小心寫成了 view。

點選報名,進入報名頁面(add.html),顯示一個表單,輸入名字(pm)和年齡(22),點選儲存,則會重定向到人員資訊列表頁,頁面顯示:

報名

pm 22
ph 18
lj 19

至此,這個簡單的專案就已經完成。

接下來用 express 框架重寫該專案之前,我們得先介紹一下 express 相關的知識。

express 基礎知識

筆者通過 express 中文網 來介紹 express。這類技術網站稱之為 cooking(烹飪) 網站。好比教我們如何烹飪,得先買菜(安裝),然後放油、放蔥薑蒜,爆炒1分鐘...,一步一步告訴我們怎麼做,相對比較簡單。

進入 express 中文網,導航的選單如下:

  • 首頁
  • 快速入門
    • 安裝
    • hello-world
    • 基本路由
    • 靜態檔案
    • FAQ
    • ...
  • 指南
    • 路由
    • 開發中介軟體
    • 使用模板引擎
    • 整合資料庫
  • API參考手冊
  • ...

Tip: 主要介紹 express 重寫報名系統需要用到的知識點,更多細節請參考 express 官網

首頁

Express - 基於 Node.js 平臺,快速、開放、極簡的 Web 開發框架。

快速入門 - 安裝

$ npm install express

快速入門 - hello-world

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

啟動服務,訪問 http://localhost:3000/ 頁面會輸出 Hello World!;如果訪問其他路徑(例如 http://localhost:3000/a),則會以 404 響應。

快速入門 - 基本路由

路由是指確定應用程式如何響應客戶端對特定端點的請求,該特定端點是URI(或路徑)和特定的HTTP請求方法(GET,POST等)。

語法:app.METHOD(PATH, HANDLER)

以下定義了 4 個路由,請看示例:

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.post('/', function (req, res) {
  res.send('Got a POST request')
})

app.put('/user', function (req, res) {
  res.send('Got a PUT request at /user')
})

app.delete('/user', function (req, res) {
  res.send('Got a DELETE request at /user')
})

快速入門 - 靜態檔案

利用 Express 託管靜態檔案。

為了提供諸如影像、CSS 檔案和 JavaScript 檔案之類的靜態檔案,請使用 Express 中的 express.static 內建中介軟體函式。

語法:express.static(root, [options])

如果需要將 public 目錄下的圖片、CSS 檔案、JavaScript 檔案對外開放,下面兩種方式都可以。

方式1:

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

// 現在,你就可以訪問 public 目錄中的所有檔案了:

http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/hello.html

方式2:

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

// 現在,你就可以通過帶有 /static 字首地址來訪問 public 目錄中的檔案了。

http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css

Tip:提供給express.static函式的路徑是相對於您啟動節點程式的目錄的。 如果從另一個目錄執行Express App,則使用要提供服務的目錄的絕對路徑更為安全:

app.use('/static', express.static(path.join(__dirname, 'public')))

更多關於 __dirname,請看本文 path 模組 章節

快速入門 - FAQ

如何處理 404 響應?

app.use(function (req, res, next) {
  res.status(404).send("Sorry can't find that!")
})

如何設定一個錯誤處理器?

app.use(function (err, req, res, next) {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

如何渲染純 HTML 檔案?
不需要!無需通過 res.render() 渲染 HTML。 你可以通過 res.sendFile() 直接對外輸出 HTML 檔案。 如果你需要對外提供的資原始檔很多,可以使用 express.static() 中介軟體。

指南 - 路由

路由路徑匹配 acd 和 abcd:

app.get('/ab?cd', function (req, res) {
    res.send('ab?cd')
})

路由引數:

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }
})

express 提供了一些響應方法,res.end()、res.redirect()、res.render()、res.sendStatus()...,可以將響應傳送到客戶端,並終止請求-響應週期。 如果沒有從路由處理程式中呼叫這些方法,則客戶端請求將被掛起。

指南 - 開發中介軟體

從接收請求,到傳送響應,我們可以加入各種中介軟體來做一些處理。中介軟體又可以傳給下一個中介軟體處理。下面我們定義了一個 myLogger 的中介軟體,請看示例:

var express = require('express')
var app = express()

var myLogger = function (req, res, next) {
  console.log('LOGGED')
  next() // {1}
}

// 使用中介軟體
app.use(myLogger)

app.get('/', function (req, res) {
  res.send('Hello World!')
})

app.listen(3000)

每次請求,node 都會輸出 LOGGED。如果將 next() (行{1})註釋,再次請求,頁面將一直轉圈圈,因為響應被掛起了。

指南 - 使用模板引擎

筆者使用的模板引擎是前文已使用過的 art-template。開啟 art-template 官網,點選 Express 選單就能看到該模板在 express 中使用的方法。請看:

npm install express-art-template

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

// view engine setup
app.engine('art', require('express-art-template')); // {20}

app.set('views', path.join(__dirname, 'views'));

// routes
app.get('/', function (req, res) {
    res.render('index.art', {                       // {21}
        user: {
            name: 'aui',
            tags: ['art', 'template', 'nodejs']
        }
    });
});

模板檔案預設是 .art,可以改成 .html,只需要將 art(行{20}和行{21}) 改為 html 即可。

指南 - 整合資料庫

MongoDB

Mongoose

Tip: 後續將會使用 Mongoose 依賴包來將 MongoDB 資料庫加入我們的專案。

API參考手冊

由於我的下載的 express 是 4.17.1,所以我參考的 API 是 4.x。

req.body - 包含在請求正文中提交的資料的鍵值對。 預設情況下,它是未定義的,並且在使用諸如 body-parser 和 multer 之類的 body-parsing 中介軟體時填充。請看示例:

var express = require('express')

var app = express()

app.use(express.json()) // for parsing application/json
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded

app.post('/profile', function (req, res, next) {
  console.log(req.body)
  res.json(req.body)
})

express 重寫

在 node 實現的專案(demo)的基礎上,共 3 處變化: add.html、index.js 和 package.json。

1、add.html:method='get' 改為 method='post'

2、index.js:

const path = require('path')
const express = require('express')
const app = express()
// 填充 req.body
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded

// view engine setup
app.engine('html', require('express-art-template'));
// 可以通過下面語句更改模板檢視的資料夾,預設是 views。
// app.set('views', path.join(__dirname, 'views'));
// 模擬資料庫
const DB = [
    {name: 'ph', age: '18'},
    {name: 'lj', age: '19'}
];
const port = 3000
// 將靜態資源對外開放
app.use('/public', express.static('public'))

app.get('/', function (req, res) {
    res.render('list.html', {
        rows: DB
    });
});

app.get('/add', function (req, res) {
    res.render('add.html', {
        rows: DB
    });
});

app.post('/submit', function (req, res) {
    const row = {}
    row.name = req.body.name
    row.age = req.body.age
    DB.unshift(row);
    res.redirect('/')
});
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

// 處理 404 響應
app.use(function (req, res, next) {
    res.status(404).send("404")
})

3、package.json(即依賴包的變化):

{
    ...
    "dependencies": {
        "express": "^4.17.1",
        "express-art-template": "^1.0.1"
    }
}

執行的效果和用node寫的一樣,但編碼更優雅。

path 模組

路徑模組提供了用於處理檔案和目錄路徑的實用程式。 可以使用以下命令訪問它:

const path = require('path');

path.basename() 方法返回路徑的最後一部分。尾部目錄分隔符將被忽略。請看示例:

> path.basename('/foo/bar/baz/asdf/quux.html');
quux.html
> path.basename('/foo/bar/baz/asdf/quux.html', '.html');
quux
> path.basename('/foo/bar/baz/asdf/');
asdf

path.dirname() 方法返回路徑的目錄名稱。尾部目錄分隔符將被忽略。請看示例:

> path.dirname('/foo/bar/baz/asdf/quux');
/foo/bar/baz/asdf

path.extname(path) 返回副檔名

> path.extname('index.html');
.html
> path.extname('index.coffee.md');
.md
> path.extname('index.');
.
> path.extname('index');
''

path.parse() 方法返回一個物件,該物件的屬性表示路徑的重要元素。請看示例:

> path.parse('/home/user/dir/file.txt');
{
  root: '/',
  dir: '/home/user/dir',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}
> path.parse('C:\\path\\dir\\file.txt');
{
  root: 'C:\\',
  dir: 'C:\\path\\dir',
  base: 'file.txt',
  ext: '.txt',
  name: 'file'
}

path.join() 方法使用特定於平臺的分隔符作為分隔符,將所有給定的路徑段連線在一起,然後對結果路徑進行規範化。請看示例:

> path.join('/foo', 'bar', 'baz/asdf', 'quux');
\\foo\\bar\\baz\\asdf\\quux
> path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
\\foo\\bar\\baz\\asdf
> path.join('/foo', 'bar', 'baz/asdf', 'quux', './../..');
\\foo\\bar\\baz
> path.join('foo', {}, 'bar');
TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received an instance of Object

path.isAbsolute() 是否是絕對路徑。請看示例:

> path.isAbsolute('/foo/bar'); 
true
> path.isAbsolute('qux/');
false
> path.isAbsolute('.');
false

__dirname

如果你將 express 專案放在 demo 目錄上一層執行 $ nodemon index,在通過瀏覽器訪問 http://localhost:3000/,頁面會出現報錯資訊:Error: Failed to lookup view "list.html" in views directory "D:\實驗樓\node-study\views"

在檔案裡面用相對路徑是不靠譜的。相對於執行 node 的目錄,node 就是這麼設計。

每個模組都有 __dirname,表示該檔案的目錄,是一個絕對路徑,還有 __filename。請看示例:

Running node example.js from /Users/mjr

console.log(__filename);
// Prints: /Users/mjr/example.js
console.log(__dirname);
// Prints: /Users/mjr

我們可以通過 path.join(__dirname, 'xxx') 來修復上面的問題。將 index.js 改為下面的程式碼即可:

const path = require('path')
const express = require('express')
const app = express()
app.use(express.urlencoded({ extended: true })) // for parsing application/x-www-form-urlencoded

// view engine setup
app.engine('html', require('express-art-template'));
// 可以通過下面語句更改模板檢視的資料夾,預設是 views
// app.set('views', path.join(__dirname, 'views'));
// 模擬資料庫
const DB = [
    {name: 'ph', age: '18'},
    {name: 'lj', age: '19'}
];
const port = 3000
// 將靜態資源對外開放
app.use('/public', express.static(path.join(__dirname, 'public')))

app.get('/', function (req, res) {
    res.render(path.join(__dirname, 'views', 'list.html'), {
        rows: DB
    });
});

app.get('/add', function (req, res) {
    res.render(path.join(__dirname, 'views', 'add.html'), {
        rows: DB
    });
});

app.post('/submit', function (req, res) {
    const row = {}
    row.name = req.body.name
    row.age = req.body.age
    DB.unshift(row);
    res.redirect('/')
});
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

// 處理 404 響應
app.use(function (req, res, next) {
    res.status(404).send("404")
})

其他章節請看:

前端學習 node 快速入門 系列

相關文章