项目地址:https://github.com/caochangkui/vue-element-responsive-demo/tree/login-register

通过 vue-cli3.0 + Element 构建项目前端,Node.js + Koa2 + MongoDB + Redis 实现数据库和接口设计,包括邮箱验证码、用户注册、用户登录、查看删除用户等功能。

1. 技术栈

2. 项目依赖:

  "dependencies": {
"axios": "^0.18.0",
"crypto-js": "^3.1.9-1",
"element-ui": "^2.4.5",
"js-cookie": "^2.2.0",
"jsonwebtoken": "^8.5.0",
"koa": "^2.7.0",
"koa-bodyparser": "^4.2.1",
"koa-generic-session": "^2.0.1",
"koa-json": "^2.0.2",
"koa-redis": "^3.1.3",
"koa-router": "^7.4.0",
"mongoose": "^5.4.19",
"nodemailer": "^5.1.1",
"nodemon": "^1.18.10",
"vue": "^2.5.21",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
}

3. 前端实现步骤

3.1 登录注册页面

通过 vue-cli3.0 + Element 构建项目前端页面

登录页(@/view/users/Login.vue):

注册页(@/view/users/Register.vue):

发送验证码前需要验证用户名和邮箱,用户名必填,邮箱格式需正确。

用户设置页(@/view/users/setting/Setting.vue)

用户登录后,可以进入用户设置页查看用户和删除用户

3.2 Vuex 状态管理

通过 vuex 实现保存或删除用户 token,保存用户名等功能。

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。

根目录下新建store文件夹,创建modules/user.js:

const user = {
state: {
token: localStorage.getItem('token'),
username: localStorage.getItem('username')
}, mutations: {
BIND_LOGIN: (state, data) => {
localStorage.setItem('token', data)
state.token = data
},
BIND_LOGOUT: (state) => {
localStorage.removeItem('token')
state.token = null
},
SAVE_USER: (state, data) => {
localStorage.setItem('username', data)
state.username = data
}
}
} export default user

创建文件 getters.js 对数据进行处理输出:

const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
username: state => state.user.username
}
export default getters

创建文件 index.js 管理所有状态:

import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import getters from './getters' Vue.use(Vuex) const store = new Vuex.Store({
modules: {
user
},
getters
}) export default store

3.3 路由控制/拦截

路由配置(router.js):

import Vue from 'vue'
import Router from 'vue-router'
const Login = () => import(/* webpackChunkName: "users" */ '@/views/users/Login.vue')
const Register = () => import(/* webpackChunkName: "users" */ '@/views/users/Register.vue')
const Setting = () => import(/* webpackChunkName: "tables" */ '@/views/setting/Setting.vue') Vue.use(Router) const router = new Router({
base: process.env.BASE_URL,
routes: [
{
path: '/login',
name: 'Login',
component: Login,
meta: {
title: '登录'
}
},
{
path: '/register',
name: 'Register',
component: Register,
meta: {
title: '注册'
}
},
{
path: '/setting',
name: 'Setting',
component: Setting,
meta: {
breadcrumb: '设置',
requireLogin: true
},
}
]
})

路由拦截:

关于vue 路由拦截参考:https://www.cnblogs.com/cckui/p/10319013.html

// 页面刷新时,重新赋值token
if (localStorage.getItem('token')) {
store.commit('BIND_LOGIN', localStorage.getItem('token'))
} // 全局导航钩子
router.beforeEach((to, from, next) => {
if (to.meta.title) { // 路由发生变化修改页面title
document.title = to.meta.title
}
if (to.meta.requireLogin) {
if (store.getters.token) {
if (Object.keys(from.query).length === 0) { // 判断路由来源是否有query,处理不是目的跳转的情况
next()
} else {
let redirect = from.query.redirect // 如果来源路由有query
if (to.path === redirect) { // 避免 next 无限循环
next()
} else {
next({ path: redirect }) // 跳转到目的路由
}
}
} else {
next({
path: '/login',
query: { redirect: to.fullPath } // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}
} else {
next()
}
}) export default router

3.4 Axios 封装

封装 Axios

// axios 配置
import axios from 'axios'
import store from './store'
import router from './router' //创建 axios 实例
let instance = axios.create({
timeout: 5000, // 请求超过5秒即超时返回错误
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
}) instance.interceptors.request.use(
config => {
if (store.getters.token) { // 若存在token,则每个Http Header都加上token
config.headers.Authorization = `token ${store.getters.token}`
console.log('拿到token')
}
console.log('request请求配置', config)
return config
},
err => {
return Promise.reject(err)
}) // http response 拦截器
instance.interceptors.response.use(
response => {
console.log('成功响应:', response)
return response
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
// 返回 401 (未授权) 清除 token 并跳转到登录页面
store.commit('BIND_LOGOUT')
router.replace({
path: '/login',
query: {
redirect: router.currentRoute.fullPath
}
})
break
default:
console.log('服务器出错,请稍后重试!')
alert('服务器出错,请稍后重试!')
}
}
return Promise.reject(error.response) // 返回接口返回的错误信息
}
) export default {
// 发送验证码
userVerify (data) {
return instance.post('/api/verify', data)
},
// 注册
userRegister (data) {
return instance.post('/api/register', data)
},
// 登录
userLogin (data) {
return instance.post('/api/login', data)
},
// 获取用户列表
getAllUser () {
return instance.get('/api/alluser')
},
// 删除用户
delUser (data) {
return instance.post('/api/deluser', data)
}
}

4. 服务端和数据库实现

在根目录下创建 server 文件夹,存放服务端和数据库相关代码。

4.1 MongoDB和Redis

创建 /server/dbs/config.js ,进行数据库和邮箱配置

// mongo 连接地址
const dbs = 'mongodb://127.0.0.1:27017/[数据库名称]' // redis 地址和端口
const redis = {
get host() {
return '127.0.0.1'
},
get port() {
return 6379
}
} // qq邮箱配置
const smtp = {
get host() {
return 'smtp.qq.com'
},
get user() {
return '1********@qq.com' // qq邮箱名
},
get pass() {
return '*****************' // qq邮箱授权码
},
// 生成邮箱验证码
get code() {
return () => {
return Math.random()
.toString(16)
.slice(2, 6)
.toUpperCase()
}
},
// 定义验证码过期时间rules,5分钟
get expire() {
return () => {
return new Date().getTime() + 5 * 60 * 1000
}
}
} module.exports = {
dbs,
redis,
smtp
}

使用 qq 邮箱发送验证码,需要在“设置/账户”中打开POP3/SMTP服务和MAP/SMTP服务。

4.2 Mongo 模型

创建 /server/dbs/models/users.js:

// users模型,包括四个字段
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const UserSchema = new Schema({
username: {
type: String,
unique: true,
required: true
},
password: {
type: String,
required: true
},
email: {
type: String,
required: true
},
token: {
type: String,
required: true
}
}) module.exports = {
Users: mongoose.model('User', UserSchema)
}

4.3 接口实现

创建 /server/interface/user.js:

const Router = require('koa-router')
const Redis = require('koa-redis') // key-value存储系统, 存储用户名,验证每个用户名对应的验证码是否正确
const nodeMailer = require('nodemailer') // 通过node发送邮件
const User = require('../dbs/models/users').Users
const Email = require('../dbs/config') // 创建和验证token, 参考4.4
const createToken = require('../token/createToken.js') // 创建token
const checkToken = require('../token/checkToken.js') // 验证token // 创建路由对象
const router = new Router({
prefix: '/api' // 接口的统一前缀
}) // 获取redis的客户端
const Store = new Redis().client // 接口 - 测试
router.get('/test', async ctx => {
ctx.body = {
code: 0,
msg: '测试',
}
}) // 发送验证码 的接口
router.post('/verify', async (ctx, next) => {
const username = ctx.request.body.username
const saveExpire = await Store.hget(`nodemail:${username}`, 'expire') // 拿到过期时间 console.log(ctx.request.body)
console.log('当前时间:', new Date().getTime())
console.log('过期时间:', saveExpire) // 检验已存在的验证码是否过期,以限制用户频繁发送验证码
if (saveExpire && new Date().getTime() - saveExpire < 0) {
ctx.body = {
code: -1,
msg: '发送过于频繁,请稍后再试'
}
return
} // QQ邮箱smtp服务权限校验
const transporter = nodeMailer.createTransport({
/**
* 端口465和587用于电子邮件客户端到电子邮件服务器通信 - 发送电子邮件。
* 端口465用于smtps SSL加密在任何SMTP级别通信之前自动启动。
* 端口587用于msa
*/
host: Email.smtp.host,
port: 587,
secure: false, // 为true时监听465端口,为false时监听其他端口
auth: {
user: Email.smtp.user,
pass: Email.smtp.pass
}
}) // 邮箱需要接收的信息
const ko = {
code: Email.smtp.code(),
expire: Email.smtp.expire(),
email: ctx.request.body.email,
user: ctx.request.body.username
} // 邮件中需要显示的内容
const mailOptions = {
from: `"认证邮件" <${Email.smtp.user}>`, // 邮件来自
to: ko.email, // 邮件发往
subject: '邀请码', // 邮件主题 标题
html: `您正在注册****,您的邀请码是${ko.code}` // 邮件内容
} // 执行发送邮件
await transporter.sendMail(mailOptions, (err, info) => {
if (err) {
return console.log('error')
} else {
Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
}
}) ctx.body = {
code: 0,
msg: '验证码已发送,请注意查收,可能会有延时,有效期5分钟'
}
}) // 接口 - 注册
router.post('/register', async ctx => {
const { username, password, email, code } = ctx.request.body // 验证验证码
if (code) {
const saveCode = await Store.hget(`nodemail:${username}`, 'code') // 拿到已存储的真实的验证码
const saveExpire = await Store.hget(`nodemail:${username}`, 'expire') // 过期时间 console.log(ctx.request.body)
console.log('redis中保存的验证码:', saveCode)
console.log('当前时间:', new Date().getTime())
console.log('过期时间:', saveExpire) // 用户提交的验证码是否等于已存的验证码
if (code === saveCode) {
if (new Date().getTime() - saveExpire > 0) {
ctx.body = {
code: -1,
msg: '验证码已过期,请重新申请'
}
return
}
} else {
ctx.body = {
code: -1,
msg: '请填写正确的验证码'
}
return
}
} else {
ctx.body = {
code: -1,
msg: '请填写验证码'
}
return
} // 用户名是否已经被注册
const user = await User.find({ username })
if (user.length) {
ctx.body = {
code: -1,
msg: '该用户名已被注册'
}
return
}
// 如果用户名未被注册,则写入数据库
const newUser = await User.create({
username,
password,
email,
token: createToken(this.username) // 生成一个token 存入数据库
}) // 如果用户名被成功写入数据库,则返回注册成功
if (newUser) {
ctx.body = {
code: 0,
msg: '注册成功',
}
} else {
ctx.body = {
code: -1,
msg: '注册失败'
}
}
}) // 接口 - 登录
router.post('/login', async (ctx, next) => {
const { username, password } = ctx.request.body let doc = await User.findOne({ username })
if (!doc) {
ctx.body = {
code: -1,
msg: '用户名不存在'
}
} else if (doc.password !== password) {
ctx.body = {
code: -1,
msg: '密码错误'
}
} else if (doc.password === password) {
console.log('密码正确')
let token = createToken(username) // 生成token
doc.token = token // 更新mongo中对应用户名的token
try {
await doc.save() // 更新mongo中对应用户名的token
ctx.body = {
code: 0,
msg: '登录成功',
username,
token
}
} catch (err) {
ctx.body = {
code: -1,
msg: '登录失败,请重新登录'
}
}
}
}) // 接口 - 获取所有用户 需要验证 token
router.get('/alluser', checkToken, async (ctx, next) => {
try {
let result = []
let doc = await User.find({})
doc.map((val, index) => {
result.push({
email: val.email,
username: val.username,
})
})
ctx.body = {
code: 0,
msg: '查找成功',
result
}
} catch (err) {
ctx.body = {
code: -1,
msg: '查找失败',
result: err
}
}
}) // 接口 - 删除用户 需要验证 token
router.post('/deluser', checkToken, async (ctx, next) => {
const { username } = ctx.request.body try {
await User.findOneAndRemove({username: username})
ctx.body = {
code: 0,
msg: '删除成功',
}
} catch (err) {
ctx.body = {
code: -1,
msg: '删除失败',
}
}
}) module.exports = {
router
}

上面实现了五个接口:

  • 发送验证码至邮箱: router.post('/verify')
  • 注册:router.post('/register')
  • 登录:router.post('/login')
  • 获取用户列表:router.get('/alluser')
  • 删除数据库中的某个用户:router.post('/deluser')

分别对应了前面 3.4 中 axios 中的5个请求地址

4.4 JSON Web Token 认证

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。详情参考:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

分别创建 /server/token/createToken.js 和 /server/token/checkToken.js

// 创建token
const jwt = require('jsonwebtoken') module.exports = function (id) {
const token = jwt.sign(
{
id: id
},
'cedric1990',
{
expiresIn: '300s'
}
) return token
}
// 验证token
const jwt = require('jsonwebtoken') // 检查 token
module.exports = async (ctx, next) => {
// 检验是否存在 token
// axios.js 中设置了 authorization
const authorization = ctx.get('Authorization')
if (authorization === '') {
ctx.throw(401, 'no token detected in http headerAuthorization')
} const token = authorization.split(' ')[1] // 检验 token 是否已过期
try {
await jwt.verify(token, 'cedric1990')
} catch (err) {
ctx.throw(401, 'invalid token')
} await next()
}

4.5 服务端入口

根目录创建 server.js:

// server端启动入口
const Koa = require('koa')
const app = new Koa();
const mongoose = require('mongoose')
const bodyParser = require('koa-bodyparser')
const session = require('koa-generic-session')
const Redis = require('koa-redis')
const json = require('koa-json') // 美化json格式化
const dbConfig = require('./server/dbs/config') const users = require('./server/interface/user.js').router // 一些session和redis相关配置
app.keys = ['keys', 'keyskeys']
app.proxy = true
app.use(
session({
store: new Redis()
})
) app.use(bodyParser({
extendTypes: ['json', 'form', 'text']
})) app.use(json()) // 连接数据库
mongoose.connect(
dbConfig.dbs,
{ useNewUrlParser: true }
) mongoose.set('useNewUrlParser', true)
mongoose.set('useFindAndModify', false)
mongoose.set('useCreateIndex', true) const db = mongoose.connection
mongoose.Promise = global.Promise // 防止Mongoose: mpromise 错误 db.on('error', function () {
console.log('数据库连接出错')
}) db.on('open', function () {
console.log('数据库连接成功')
}) // 路由中间件
app.use(users.routes()).use(users.allowedMethods()) app.listen(8888, () => {
console.log('This server is running at http://localhost:' + 8888)
})

5. 跨域处理

详情参考:https://www.cnblogs.com/cckui/p/10331432.html

vue 前端启动端口9527 和 koa 服务端启动端口8888不同,需要做跨域处理,打开vue.config.js:

devServer: {
port: 9527,
https: false,
hotOnly: false,
proxy: {
'/api': {
target: 'http://127.0.0.1:8888/', // 接口地址
changeOrigin: true,
ws: true,
pathRewrite: {
'^/': ''
}
}
}
}

6. 接口对接

import axios from '../../axios.js'
import CryptoJS from 'crypto-js' // 用于MD5加密处理

发送验证码:

// 用户名不能为空,并且验证邮箱格式
sendCode() {
let email = this.ruleForm2.email
if (this.checkEmail(email) && this.ruleForm2.username) {
axios.userVerify({
username: encodeURIComponent(this.ruleForm2.username),
email: this.ruleForm2.email
}).then((res) => {
if (res.status === 200 && res.data && res.data.code === 0) {
this.$notify({
title: '成功',
message: '验证码发送成功,请注意查收。有效期5分钟',
duration: 1000,
type: 'success'
}) let time = 300
this.buttonText = '已发送'
this.isDisabled = true
if (this.flag) {
this.flag = false;
let timer = setInterval(() => {
time--;
this.buttonText = time + ' 秒'
if (time === 0) {
clearInterval(timer);
this.buttonText = '重新获取'
this.isDisabled = false
this.flag = true;
}
}, 1000)
}
} else {
this.$notify({
title: '失败',
message: res.data.msg,
duration: 1000,
type: 'error'
})
}
})
}
}

注册:

submitForm(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
axios.userRegister({
username: encodeURIComponent(this.ruleForm2.username),
password: CryptoJS.MD5(this.ruleForm2.pass).toString(),
email: this.ruleForm2.email,
code: this.ruleForm2.smscode
}).then((res) => {
if (res.status === 200) {
if (res.data && res.data.code === 0) {
this.$notify({
title: '成功',
message: '注册成功。',
duration: 2000,
type: 'success'
})
setTimeout(() => {
this.$router.push({
path: '/login'
})
}, 500)
} else {
this.$notify({
title: '错误',
message: res.data.msg,
duration: 2000,
type: 'error'
})
}
} else {
this.$notify({
title: '错误',
message: `服务器请求出错, 错误码${res.status}`,
duration: 2000,
type: 'error'
})
}
})
} else {
console.log("error submit!!");
return false;
}
})
},

登录:

login(formName) {
this.$refs[formName].validate(valid => {
if (valid) {
axios.userLogin({
username: window.encodeURIComponent(this.ruleForm.name),
password: CryptoJS.MD5(this.ruleForm.pass).toString()
}).then((res) => {
if (res.status === 200) {
if (res.data && res.data.code === 0) {
this.bindLogin(res.data.token)
this.saveUser(res.data.username)
this.$notify({
title: '成功',
message: '恭喜,登录成功。',
duration: 1000,
type: 'success'
})
setTimeout(() => {
this.$router.push({
path: '/'
})
}, 500)
} else {
this.$notify({
title: '错误',
message: res.data.msg,
duration: 1000,
type: 'error'
})
}
} else {
this.$notify({
title: '错误',
message: '服务器出错,请稍后重试',
duration: 1000,
type: 'error'
})
}
})
}
})
},

7. 启动项目 测试接口

7.1 vue端:

$ npm run serve

7.2 启动mogod:

$ mongod

7.3 启动Redis:

$ redis-server

7.4 启动服务端server.js:

安装 nodemon 热启动辅助工具:

$ npm i nodemon
$ nodemon server.js

8. 项目目录

基于 Vue + Koa2 + MongoDB + Redis 实现一个完整的登录注册的更多相关文章

  1. 基于RabbitMQ和Swoole实现的一个完整的异步任务系统

    从最开始的使用redis实现的单进程消费的异步任务系统到加入swoole的多进程消费模式,现在,我们的异步任务系统终于又能迈进一步. 因为有了前面两个简单系统的经验,这回基于RabbitMQ的异步任务 ...

  2. 基于node+koa2+mongodb实现简单的导航管理系统

    基于node+koa2+mongodb实现简单的导航管理系统 项目说明 本项目gitbub地址 https://github.com/xuess/nav-admin,喜欢请star 基于node 实现 ...

  3. 一个基于Vue.js+Mongodb+Node.js的博客内容管理系统

    这个项目最初其实是fork别人的项目.当初想接触下mongodb数据库,找个例子学习下,后来改着改着就面目全非了.后台和数据库重构,前端增加了登录注册功能,仅保留了博客设置页面,但是也优化了. 一.功 ...

  4. vue koa2 mongodb 从零开始做个人博客(一) 登录注册功能前端部分

    0.效果演示 插入视频插不进来,就很烦.可以出门右拐去优酷看下(点我!). 1.准备工作 1.1前端框架 前端使用了基于vue.js的nuxt.js.为什么使用nuxt.js? 首先我做的是博客的项目 ...

  5. 记小白的一次基于vue+express+mongodb个人站开发

    学了vue和node一段时间了,折腾了一些零零散散的小东西.马上大四了要出去找工作了,所以早就想搭一个个人站作为一次较为全面的总结.因为没有设计功底,界面设计使我这种强迫症患者苦不堪言.幸而到最后花了 ...

  6. vue koa2 mongodb 从零开始做个人博客(二) 登录注册功能后端部分

    0.效果演示 插入视频插不进来,就很烦.可以出门右拐去优酷看下(点我!). 1.后端搭建 1.1项目结构 首先看一下后端的server目录 挨个解释一下 首先dbs文件夹顾名思义,操作数据库的,mod ...

  7. 7. Swift 基于Xmpp和openfire实现一个简单的登录注册

    1. 基本步骤:首先导入Xmpp框架,配置环境 ->由于我们使用的是OC的Xmpp框架,再进行Swift开发时需要进行桥接. 具体方法就是创建一个基于c的.h的头文件,然后将我们需要编译OC的语 ...

  8. JQuery+Ajax+Struts2+Hibernate 实现完整的登录注册

    写在最前: 下午有招聘会,不想去,总觉得没有准备好,而且都是一些不对口的公司,可是又静不下心来,就来写个博客. 最近在仿造一个书城的网站:http://www.yousuu.com ,UI直接拿来用, ...

  9. webpack+vue+koa+mongoDB,从零开始搭建一个网站

    github 地址 https://github.com/wangxiaoxi... webpakc+vue的搭建1.新建项目文件夹(see-films);2.npm init //初始化项目3.搭建 ...

随机推荐

  1. coTurn 使用测试方法

    做个记录 1.从"../examples/etc/" 目录拷贝turnserver.conf文件到"/usr/local/etc/"目录 2.修改配置文件 主要 ...

  2. 【公众号系列】SAP S/4 HANA 1809请查收

    公众号:SAP Technical 本文作者:matinal 原文出处:http://www.cnblogs.com/SAPmatinal/ 原文链接:[公众号系列]SAP S/4 HANA 1809 ...

  3. 监控MySQL或Web服务是否正常

    在工作中,我们往往利用脚本定时监控本地.远端数据库服务端或Web服务是否运行正常,例如:负载高.cup高.连接数满了等.... 方法一:根据端口 本地:netstat/ss/lsof ①   nets ...

  4. NodeJS二进制包安装和快捷键配置(适用于U盘版安装配置)

    首先下载NodeJS二进制安装包:https://nodejs.org/dist/v10.15.3/node-v10.15.3-win-x64.zip 在D盘新建NodeJS文件夹,解压node-v1 ...

  5. GitHub-标签管理

    参考博文:廖雪峰Git教程 1. 创建标签 切换到需要打标签的分支上,之后打标签 [root@mini05 zhangtest]# git branch dev * master [root@mini ...

  6. Mybatis报错 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.binding.BindingException: Parameter 'parentCode' not found. Available parameters are [0, 1, param1, param2]

    orcal数据库使用mybatis接收参数,如果传的是多个参数,需要使用#{0},#{1},...等代替具体参数名,0 代表第一个参数,1 代表第二个参数,以此类推. 错误语句: <select ...

  7. 谱聚类算法(Spectral Clustering)

        谱聚类(Spectral Clustering, SC)是一种基于图论的聚类方法--将带权无向图划分为两个或两个以上的最优子图,使子图内部尽量相似,而子图间距离尽量距离较远,以达到常见的聚类的 ...

  8. python3编写网络爬虫22-爬取知乎用户信息

    思路 选定起始人 选一个关注数或者粉丝数多的大V作为爬虫起始点 获取粉丝和关注列表 通过知乎接口获得该大V的粉丝列表和关注列表 获取列表用户信息 获取列表每个用户的详细信息 获取每个用户的粉丝和关注 ...

  9. Go学习笔记04-函数

    目录 函数定义 函数示例 小结 函数定义 函数定义与变量定义相似, func function_name(var1, var2, var3, ...) (return_type1, return_ty ...

  10. 12.scrapy框架之递归解析和post请求

    今日概要 递归爬取解析多页页面数据 scrapy核心组件工作流程 scrapy的post请求发送 今日详情 1.递归爬取解析多页页面数据 - 需求:将糗事百科所有页码的作者和段子内容数据进行爬取切持久 ...