前言
Connect
和Express
是两个热门的Node模块。Express
就是在Connect
的基础上,通过添加高层糖衣扩展和搭建出来的。
Connect
创建简单的Connect
程序
指令:
npm install connect@3.4.0
在要安装的模块后加@可以指定版本号
简单的Connect
程序应该是这样的:
const app=require('connect')();
app.use((req,res,next)=>{
res.end('Hello, world!');
});
app.listen(3000);
这个程序会用Hello, World!做出响应。也就是访问http://localhost:3000
的时候,页面显示Hello, world!
。传给app.use
的函数是个中间件,它以文本Hello, world!
作为响应结束了请求处理过程。中间件是所有Connect
和Express
程序的基础。
Connect
中间件的工作机制
Connect
中间件就是JavaScript
函数。这个函数一般会有三个参数:请求对象、响应对象、以及一个名为next的回调函数。一个中间件完成自己的工作,要执行后续的中间件时,可以调用这个next回调函数。
在中间件运行之前,Connect
会用分派器接管请求对象,然后交给程序中的第一个中间件。
借助中间件API,可以把一些小的构件块组合到一起,实现复杂的处理逻辑。
组合中间件
Connect中的use方法就是用来组合中间件的。我们先来定义两个中间件函数,然后把它们都添加到程序中。
const connect=require('connect');
function logger(req,res,next){
console.log('%s %s', req.method, req.url);
next();
}
function hello(req,res){
res.setHeader('Content-Type', 'text/plain');
res.end('hello world');
}
connect()
.use(logger)
.use(hello)
.listen(3000);
这两个中间件的名称签名不一样:一个有next,一个没有。因为后面这个中间件完成了HTTP响应,再也不需要把控制权交还给分派器了。
use()函数返回的是Connect程序的实例,支持方法链。也可以不写成链式调用的形式:
const app=connect();
app.use(logger);
app.use(hello);
app.listen(3000);
需要注意的是,如果某个中间件不调用next()
,那链在它后面的中间件就不会被调用。所以如果logger和hello两个函数的调用顺序反过来,则logger是不会执行的。
使用错误处理中间件
Connect
中有一种用来处理错误的中间件变体。跟常规的中间件相比,除了请求和响应对象外,错误处理中间件的参数中还多了一个错误对象。
//Connect中的错误处理中间件
const env=process.env.NODE_ENV||'development';
function errorHandler(err,req,res,next){
res.statusCode=500;
switch(env){
//开发模式和生产模式的响应不同
case 'development':
console.error('Error:');
console.error(err);
res.setHeader('Content-Type','application/json');
res.end(JSON.stringify(err));
break;
default:
res.end('Server error');
}
}
module.exports=errorHandler;
开发环境(development):开发同学开发时使用的环境,每位开发同学在自己的dev分支上工作,开发到一定程度后,各位同学会合并代码,进行联调。
生产环境(production):线上环境,用户使用的环境。由特定人员来维护。
设置环境变量
UNIX系统中设置环境变量的指令:
$ NODE_ENV=production node appWindows中用这个:
$ set NODE_ENV=production
$ node app
产品在开发环境中运行时,你可能想要看到尽可能详细的日志;但在生产环境中,你可能想让日志尽量精简,可能还要用gzip进行压缩。
用NODE_ENV设定程序的模式,Connect一般会根据环境变量NODE_ENV(process.env.NODE_ENV)
来切换不同服务器环境(比如生产环境和开发环境)下的行为。
当Connect
遇到错误时,它会切换,只去调用错误处理中间件。
假设有一个允许用户登录到管理区域的博客程序。如果负责用户路由的中间件引发了一个错误,则中间件blog
和admin
都会被跳过,因为它们不是错误处理中间件(只有三个参数)。然后Connect
看到接受错误参数的errorHandler
,就会调用它。中间件看起来像下面这样:
connect()
.use(router(require('./routes/user')))
.use(router(require('./routes/blog'))) //跳过
.use(router(require('./routes/admin'))) //跳过
.use(errorHandler);
Express
Express
是非常流行的Web
框架,以前是在Connect
的基础上搭建的。尽管提供了一些基本的功能,比如静态文件服务、URL路由和程序配置等,但它依然是极简的Web框架。
简单的Express
程序
const express=require('express');
const app=express();
app.get('/',(req,res)=>{
res.send('Hello');
});
app.listen(3000);
生成程序框架
安装指令:
npm install -g express-generator
生成程序
用-e或(--ejs)
指定要使用的模板引擎是EJS(EJS是一个嵌入JavaScript模板引擎,通过编译生成HTML代码)。执行express -e shoutbox
,shoutbox
是文件名。一个功能完备的程序会出现在shoutbox
目录中。其中会有描述项目和依赖项的package.json
文件、程序主文件、public
目录,以及一个放路由处理器的目录。
Express路由入门
根据官网定义:
Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, and so on).
路由的定义:
app.METHOD(PATH, HANDLER)
app
是express的一个实例METHOD
是HTTP
请求方法(小写)PATH
是服务器上的路径HANDLER
是在这个路径上发起HTTP
请求时执行的回调函数
Respond with Hello World!
on the homepage:
app.get('/', function (req, res) {
res.send('Hello World!')
})
Respond to POST request on the root route (/
), the application’s home page:
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')
})
做个简单的留言板程序
效果:
步骤:
- 创建消息模型
- 添加与消息相关的路由
- 创建消息表单
- 添加业务逻辑,用提交上来的表单数据创建消息
创建消息模型
在创建消息模型之前,需要先安装Node redis
模块。执行命令npm install --save redis
。而且要安装Redis
,Windows系统可以使用Redis Chocolately
。
安装教程:https://www.cnblogs.com/julyluo/p/6646155.html
官网没有windows版本,github下载速度太慢。
Redis就是一个数据库,可以将我们post的留言存储起来。
-
创建保存在线留言板消息条目的模型。
创建
models/entry.js
文件:'use strict'; const redis = require('redis'); const db = redis.createClient(); class Entry { constructor(obj) { for (let key in obj) { this[key] = obj[key]; } } static getRange(from, to, cb) { db.lrange('entries', from, to, (err, items) => { if (err) return cb(err); let entries = []; items.forEach((item) => { entries.push(JSON.parse(item)); }); cb(null, entries); }); } save(cb) { const entryJSON = JSON.stringify(this); db.lpush( 'entries', entryJSON, (err) => { if (err) return cb(err); cb(); } ); } static count(cb) { db.llen('entries', cb); } } module.exports = Entry;
-
创建消息表单
在
app.js
中添加如下路由部分app.get('/post', entries.form); app.post('/post', entries.submit);
即:使用get向
/post
发起http请求时,执行entries.form
回调函数。使用post向/post
发起http请求时,执行entries.submit
回调。
这个
entries
是从routes
中导入的模块。看一下定义:表单模块部分:
//routes/entries.js exports.form = (req, res) => { res.render('post', { title: 'Post' }); };
这里是用
views/post.ejs
,即表单,去渲染页面。//views/post.ejs <!DOCTYPE html> <html> <head> <title><%= title %></title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <% include menu %> <h1><%= title %></h1> <p>Fill in the form below to add a new post.</p> <form action='/post' method='post'> <p> <input type='text' name='entry[title]' placeholder='Title' /> </p> <p> <textarea name='entry[body]' placeholder='Body'></textarea> </p> <p> <input type='submit' value='Post' /> </p> </form> </body> </html>
表单中用了形如
entry[title]
之类的输入控件名称,需要用扩展的消息体解析器来解析。找到app.js
将app.use(bodyParser.urlencoded({ extended: true }));
改为:
app.use(bodyParser.urlencoded({ extended: true }));
-
实现消息提交功能
//将代码添加到文件routes/entries.js中,实现用表单提交上来的数据创建消息 exports.submit = (req, res, next) => { const data = req.body.entry; const user = res.locals.user; const username = user ? user.name : null; const entry = new Entry({ username: username, title: data.title, body: data.body }); entry.save((err) => { if (err) return next(err); res.redirect('/'); }); };
这里使用了Common JS进行模块化开发
-
添加显示消息的首页
在
routes/entries.js
中添加:const Entry = require('../models/entry'); exports.list = (req, res, next) => { const page = req.page; Entry.getRange(0, -1, (err, entries) => { if (err) return next(err); res.render('entries', { title: 'Entries', entries: entries }); }); };
-
视图
entries.ejs
<!DOCTYPE html> <html> <head> <title><%= title %></title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <% include menu %> <% entries.forEach((entry) => { %> <div class='entry'> <h3><%= entry.title %></h3> <p><%= entry.body %></p> <p>Posted by <%= entry.username %></p> </div> <% }) %> </body> </html>
在程序运行之前先在同一目录下创建menu.ejs创建菜单模板文件,后续会用到。目前空白着先。
-
添加与消息相关的路由
在
app.js
中:const entries = require('./routes/entries');
添加路由:
app.get('/', entries.list);
-
运行程序
先以管理员身份进入CMD,运行
redis-server
启用Redis
。在
terminal
中运行npm start
,访问http://localhost:3000/post
点击post后跳转到首页:
完成。