20、在服务器上运行 TypeScript 构建 Express 图书应用

在服务器上运行 TypeScript 构建 Express 图书应用

1. 基础配置与简单 Express 程序

1.1 package.json 配置

首先,我们需要一个 package.json 文件来管理项目的依赖和配置。以下是一个示例:

{
  "name": "node-app",
  "version": "0.0.1",
  "description": "NodeApp",
  "main": "server.js",
  "author": {
    "name": "Steve Fenton"
  },
  "dependencies": {
    "express": "4.15.4"
  },
  "devDependencies": {
    "@types/node": "8.0.26",
    "@types/express": "4.0.37"
  }
}

这个文件指定了项目的入口文件为 server.js ,并包含了 Express 依赖以及 Node 和 Express 的类型定义。

1.2 简单 Express 程序示例

以下是一个简单的 Express 程序示例:

import * as express from 'express';
const portNumber = 8080;
const app = express();
app.get('/', (request, response) => {
    response.send('You requested ' + request.query.firstname + ' ' + request.query.lastname);
})
app.listen(portNumber, 'localhost', () => {
    console.log('Listening on localhost:' + portNumber);
});

在这个示例中,当访问 http://localhost:8080/?firstname=John&lastname=Smith 时,会返回 “You requested John Smith”。

1.3 多路由处理

我们可以为不同的路由提供不同的处理函数,示例代码如下:

import * as express from 'express';
const portNumber = 8080;
const app = express();
app.get('/', (request, response) => {
    response.send('You requested ' + request.query.firstname + ' ' + request.query.lastname);
})
app.get('/One/', (request, response) => {
    response.send('You got handler One');
});
app.get('/Two/', (request, response) => {
    response.send('You got handler Two');
});
app.listen(portNumber, 'localhost', () => {
    console.log('Listening on localhost:' + portNumber);
});

不同的路由会有不同的响应:
| 路由地址 | 响应内容 |
| ---- | ---- |
| http://localhost:8080/One/ | “You got handler One” |
| http://localhost:8080/Two/ | “You got handler Two” |

1.4 错误处理

可以通过 app.use 方法为应用提供一个通用的错误处理函数,示例如下:

import * as express from 'express';
const portNumber = 8080;
const app = express();
app.get('/', (request, response) => {
    throw new Error('Deliberate Error!');
})
app.listen(portNumber, 'localhost', () => {
    console.log('Listening on localhost:' + portNumber);
});
app.use(function (error, request, response, next) {
    console.error(error.message);
    response.status(500).send('An error has occurred.');
});

当发生错误时,完整的错误堆栈会显示在命令窗口,而浏览器会显示通用的错误消息。

1.5 中间件

Express 中的中间件是一组按责任链组织的函数。可以通过 app.use 添加中间件,中间件函数必须遵循以下两种签名之一:
- Request Handler (request: Request, response: Response, next: NextFunction) => void;
- Error Request Handler (error: any, request: Request, response: Response, next: NextFunction) => void;

每个中间件依次被调用,必须调用 next() 函数调用下一个中间件,或者结束响应。如果没有中间件结束响应,将返回 404 错误。

2. Express 图书项目搭建

2.1 项目准备

要搭建一个 Express 图书项目,需要安装以下软件和 NPM 包:
- MongoDB Community Server,可从 www.mongodb.com/download-center 下载。
- 以下 NPM 包:

{
  "name": "pro-typescript-book-app",
  "version": "0.0.1",
  "description": "An example book application",
  "main": "server.js",
  "types": "server.d.ts",
  "author": {
    "name": "Steve.Fenton"
  },
  "dependencies": {
    "body-parser": "1.17.2",
    "express": "4.15.4",
    "method-override": "2.3.9",
    "mongoose": "4.11.9",
    "pug": "2.0.0-rc.3"
  },
  "devDependencies": {
    "@types/body-parser": "1.16.5",
    "@types/express": "4.0.37",
    "@types/method-override": "0.0.30",
    "@types/mongoose": "4.7.21",
    "@types/node": "8.0.26",
    "@types/pug": "2.0.4"
  }
}

2.2 服务器配置

以下是 server.ts 文件的配置:

import * as express from 'express';
import * as http from 'http';
import * as path from 'path';
import * as bodyParser from 'body-parser';
import * as methodOverride from 'method-override';
import * as routes from './routes/index';
const portNumber = 8080;
const app = express();
app.set('port', portNumber);
// Configure view templates
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
http.createServer(app).listen(app.get('port'), () => {
    console.log('Express server listening on port ' + app.get('port'));
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(methodOverride());
// routes
app.get('/', routes.index);
// static files, such as .css files
app.use(express.static('.'));

2.3 路由处理

/routes/index.ts 文件用于处理首页路由:

import * as express from 'express';
/* GET home page. */
export function index(request: express.Request, response: express.Response) {
    response.render('index', { title: 'Express' });
};

2.4 Pug 模板

首页 Pug 模板
extends layout
block content
  h1= title
  p Welcome to #{title}
  a(href='/book') Books
共享布局 Pug 模板
doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/style.css')
  body
    block content
CSS 样式表
body {
    padding: 50px;
    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
    color: #00B7FF;
}

2.5 运行 Express 服务器

使用以下命令运行 Express 服务器:

node server.js

运行后,在浏览器中访问 localhost:8080 即可看到首页。

2.6 添加图书路由

路由处理文件 routes/book.ts
import * as express from 'express';
declare var next: (error: any) => void;
/* GET /book */
export function list(request: express.Request, response: express.Response) {
    response.render('book', { 'title': 'Books', 'books': [] });
};
图书页面 Pug 模板 views/book.pug
extends layout
block content
  h1= title
  p Welcome to the #{title} page.
  p #{message}
更新 server.ts 文件
import * as book from './routes/book';
// ...
// routes
app.get('/', routes.index);
app.get('/book', book.list);

2.7 数据收集

更新图书页面 Pug 模板
extends layout
block content
  h1= title
  p Welcome to the #{title} page.
  p #{message}
  form(method='post')
    fieldset
        legend Add a Book
        div
            label Title *
                br
                input(type='text', name='book_title', required)
        div
            label Author *
                br
                input(type='text', name='author', required)
        div
            label ISBN
                br
                input(type='text', name='book_isbn', pattern='(?:(?=.{17}$)97[89][ -](?:[0-9]+[ -]){2}[0-9]+[ -][0-9]|97[89][0-9]{10}|(?=.{13}$)(?:[0-9]+[ -]){2}[0-9]+[ -][0-9Xx]|[0-9]{9}[0-9Xx])')
        div
            button Save
更新 routes/book.ts 文件
import * as express from 'express';
declare var next: (error: any) => void;
/* GET /book */
export function list(request: express.Request, response: express.Response) {
    response.render('book', { 'title': 'Books', 'books': [] });
};
/* POST /book */
export function submit(request: express.Request, response: express.Response) {
    const newBook = new Book({
        title: request.body.book_title,
        author: request.body.author,
        isbn: request.body.book_isbn
    });
    response.render('book', { title: 'Books', 'books': [newBook] });
}
更新 app.ts 文件中的路由
// routes
app.get('/', routes.index);
app.get('/book', book.list);
app.post('/book', book.submit);

2.8 安装 Mongoose

安装 MongoDB
  1. https://www.mongodb.com/download-center 下载 MongoDB 社区服务器。
  2. 创建 c:\data\db 目录(默认存储目录)。
  3. 在命令窗口中运行如下命令启动 MongoDB 服务器:
C:\Program Files\MongoDB\Server\3.4\bin\mongod.exe

2.9 数据存储

更新 routes/book.ts 文件
import * as express from 'express';
import * as mongoose from 'mongoose';
declare var next: (error: any) => void;
// MongoDB typically runs on port 27017
mongoose.connect('mongodb://localhost:27017/books', { useMongoClient: true });
// Defines a book
interface Book extends mongoose.Document {
    title: string;
    author: string;
    isbn: string;
}
// Defines the book database schema
const bookSchema = new mongoose.Schema({
    title: String, author: String, isbn: String
});
const Book = mongoose.model<Book>('Book', bookSchema);
/* GET /book */
export function list(request: express.Request, response: express.Response) {
    Book.find({})
        .then((res) => {
            response.render('book', { 'title': 'Books', 'books': res });
        })
        .catch((err) => {
            return next(err);
        });
};
/* POST /book */
export function submit(request: express.Request, response: express.Response) {
    const newBook = new Book({
        title: request.body.book_title,
        author: request.body.author,
        isbn: request.body.book_isbn
    });
    newBook.save()
        .then((res) => {
            response.redirect('/book');
        })
        .catch((err) => {
            return next(err);
        });
}
更新 Pug 模板以显示数据
table
    thead
        tr
            th Title
            th Author
            th ISBN
    tbody
        if books
            each book in books
                tr
                    td= book.title
                    td= book.author
                    td= book.isbn

2.10 数据存储流程

graph LR
    A[用户提交表单] --> B[调用 submit 函数]
    B --> C[创建新 Book 对象]
    C --> D[调用 save 方法保存到数据库]
    D --> E{保存成功?}
    E -- 是 --> F[重定向到 /book 页面]
    E -- 否 --> G[错误处理]

2.11 数据查询流程

graph LR
    A[用户访问 /book 页面] --> B[调用 list 函数]
    B --> C[调用 Book.find 方法查询数据库]
    C --> D{查询成功?}
    D -- 是 --> E[渲染页面并显示书籍数据]
    D -- 否 --> F[错误处理]

通过以上步骤,我们搭建了一个完整的 Express 图书应用,实现了多路由处理、数据收集和存储功能。

3. 应用功能优化与拓展

3.1 数据验证优化

在之前的代码中,虽然在 HTML 表单中对 ISBN 进行了简单的格式验证,但在服务器端也需要进行更严格的验证。可以在 routes/book.ts 文件中对提交的数据进行验证,示例如下:

import * as express from 'express';
import * as mongoose from 'mongoose';
declare var next: (error: any) => void;
// MongoDB typically runs on port 27017
mongoose.connect('mongodb://localhost:27017/books', { useMongoClient: true });
// Defines a book
interface Book extends mongoose.Document {
    title: string;
    author: string;
    isbn: string;
}
// Defines the book database schema
const bookSchema = new mongoose.Schema({
    title: { type: String, required: true },
    author: { type: String, required: true },
    isbn: { 
        type: String, 
        validate: {
            validator: function(v) {
                return /(?:(?=.{17}$)97[89][ -](?:[0-9]+[ -]){2}[0-9]+[ -][0-9]|97[89][0-9]{10}|(?=.{13}$)(?:[0-9]+[ -]){2}[0-9]+[ -][0-9Xx]|[0-9]{9}[0-9Xx])/.test(v);
            },
            message: props => `${props.value} is not a valid ISBN!`
        },
        required: false
    }
});
const Book = mongoose.model<Book>('Book', bookSchema);
/* GET /book */
export function list(request: express.Request, response: express.Response) {
    Book.find({})
        .then((res) => {
            response.render('book', { 'title': 'Books', 'books': res });
        })
        .catch((err) => {
            return next(err);
        });
};
/* POST /book */
export function submit(request: express.Request, response: express.Response) {
    const newBook = new Book({
        title: request.body.book_title,
        author: request.body.author,
        isbn: request.body.book_isbn
    });
    newBook.validate((err) => {
        if (err) {
            response.render('book', { title: 'Books', 'books': [], error: err.message });
        } else {
            newBook.save()
               .then((res) => {
                    response.redirect('/book');
                })
               .catch((err) => {
                    return next(err);
                });
        }
    });
}

3.2 分页功能实现

随着数据库中书籍数量的增加,一次性查询所有书籍可能会导致性能问题。可以实现分页功能,只查询指定页码的数据。在 routes/book.ts 文件中修改 list 函数,示例如下:

/* GET /book */
export function list(request: express.Request, response: express.Response) {
    const page = parseInt(request.query.page) || 1;
    const limit = 10;
    const skip = (page - 1) * limit;
    Book.find({})
       .skip(skip)
       .limit(limit)
       .then((res) => {
            Book.countDocuments({}).then((count) => {
                const totalPages = Math.ceil(count / limit);
                response.render('book', { 
                    'title': 'Books', 
                    'books': res,
                    currentPage: page,
                    totalPages: totalPages
                });
            });
        })
       .catch((err) => {
            return next(err);
        });
};

同时,在 views/book.pug 模板中添加分页导航:

if totalPages > 1
    nav
        ul.pagination
            if currentPage > 1
                li.page-item
                    a.page-link(href=`/book?page=${currentPage - 1}`) Previous
            each page in Array.from({ length: totalPages }, (_, i) => i + 1)
                li.page-item(class=page === currentPage ? 'active' : '')
                    a.page-link(href=`/book?page=${page}`)= page
            if currentPage < totalPages
                li.page-item
                    a.page-link(href=`/book?page=${currentPage + 1}`) Next

3.3 搜索功能添加

为了方便用户查找特定的书籍,可以添加搜索功能。在 routes/book.ts 文件中添加搜索逻辑,示例如下:

/* GET /book */
export function list(request: express.Request, response: express.Response) {
    const page = parseInt(request.query.page) || 1;
    const limit = 10;
    const skip = (page - 1) * limit;
    const searchQuery = request.query.search;
    let query = {};
    if (searchQuery) {
        query = {
            $or: [
                { title: { $regex: searchQuery, $options: 'i' } },
                { author: { $regex: searchQuery, $options: 'i' } },
                { isbn: { $regex: searchQuery, $options: 'i' } }
            ]
        };
    }
    Book.find(query)
       .skip(skip)
       .limit(limit)
       .then((res) => {
            Book.countDocuments(query).then((count) => {
                const totalPages = Math.ceil(count / limit);
                response.render('book', { 
                    'title': 'Books', 
                    'books': res,
                    currentPage: page,
                    totalPages: totalPages,
                    searchQuery: searchQuery
                });
            });
        })
       .catch((err) => {
            return next(err);
        });
};

views/book.pug 模板中添加搜索表单:

form(method='get')
    input(type='text', name='search', placeholder='Search by title, author or ISBN', value=searchQuery)
    button(type='submit') Search

3.4 功能拓展对比

功能 实现方式 优点 缺点
数据验证优化 在服务器端使用 Mongoose 验证 更严格的数据验证,提高数据质量 增加代码复杂度
分页功能实现 使用 skip limit 方法 提高性能,改善用户体验 需要额外的逻辑处理
搜索功能添加 使用 $regex 进行模糊查询 方便用户查找特定书籍 可能影响查询性能

4. 应用部署与维护

4.1 部署流程

  1. 环境准备 :确保服务器上安装了 Node.js、MongoDB 和相关的 NPM 包。
  2. 代码部署 :将项目代码上传到服务器,可以使用 Git 进行版本控制。
  3. 配置环境变量 :设置数据库连接字符串等环境变量。
  4. 启动应用 :在服务器上使用 node server.js 命令启动 Express 服务器。

4.2 维护建议

  • 日志记录 :使用日志记录工具(如 Winston)记录应用的运行状态和错误信息。
  • 监控性能 :使用工具(如 New Relic)监控应用的性能,及时发现和解决性能问题。
  • 定期备份 :定期备份 MongoDB 数据库,防止数据丢失。

4.3 应用维护流程

graph LR
    A[日常监控] --> B{是否有问题?}
    B -- 是 --> C[分析问题]
    C --> D[修复问题]
    D --> E[测试修复]
    E --> F{是否通过测试?}
    F -- 是 --> G[部署修复]
    F -- 否 --> C
    B -- 否 --> A
    G --> H[更新日志]
    H --> A

通过以上步骤,我们不仅搭建了一个完整的 Express 图书应用,还对其进行了功能优化和拓展,并了解了应用的部署和维护方法。在实际开发中,可以根据需求进一步完善和优化应用,以满足不同用户的需求。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值