Nodejs实战 —— Node Web 程序是什么
读 《node.js 实战 2.0》,进行学习记录总结。
Node Web 程序是什么
了解 Node Web 程序的结构
典型的 Node Web 程序是由下面几部分组成的:
- 'package.json : 一个包含依赖项列表和运行这个程序的命令的文件;'
- 'public:静态资源文件'
- 'node_modules: 项目的依赖项'
- '程序代码:'
- 'app.js 或 index.js :设置程序的代码'
- 'models: 数据库模型'
- 'views: 渲染页面的模板'
- 'controllers 或 routes : HTTP 请求处理器'
- 'middleware: 中间件组件'
开始一个新的 Web 程序
创建一个新目录,然后运行:
mkdir later
cd later
npm init -fy
安装 express
npm i express -S
可以在 package.json,看到 express 已经被安装上去了
"dependencies": {
"express": "^4.16.4"
}
如果要卸载可以使用下面的命令:
npm rm express -S
创建一个简单的服务器
// index.js
const express = require('express');
// express() 创建一个程序实例
const app = express();
const port = process.env.PORT || 3000;
// 添加路由处理器
app.get('/', (req, res) => {
res.send('Hello World');
});
// 程序实例绑定到一个 TCP端口
app.listen(port, () => {
console.log(`Express web app available at localhost: ${port}`);
});
新增 package.json 文件的 npm 脚本
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
运行程序:
npm start
可以看到:
跟其他平台比较
如果在 PHP 实现上面的程序,代码如下:
<?php echo '<p>Hello World</p>'; ?>
只有一行,并且一看就明白,那么这个更加复杂的 Node 示例有什么优点呢?二者是编程范 式上的区别:用 PHP,程序是页面;Node:程序是服务器,这个 Node 示例可以完全控制请求和响应,不用配置服务器就可以做所有事情,如果要用 HTTP 压缩或 URL 转发,可以将这些功能作为程序的逻辑来实现。不需要把 HTTP 程序和逻辑分开,它们是程序的一部分。
搭建一个 RESTful Web 服务
RESTful 服务可以方便创建和保存文件,将杂乱的 Web 页面变得整洁。
设计 RESTful 服务要想好操作,映射到 Express 路由上面。此例子,需要保存文章、获取文章、获取包含所有文章的列表和删除不再需要的文章这几个功能。分别对应下面的路由:
- 'POST /articles:创建新文章'
- 'GET /articles/:id :获取指定文章'
- 'GET /articles:获取所有文章'
- 'DELETE /articles/:id :删除指定文章'
简单的 Express 程序实现了这些路由,现在使用 JavaScript 来存储文章
// index.js
const express = require('express');
// express() 创建一个程序实例
const app = express();
// 数组定义 文章articles
const articles = [
{
title: 'Example'
}
];
const port = process.env.PORT || 3000;
// 添加路由处理器
app.get('/', (req, res) => {
res.send('Hello World');
});
// 获取所有文章
app.get('/articles', (req, res, next) => {
res.send(articles);
});
// 创建一篇文章
app.post('/articles', (req, res, next) => {
res.send('OK');
});
// 获取指定文章
app.get('/articles/:id', (req, res, next) => {
const id = req.params.id;
console.log('Fetching:', id);
res.send(articles[id]);
});
// 删除指定文章
app.delete('/articles/:id', (req, res, next) => {
const id = req.params.id;
console.log('Deleting:', id);
delete articles[id];
res.send({
message: 'Deleted'
});
});
// 程序实例绑定到一个 TCP端口
app.listen(port, () => {
console.log(`Express web app available at localhost: ${port}`);
});
Express 能自动将数组转换成 JSON 响应。
创建文章,处理 POST 请求需要消息体解析。安装解析器
npm i -S body-parser
添加消息解析器:
// 消息解析器
const bodyParser = require('body-parser');
app.use(bodyParser);
// 支持编码为 JSON 的消息请求体
app.use(bodyParser.json());
// 支持编码为表单的请求消息体
app.use(
bodyParser.urlencoded({
extended: true
})
);
// 创建一篇文章
app.post('/articles', (req, res, next) => {
const article = {
title: req.body.title
};
articles.push(article);
res.send(article);
});
添加数据库
就往 Node 程序中添加数据库而言,并没有一定之规,但一般会涉及下面几个步骤。
(1) 决定想要用的数据库系统。 (2) 在 npm 上看看那些实现了数据库驱动或对象-关系映射(ORM)的热门模块。 (3) 用 npm --save 将模块添加到项目中。 (4) 创建模型,封装数据库访问 API。 (5) 把这些模型添加到 Express 路由中。
创建数据库之前,添加路由处理代码,程序汇中的 HTTP 路由处理会向模型发起一个简单的调用
app.get('/articles', (req, res, next) => {
Article.all((err, articles) => {
if (err) return err;
res.send(articles);
});
});
这个 HTTP 路由是用来获取所有文章的,所以对应的模型方法应该类似于 Article.all 。这要取决于数据库 API,一般来说应该是 Article.find({}, cb) 和 Article.fetchAll().then(cb) ,其中的 cb 是回调(callback)的缩写.
上例子中选择了 SQLite 数据库,还有热门的 sqlite3 模块。SQLite 是进程内数据库, 所以很方便:你不需要在系统上安装一个后台运行的数据库。你添加的所有数据都会写到一个文件里,也就是说程序停掉后再起来时数据还在,所以非常适合入门学习时用。
制作自己的模型 API
文章应该能被创建、获取、删除,所以模型 Articles 应该提供一下的方法:
- 'Article.all(cb):返回文章'
- 'Article.find(id, cd):给定 ID, 找到对应的文章'
- 'Article.create({title, content}, cb):创建一篇有标题和内容的文章'
- 'Article.delete(id, cb):根据 ID 删除文章'
这些都可以用 sqlite3 模块实现。有了这个模块,我们可以用 db.all 获取多行数据,用 db.get 获取一行数据。不过先要有数据库连接。
安装 sqlite3:
npm i -S sqlite3
接着在 ds.js 中实现代码完成上述功能。
const sqlite3 = require('sqlite3');
const dbName = 'later.sqlite';
// 新建并连接到数据库文件
const db = new sqlite3.Database(dbName);
db.serialize(() => {
// 如果还没有,创建一个 articles 表
const sql = `
CREATE TABLE IF NOT EXISTS articles (id integer primary key, title, content TEXT)
`;
db.run(sql);
});
// 定义模型类 Article
class Article {
// 获取所有文章
static all(cb) {
db.all('SELECT * FROM articles', cb);
}
// 选择一篇指定的文章
static find(id, cb) {
db.find('SELECT * FROM articles WHERE id = ?', id, cb);
}
static create(data, cb) {
// 问号表示参数
const sql = `INSERT INTO articles(title, content) VALUES (?, ?)`;
db.run(sql, data, tile, data.contont, cb);
}
static delete(id, cb) {
if (!id) return cb(new Error('please provide an id'));
db.run('DELETE FROM articles WHERE id = ?', id, cb);
}
}
module.exports = db;
module.exports.Article = Article;
上面例子中创建了一个 名为 Article 的对象,它可以用标准的 SQL 和 sqlite3 创建、获取和删除数据。首先用 sqlite3. Database 打开一个数据库文件,然后创建表的 articles 。这里用到了 SQL 语法 IF NOT EXISTS , 以防一不小心重新运行了代码删除了前面的表重新创建一个。
数据库和表准备好了之后,就可以进行查询。用 sqlite3 的 all 方法可以获取的所有文章。用带问号的查询语法提供具体值的方法可以获取指定文章,sqlite3 会把 ID 插入到查询语句中,最后用 run 方法插入和删除数据。
基本的数据库功能已经实现了,接着我们把它添加到 HTTP 路由中。下面这段代码添加了所有方法,除了 POST 。(因为需要用到 readability 模块,但你还没有装好,所以要单独处理。)
const express = require('express');
// express() 创建一个程序实例
const app = express();
// 消息解析器
const bodyParser = require('body-parser');
// 加载数据库模块 Article 类
const Article = require('./db').Article;
app.use(bodyParser);
// 支持编码为 JSON 的消息请求体
app.use(bodyParser.json());
// 支持编码为表单的请求消息体
app.use(
bodyParser.urlencoded({
extended: true
})
);
// 数组定义 文章articles
const articles = [
{
title: 'Example'
}
];
const port = process.env.PORT || 3000;
// 添加路由处理器
app.get('/', (req, res) => {
res.send('Hello World');
});
// 获取所有文章
app.get('/articles', (req, res, next) => {
Article.all((err, articles) => {
if (err) return err;
res.send(articles);
});
});
// 获取指定文章
app.get('/articles/:id', (req, res, next) => {
const id = req.params.id;
Article.find(id, (err, article) => {
if (err) return next(err);
res.send(article);
});
});
// 删除指定文章
app.delete('/articles/:id', (req, res, next) => {
const id = req.params.id;
Article.delete(id, err => {
if (err) return next(err);
res.send({
message: 'Deleted'
});
});
});
// 程序实例绑定到一个 TCP端口
app.listen(port, () => {
console.log(`Express web app available at localhost: ${port}`);
});
module.exports = app;
db.js 文件放在同个目录,Node 会加载那个模块,然后用它获取所有文章,查找特定文章和删除一篇文章。
最后一件事情是实现创建文章的功能。还要用 readability 算法处理它们。
让文章可读并存起来
RSETful API 已经搭建好了,数据也可以持续化到数据库中,接下来该写代码把网页转换成简化版的 “阅读视图”。不过我们不用自己实现, npm 中有这样的模块。
在 npm 上搜索 readability 会找到很多模块。我们试一下 node-readability:
npm i node-readability -S
这个模块提供了一个异步函数,可以下载指定 URL 页面并将 HTML 转换成简版。
const read = require('node-readability');
const url = 'http://www.manning.com/cantelon2/';
read(url, (err, result) => {
// 结果有.title 和 content
});
还可以和数据库类结合起来,用 Article.create 方法保存文章:
read(url, (err, result) => {
Article.create({
title: result: title,
content: result.content
}, (err, article) => {
// 将文章保存到数据中
});
});
打开 index.js 添加新的 app.post 路由处理器,用上面的方法方法实现下载和保存文章的功能。
const read = require('node-readability');
// 创建一篇文章
app.post('/articles', (req, res, next) => {
// 从 POST 消息体重得到 URL
const url = req.body.url;
read(url, (err, result) => {
// 用 readabiliry 模块获取这个 URL 指向的页面
if (err || !result) res.status(500).send('Error downloading article');
Article.create(
{
title: result.title,
content: result.content
},
(err, article) => {
if (err) return err;
// 文章保存成功后发送提示
res.send('OK');
}
);
});
});
上面代码中,先从 POST 消息体中得到 URL ,然后用 node-readability 模块获取这个 URL 指向的页面,用模型类 Article 保存文章,如果有错误,将处理权交给 express 的中间件栈;否则,将 JSON 格式的文章发送给客户端。
添加用户界面
给 Express 项目添加界面需要做的几件事情,首先是使用模块引擎。
支持多种格式
基本做法是用 Express 的 res.format 方法,它可以根据请求发送响应格式的响应。它的用法如下,提供一个包含格式已经对应的响应函数的列表:
res.format({
html: () => {
res.render('articles.ejs', {
articles: articles
});
},
json: () => {
res.send(articles);
}
});
这段代码中, res.render 会渲染 view 文件夹下的 articles.ejs。但这需要安装模板引擎并创建相应的模板。
渲染引擎
模板引擎有很多,EJS 属于简单易学的那种。安装:
npm i ejs -S
res.render 可以渲染 EJS 格式 的 HTML 文件。在 view 文件夹中创建 articles.ejs
<% include head %>
<ul>
<% articles.forEach(article =>{ %>
<li>
<a href="/articles/<%= article.id %>">
<%= article.title %>
</a>
</li>
<% }) %>
</ul>
<% include foot%>
页眉和页脚模块
<!-- '页眉 -->'
<html>
<head>
<title></title>
</head>
<body>
<div class="container">
<!-- '页脚 --> '
</div>
</body>
</html>
用 npm 管理客户端依赖项
模块搞定之后,就是样式了。安装 bootstrap
npm install bootstrap --save
然后在 node_modules/boostrap 里面会看到源码,在 dist/css 里面有 css 文件,要让服务器响应静态文件请求才可以使用这些文件。
Express 自带了 一个名为 express.static 中间件,可以给浏览器发送客户端 JavaScript、图片和 CSS 文件。只要将它指向包含这些文件的目录,浏览器就可以访问到这些文件了。
// 加载 css文件
app.use('/css/bootstrap.css', express.static('node_modules/bootstrap/dist/css/bootstrap.css'));
接着在头模块中引入
<link rel="stylesheet" href="/css/bootstrap.css" />
这只是 Bootstrap 的 CSS。它还有很多文件,包括图标、字体以及 jQuery 插件。你可以往项目里添加更多文件,或者用工具把它们打包成一个文件,让浏览器更容易加载。
总结
- '用 npm init 和 Express 可以快速搭建出一个 Node Web 应用程序'
- 'npm install 是安装依赖项的命令'
- '可以用 Express 制作带有 RESTful API 的 Web 程序'
- '选择适合的数据库系统和数据库模板根据个人需求'
- '对于小项目来说,SQLite 很好用'
- '在 Express 中用 EJS 渲染模板很容易'
- 'Express 支持很多模块引擎,包括 Pug 和 Mustache'