Express入门

607 阅读8分钟

内容概要

  • Express.js是什么?
  • 使用中间件
  • 优雅的处理Requests和Responses 
  • 返回HTML页面

背景

服务端逻辑很复杂,我们需要更多关注的是业务逻辑,而不是底层细节 ,框架可以隐藏底层细节,提供帮助函数,工具与规则来帮助构建应用。 

Express.js是什么

Express.js 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。

Express.js以外的选择

  • 纯Node.js
  •  Koa
  •  Sails.js
  •  Hapi.js
  •  ...

 安装Express.js

npm install --save express
npm install --save-dev nodemon

使用nodemon启动服务,可以监听文件的变动,并自动重启服务。

package.json

{
    "name": "node-express", 
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "start": "nodemon app.js",
        "start-server": "node app.js"
    },
    "author": "",
    "license": "MIT",
    "dependencies": {
        "express": "^4.17.1"
    },
    "devDependencies": {
        "nodemon": "^2.0.2"
    }
}

添加中间件 

app.js

const http = require('http');
const express = require('express');
const app = express();

// 所有请求都会进入到中间件
app.use((req, res, next) => {
    console.log('In the middleware!');
    next(); // 使请求进入到下一个中间件
});

app.use((req, res, next) => {
    console.log('In another middleware!');
});

const server = http.createServer(app);
server.listen(3000);

终端运行

npm start

启动服务,浏览器访问localhost:3000,可以在终端上看到进入中间件的日志

中间件是怎样工作的

在浏览器上没有看到消息返回,我们可以在中间件中调用send接口返回消息

app.use((req, res, next) => {
    console.log('In another middleware!');
    res.send('<h1>Hello from Express!</h1>');
});

在浏览器中就可以看到Hello from Express!

Express.js send源码分析 

在github的express库的response.js文件send函数中可以看到以下代码

case 'string':
    if (!this.get('Content-Type')) {
        this.type('html');
    }
    break;

传参为字符串并未指定Content-Type时,默认设为html类型

处理不同的路由

app.use('/', (req, res, next) => {    
    console.log('This always runs!');    
    next();
});

app.use('/add-product', (req, res, next) => {  
    console.log('In another middleware!');  
    res.send('<h1>添加商品页</h1>');
});

app.use('/', (req, res, next) => {  
    console.log('In another middleware!');  
    res.send('<h1>Hello from Express!</h1>');
});

在app.use第一个参数传入请求路径,可以处理不同的请求

限制只处理POST请求

app.post('/product', (req, res, next) => {    
    res.redirect('/');
});

使用post,只有POST请求才能进入这个中间件

解析请求

可以使用body-parser包来解析请求

npm install --save body-parser

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));

app.use('/add-product', (req, res, next) => {  
    res.send('<form action="/product" method="POST"><input type="text" name="title"><button type="submit">添加商品</button></form>');
});

app.post('/product', (req, res, next) => {   
    console.log(req.body);    
    res.redirect('/');
});

app.use('/', (req, res, next) => {  
    res.send('<h1>Hello from Express!</h1>');
});

app.listen(3000);

在浏览器访问http://localhost:3000/add-product,在输入框输入商品名称,点击添加商品,在终端可以看到请求参数{ title: '测试' }

使用Express路由 

使用Express路由将路由处理代码移到单独的文件中

创建文件/routes/admin.js,/routes/shop.js

/routes/admin.js

const express = require('express');
const router = express.Router();

router.get('/add-product', (req, res, next) => {  
    res.send('<form action="/product" method="POST"><input type="text" name="title"><button type="submit">添加商品</button></form>'  );
});

router.post('/add-product', (req, res, next) => {  
    console.log(req.body);  
    res.redirect('/');
});

module.exports = router;

/routes/shop.js

const express = require('express');
const router = express.Router();

router.get('/', (req, res, next) => {  
    res.send('<h1>Hello from Express!</h1>');
});

module.exports = router;

app.js

const express = require('express');
const bodyParser = require('body-parser');

const app = express();

const adminRoutes = require('./routes/admin');
const shopRoutes = require('./routes/shop');

app.use(bodyParser.urlencoded({ extended: false }));

app.use(adminRoutes);
app.use(shopRoutes);

app.listen(3000);

使用Express路由使逻辑分离,在各自模块中处理自己的逻辑

添加404错误页面

app.js

app.use((req, res, next) => {    
    res.status(404).send('<h1>页面没有找到</h1>');
});

在所有中间件后加入以上代码,当访问不存在的路径时,会返回页面没有找到的提示,status设置返回消息的状态码为404

过滤路径 

app.use('/admin', adminRoutes);

在use的第一个参数传入路径参数/admin,那么adminRoutes中的所有路径前面都加上了/admin

此时/routes/admin.js的路径变为

// /admin/add-product => GET
router.get('/add-product', (req, res, next) => {  
    res.send('<form action="/admin/add-product" method="POST"><input type="text" name="title"><button type="submit">添加商品</button></form>');
});

// /admin/add-product => POST
router.post('/add-product', (req, res, next) => {  
    console.log(req.body);  
    res.redirect('/');
});

创建HTML页面 

/views/add-product.html

<!DOCTYPE html>
    <html lang="en">
    <head>    
        <meta charset="UTF-8">    
        <meta name="viewport" content="width=device-width, initial-scale=1.0">    
        <meta http-equiv="X-UA-Compatible" content="ie=edge">    
        <title>商品管理</title>
    </head>
    <body>    
        <header>        
            <nav>            
                <ul>                
                    <li><a href="/">商店</a></li>                
                    <li><a href="/admin/add-product">商品管理</a></li>            
                </ul>        
            </nav>    
        </header>    
        <main>        
            <form action="/admin/add-product" method="POST">            
                <input type="text" name="title">            
                <button type="submit">添加商品</button>        
            </form>    
        </main>
    </body>
</html>

/views/shop.html

<!DOCTYPE html>
<html lang="en">
    <head>   
         <meta charset="UTF-8">    
        <meta name="viewport" content="width=device-width, initial-scale=1.0">    
        <meta http-equiv="X-UA-Compatible" content="ie=edge">    
        <title>商店</title>
    </head>
    <body>    
        <header>        
            <nav>            
                <ul>                
                    <li><a href="/">商店</a></li>                
                    <li><a href="/admin/add-product">商品管理</a></li>            
                </ul>        
            </nav>    
        </header>    
        <main>        
            <h1>我的商品</h1>        
            <p>商品列表...</p>    
        </main>
    </body>
</html>

提供HTML页面服务 

修改/routes/admin.js和/routes/shop.js将HTML页面内容返回给前端

/routes/admin.js

const path = require('path');
const express = require('express');

const router = express.Router();

// /admin/add-product => GET
router.get('/add-product', (req, res, next) => {  
    res.sendFile(path.join(__dirname, '../', 'views', 'add-product.html'));
});

// /admin/add-product => POST
router.post('/add-product', (req, res, next) => {  
    console.log(req.body);  
    res.redirect('/');
});

module.exports = router;

注意路径拼接使用path.join可以不用管操作系统路径分隔符/和\的差异

返回404页面 

添加文件/views/404.html

<!DOCTYPE html>
<html lang="en">
    <head>    
        <meta charset="UTF-8">    
        <meta name="viewport" content="width=device-width, initial-scale=1.0">    
        <meta http-equiv="X-UA-Compatible" content="ie=edge">    
        <title>页面没有找到</title>
    </head>
    <body>    
        <h1>页面没有找到!</h1>
    </body>
</html>

app.js增加以下代码

app.use((req, res, next) => {    
    res.status(404).sendFile(path.join(__dirname, 'views', '404.html'));
});

使用帮助函数来导航 

添加文件/util/path.js

const path = require('path');

module.exports = path.dirname(process.mainModule.filename);

process.mainModule.filename为启动服务的文件完整路径

path.dirname(process.mainModule.filename)便可以获取到服务启动的路径

修改/routes/admin.js的路径拼接

const rootDir = require('../util/path');

// /admin/add-product => GET
router.get('/add-product', (req, res, next) => {  
    res.sendFile(path.join(rootDir, 'views', 'add-product.html'));
});

修改/routes/shop.js的路径拼接

const rootDir = require('../util/path');

router.get('/', (req, res, next) => {  
    res.sendFile(path.join(rootDir, 'views', 'shop.html'));
});

为页面加上样式

 添加文件/public/css/main.css

body {    
    padding: 0;    
    margin: 0;
    font-family: sans-serif;
}
main {
    padding: 1rem;
}
.main-header {
    width: 100%;
    height: 3.5rem;
    background-color: #dbc441;
    padding: 0 1.5rem;
}
.main-header__nav {
    height: 100%;
    display: flex;
    align-items: center;
}
.main-header__item-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
}
.main-header__item {
    margin: 0 1rem;
    padding: 0;
}
.main-header__item a {
    text-decoration: none;
    color: black;
}
.main-header__item a:hover,
.main-header__item a:active,
.main-header__item a.active {
    color: #3e00a1;
}

添加文件/public/css/products.css

.product-form {
    width: 20rem;
    max-width: 90%;
    margin: auto;
}
.form-control {
    margin: 1rem 0;
}
.form-control label,
.form-control input {
    display: block;
    width: 100%;
}
.form-control input {
    border: 1px solid #dbc441;
    font: inherit;
    border-radius: 2px;
}
button {
    font: inherit;
    border: 1px solid #3e00a1;
    color: #3e00a1;
    background: white;
    border-radius: 3px;
    cursor: pointer;
}
button:hover,
button:active {
    background-color: #3e00a1;
    color: white;
}

/views/add-product.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>商品管理</title>
        <link rel="stylesheet" href="/css/main.css">
        <link rel="stylesheet" href="/css/product.css">
    </head>
    <body>
        <header class="main-header"> 
            <nav class="main-header__nav">
                <ul class="main-header__item-list"> 
                    <li class="main-header__item"><a href="/">商店</a></li>
                    <li class="main-header__item"><a class="active" href="/admin/add-product">商品</a></li>
                </ul>
            </nav>
        </header>
        <main> 
            <form class="product-form" action="/admin/add-product" method="POST">
                <div class="form-control">
                    <label for="title">名称</label>
                    <input type="text" name="title" id="title">
                </div>
                <button type="submit">添加商品</button> 
            </form>
        </main>
    </body>
</html>

/views/shop.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>商品管理</title>
        <link rel="stylesheet" href="/css/main.css">
    </head>
    <body>
        <header class="main-header">
            <nav class="main-header__nav">
                <ul class="main-header__item-list">
                    <li class="main-header__item"><a class="active" href="/">商店</a></li>
                    <li class="main-header__item"><a href="/admin/add-product">商品管理</a></li>
                </ul>
            </nav>
        </header>
        <main>
            <h1>我的商品</h1>
            <p>商品列表...</p>
        </main>
    </body>
</html>

/views/404.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>页面没有找到</title>
        <link rel="stylesheet" href="/css/main.css">
    </head>
    <body>
        <header class="main-header">
            <nav class="main-header__nav">
                <ul class="main-header__item-list">
                    <li class="main-header__item"><a class="active" href="/">商店</a></li>
                    <li class="main-header__item"><a href="/admin/add-product">商品管理</a></li>
                </ul>
            </nav>
        </header>
        <h1>页面没有找到!</h1>
    </body>
</html>

此时访问localhost:3000,会发现css文件找不到,在下一节会来解决这个问题

创建静态文件服务

在app.js中增加以下代码来创建静态文件服务

app.use(express.static(path.join(__dirname, 'public')));

静态文件请求便会转到public目录下,此时再访问localhost:3000,就能请求到css文件了

总结 

Express.js是什么:

  • Express.js是Node.js框架-提供了很多实用的函数和工具以及应该该怎样构建的规则(中间件)
  • 高度易扩展,其它包可以插入到Express.js中使用(中间件)

中间件,next()和response

  • Express.js重度依赖中间件函数-可以很容易的通过调用use()来添加
  • 中间件函数处理请求,调用next()将请求导入到下一个函数或发送回复
路由
  • 通过path和method来过滤请求
  • 使用express.Router优雅的将路由分离到文件中
文件服务
  • 使用sendFile()发送HTML文件
  • 如果请求你的就是一个文件(例如css文件),可以通过express.static()为这些文件启动静态服务