2026/5/13 1:37:06
网站建设
项目流程
商丘网站制作与设计,菏泽市网站建设,警惕网站免费看手机,哪些网站可以做代理第一章#xff1a;为什么需要专业表单验证#xff1f;在第3篇中#xff0c;我们通过 request.form.get() 简单获取数据#xff0c;并用 .strip() 去除空格。这种方式存在明显缺陷#xff1a;无结构化校验#xff1a;无法统一管理验证逻辑错误反馈粗糙#xff1a;仅靠前端…第一章为什么需要专业表单验证在第3篇中我们通过request.form.get()简单获取数据并用.strip()去除空格。这种方式存在明显缺陷无结构化校验无法统一管理验证逻辑错误反馈粗糙仅靠前端required后端无兜底安全风险未过滤特殊字符、超长输入等扩展困难新增字段需重复编写验证代码解决方案引入WTForms—— Python 最流行的表单处理库Flask 官方推荐搭配Flask-WTF使用。第二章集成 Flask-WTF 实现健壮表单2.1 安装与配置pip install Flask-WTF更新requirements.txtFlask3.0.3 Flask-SQLAlchemy3.1.1 Flask-WTF1.2.1 # 新增 SQLAlchemy2.0.30 Werkzeug3.0.3注意Flask-WTF内置 CSRF跨站请求伪造保护需配置密钥。在config.py中强化密钥设置import os import secrets class Config: # 优先从环境变量读取否则生成随机值 SECRET_KEY os.environ.get(SECRET_KEY) or secrets.token_hex(16) SQLALCHEMY_TRACK_MODIFICATIONS False2.2 创建任务表单类新建forms.py与models.py同级from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired, Length, ValidationError from models import Todo class TodoForm(FlaskForm): 任务添加/编辑表单 title StringField( 任务标题, validators[ DataRequired(message任务内容不能为空), Length(min1, max100, message任务长度需在1-100字符之间) ], render_kw{ # 传递给 HTML input 的属性 placeholder: 请输入任务内容1-100字符, class: layui-input } ) submit Submit(field添加任务, render_kw{class: layui-btn}) def validate_title(self, field): 自定义验证禁止纯空白字符 if not field.data.strip(): raise ValidationError(任务内容不能全为空格) # 可选敏感词过滤示例 # forbidden_words [垃圾, 广告] # for word in forbidden_words: # if word in field.data: # raise ValidationError(f任务内容不能包含敏感词: {word})关键说明DataRequired非空验证比InputRequired更严格Length长度限制render_kw自动为input添加 class 和 placeholdervalidate_title自定义验证方法方法名必须为validate_字段名2.3 在视图函数中使用表单修改routes/main.pyfrom flask import Blueprint, render_template, request, redirect, url_for, flash from models import db, Todo from forms import TodoForm # 新增导入 main Blueprint(main, __name__) main.route(/, methods[GET, POST]) # 支持 POST def index(): form TodoForm() # 实例化表单 # 处理表单提交 if form.validate_on_submit(): title form.title.data.strip() new_todo Todo(titletitle) db.session.add(new_todo) db.session.commit() flash(任务添加成功, success) # 消息闪现 return redirect(url_for(main.index)) # 处理搜索 query request.args.get(q, ).strip() if query: todos Todo.query.filter(Todo.title.contains(query)).all() else: todos Todo.query.order_by(Todo.created_at.desc()).all() return render_template(index.html, formform, todostodos, search_queryquery)重要变更路由支持POST方法使用form.validate_on_submit()自动处理 GET/POST 判断和验证验证成功后使用flash()发送成功消息2.4 在模板中渲染表单更新templates/index.html的表单部分!-- 替换原表单 -- form classlayui-form methodPOST {{ form.hidden_tag() }} !-- 必须输出 CSRF 令牌 -- div classlayui-form-item div classlayui-input-block stylemargin-left: 0; div classlayui-input-inline stylewidth: 500px; {{ form.title(classlayui-input) }} !-- 显示字段错误 -- {% if form.title.errors %} ul classlayui-form-mid layui-text stylecolor:#FF5722; margin-top:5px; {% for error in form.title.errors %} li{{ error }}/li {% endfor %} /ul {% endif %} /div {{ form.submit(classlayui-btn) }} /div /div /form !-- 显示全局消息如 flash -- {% with messages get_flashed_messages(with_categoriestrue) %} {% if messages %} {% for category, message in messages %} div classlayui-alert layui-alert-{{ success if categorysuccess else error }} stylemargin: 15px 0; padding: 10px; background: #f6f8fa; border-left: 4px solid {{ #52C41A if categorysuccess else #F5222D }}; {{ message }} /div {% endfor %} {% endif %} {% endwith %}Layui 样式适配技巧layui-form-mid用于表单中间提示文本自定义 alert 样式模拟 Layui 风格Layui 无内置 alert 组件form.hidden_tag()输出隐藏的 CSRF token 字段✅效果当输入空格或超长时页面会显示红色错误提示成功添加后显示绿色成功消息。第三章全局错误处理与友好提示3.1 为什么需要自定义错误页默认的 Flask 错误页面如 404 Not Found对用户不友好且暴露技术细节。我们需要统一错误页面风格继承 Layui隐藏敏感信息提供返回首页的链接3.2 创建错误模板新建templates/errors/404.html{% extends base.html %} {% block title %}页面未找到 - 404{% endblock %} {% block header %}哎呀页面走丢了{% endblock %} {% block content %} div styletext-align: center; padding: 40px 0; color: #999; i classlayui-icon stylefont-size: 60px;#xe61c;/i h2 stylemargin: 20px 0;404 - 您访问的页面不存在/h2 p可能是地址输入错误或页面已被移除/p a href{{ url_for(main.index) }} classlayui-btn layui-btn-primary stylemargin-top: 20px; 返回首页 /a /div {% endblock %}新建templates/errors/500.html{% extends base.html %} {% block title %}服务器内部错误 - 500{% endblock %} {% block header %}服务器开小差了...{% endblock %} {% block content %} div styletext-align: center; padding: 40px 0; color: #999; i classlayui-icon stylefont-size: 60px; color: #F5222D;#xe608;/i h2 stylemargin: 20px 0;500 - 服务器内部错误/h2 p我们的工程师已收到通知正在紧急修复/p a href{{ url_for(main.index) }} classlayui-btn stylemargin-top: 20px; 返回首页 /a /div {% endblock %}3.3 注册错误处理器在app.py的create_app函数中注册def create_app(config_namedefault): app Flask(__name__) app.config.from_object(config[config_name]) db.init_app(app) # 注册蓝图 from routes.main import main as main_blueprint app.register_blueprint(main_blueprint) # 新增错误处理器 app.errorhandler(404) def page_not_found(e): return render_template(errors/404.html), 404 app.errorhandler(500) def internal_server_error(e): # 记录错误日志见下一节 app.logger.error(fServer Error: {e}) return render_template(errors/500.html), 500 with app.app_context(): db.create_all() return app测试方法访问http://127.0.0.1:5000/nonexistent应显示自定义 404 页。第四章记录应用日志4.1 配置日志在create_app中添加日志配置import logging from logging.handlers import RotatingFileHandler import os def create_app(config_namedefault): app Flask(__name__) app.config.from_object(config[config_name]) # 日志配置 if not app.debug: # 生产环境写入文件 if not os.path.exists(logs): os.mkdir(logs) file_handler RotatingFileHandler( logs/todo.log, maxBytes10240, # 10KB backupCount10 ) file_handler.setFormatter(logging.Formatter( %(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d] )) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) app.logger.info(Todo application startup) else: # 开发环境输出到控制台 logging.basicConfig(levellogging.DEBUG) # ... 其他初始化代码 ...4.2 在关键位置添加日志例如在删除操作中记录# routes/main.py main.route(/delete/int:todo_id, methods[POST]) # 注意改为 POST见下节 def delete_todo(todo_id): todo Todo.query.get_or_404(todo_id) title todo.title # 保存标题用于日志 db.session.delete(todo) db.session.commit() current_app.logger.info(fTask deleted: ID{todo_id}, Title{title}) flash(任务已删除, info) return redirect(url_for(main.index))注意需从flask导入current_app。第五章前端交互体验优化5.1 回车快速提交任务在base.html的script中添加// 监听回车提交 document.addEventListener(DOMContentLoaded, function() { const input document.querySelector(input[nametitle]); if (input) { input.addEventListener(keypress, function(e) { if (e.key Enter) { e.preventDefault(); // 阻止默认换行 this.form.submit(); // 提交表单 } }); } });5.2 删除操作改用 POST CSRF 保护为什么GET 请求不应修改数据RESTful 原则且易被恶意链接利用。步骤1修改删除路由为 POST# routes/main.py from flask_wtf.csrf import validate_csrf # 用于手动验证 CSRF main.route(/delete/int:todo_id, methods[POST]) def delete_todo(todo_id): # 手动验证 CSRF因未使用 WTForms 表单 try: validate_csrf(request.form.get(csrf_token)) except: flash(无效请求请重试, error) return redirect(url_for(main.index)) todo Todo.query.get_or_404(todo_id) db.session.delete(todo) db.session.commit() flash(任务已删除, info) return redirect(url_for(main.index))步骤2更新删除按钮为表单提交修改templates/index.html中的删除按钮!-- 替换原删除链接 -- form methodPOST action{{ url_for(main.delete_todo, todo_idtodo.id) }} styledisplay:inline; onsubmitreturn confirm(确定要删除「{{ todo.title }}」吗) {{ form.hidden_tag() }} !-- 复用表单的 CSRF token -- button typesubmit classlayui-btn layui-btn-xs layui-btn-danger删除/button /form优势符合安全规范防止 CSRF 攻击。5.3 添加“标记全部完成”功能后端路由main.route(/complete_all, methods[POST]) def complete_all(): try: validate_csrf(request.form.get(csrf_token)) except: flash(操作失败, error) return redirect(url_for(main.index)) Todo.query.update({Todo.done: True}) db.session.commit() flash(所有任务已标记为完成, success) return redirect(url_for(main.index))前端按钮在任务列表上方添加!-- 在搜索表单下方添加 -- {% if todos %} form methodPOST action{{ url_for(main.complete_all) }} stylemargin: 10px 0; {{ form.hidden_tag() }} button typesubmit classlayui-btn layui-btn-sm layui-btn-warm全部标记完成/button /form {% endif %}Layui 颜色扩展.layui-btn-warm需自定义 CSS橙色.layui-btn-warm { background-color: #ff9700 !important; }第六章数据库查询优化与分页6.1 为什么需要分页当任务数量超过 1000 条时一次性加载会导致页面渲染卡顿内存占用过高数据库查询缓慢6.2 使用 SQLAlchemy 分页修改首页路由main.route(/) def index(): form TodoForm() page request.args.get(page, 1, typeint) # 获取页码 query_str request.args.get(q, ).strip() # 构建查询 query Todo.query if query_str: query query.filter(Todo.title.contains(query_str)) query query.order_by(Todo.created_at.desc()) # 执行分页每页10条 pagination query.paginate( pagepage, per_page10, error_outFalse # 页码超出范围时不报错 ) todos pagination.items return render_template( index.html, formform, todostodos, search_queryquery_str, paginationpagination # 传递分页对象 )6.3 在模板中渲染分页在templates/index.html任务列表下方添加!-- 分页组件 -- {% if pagination.pages 1 %} div stylemargin: 20px 0; text-align: center; div classlayui-btn-group {% if pagination.has_prev %} a href{{ url_for(main.index, pagepagination.prev_num, qsearch_query) }} classlayui-btn layui-btn-primary layui-btn-sm上一页/a {% endif %} {% for p in pagination.iter_pages() %} {% if p %} {% if p pagination.page %} a classlayui-btn layui-btn-sm stylebackground:#1E9FFF;{{ p }}/a {% else %} a href{{ url_for(main.index, pagep, qsearch_query) }} classlayui-btn layui-btn-primary layui-btn-sm{{ p }}/a {% endif %} {% else %} span classlayui-btn layui-btn-disabled layui-btn-sm…/span {% endif %} {% endfor %} {% if pagination.has_next %} a href{{ url_for(main.index, pagepagination.next_num, qsearch_query) }} classlayui-btn layui-btn-primary layui-btn-sm下一页/a {% endif %} /div /div {% endif %}效果自动显示页码导航支持搜索结果分页。6.4 为搜索字段添加数据库索引加速LIKE %关键词%查询# models.py class Todo(db.Model): # ... 其他字段 ... title db.Column(db.String(100), nullableFalse) # 添加索引 __table_args__ (db.Index(idx_title, title),)注意SQLite 的LIKE查询对索引利用有限但在 MySQL/PostgreSQL 中效果显著。第七章代码结构终极优化7.1 完善应用工厂模式当前app.py已具备工厂雏形进一步标准化# app.py from flask import Flask from config import config from models import db from forms import csrf # 稍后创建 def create_app(config_namedefault): app Flask(__name__) app.config.from_object(config[config_name]) # 初始化扩展 db.init_app(app) csrf.init_app(app) # 初始化 CSRF 保护 # 注册蓝图 from routes.main import main as main_blueprint app.register_blueprint(main_blueprint) # 错误处理器 app.errorhandler(404) def page_not_found(e): return render_template(errors/404.html), 404 app.errorhandler(500) def internal_server_error(e): app.logger.error(fServer Error: {e}) return render_template(errors/500.html), 500 # 初始化数据库 with app.app_context(): db.create_all() return app7.2 独立 CSRF 保护配置新建extensions.py可选但更清晰# extensions.py from flask_wtf.csrf import CSRFProtect csrf CSRFProtect()然后在forms.py中# forms.py from flask_wtf import FlaskForm from extensions import csrf # 改为从这里导入 # ... 其他代码不变 ...并在app.py中from extensions import csrf # ... csrf.init_app(app)7.3 最终项目结构flask-todo-layui/ ├── app.py ├── config.py ├── extensions.py # 新增扩展实例 ├── forms.py ├── models.py ├── requirements.txt ├── logs/ # 运行后生成 ├── templates/ │ ├── base.html │ ├── index.html │ └── errors/ │ ├── 404.html │ └── 500.html └── routes/ ├── __init__.py └── main.py