案例
零. 准备数据—抓包,入库
-
用crawler抓取包
var Crawler = require("crawler"); var c = new Crawler({ maxConnections: 10, // This will be called for each crawled page callback: function (error, res, done) { if (error) { console.log(error); } else { var $ = res.$; // $ is Cheerio by default //a lean implementation of core jQuery designed specifically for the server // console.log(JSON.parse(res.body)); // 包含所有英雄的信息 JSON.parse(res.body).forEach((v)=>{ // 拼接所有英雄的详情页 // console.log(`https://pvp.qq.com/web201605/herodetail/${v.ename}.shtml`) // 详情请求 herotext.queue(`https://pvp.qq.com/web201605/herodetail/${v.ename}.shtml`); }) } done(); } }); c.queue('https://pvp.qq.com/web201605/js/herolist.json'); //存放所有英雄信息 var heros = [] // 第二个请求:详情爬虫实例 var herotext = new Crawler({ maxConnections: 10, // This will be called for each crawled page callback: function (error, res, done) { if (error) { console.log(error); } else { var $ = res.$ // 把获取到的英雄信息添加到数组中 heros.push({ heroName: $('.cover-name').text(), heroSkill: $('.skill-name>b').first().text(), heroIcon:'https:' + $('.ico-play').prev('img').attr('src'), isDelete:false }) // console.log($('.cover-name').text(),$('.skill-name>b').first().text()) // console.log($('.ico-play').prev('img').attr('src')) } done(); } });
-
用mysql-ithm入库
const my = require('mysql-ithm'); // 所有请求做完后入库 herotext.on('drain',()=>{ heroModel.insert(heros,(err,results)=>{ console.log(err); console.log(results); if (!err) console.log('增加成功'); }); }) // Queue just one URL, with default callback // herotext.queue('https://pvp.qq.com/web201605/js/herolist.json'); my.connect({ host: 'localhost',//数据库地址 port: '3306', user: 'root',//用户名,没有可不填 password: '0000',//密码,没有可不填 database: 'cqmanage'//数据库名称 }); //3.创建Model(表格模型:负责增删改查) //如果table表格存在则连接,不存在则自动创建 let heroModel = my.model('hero', { heroName: String, heroSkill: String, heroIcon: String, isDelete: String });
要等所有请求执行完之后在入库
一. 需要建立的接口
查询英雄列表
查询英雄详情
编辑英雄
删除英雄
新增英雄
验证码
用户注册
用户登录
退出登录
二. 服务器框架
// 导包
const express = require('express');
// 创建服务器
let app = express();
// 托管静态资源
app.use(express.static('www'));
app.use(express.static('upload'));
// 配置中间件
app.use(express.static('upload'));
// 写接口
// 查询英雄列表(没有删除的)
app.get('/hero/list',(req,res)=>{
// 有可能会差某一个,或者全部
res.send('测试')
})
// 查询英雄详情
app.get('/hero/info', (req, res) => {
res.send('测试')
})
// 编辑英雄
app.post('/hero/update', (req, res) => {
res.send('测试')
})
// 删除英雄(软删除)
app.post('/hero/delete', (req, res) => {
res.send('测试')
})
// 新增英雄
app.post('/hero/add', (req, res) => {
res.send('测试')
})
// 验证码
app.get('/hero/captcha', (req, res) => {
res.send('测试')
})
// 用户注册
app.post('/user/register', (req, res) => {
res.send('测试')
})
// 用户登录
app.post('/user/login', (req, res) => {
res.send('测试')
})
// 退出登录
app.get('/user/logout', (req, res) => {
res.send('测试')
})
// 开启服务器
app.listen(8088,()=>{
console.log('服务器已开启')
})
三. 密码加密
-
常用加密方式
-
base64: 容易被解密
-
AES:同时有密文和秘钥才能解密
-
rsa:由已知加密密钥推导出解密密钥在计算上是不可行的”密码体制
-
md5(密码常用):加密不可逆,不能解密
-
md5的解密用的是撞库操作,事先把常用的md5加密后的密文保存在库中。然后把需要解密的密文到库中遍历
-
为了避免撞库解密,一般md5加密会”加盐“。”加盐“后很难破解
-
-
四. Cookie
-
工作原理
- 当一个用户第一次访问浏览器时,服务器会生成一个cookie, 并且在响应头中添加cookie后返回给浏览器
- 浏览器会自动将cookie保存, 当下一次访问同一服务器时会自动将cookie放入请求头中发给服务器,这样服务器就可以识别用户
- 每一个服务器都有自己的cookie (cookie的域名识别)
- cookie是由服务器生成的
- 虽然浏览器本身也可以添加cookie,但是保持用户状态的cookie一定是由服务器生成的(seesion)
- 浏览器访问同一域名的请求时会自动将cookie发给服务器, 我们开发人员无需编写任何代码
- 如果有则发送,没有则不发
- cookie是在请求头中的
- cookie不能跨浏览器
-
执行过程
- 用户登录把账号和密码发给服务器
- 服务器验证账号密码,正确则返回一个cookie
- 客户端自动保存cookie,且以后每次请求都会自动把cookie发给服务器
- 服务器判断发过来的cookie是不是刚才发过去的,不是就不让访问
-
cookie和session
- cookie的键和值都存在游览器端
- sessio也把sessionid存在游览器端,但真正的数据存在服务器端
-
cookie使用:
- 设置响应头即可
res.writeHead(200,{ 'Content-Type':'text/plain; charset=utf-8', 'Set-Cookie':'userid=123456' // 实际开发返回的是密文 })
-
cookie-session包
下载:npm i cookie-session
使用:
var cookieSession = require('cookie-session') var express = require('express') var app = express() app.set('trust proxy', 1) // trust first proxy app.use(cookieSession({ name: 'session', keys: ['key1', 'key2'] })) app.use(function (req, res, next) { // Update views req.session.views = (req.session.views || 0) + 1 // Write response res.end(req.session.views + ' views') }) app.listen(3000)
五. 接口
-
判断登录
用到了cookie,后面有提到
// 判断登录接口 app.get('/isLoagin',(req,res)=>{ // 登录 res.send(req.session.user); })
-
查询英雄列表(没有删除的)
有参数就查询某几个,没有参数就查询全部
涉及到的点
模糊查询:会把所有包含关键字的查询出来
- select * from 表名 where 字段 like ‘%关键字%’
查询并且语法:and
- select * from 表名 where 字段 like ‘%关键字%’ and 字段名 = ‘条件’
// 查询英雄列表(没有删除的) app.get('/hero/list',(req,res)=>{ // console.log(search); // 有参数就有值,没有就undefined let {search} = req.query; if(!search){ // 没有参数,查询所有 heroModel.find('isDelete="false"',(err,results)=>{ if(err){ res.send({ code: 500, msg: '服务器内部错误' }) }else{ res.send({ code: 200, data:results }) } }) }else{ // 有参数,查询某几个 heroModel.find(`heroName like "%${search}%" and isDelete="false"`, (err, results) => { if (err) { res.send({ code: 500, msg: '服务器内部错误' }) } else { res.send({ code: 200, data: results }) } }) } })
-
查询英雄详情
获取id,根据id查询信息然后返回
app.get('/hero/info', (req, res) => { let {id} = req.query; // 获取get请求中的英雄ID heroModel.find(`id = ${id}`, (err, results) => { if (err) { res.send({ code: 500, msg: '服务器内部错误' }) } else { res.send({ code: 200, // 返回的是数组,取第0项的值就可以了,不用返回数组 data:results[0] }) } }); })
-
编辑英雄
因为有可能不改头像,只改名字和技能。
所以要先用file判断改没改头像,当file为undefined表示不改头像
用if判断,如果不为undefined则修改头像
app.post('/hero/update', (req, res) => { // console.log(req.file); // 没传值是undefined let {id,heroName,heroSkill} = req.body; let obj = { heroName, heroSkill }; if (req.file != undefined) { // 传头像了代表修改头像 obj.heroIcon = 'http://127.0.0.1:8088/' + req.file.filename; } // 把数据更新到数据库中 heroModel.update(`id = ${id}`, obj, (err, results) => { if (err) { res.send({ code: 500, msg: '服务器内部错误' }) } else { res.send({ code: 200, msg: '修改成功' }) } }); })
-
删除英雄
接收要删除的英雄id,根据id软删除
app.post('/hero/delete', (req, res) => { // 接收要删除的英雄id let {id} = req.body; heroModel.update(`id = ${id}`, {isDelete : 'true'}, (err, results) => { if (err) { res.send({ code: 500, msg: '服务器内部错误' }) } else { res.send({ code: 200, msg: '删除成功' }) } }); })
-
新增英雄
接收英雄名、英雄技能、头像
req.file.filename----头像文件
req.body----非文件参数
app.post('/hero/add', upload.single('heroIcon'), (req, res) => { // 接收英雄名、英雄技能、头像 let {heroName,heroSkill} = req.body; let heroIcon = 'http://127.0.0.1:8088/' + req.file.filename; heroModel.insert({ heroName, heroSkill, heroIcon, isDelete:false },(err, results) => { if (err) { res.send({ code: 500, msg: '服务器内部错误' }) } else { res.send({ code: 200, msg: '新增成功' }) } }); })
-
验证码
- 过程:
首先服务器生成一个验证码,验证码图片传给游览器,自己留一份验证码文本
用户输入的验证码文字传给服务器,服务器通过传过来的文字和保留的文本,验证是否正确
- 使用到了一个插件svg-captcha来生成验证码
// 验证码 app.get('/captcha', (req, res) => { // 创建一个验证码 var captcha = svgCaptcha.create({ size:4, ignoreChars: '0o1il', noise:3, color:'blue', background:'black' }); console.log(captch.text); // 验证码文本 // 返回验证码 res.type('svg'); res.status(200).send(captcha.data); })
-
用户注册
- 获取用户输入账号密码和验证码,并把密码加密发给服务器
- 判断验证码是否正确,正确则把账号和加密后的密码存入数据库
app.post('/user/register', (req, res) => { // 接收账号密码验证码 let {userName,passWord,code} = req.body; // 判断验证码是否正确 if (code.toLocaleLowerCase() != cap.toLocaleLowerCase()){ // 验证码错误 res.send({ code:402, msg:'验证码错误' }) }else{ // 验证码正确 userModel.find(`userName='${userName}'`,(err,results)=>{ if(err){ res.send({ code: 500, msg: '服务器错误'+err }) }else{ if(results.length>0){ res.send({ code: 401, msg: '用户名已存在' }) }else{ // 用户名不存在则创建 userModel.insert({ userName, passWord },(err,results)=>{ if(err){ res.send({ code: 401, msg: '服务器错误,注册失败'+err }) }else{ res.send({ code: 200, msg:'注册成功' }) } }) } } }) } })
-
用户登录
- 获取用户输入账号密码,并把密码加密发给服务器
- 服务器去数据库中查找有没有账号
- 有账号则对比服务器和用户输入的加密后的密码是否一致
- 一致则登录成功,否则失败
// 用户登录 app.post('/user/login', (req, res) => { // 获取用户登录时输入的用户名和密码 let {userName,passWord} = req.body; // 判断账号密码是否一致 userModel.find(`userName='${userName}' and passWord='${passWord}'`, (err, results) => { if(err){ res.send({ code: 500, msg: '服务器错误' + err }) }else{ if (results.length > 0) { res.send({ code: 200, msg: '登录成功' }) }else{ res.send({ code: 401, msg: '账号或密码错误' }) } } }) })
-
退出
清除cookie,重定向到登录页
// 退出登录 app.get('/user/logout', (req, res) => { req.session = null; res.writeHead(302,{ 'Location':'../login.html' }); res.end(); })
六. 前端页面
-
主页
<script> // 入口函数 $(function () { function getDate(){ // 进页面直接发请求 $.ajax({ type: 'get', url: 'http://127.0.0.1:8088/hero/list', success: function (backData) { if (backData.code == 200) { // 调用模板引擎的核心方法 var resHtml = template('heroListtemp', backData); $('tbody').html(resHtml); } else { alert(backData.msg); } } }) } getDate(); // 查询英雄名 // 去掉默认的提交行为,获取用户输入 $('#searchBtn').on('click',(e)=>{ // 去掉默认提交 e.preventDefault(); console.log('dddddd'); // 获取用户输入 let search = $('#search').val().trim(); $.ajax({ type: 'get', url: 'http://127.0.0.1:8088/hero/list', data: { search }, success: function (backData) { if (backData.data.length == 0) { $('tbody').html('没有数据'); return } if (backData.code == 200) { // 调用模板引擎的核心方法 var resHtml = template('heroListtemp', backData); $('tbody').html(resHtml); } else { alert(backData.msg); } } }) }) // 删除 给所有删除按钮设置点击事件(事件委托) $('tbody').on('click','.btn-delet',function(){ if(confirm('你确定要删除嘛')){ // 获取当前点击删除的英雄id var id = $(this).attr('data-id') $.ajax({ type: 'post', url: 'http://127.0.0.1:8088/hero/delete', data: { id }, success: function (backData) { if (backData.code == 200) { alert('删除成功') getDate(); } else { alert(backData.msg); } } }) } }) }) </script>
-
用户登录页
<script> // 入口函数 $(function () { // 登录按钮的点击事件 $('.btn-login').on('click', function (e) { // 阻止表单的默认 e.preventDefault(); let userName = $('#username').val().trim(); // 密码md5加密 $('#password').val(md5($('#password').val().trim(), 'calaxh')); let passWord = $('#password').val().trim(); // 1.3 ajax发送请求 $.ajax({ url:'http://127.0.0.1:8088/user/login', type:'post', dataType:'json', data:{ userName, passWord }, // data:$('#form').serialize(), success: function(backData){ if (backData.code == 200) { alert(backData.msg); // 跳转首页 window.location.href='./index.html'; } else { alert(backData.msg); } } }); }) }); </script>
-
用户注册页
<script> // 入口函数 $(function () { // 点击验证码图片刷新验证码 $('.code').click(function () { /* img标签自带缓存功能,如果src一致,则只发一次请求 解决方案:添加随机参数 */ $(this).attr('src', `http://127.0.0.1:8088/captcha?sb=${Math.random()}`); }); // 注册按钮的点击事件 $('#register').click(function (e) { // 去除默认提交 e.preventDefault(); // 获取用户输入 let userName = $('#username').val().trim(); // md5加密 md5(); // 第一个参数:要加密的明文 // 第二个参数:加盐(作用:增加密码的复杂度;特点:同样的明文,盐不同,得到的密文不同) $('#password').val(md5($('#password').val().trim(), 'calaxh')); let code = $('#code').val().trim(); // 在密码加密之后获取密码,获取到的就是密文 let passWord = $('#password').val().trim(); // 发送ajax请求 $.ajax({ url: 'http://127.0.0.1:8088/user/register', type: 'post', dataType: 'json', data: { userName, passWord, code }, // data: $('#form').serialize(), success: function (backData) { if (backData.code == 200) { alert('注册成功'); // 跳转首页 window.location.href = "./login.html"; } else { alert(backData.msg); } } }); }) }) </script>
-
添加英雄页
<script> // 入口函数 $(function () { // 图片预览 $('#icon').on('change',function (){ // 获取选择的文件 let file = this.files[0]; // 生成文件url let url = URL.createObjectURL(file); // 将url赋值给img的src $('#iconImg').attr('src', url); }); // 文件上传 $('.btn-insert').on('click',function(e){ // 禁用表单默认提交事件 e.preventDefault(); // 创建FormData对象 获取新增英雄的信息 var formd = new FormData(document.querySelector('form')); $.ajax({ url:'http://127.0.0.1:8088/hero/add', type:'post', data: formd, contentType: false, processData: false, success: function(backData){ console.log(backData); if (backData.code==200) { alert('新增成功'); // 新增成功跳转首页 window.location.href='./index.html'; } else { alert(backData.msg); } } }); }); }) </script>
-
修改英雄页
<script> // 入口函数 $(function () { // 获取从传递过来的英雄id let id = window.location.search.split('=')[1]; // 是否直接跳转到当前页面 if(id == undefined){ window.location.href = './index.html'; } // 发送ajax请求 $.ajax({ url: 'http://127.0.0.1:8088/hero/info', type: 'get', data: { id }, success: function (backData) { if(backData.code == 200){ $('#id').val(id); $('#name').val(backData.data.heroName); $('#skill').val(backData.data.heroSkill); $('#iconImg').attr('src', backData.data.heroIcon); } else { alert(backData.msg); } // console.log(backData); } }); // 头像预览 $('#icon').on('change', function () { // 获取选择的文件 let file = this.files[0]; // 生成文件url let url = URL.createObjectURL(file); // 将url赋值给img的src $('#iconImg').attr('src', url); }); // 修改数据 // 给编辑按钮设置点击事件 $('.btn-save').on('click',function(e){ // 去除默认提交行为 e.preventDefault(); // 获取编辑后的英雄信息 var formd = new FormData(document.querySelector('form')); // 发送ajax请求 $.ajax({ url: 'http://127.0.0.1:8088/hero/update', type: 'post', data: formd, contentType: false, processData: false, success: function (backData) { // console.log(backData); if (backData.code == 200) { alert('修改成功'); // 新增成功跳转首页 window.location.href = './index.html'; } else { alert(backData.msg); } } }); }) }) </script>
七. 优化后接口完整代码
// 导包
const express = require('express'); // 服务器相关
const hm = require('mysql-ithm'); // 数据库操作相关
const multer = require('multer'); // 数据库文件处理
const bodyParser = require('body-parser');
var svgCaptcha = require('svg-captcha'); // 验证码
const cros = require('cors'); // 解决跨域
const cookieSession = require('cookie-session'); // cookie
const path = require('path');
// 新建文件夹
var upload = multer({ dest: 'uploads/' })
//连接数据库
//如果数据库存在则连接,不存在则会自动创建数据库
hm.connect({
host: 'localhost',//数据库地址
port: '3306',
user: 'root',//用户名,没有可不填
password: '0000',//密码,没有可不填
database: 'cqmanage'//数据库名称
});
// 创建Model(表格模型:负责增删改查)
// 如果table表格存在则连接,不存在则自动创建
let heroModel = hm.model('hero', {
heroName: String,
heroSkill: String,
heroIcon: String,
isDelete: String
});
let userModel = hm.model('user', {
userName: String,
passWord: String,
});
// 创建服务器
let app = express();
// 托管静态资源
app.use(express.static(path.join(__dirname,'www')));
app.use(express.static('uploads'));
// 配置中间件
app.use(cros()); // 解决跨域问题
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieSession({
name: 'session',
keys: ['calca', 'ttsxsa'], // 加盐
maxAge: 24*60*60*1000 // 过期的时间 24小时
}))
// 写接口
// 判断登录接口
app.get('/isLoagin',(req,res)=>{
// 登录
res.send(req.session.user);
})
// 查询英雄列表(没有删除的)
app.get('/hero/list', (req,res)=>{
// 查询当前cookie,如果没有登录直接访问list接口,那么cookie是undefined
// 如果登录之后访问会得到值
// console.log(req.session.user);
// 有可能会查询某几个,或者全部
// console.log(search); // 有参数就有值,没有就undefined
let {search} = req.query;
if(!search){
// 没有参数,查询所有
heroModel.find('isDelete="false"',(err,results)=>{
if(err){
res.send({
code: 500,
msg: '服务器内部错误'
})
}else{
res.send({
code: 200,
data:results
})
}
})
}else{
// 有参数,查询某几个
// 模糊查询:select * from 表名 where 字段 like ‘%关键字%’ 会把所有包含关键字的查询出来
heroModel.find(`heroName like "%${search}%" and isDelete="false"`, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '服务器内部错误'
})
} else {
res.send({
code: 200,
data: results
})
}
})
}
})
// 查询英雄详情
app.get('/hero/info', (req, res) => {
// 接收英雄ID,根据ID查询信息然后返回
let {id} = req.query; // 获取get请求中的英雄ID
heroModel.find(`id = ${id}`, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '服务器内部错误'
})
} else {
res.send({
code: 200,
// 返回的是数组,取第0项的值就可以了,不用返回数组
data:results[0]
})
}
});
})
// 编辑英雄
app.post('/hero/update',upload.single('heroIcon'), (req, res) => {
// 因为有可能不改头像,只改名字和技能。所以要先用file判断改没改头像
// console.log(req.file); // 没传值是undefined
let {id,heroName,heroSkill} = req.body;
let obj = {
heroName,
heroSkill
};
if (req.file != undefined) {
// 传头像了代表修改头像
obj.heroIcon = 'http://127.0.0.1:8088/' + req.file.filename;
}
// 把数据更新到数据库中
heroModel.update(`id = ${id}`, obj, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '服务器内部错误'
})
} else {
res.send({
code: 200,
msg: '修改成功'
})
}
});
})
// 删除英雄(软删除)
app.post('/hero/delete', (req, res) => {
// 接收要删除的英雄id
let {id} = req.body;
heroModel.update(`id = ${id}`, {isDelete : 'true'}, (err, results) => {
if (err) {
res.send({
code: 500,
msg: '服务器内部错误'
})
} else {
res.send({
code: 200,
msg: '删除成功'
})
}
});
})
// 新增英雄
app.post('/hero/add', upload.single('heroIcon'), (req, res) => {
// 接收英雄名、英雄技能、头像
// console.log(req.file.filename); // 头像文件
// console.log(req.body); // 非文件参数
let {heroName,heroSkill} = req.body;
let heroIcon = 'http://127.0.0.1:8088/' + req.file.filename;
heroModel.insert({
heroName,
heroSkill,
heroIcon,
isDelete:false
},(err, results) => {
if (err) {
res.send({
code: 500,
msg: '服务器内部错误'
})
} else {
res.send({
code: 200,
msg: '新增成功'
})
}
});
})
// 验证码
var cap = '';
app.get('/captcha', (req, res) => {
// 创建一个验证码
var captcha = svgCaptcha.create({
size:4,
ignoreChars: '0o1il',
noise:3,
color:'blue',
background:'black'
});
// console.log(captch.text); // 验证码文本
// req.session.captcha = captcha.text;
cap = captcha.text;
// 返回验证码
res.type('svg');
res.status(200).send(captcha.data);
})
// 用户注册
app.post('/user/register', (req, res) => {
// 接收账号密码验证码
let {userName,passWord,code} = req.body;
// 判断验证码是否正确
if (code.toLocaleLowerCase() != cap.toLocaleLowerCase()){
// 验证码错误
res.send({
code:402,
msg:'验证码错误'
})
}else{
// 验证码正确
userModel.find(`userName='${userName}'`,(err,results)=>{
if(err){
res.send({
code: 500,
msg: '服务器错误'+err
})
}else{
if(results.length>0){
res.send({
code: 401,
msg: '用户名已存在'
})
}else{
// 用户名不存在则创建
userModel.insert({
userName,
passWord
},(err,results)=>{
if(err){
res.send({
code: 401,
msg: '服务器错误,注册失败'+err
})
}else{
res.send({
code: 200,
msg:'注册成功'
})
}
})
}
}
})
}
})
// 用户登录
app.post('/user/login', (req, res) => {
// 获取用户登录时输入的用户名和密码
let {userName,passWord} = req.body;
// 判断账号密码是否一致
userModel.find(`userName='${userName}' and passWord='${passWord}'`, (err, results) => {
if(err){
res.send({
code: 500,
msg: '服务器错误' + err
})
}else{
if (results.length > 0) {
// 账号密码没问题,响应会取cookie
req.session.user = {userName,passWord};
res.send({
code: 200,
msg: '登录成功'
})
}else{
res.send({
code: 401,
msg: '账号或密码错误'
})
}
}
})
})
// 退出登录
app.get('/user/logout', (req, res) => {
// 清除cookie,重定向到登录页
req.session = null;
res.writeHead(302,{
'Location':'../login.html'
});
res.end();
})
// 开启服务器
app.listen(8088,()=>{
console.log('服务器已开启')
})