使用Python轻量级Web框架Flask打造个人博客项目
0、前言:
1、项目设计流程:
2、架构设计:
3、数据库设计:

4、项目框架展示
5、项目主要文件代码展示
# Flask个人博客项目
from App import creat_app
app = creat_app()
if __name__ == '__main__':
app.run(debug=True)
# models.py : 模型,数据库
'''
模型 === 数据库
类 ——> 表结构
类属性 ——> 表字段
一个对象 ——> 表的一行数据
'''
from ..exts import db # 导入db对象就能通过python实现ORM技术,避免了写SQL语句。
# 分类数据表
class CategoryModel(db.Model):
# 表名
__tablename__ = 'tb_category' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
describe = db.Column(db.Text(), default='describe')
# 关联外键(可以在分类表中获取对应的所有文章)
articles = db.relationship('ArticleModel', backref='category', lazy='dynamic')
# 文章数据表
class ArticleModel(db.Model):
# 表名
__tablename__ = 'tb_article' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
keyword = db.Column(db.String(255), default='keyword')
content = db.Column(db.Text(), default='content')
img = db.Column(db.Text(), default='img')
# 外键
category_id = db.Column(db.Integer, db.ForeignKey(CategoryModel.id))
# 相册
class PhotoModel(db.Model):
# 表名
__tablename__ = 'tb_photo'
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
url = db.Column(db.Text())
name = db.Column(db.String(30), unique=True)
describe = db.Column(db.Text(), default='describe')
from ..exts import db # 导入db对象就能通过python实现ORM技术,避免了写SQL语句。
# 模型Model:类
# 必须继承 db.Model User才能从普通的类变成模型
class AdminUserModel(db.Model):
# 表名
__tablename__ = 'tb_adminuser' # 数据迁移就是让模型变成表,ORM就是让类变成模型
# 定义表字段
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(30), unique=True)
passwd = db.Column(db.String(30))
# 通过orm技术得到的db,就是用于替代数据库,后面用到数据库相关操作,可以检索。
# 在views.py中放路由和视图函数
from flask import Blueprint, render_template, request
from ..models.models import * #后面是用views来控制数据库的,所以要在views中导入models文件
# 蓝图(蓝图可以有多个,方便对路径进行区分)
blog = Blueprint('blog', __name__)
# 博客首页
@blog.route('/')
@blog.route('/index/')
def index():
# 修改首页中“我的相册”当中的6张图,调用photos数据库中前6张图片
# 修改首页中“文章分类”
# 修改首页中“iKun推荐”
# 修改首页中右侧的文章
photos = PhotoModel.query.limit(6)
categories = CategoryModel.query.all()
articles = ArticleModel.query.all()
commend_arc = articles[:4]
return render_template('home/index.html',
photos=photos,
categories=categories,
articles=commend_arc,
arts = articles)
# 我的相册
@blog.route('/photos/')
def blog_photos():
photos = PhotoModel.query.all()
return render_template('home/photos.html', photos = photos)
# 我的日记
@blog.route('/article/')
def blog_aritcle():
articles = ArticleModel.query.all()
categories = CategoryModel.query.all()
return render_template('home/article.html', articles = articles, categories = categories)
# 关于我
@blog.route('/about/')
def blog_about():
photos1 = PhotoModel.query.all()
categories = CategoryModel.query.all()
photos = photos1[:6]
return render_template('home/about.html', photos = photos, categories = categories)
# 在views.py中放路由和视图函数
from flask import Blueprint, render_template, request, redirect, jsonify
from ..models.models_admin import * #后面是用views来控制数据库的,所以要在views中导入models文件
from ..models.models import * # 首页面需要查询models当中的表
import time
# 蓝图(蓝图可以有多个,方便对路径进行区分)
admin = Blueprint('admin', __name__)
# ---------------------- 后台管理 ----------------------
# 装饰器:做登录验证,就不用每次都写一遍登陆验证了
from functools import wraps # 为了在多个函数使用装饰器时,能够让函数名正确传递
def login_required(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 判断用户是否登录
# 获取cookie,得到登录的用户
user_id = request.cookies.get('user_id', None)
if user_id:
# 登陆过,进入后台管理系统
user = AdminUserModel.query.get(user_id)
request.user = user
return fn(*args, **kwargs) # 登陆过就去调用对应的视图函数
else:
# 没有登陆过,跳转到登录页面
return redirect('/admin/login/')
return inner
# 后台管理——首页面
@admin.route('/admin/')
@admin.route('/admin/index/')
@login_required
def index():
# user_id = request.cookies.get('user_id', None) # 默认值给空,如果没有的话,默认为空
# if user_id:
# # 登陆过或登录成功,有cookie的情况
# user = AdminUserModel.query.get(user_id)
# categorys = CategoryModel.query.filter()
# articles = ArticleModel.query.filter()
# photos = PhotoModel.query.filter()
# return render_template('admin/index.html',
# username = user.name,
# categorys=categorys,
# articles=articles,
# photos=photos)
# else:
# return redirect('/admin/login/')
user = request.user
categorys = CategoryModel.query.filter()
articles = ArticleModel.query.filter()
photos = PhotoModel.query.filter()
return render_template('admin/index.html',
username=user.name,
categorys=categorys,
articles=articles,
photos=photos)
# 后台管理——登录
@admin.route('/admin/login/',methods=['GET', 'POST'])
def admin_login():
if request.method == 'GET': # 网页向服务器请求数据
return render_template('admin/login.html')
elif request.method == 'POST': # 网页向服务器提交数据
username = request.form.get('username') # 用户名
userpwd = request.form.get('userpwd') # 密码
# 登陆验证
user = AdminUserModel.query.filter_by(name = username, passwd = userpwd).first()
if user:
# 登陆成功
response = redirect('/admin/index/')
'''
1、回顾下cookie的工作流程:前端发送请求-后端返回前端一个cookie-前端保存cookie-前端在cookie有效期
登录可以免密登录。
2、注意下面设置response的cookie中参数时,'user_id'是cookie的名称, str(user.id)是cookie的值,
cookie值要求必须是每个用户独一无二的。
'''
response.set_cookie('user_id', str(user.id), max_age=7*12*3600) # cookie设置7天有效
return response
else:
return 'login failed'
# 后台管理——退出登录
@admin.route('/admin/logout/')
def admin_logout():
response = redirect('/admin/login/')
response.delete_cookie('user_id')
return response
# ---------------------- 后台管理——分类管理 ----------------------
@admin.route('/admin/category/')
@login_required
def admin_category():
user = request.user
categories = CategoryModel.query.all()
return render_template('/admin/category.html',
username = user.name,
categories = categories)
# 分类页面添加分类功能实现
@admin.route('/admin/addcategory/', methods=['GET','POST'])
@login_required
def add_category():
if request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 添加分类(在数据表中创建一条数据就是创建一个对象)
category = CategoryModel()
category.name = name
category.describe = describe
try:
db.session.add(category)
db.session.commit()
except Exception as e:
print('e',e)
db.session.rollback()
return redirect('/admin/category/') # 不论成功还是失败,重新跳转,相当于刷新了页面
return '请求方式错误'
# 分类页面删除分类功能实现
@admin.route('/admin/delcategory/', methods=['GET','POST'])
@login_required
def del_category():
if request.method == 'POST':
# 先查询
id = request.form.get('id')
# print(id)
category = CategoryModel.query.get(id)
# 再删除
try:
db.session.delete(category)
db.session.commit()
except Exception as e:
print('e:',e)
return jsonify({'code':200,'msg':'删除成功!'})
else:
return jsonify({'code':400,'msg':'请求方式错误!'})
# 分类页面修改分类功能实现
@admin.route('/admin/updatecategory/<id>/', methods=['GET','POST'])
@login_required
def update_category(id):
user = request.user
if request.method == 'GET':
category = CategoryModel.query.get(id)
return render_template('admin/category_update.html',
username = user.name,
category=category)
elif request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 修改
category = CategoryModel.query.get(id)
category.name = name
category.describe = describe
# 提交
try:
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return redirect('/admin/category/')
else:
return "请求方式错误!"
# ---------------------- 后台管理——文章管理 ----------------------
# 文章管理
@admin.route('/admin/article/')
@login_required
def admin_article():
user = request.user
articles = ArticleModel.query.all()
return render_template('/admin/article.html',
username = user.name,
articles = articles)
# 删除文章
@admin.route('/admin/delarticle/', methods=['GET','POST'])
@login_required
def del_article():
if request.method == 'POST':
id = request.form.get('id')
article = ArticleModel.query.get(id)
try:
db.session.delete(article)
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return jsonify({'code': 500, 'msg': '删除失败'})
return jsonify({'code':200, 'msg':'删除文章成功' })
return jsonify({'code':400, 'msg':'请求方式错误!' })
# 添加文章
@admin.route('/admin/addarticle/', methods=['GET','POST'])
@login_required
def add_article():
if request.method == 'GET':
categories = CategoryModel.query.all()
articles = ArticleModel.query.all()
return render_template('/admin/article_add.html',
username = request.user.name,
articles = articles,
categories = categories)
elif request.method == 'POST':
# 添加文章
name = request.form.get('name')
keywords = request.form.get('keywords')
content = request.form.get('content')
category = request.form.get('category')
img = request.files.get('img')
# 图片存储路径
img_name = f'{time.time()}-{img.filename}'
img_url = f'/static/home/uploads/{img_name}'
# 添加文章
try:
article = ArticleModel()
article.name = name
article.keyword = keywords
article.content = content
article.img = img_url
article.category_id = category
db.session.add(article)
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
db.session.flush()
else:
# 如果添加数据库成功,就要把图片存入本地
img_data = img.read()
with open(f'App/{img_url}','wb') as fp:
fp.write(img_data)
fp.flush()
return redirect('/admin/article')
# 修改文章
@admin.route('/admin/updatearticle/<id>/', methods=['GET','POST'])
@login_required
def upddate_article(id):
article = ArticleModel.query.get(id)
if request.method == 'GET':
categories = CategoryModel.query.all()
return render_template('/admin/article_update.html/',
username=request.user.name,
categories=categories,
article=article)
elif request.method == 'POST':
# 修改文章
name = request.form.get('name')
keywords = request.form.get('keywords')
content = request.form.get('content')
category = request.form.get('category')
img = request.files.get('img')
# 图片存储路径
img_name = f'{time.time()}-{img.filename}'
img_url = f'/static/home/uploads/{img_name}'
# 添加文章
try:
article.name = name
article.keyword = keywords
article.content = content
article.img = img_url
article.category_id = category
db.session.commit()
except Exception as e:
print('e:', e)
db.session.rollback()
db.session.flush()
else:
# 如果添加数据库成功,就要把图片存入本地
img_data = img.read()
with open(f'App/{img_url}', 'wb') as fp:
fp.write(img_data)
fp.flush()
return redirect('/admin/article')
from flask_sqlalchemy import SQLAlchemy # orm技术
from flask_migrate import Migrate # 数据迁移技术
db = SQLAlchemy()
migrate = Migrate()
def init_exts(app):
db.init_app(app=app)
migrate.init_app(app=app, db=db)
# __init__.py : 初始化文件,创建Flask应用
from flask import Flask
from .views.views import blog
from .views.views_admin import admin
from .exts import init_exts
def creat_app():
app = Flask(__name__)
# 注册蓝图
app.register_blueprint(blueprint=blog)
app.register_blueprint(blueprint=admin)
# 配置数据库(配置不同数据库软件,就要用不同配置,配置的目的,就是在用到数据库的时候让项目知道找什么数据库,去哪找数据库)
# db_uri = 'sqlite:///sqlite3.db'
db_uri = 'mysql+pymysql://root:123456@localhost:3306/blogdb' # mysql的配置
app.config['SQLALCHEMY_DATABASE_URI'] = db_uri
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 禁止对象追踪修改(为了不浪费服务器资源进行的设置)
# 初始化插件
init_exts(app=app)
return app
6、用户管理页面与展示页面设计:

7、展示页面数据交互方式


8、用户管理页面实现:增、删、改、查
8.1、关键问题概述
8.2、关键问题详述
如下面的代码中,首先通过装饰器模板写好内部函数inner,在inner中进行判断,如果从cookies中得到了user_id,就证明了之前有登陆过,就可以获取user_id,否则就返回None给到user_id。如果有登录过,就从inner函数中调用外部函数fn,fn就是对应跳转页面的视图函数,就能够跳转到相应的页面中。下面的首页面加了登录验证的装饰器,就可以做登录判断了,当然也可以每个视图函数都写一次登录判断。
# 装饰器:做登录验证,就不用每次都写一遍登陆验证了
from functools import wraps # 为了在多个函数使用装饰器时,能够让函数名正确传递
def login_required(fn):
@wraps(fn)
def inner(*args, **kwargs):
# 判断用户是否登录
# 获取cookie,得到登录的用户
user_id = request.cookies.get('user_id', None)
if user_id:
# 登陆过,进入后台管理系统
user = AdminUserModel.query.get(user_id)
request.user = user
return fn(*args, **kwargs) # 登陆过就去调用对应的视图函数
else:
# 没有登陆过,跳转到登录页面
return redirect('/admin/login/')
return inner
# 登录首页的视图函数
@admin.route('/admin/')
@admin.route('/admin/index/')
@login_required
def index():
user = request.user
categorys = CategoryModel.query.filter()
articles = ArticleModel.query.filter()
photos = PhotoModel.query.filter()
return render_template('admin/index.html',
username=user.name,
categorys=categorys,
articles=articles,
photos=photos)
以该项目中的 类型数据表 为例,如下图,可以看到该数据库中主要的字段有三个:id、name、describe,由于id是自增长的,所以只需要有name和describe就可以添加一条数据给到数据表。

因此,后台管理页面中,给 类型数据表 添加新类型,就需要设计一个新的页面来收集name和describe这两个字段,如下图所以就是给 类型数据表 添加新类型的页面。

当然,这个功能也一定是通过Flask当中的视图函数实现的。如下所示的视图函数add_category就实现了该功能,首先,添加路由装饰器和登陆验证装饰器,因为是添加数据,需要把页面表单中的数据提交到后台的视图函数中,所以视图函数首先会判断request的方式是不是POST,如果是,就说明表单正在往视图函数提交数据,这个时候,视图函数就可以通过request(全局对象,在flask处理请求时自动创建)获取到表单当中提交过来的数据,然后创建数据表对象,给字段赋值,然后尝试添加到数据表当中即可。添加完成后,重定向到分类页面,重定向就可以重新加载一次,就能够看到我们添加数据的操作是否成功。
# 以下代码写在views_admin.py当中
# 分类页面添加分类功能实现
@admin.route('/admin/addcategory/', methods=['GET','POST'])
@login_required
def add_category():
if request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 添加分类(在数据表中创建一条数据就是创建一个对象)
category = CategoryModel()
category.name = name
category.describe = describe
try:
db.session.add(category)
db.session.commit()
except Exception as e:
print('e',e)
db.session.rollback()
return redirect('/admin/category/') # 不论成功还是失败,重新跳转,相当于刷新了页面
return '请求方式错误'
这个过程也是需要前端代码配合才行,如下就是前端表单的html代码,可以看到,表单中两处input都是通过name命名的,这样,就能在视图函数中通过request获取对应的表单内容。
<form action="/admin/addcategory/" method="post" autocomplete="off">
<div class="form-group">
<label for="category-name">分类名称</label>
<input type="text" id="category-name" name="name" class="form-control"
placeholder="在此处输入分类名称" required autocomplete="off">
</div>
<div class="form-group">
<label for="category-describe">分类描述</label>
<textarea class="form-control" id="category-describe" name="describe" rows="4"
autocomplete="off"></textarea>
</div>
<button class="btn btn-primary" type="submit" name="submit">添加新分类</button>
</form>
同样以该项目中的 类型数据表 为例,如下图所示,就是分类管理中展示的所有 类型数据表 的值,通过html当中的a标签实现删除操作

首先通过html当中的代码,可以看出,删除操作相对复杂,需要通过js向视图函数提交要删除数据的id,然后再接收来自视图函数的消息,判断是否删除成功,如果删除成功,就在js当中刷新页面。
{% for category in categories %}
<tr>
<td>{{ category.id }}</td>
<td>{{ category.name }}</td>
<td>
<a href="/admin/updatecategory/{{ category.id }}/">修改</a>
<a href="#" cid="{{ category.id }}">删除</a>
</td>
</tr>
{% endfor %}
<script>
//是否确认删除
$(function () {
$("#main table tbody tr td a").click(function () {
var that = $(this);
var id = that.attr("cid"); //对应id
if (event.srcElement.outerText === "删除") {
if (window.confirm("此操作不可逆,是否确认?")) {
$.post('/admin/delcategory/', {'id': id}, function (data){
console.log(data.msg)
if (data.code == 200){
location.reload()
}
})
}
}
})
});
</script>
对于视图函数而言,删除操作就是只需要知道你想删除的数据在数据表中对应的id,然后通过表操作直接删除该数据即可。
# 以下代码写在views_admin.py当中
# 分类页面删除分类功能实现
@admin.route('/admin/delcategory/', methods=['GET','POST'])
@login_required
def del_category():
if request.method == 'POST':
# 先查询
id = request.form.get('id')
# print(id)
category = CategoryModel.query.get(id)
# 再删除
try:
db.session.delete(category)
db.session.commit()
except Exception as e:
print('e:',e)
return jsonify({'code':200,'msg':'删除成功!'})
else:
return jsonify({'code':400,'msg':'请求方式错误!'})
同样以该项目中的 类型数据表 为例,参考 对数据表的删除操作 讲解中的前端页面截图和html代码即可,可以知道,通过模板语言加载过程中,就已经可以通过 category.id 拿到分类表中要修改的数据的id了,通过a连接跳转到路由 /admin/updatecategory/{{ category.id }}/ 对应的新页面,这个过程中带着参数即可,然后这个参数就会被传递到该路由对应的视图函数当中,视图函数如下,注意路由写的时候也要带着参数,由于a标签打开路由页面的请求是GET请求,所以在判断请求方式是GET时,就能获取到传递过来的参数id,然后通过id,查找到对应的数据表当中的数据对象。
# 以下代码写在views_admin.py当中
# 分类页面修改分类功能实现
@admin.route('/admin/updatecategory/<id>/', methods=['GET','POST'])
@login_required
def update_category(id):
user = request.user
if request.method == 'GET':
category = CategoryModel.query.get(id)
return render_template('admin/category_update.html',
username = user.name,
category=category)
elif request.method == 'POST':
name = request.form.get('name')
describe = request.form.get('describe')
# 修改
category = CategoryModel.query.get(id)
category.name = name
category.describe = describe
# 提交
try:
db.session.commit()
except Exception as e:
print('e:',e)
db.session.rollback()
return redirect('/admin/category/')
else:
return "请求方式错误!"
然后,将查到的数据同模板(admin/category_update.html)一同打开,如下图所示就是修改页面中,对应修改数据填充后的展示结果。
admin/category_update.html(修改页面)中html就会有一个表单,类似添加页面,在写好之后,点击更新分类,就会修改对应的数据表当中的数据,点击更新分类,前端页面就会提交表单到路由函数update_category中,此时请求方式就是POST请求,然后通过request获取对应的数据,再查询数据表得到对应的数据对象,替换其字段的值即可完成修改,修改完成后,重定向刷新页面。
总结:
作者:疋瓞