# Express项目实战

现在我们把前面学习的知识通过项目实战的形式进行应用,项目功能列表

  1. 用户注册
  2. 用户登陆
  3. 王者荣耀英雄数据管理(包含增删改查)

# 1. 创建Express服务器

  1. 安装Express依赖
npm install express --save
  1. 创建服务器,监听3000端口
// app.js代码
// 引入express
const express = require('express');
// 创建个 express 服务器
const app = express();
// 端口监听
app.listen(3000, () => {
    console.log('服务器启动成功提示');
});

# 2. 设置静态资源

静态资源就是我们在浏览器输入资源地址,服务器不做任何逻辑处理,直接把资源返回。

项目有两个目录需要设置为静态目录,分别是public用于存放html、css、js、图片和uploads用于存放上传图片目录。

// 设置静态资源目录
app.use(express.static('public'))
app.use( '/uploads' ,express.static('uploads'))

配置完毕后,可以通过

http://localhost:3000/add.html访问public/add.html文件

http://localhost:3000/uploads/icon.jpeg访问uploads/icon.jpeg文件

# 3. body-parser的使用

  1. 安装body-parser
npm install body-parser --save
  1. 配置中间件
// 使用bodyParser中间件处理post请求参数
app.use(bodyParser.urlencoded());

# 4. 封装路由

1 、新建router.js文件,配置路由信息

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

// 导出路由
module.exports = router;

2、在app.js文件应用路由

// 引入路由
const router = require('./router')
// 使用路由
app.use(router)

# 5. 数据库准备

# 5.1 创建数据库表结构

-- 创建hero表,记录英雄信息
create table heros(
	id int not null primary key auto_increment, 
	name varchar(255) not null, 
	gender varchar(255),
	img varchar(255),
	isdelete tinyint default 0)
	character set utf8;
-- 创建user表,用来记录用户注册信息
create table user (
	id int not null primary key auto_increment, 
	username varchar(255) not null, 
	password varchar(255))
	character set utf8;

# 5.2 nodejs连接数据库

  1. 安装nodejs操作MySQL数据库依赖
npm install mysql --save

2、创建model.js文件连接数据库

// 导入MySQL模块
const mysql = require('mysql');

// 创建一个数据库连接
var connection = mysql.createConnection({
    host:       '127.0.0.1'  // 数据库服务器的地址
    user:       'root',         // 数据库的账号 
    password:   'root',         // 数据库的密码
    database:   'heima'     // 数据库名称
});

// 连接数据库或打开数据库
connection.connect();

# 6. 用户注册

# 6.1 服务端代码编写

  1. model.js加入注册插入数据库代码
// 注册新用户
const register = function(data, callback) {
  const { username, password } = data;
  connection.query('insert into user set ?', { username, password }, callback)
}
// 导出方法
module.exports = {
  register
}
  1. router.js加入注册地址监听
// 用户注册
router.post('/register', (req, res) => {
  // 调用model层,把数据存到数据库
  model.register(req.body, (err, result) => {
    if (err) {
      res.json({ code: -1, msg: err })
    } else {
      res.json({ code: 200, msg: '注册成功'});
    }
  })
})

# 6.2 客户端代码编写

<!-- 需要使用引入第三方库进行md5加密 -->
<script src="./lib/md5.min.js"></script>
function register() {
  // 获取用户名和密码
  const username = $('#userName').val();
  // 处于安全考虑,对密码进行md5加密
  const password = md5($('#userPwd').val());
  if (username && password) {
    $.post('/register', { username, password }, (res) => {
      if (res.code === 200) {
        alert('恭喜,注册成功!')
      }
    })
  } else {
    alert('用户名或密码不能为空')
  }
}

# 7. 用户登陆

# 7.1 服务端代码编写

  1. model.js加入登录的数据查询
// 用户登录
const login = function(data, callback) {
  // 查找数据库,是否有匹配的用户
  const { username, password } = data;
  connection.query('select * from user where username=? and password=?', [ username, password], callback)
}
  1. router.js加入登录/login路由
// 用户登陆
router.post('/login', (req, res) => {
    model.login(req.body, (err, result) => {
      if (err) {
        res.json({ code: -1, msg: err })
      } else if (result.length === 0) {
        res.json({ code: -1, msg: '用户不存在或者密码错误' })
      } else {
        res.json({ code: 200, msg: '登录成功' })
      }
    })
});

# 7.2 客户端代码编写

  <script src="./lib/md5.min.js"></script>
  <script>
    function login() {
      // 获取用户名和密码
      const username = $('#userName').val();
      const password = md5($('#userPwd').val());
      $.post('/login', { username, password }, (res) => {
        if (res.code === 200) {
          alert('登录成功')
        } else {
          alert(res.msg);
        }
      })
    }
  </script>

# 8.获取所有英雄数据

# 8.1 服务端实现

router.js

// 获取所有英雄数据
router.get('/getHeroList', tokenUtil.checkRole, (req, res) => {
  model.getHeroList((err, result) => {
    if (err) {
      res.json({ code: -1, msg: err });
    } else {
      res.json({ code: 200, data: result });
    }
  });
});

model.js

// 获取所有英雄数据
const getHeroList = function(callback) {
  connection.query('select * from heros where isdelete=0 order by id', callback)
}

# 8.2 客户端实现

获取英雄数据

$.ajax({
  url: '/getHeroList',
  success: function (res) {
    // 调用模板引擎
    let html = template('herosTemp', res)
    $('#tbody').html(html)
  }
})

使用art-template渲染模版

  <script type="text/template" id='herosTemp'>
    {{each data}}
      <tr>
        <td>{{$index + 1}}</td>
        <td>{{$value.name}}</td>
        <td>{{$value.gender}}</td>
        <td><img src="/uploads/{{$value.img}}"></td>
        <td>
          <a href="./edit.html?id={{$value.id}}">修改</a>
          <a href="javascript:;" onclick='del({{$value.id}})'>删除</a>
        </td>
      </tr>
    {{/each}}
  </script>

# 9.新增英雄

image-20200717173531236

# 9.1 服务端实现

router.js

// 英雄数据的新增
router.post('/addHero', (req, res) => {
  model.addHero(req.body, (err, result) => {
    if (err) {
      res.json({ code: -1, msg: err });
    } else {
      res.json({ code: 200, msg: '添加成功' });
    }
  });
});
// 图片上传
router.post('/uploadFile', (req, res) => {
  var form = new formidable.IncomingForm();
  form.uploadDir = __dirname + '/uploads';
  form.keepExtensions = true;
  form.parse(req, (err, fields, files) => {
    if (err) {
      res.json({
        code: 201,
        msg: '上传失败',
      });
    } else {
      const filename = path.basename(files.img.path);
      res.json({
        code: 200,
        msg: '上传成功',
        img: filename,
      });
    }
  });
});

model.js

// 新增英雄
const addHero = function(hero, callback) {
  connection.query('insert into heros set ?', hero, callback)
}

# 9.2 客户端实现

// 实现文件上传
function upload() {
  let myfile = $('#img')[0].files[0];
  let formdata = new FormData();
  formdata.append('img', myfile);
  $.ajax({
    type: 'post',
    url: '/uploadFile',
    data: formdata,
    processData: false, // 告诉ajax不要进行数据的处理,formdata自己来处理
    contentType: false, // 造成ajax不要对数据编码,formdata自己来编码
    success: function (res) {
      console.log(res);
      if (res.code == 200) {
        // 实现图片预览
        $('#photo').attr('src', '/uploads/' + res.img);
        // 将图片名称存储到指定的隐藏域
        $('.userimg').val(res.img);
      }
    },
  });
}

# 10. 登录状态保持与Token机制(重点)

在讲token机制前,先跟同学们聊一下大家有没去过长隆水上乐园,我们进入乐园前是需要在门口进行验票,验证已经购票才可以进入游玩,但是总有人喜欢翻墙,不买票就去游玩,那乐园为了防止这种情况,就增加了一个验票流程,不但在门口需要验票,在每个游乐项目都要验票。

虽然可以杜绝翻墙的现象,但是游客就不舒服了,因为每次都要验票,排队好久才能游玩,体验实在太差。游乐场又想了一个办法,在大门口验票完后给大家发一个手环,当游玩其他项目的时候,看到戴着手环的游客就放行。

这个手环其实就是我们后面要讲到的token。

# 10.1 HTTP是无状态协议

无状态的意思是,你第一次发送请求登录成功后,第二次发送请求其他接口,服务端是不知道你已经登录成功,第二次请求和第一次请求是没有任何关联的。

Http的这个特性就产生了一个问题,导致登录状态的丢失,为了解决这个问题,你当然可以像游乐场解决翻墙问题一样,在每次请求的时候都验证用户的账号密码,但是重复的验证操作,会导致计算机资源的消耗,用户的使用体验就会变差。

游乐场也发现每次验票体验太差,后来给游客们发放手环解决验证问题,同样,计算机也可以利用这一机制解决该问题,我们把这个手环称为token。

# 10.2 实现简单token

createToken: 生成token

verifyToken: 验证token

checkRole: 验证是否有权限

const jwt = require("jsonwebtoken");
// 密钥,用来验证真伪
const key = "heima";
// 生成token
const createToken = function (username) {
  const token = jwt.sign({ username }, key, { expiresIn: "1h" });
  return token;
};

// 验证token是否有效
const verifyToken = function (token) {
  try {
    jwt.verify(token, key);
    return true;
  } catch (err) {
    return false;
  }
};

// 权限验证
const checkRole = function (req, res, next) {
  const token = req.headers.authorization;
  if (token) {
    const isValidate = verifyToken(token);
    if (isValidate) {
      next();
    } else {
      res.send({ code: -1, msg: "没有访问权限" });
    }
  } else {
    res.send({ code: -1, msg: "token不能为空" });
  }
};

module.exports = {
  createToken,
  verifyToken,
  checkRole
};

# 10.3 改造登录功能

服务端需要在登录成功后生成token,发送给客户端

router.post('/login', (req, res) => {
    model.login(req.body, (err, result) => {
      if (err) {
        res.json({ code: -1, msg: err })
      } else if (result.length === 0) {
        res.json({ code: -1, msg: '用户不存在或者密码错误' })
      } else {
        // 登录成功生成token
        const userToken = tokenUtil.createToken(req.body.username)
        res.json({ code: 200, msg: '登录成功', token: userToken })
      }
    })
});

客户端收到token后,需要保存token

$.post('/login', { username, password }, (res) => {
  if (res.code === 200) {
    //  把token存储到本地
    window.localStorage.setItem('token', res.token)
    alert('登录成功')
  } else {
    alert(res.msg)
  }
})

# 10.4 请求接口时把token带上

请求获取英雄列表时,把token发送给服务端

$.ajax({
  method: 'get',
  url: '/getHeroList',
  headers: {
    'Authorization': window.localStorage.getItem('token') // 把token放到请求头
  },
  success: (res) => {
    console.log(res)
  }
})

# 10.5 服务端验证token是否有效

// 请求头获取token
const token = req.headers.authorization;
// 验证token是否有效
const result = tokenUtil.verifyToken(token);

# 10.6 更好的验证方法

可以封装一个独立的验证权限的方法checkRole

// utils/token.js
// 权限验证
const checkRole = function(req, res, next) {
  const token = req.headers.authorization
  if (token) {
    const isValidate = verifyToken(token);
    if (isValidate) {
      next();
    } else {
      res.send({ code: -1, msg: '没有访问权限' })
    }
  } else {
    res.send({ code: -1, msg: 'token不能为空' })
  }
  
}

在路由调用

router.get('/getHeroList', tokenUtil.checkRole, (req, res) => {
  model.getHeroList((err, result) => {
    if (err) {
      res.json({ code: -1, msg: err })
    } else {
      res.json({ code: 200, data: result});
    }
  });
});