前端学习 node 快速入门 系列 —— 报名系统 - [express]

其他章节请看:

前端学习 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 快速入门 系列

posted @ 2021-04-03 20:46  彭加李  阅读(602)  评论(0编辑  收藏  举报