在服务器上运行 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
- 从 https://www.mongodb.com/download-center 下载 MongoDB 社区服务器。
- 创建
c:\data\db目录(默认存储目录)。 - 在命令窗口中运行如下命令启动 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 部署流程
- 环境准备 :确保服务器上安装了 Node.js、MongoDB 和相关的 NPM 包。
- 代码部署 :将项目代码上传到服务器,可以使用 Git 进行版本控制。
- 配置环境变量 :设置数据库连接字符串等环境变量。
- 启动应用 :在服务器上使用
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 图书应用,还对其进行了功能优化和拓展,并了解了应用的部署和维护方法。在实际开发中,可以根据需求进一步完善和优化应用,以满足不同用户的需求。
超级会员免费看
30

被折叠的 条评论
为什么被折叠?



