转自 LPZ 老师
原生的 http 模块在某些方面体现不足以应答咱们的开发需要,所以咱们就须要应用框架来放慢咱们的开发效率,框架的目标就是提高效率,让咱们的代码更对立。
在 Node 中,有很多 Web 开发框架,咱们这里以学习 Express
为主。
Express 介绍
- Express 是一个基于 Node.js 平台,疾速、凋谢、极简的 web 开发框架。
- 作者:tj
-
- tj 集体博客
-
- 出名的开源我的项目创建者和协作者
-
- Express、commander、ejs、co、Koa…
-
- 曾经来到 Node 社区,转 Go 了
-
- 知乎 – 如何对待 TJ 发表退出 Node.js 开发,转向 Go?
- 丰盛的 API 反对,弱小而灵便的中间件个性
- Express 不对 Node.js 已有的个性进行二次形象,只是在它之上扩大了 Web 利用所需的基本功能
- 有很多风行框架基于 Express
- Express 官网
- Express 中文文档(非官方)
- Express GitHub 仓库
起步
装置
参考文档:http://expressjs.com/en/start…
# 创立并切换到 myapp 目录
mkdir myapp
cd myapp
# 初始化 package.json 文件
npm init -y
# 装置 express 到我的项目中
npm i express
Hello World
参考文档:http://expressjs.com/en/start…
// 0. 加载 Express
const express = require("express");
// 1. 调用 express() 失去一个 app
// 相似于 http.createServer()
const app = express();
// 2. 设置申请对应的处理函数
// 当客户端以 GET 办法申请 / 的时候就会调用第二个参数:申请处理函数
app.get("/", (req, res) => {res.send("hello world");
});
// 3. 监听端口号,启动 Web 服务
app.listen(3000, () => console.log("app listening on port 3000!"));
根本路由
参考文档:http://expressjs.com/en/start…
路由(Routing)是由一个 URI(或者叫门路标识)和一个特定的 HTTP 办法(GET、POST 等)组成的,波及到利用如何解决响应客户端申请。
每一个路由都能够有一个或者多个处理器函数,当匹配到路由时,这个 / 些函数将被执行。
路由的定义的构造如下:
app.METHOD(PATH, HANDLER);
其中:
app
是 express 实例METHOD
是一个 HTTP 申请办法PATH
是服务端门路(定位标识)HANDLER
是当路由匹配到时须要执行的处理函数
上面是一些根本示例。
Respond with Hello World!
on the homepage:
// 当你以 GET 办法申请 / 的时候,执行对应的处理函数
app.get("/", function(req, res) {res.send("Hello World!");
});
Respond to POST request on the root route (/
), the application’s home page:
// 当你以 POST 办法申请 / 的时候,指定对应的处理函数
app.post("/", function(req, res) {res.send("Got a POST request");
});
Respond to a PUT request to the /user
route:
app.put("/user", function(req, res) {res.send("Got a PUT request at /user");
});
Respond to a DELETE request to the /user
route:
app.delete("/user", function(req, res) {res.send("Got a DELETE request at /user");
});
For more details about routing, see the routing guide.
解决动态资源
参考文档:http://expressjs.com/en/start…
// 凋谢 public 目录中的资源
// 不须要拜访前缀
app.use(express.static("public"));
// 凋谢 files 目录资源,同上
app.use(express.static("files"));
// 凋谢 public 目录,限度拜访前缀
app.use("/public", express.static("public"));
// 凋谢 public 目录资源,限度拜访前缀
app.use("/static", express.static("public"));
// 凋谢 publi 目录,限度拜访前缀
// path.join(__dirname, 'public') 会失去一个动静的绝对路径
app.use("/static", express.static(path.join(__dirname, "public")));
应用模板引擎
参考文档:
- Using template engines with Express
咱们能够应用模板引擎解决服务端渲染,然而 Express 为了放弃其极简灵便的个性并没有提供相似的性能。
同样的,Express 也是凋谢的,它反对开发人员依据本人的需要将模板引擎和 Express 联合实现服务端渲染的能力。
配置应用 art-template 模板引擎
参考文档:
- art-template 官网文档
这里咱们以 art-template 模板引擎为例演示如何和 Express 联合应用。
装置:
npm install art-template express-art-template
配置:
// 第一个参数用来配置视图的后缀名,这里是 art,则你存储在 views 目录中的模板文件必须是 xxx.art
// app.engine('art', require('express-art-template'))
// 这里我把 art 改为 html
app.engine("html", require("express-art-template"));
应用示例:
app.get("/", function(req, res) {
// render 办法默认会去我的项目的 views 目录中查找 index.html 文件
// render 办法的实质就是将读取文件和模板引擎渲染这件事儿给封装起来了
res.render("index.html", {title: "hello world"});
});
如果心愿批改默认的 views
视图渲染存储目录,能够:
// 第一个参数 views 是一个特定标识,不能乱写
// 第二个参数给定一个目录门路作为默认的视图查找目录
app.set("views", 目录门路);
其它常见模板引擎
JavaScript 模板引擎有很多,并且他们的性能都大抵雷同,然而不同的模板引擎也各有本人的特色。
大部分 JavaScript 模板引擎都能够在 Node 中应用,上面是一些常见的模板引擎。
- ejs
- handlebars
- jade
-
- 后改名为 pug
- nunjucks
解析表单 post 申请体
参考文档:
- GitHub – body-parser
在 Express 中没有内置获取表单 POST 申请体的 API,这里咱们须要应用一个第三方包:body-parser
。
装置:
npm install --save body-parser
配置:
var express = require("express");
// 0. 引包
var bodyParser = require("body-parser");
var app = express();
// 配置 body-parser
// 只有退出这个配置,则在 req 申请对象上会多进去一个属性:body
// 也就是说你就能够间接通过 req.body 来获取表单 POST 申请体数据了
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false}));
// parse application/json
app.use(bodyParser.json());
应用:
app.use(function(req, res) {res.setHeader("Content-Type", "text/plain");
res.write("you posted:\n");
// 能够通过 req.body 来获取表单 POST 申请体数据
res.end(JSON.stringify(req.body, null, 2));
});
应用 Session
参考文档:https://github.com/expressjs/…
装置:
npm install express-session
配置:
// 该插件会为 req 申请对象增加一个成员:req.session 默认是一个对象
// 这是最简略的配置形式,暂且先不必关怀外面参数的含意
app.use(
session({
// 配置加密字符串,它会在原有加密根底之上和这个字符串拼起来去加密
// 目标是为了减少安全性,避免客户端歹意伪造
secret: "itcast",
resave: false,
saveUninitialized: false // 无论你是否应用 Session,我都默认间接给你调配一把钥匙
})
);
应用:
// 增加 Session 数据
req.session.foo = "bar";
// 获取 Session 数据
req.session.foo;
提醒:默认 Session 数据是内存存储的,服务器一旦重启就会失落,真正的生产环境会把 Session 进行长久化存储。
路由
参考文档:
- Routing
一个十分根底的路由:
var express = require("express");
var app = express();
// respond with "hello world" when a GET request is made to the homepage
app.get("/", function(req, res) {res.send("hello world");
});
路由办法
// GET method route
app.get("/", function(req, res) {res.send("GET request to the homepage");
});
// POST method route
app.post("/", function(req, res) {res.send("POST request to the homepage");
});
路由门路
This route path will match requests to the root route, /.
app.get("/", function(req, res) {res.send("root");
});
This route path will match requests to /about.
app.get("/about", function(req, res) {res.send("about");
});
This route path will match requests to /random.text.
app.get("/random.text", function(req, res) {res.send("random.text");
});
Here are some examples of route paths based on string patterns.
This route path will match acd and abcd.
app.get("/ab?cd", function(req, res) {res.send("ab?cd");
});
This route path will match abcd, abbcd, abbbcd, and so on.
app.get("/ab+cd", function(req, res) {res.send("ab+cd");
});
This route path will match abcd, abxcd, abRANDOMcd, ab123cd, and so on.
app.get("/ab*cd", function(req, res) {res.send("ab*cd");
});
This route path will match /abe and /abcde.
app.get("/ab(cd)?e", function(req, res) {res.send("ab(cd)?e");
});
Examples of route paths based on regular expressions:
This route path will match anything with an“a”in the route name.
app.get(/a/, function(req, res) {res.send("/a/");
});
This route path will match butterfly and dragonfly, but not butterflyman, dragonflyman, and so on.
app.get(/.*fly$/, function(req, res) {res.send("/.*fly$/");
});
动静门路
Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: {"userId": "34", "bookId": "8989"}
定义动静的路由门路:
app.get("/users/:userId/books/:bookId", function(req, res) {res.send(req.params);
});
路由解决办法
app.route()
express.Router
Create a router file named router.js in the app directory, with the following content:
const express = require("express");
const router = express.Router();
router.get("/", function(req, res) {res.send("home page");
});
router.get("/about", function(req, res) {res.send("About page");
});
module.exports = router;
Then, load the router module in the app:
const router = require("./router");
// ...
app.use(router);
在 Express 中获取客户端申请参数的三种形式
例如,有一个地址:/a/b/c?foo=bar&id=123
查问字符串参数
获取 ?foo=bar&id=123
console.log(req.query);
后果如下:
{
foo: 'bar',
id: '123'
}
申请体参数
POST
申请才有申请体,咱们须要独自配置 body-parser
中间件才能够获取。
只有程序中配置了 body-parser
中间件,咱们就能够通过 req.body
来获取表单 POST
申请体数据。
req.body
// => 失去一个申请体对象
动静的门路参数
在 Express 中,反对把一个路由设计为动静的。例如:
// /users/:id 要求必须以 /users/ 结尾,:id 示意动静的,1、2、3、abc、dnsaj 任意都行
// 留神:: 冒号很重要,如果你不加,则就变成了必须 === /users/id
// 为啥叫 id,因为是动静的门路,服务器须要独自获取它,所以得给它起一个名字
// 那么咱们就能够通过 req.params 来获取门路参数
app.get("/users/:id", (req, res, next) => {console.log(req.params.id);
});
// /users/*/abc
// req.params.id
app.get("/users/:id/abc", (req, res, next) => {console.log(req.params.id);
});
// /users/*/*
// req.params.id
// req.params.abc
app.get("/users/:id/:abc", (req, res, next) => {console.log(req.params.id);
});
// /*/*/*
// req.params.users
app.get("/:users/:id/:abc", (req, res, next) => {console.log(req.params.id);
});
// /*/id/*
app.get("/:users/id/:abc", (req, res, next) => {console.log(req.params.id);
});
中间件
参考文档:
- Writing middleware for use in Express apps
- Using middleware
Express 的最大特色,也是最重要的一个设计,就是中间件。一个 Express 利用,就是由许许多多的中间件来实现的。
为了了解中间件,咱们先来看一下咱们现实生活中的自来水厂的净水流程。
在上图中,自来水厂从获取水源到污染解决交给用户,两头经验了一系列的解决环节,咱们称其中的每一个解决环节就是一个中间件。这样做的目标既进步了生产效率也保障了可维护性。
一个简略的中间件例子:打印日志
app.get("/", (req, res) => {console.log(`${req.method} ${req.url} ${Date.now()}`);
res.send("index");
});
app.get("/about", (req, res) => {console.log(`${req.method} ${req.url} ${Date.now()}`);
res.send("about");
});
app.get("/login", (req, res) => {console.log(`${req.method} ${req.url} ${Date.now()}`);
res.send("login");
});
在下面的示例中,每一个申请处理函数都做了一件同样的事件:申请日志性能(在控制台打印以后申请办法、申请门路以及申请工夫)。
针对于这样的代码咱们天然想到了封装来解决:
app.get("/", (req, res) => {// console.log(`${req.method} ${req.url} ${Date.now()}`)
logger(req);
res.send("index");
});
app.get("/about", (req, res) => {// console.log(`${req.method} ${req.url} ${Date.now()}`)
logger(req);
res.send("about");
});
app.get("/login", (req, res) => {// console.log(`${req.method} ${req.url} ${Date.now()}`)
logger(req);
res.send("login");
});
function logger(req) {console.log(`${req.method} ${req.url} ${Date.now()}`);
}
这样的做法天然没有问题,然而大家想一想,我当初只有三个路由,如果说有 10 个、100 个、1000 个呢?那我在每个申请路由函数中都手动调用一次也太麻烦了。
好了,咱们不卖关子了,来看一下咱们如何应用中间件来解决这个简略的小性能。
app.use((req, res, next) => {console.log(`${req.method} ${req.url} ${Date.now()}`);
next();});
app.get("/", (req, res) => {res.send("index");
});
app.get("/about", (req, res) => {res.send("about");
});
app.get("/login", (req, res) => {res.send("login");
});
function logger(req) {console.log(`${req.method} ${req.url} ${Date.now()}`);
}
下面代码执行之后咱们发现任何申请进来都会先在服务端打印申请日志,而后才会执行具体的业务处理函数。那这个到底是怎么回事?
中间件的组成
中间件函数能够执行以下任何工作:
- 执行任何代码
- 批改 request 或者 response 响应对象
- 完结申请响应周期
- 调用下一个中间件
中间件分类
- 应用程序级别中间件
- 路由级别中间件
- 错误处理中间件
- 内置中间件
- 第三方中间件
应用程序级别中间件
不关怀申请门路:
var app = express();
app.use(function(req, res, next) {console.log("Time:", Date.now());
next();});
限定申请门路:
app.use("/user/:id", function(req, res, next) {console.log("Request Type:", req.method);
next();});
限定申请办法:
app.get("/user/:id", function(req, res, next) {res.send("USER");
});
多个处理函数:
app.use(
"/user/:id",
function(req, res, next) {console.log("Request URL:", req.originalUrl);
next();},
function(req, res, next) {console.log("Request Type:", req.method);
next();}
);
多个路由处理函数:
app.get(
"/user/:id",
function(req, res, next) {console.log("ID:", req.params.id);
next();},
function(req, res, next) {res.send("User Info");
}
);
// handler for the /user/:id path, which prints the user ID
app.get("/user/:id", function(req, res, next) {res.end(req.params.id);
});
最初一个例子:
app.get(
"/user/:id",
function(req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id === "0") next("route");
// otherwise pass the control to the next middleware function in this stack
else next();},
function(req, res, next) {
// render a regular page
res.render("regular");
}
);
// handler for the /user/:id path, which renders a special page
app.get("/user/:id", function(req, res, next) {res.render("special");
});
路由级别中间件
创立路由实例:
var router = express.Router();
示例:
var app = express();
var router = express.Router();
// a middleware function with no mount path. This code is executed for every request to the router
router.use(function(req, res, next) {console.log("Time:", Date.now());
next();});
// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path
router.use(
"/user/:id",
function(req, res, next) {console.log("Request URL:", req.originalUrl);
next();},
function(req, res, next) {console.log("Request Type:", req.method);
next();}
);
// a middleware sub-stack that handles GET requests to the /user/:id path
router.get(
"/user/:id",
function(req, res, next) {
// if the user ID is 0, skip to the next router
if (req.params.id === "0") next("route");
// otherwise pass control to the next middleware function in this stack
else next();},
function(req, res, next) {
// render a regular page
res.render("regular");
}
);
// handler for the /user/:id path, which renders a special page
router.get("/user/:id", function(req, res, next) {console.log(req.params.id);
res.render("special");
});
// mount the router on the app
app.use("/", router);
另一个示例:
var app = express();
var router = express.Router();
// predicate the router with a check and bail out when needed
router.use(function(req, res, next) {if (!req.headers["x-auth"]) return next("router");
next();});
router.get("/", function(req, res) {res.send("hello, user!");
});
// use the router and 401 anything falling through
app.use("/admin", router, function(req, res) {res.sendStatus(401);
});
错误处理中间件
app.use(function(err, req, res, next) {console.error(err.stack);
res.status(500).send("Something broke!");
});
内置中间件
- express.static serves static assets such as HTML files, images, and so on.
- express.json parses incoming requests with JSON payloads. NOTE: Available with Express 4.16.0+
- express.urlencoded parses incoming requests with URL-encoded payloads. NOTE: Available with Express 4.16.0+
官网反对的中间件列表:
- https://github.com/senchalabs…
第三方中间件
官网中间件资源:http://expressjs.com/en/resou…
晚期的 Express 内置了很多中间件。起初 Express 在 4.x 之后移除了这些内置中间件,官网把这些功能性中间件以包的模式独自提供进去。这样做的目标是为了放弃 Express 自身极简灵便的个性,开发人员能够依据本人的需要去灵便的定制。上面是官网提供的一些罕用的中间件解决方案。
Middleware module | Description | Replaces built-in function (Express 3) |
---|---|---|
body-parser | Parse HTTP request body. See also: body, co-body, and raw-body. | express.bodyParser |
compression | Compress HTTP responses. | express.compress |
connect-rid | Generate unique request ID. | NA |
cookie-parser | Parse cookie header and populate req.cookies . See also cookies and keygrip. |
express.cookieParser |
cookie-session | Establish cookie-based sessions. | express.cookieSession |
cors | Enable cross-origin resource sharing (CORS) with various options. | NA |
csurf | Protect from CSRF exploits. | express.csrf |
errorhandler | Development error-handling/debugging. | express.errorHandler |
method-override | Override HTTP methods using header. | express.methodOverride |
morgan | HTTP request logger. | express.logger |
multer | Handle multi-part form data. | express.bodyParser |
response-time | Record HTTP response time. | express.responseTime |
serve-favicon | Serve a favicon. | express.favicon |
serve-index | Serve directory listing for a given path. | express.directory |
serve-static | Serve static files. | express.static |
session | Establish server-based sessions (development only). | express.session |
timeout | Set a timeout period for HTTP request processing. | express.timeout |
vhost | Create virtual domains. | express.vhost |
中间件利用
输入申请日志中间件
性能:实现为任何申请打印申请日志的性能。
logger.js
定义并导出一个中间件处理函数:
module.exports = (req, res, next) => {console.log(`${req.method} -- ${req.path}`);
next();};
app.js
加载应用中间件处理函数:
app.use(logger);
对立解决动态资源中间件
性能:实现 express.static() 动态资源解决性能
static.js
定义并导出一个中间件处理函数:
const fs = require("fs");
const path = require("path");
module.exports = function static(pathPrefix) {return function(req, res, next) {const filePath = path.join(pathPrefix, req.path);
fs.readFile(filePath, (err, data) => {if (err) {
// 持续往后匹配查找能解决该申请的中间件
// 如果找不到,则 express 会默认发送 can not get xxx
return next();}
res.end(data);
});
};
};
app.js
加载并应用 static 中间件处理函数:
// 不限定申请门路前缀
app.use(static("./public"));
app.use(static("./node_modules"));
// 限定申请门路前缀
app.use("/public", static("./public"));
app.use("/node_modules", static("./node_modules"));
错误处理
参考文档:
- Error handling
罕用 API
参考文档:
- 4.x API
express
- express.json
- express.static
- express.Router
- express.urlencoded()
Application
- app.set
- app.get
- app.locals
Request
- req.app
- req.query
- req.body
- req.cookies
- req.ip
- req.hostname
- Req.method
- req.params
- req.path
- req.get()
Response
- res.locals
- res.append()
- res.cookie()
- res.clearCookie()
- res.download()
- res.end()
- res.json()
- res.jsonp()
- res.redirect()
- res.render()
- res.send()
- res.sendStatus()
- res.set()
- res.status()
Router
- router.all()
- router.METHOD()
- router.use()
小案例
案例 Github 仓库地址:https://github.com/lipengzhou…
零、筹备
残缺目录构造如下:
.
├── node_modules npm 装置的第三方包目录,应用 npm 装包会主动创立
├── public 页面须要应用的动态资源
│ ├── css
│ ├── js
│ ├── img
│ └── ...
├── views 所有视图页面(只存储 html 文件)│ ├── publish.html
│ └── index.html
├── app.js 服务端程序入口文件,执行该文件会启动咱们的 Web 服务器
├── db.json 这里充当咱们的数据库
├── README.md 我的项目阐明文档
├── package.json 我的项目包阐明文件,存储第三方包依赖等信息
└── package-lock.json npm 的包锁定文件,用来锁定第三方包的版本和进步 npm 下载速度
# 创立我的项目目录
mkdir guestbook
# 进入我的项目目录
cd guestbook
# 初始化 package.json 文件
npm init -y
# 将 Express 装置到我的项目中
npm install express
一、Hello World
// 0. 加载 Express
const express = require("express");
// 1. 调用 express() 失去一个 app
// 相似于 http.createServer()
const app = express();
// 2. 设置申请对应的处理函数
// 当客户端以 GET 办法申请 / 的时候就会调用第二个参数:申请处理函数
app.get("/", (req, res) => {res.send("hello world");
});
// 3. 监听端口号,启动 Web 服务
app.listen(3000, () => console.log("app listening on port 3000!"));
二、配置模板引擎
参见:Express – 应用模板引擎
三、路由设计
申请办法 | 申请门路 | 作用 |
---|---|---|
GET | / | 渲染 index.html |
GET | /publish | 渲染 publish.html |
POST | /publish | 解决发表留言 |
app.get("/", function(req, res) {// ...});
app.get("/publish", function(req, res) {// ...});
app.post("/publish", function(req, res) {// ...});
四、走通页面渲染跳转
app.get("/", function(req, res) {res.render("index.html");
});
app.get("/publish", function(req, res) {res.render("publish.html");
});
五、装置解决 Bootstrap 款式文件
装置 bootstrap
到我的项目中:
npm install bootstrap
将 node_modules
目录凋谢进去:
app.use("/node_modules/", express.static("./node_modules/"));
六、将数据库中的 post 渲染到首页
JavaScript 后盾解决:
app.get("/", function(req, res) {fs.readFile("./db.json", function(err, data) {if (err) {
return res.render("500.html", {errMessage: err.message});
}
try {data = JSON.parse(data.toString());
res.render("index.html", {posts: data.posts});
} catch (err) {
return res.render("500.html", {errMessage: err.message});
}
});
});
index.html 页面模板字符串:
<ul class="list-group">
{{each posts}}
<li class="list-group-item">
<span class="badge">{{$value.time}}</span>
<span>{{$value.name}}</span> 说:<span>{{$value.content}}</span>
</li>
{{/each}}
</ul>
七、配置解析表单 post 申请体
参见:Express – 解析表单 post 申请体
八、解决 publish 表单提交
app.post("/publish", function(req, res) {
var body = req.body;
fs.readFile("./db.json", function(err, data) {if (err) {
return res.render("500.html", {errMessage: err.message});
}
try {data = JSON.parse(data.toString());
var posts = data.posts;
var last = posts[posts.length - 1];
// 生成数据增加到 post 数组中
posts.unshift({
id: last ? last.id + 1 : 1,
name: body.name,
content: body.content,
time: moment().format("YYYY-MM-DD HH:mm:ss") // moment 是一个专门用来解决工夫的 JavaScript 库
});
// 把对象转成字符串存储到文件中
// try-catch 无奈捕捉异步代码的异样
fs.writeFile("./db.json", JSON.stringify(data), function(err) {if (err) {
return res.render("500.html", {errMessage: err.message});
}
// 代码执行到这里,阐明写入文件胜利了
// 在 Express 中,咱们能够应用 res.redirect() 实现服务端重定向的性能
res.redirect("/");
});
} catch (err) {
return res.render("500.html", {errMessage: err.message});
}
});
});
九、案例优化:提取数据操作模块
const {readFile, writeFile} = require("fs");
const dbPath = "./db.json";
exports.getDb = getDb;
// 封装带来的益处:// 1. 可维护性
// 2. 其次才是重用
exports.addPost = (post, callback) => {getDb((err, dbData) => {if (err) {return callback(err);
}
// 获取数组中最初一个元素
const last = dbData.posts[dbData.posts.length - 1];
// 增加数据的 id 主动增长
post.id = last ? last.id + 1 : 1;
// 创立工夫
post.createdAt = "2018-2-2 11:57:06";
// 将数据增加到数组中(这里还并没有长久化存储)dbData.posts.push(post);
// 将 dbData 对象转成字符串长久化存储到文件中
const dbDataStr = JSON.stringify(dbData);
writeFile(dbPath, dbDataStr, err => {if (err) {return callback(err);
}
// Express 为 res 响应对象提供了一个工具办法:redirect 能够便捷的重定向
// res.redirect('/')
callback(null);
});
});
};
function getDb(callback) {readFile(dbPath, "utf8", (err, data) => {if (err) {return callback(err);
}
callback(null, JSON.parse(data));
});
}