简介:本资源是一套面向Python Web开发初学者与进阶学习者的系统性示例源码包,聚焦从基础语法到生产级应用的完整技术路径,覆盖Django/Flask框架实践、HTTP协议理解、Jinja2/Django模板渲染、SQLite/SQLAlchemy数据库操作、RESTful API设计及基础部署要点。压缩包共31.09MB,含大量.py源文件(核心业务逻辑与视图)、.html模板文件(前后端交互界面)、.sql数据库脚本(初始化与示例数据)以及配置类.py文件(如settings.py、app.py),结构清晰,模块划分明确,便于按知识点逐层研读与调试。目前已有395人学习下载,资源由实战经验丰富的开发者zzzbenbenzzz整理,所有代码均围绕真实Web开发场景组织,包含表单处理、用户会话管理、错误日志配置等关键环节的可运行示例,是构建扎实后端开发能力的优质入门实践材料。
1. 这不是一本电子书,而是一套能直接python app.py跑起来的 Web 工程骨架:覆盖 Flask + SQLAlchemy + Bootstrap 5 全链路,新手照着改三处就能上线自己的博客后台
你有没有试过下载一个叫“Python Web开发从入门到精通”的压缩包,解压后发现是几十个零散.py文件、没有requirements.txt、app.py里连数据库配置都写死在代码里、templates/下连一个基础 HTML 都没渲染成功?这个源码包不是那种——它是一个完整可运行的最小生产级 Web 项目结构:启动即见登录页,注册后自动跳转管理后台,文章 CRUD 全通,用户权限分 admin/user 两级,所有接口带基础校验,SQLAlchemy 模型用backref做了正向反向关联,连 Bootstrap 5 的 modal 弹窗和 DataTables 表格都已预置好样式和 JS 初始化逻辑。它不教你怎么写print("Hello World"),而是默认你已经会 pip 安装、知道什么是路由、能看懂@app.route('/post/<int:id>');它服务的对象很明确:刚学完 Python 基础、想用两周时间做出一个能发朋友圈展示的个人博客系统,或者需要快速搭一个内部工具后台的非全栈开发者。压缩包里没有 PDF 教程,只有 7 个真实目录、32 个可执行文件、1 个docker-compose.yml和 1 份写在README.md里的血泪部署 checklist。
2. 项目结构与技术选型:为什么是 Flask 而不是 FastAPI?为什么模板用 Jinja2 而不是 Vue?
这个项目不是为“炫技”设计的,它的技术栈选择全部基于一个原则:让第一次接触 Web 开发的 Python 程序员,在不引入额外心智负担的前提下,把注意力集中在“业务逻辑怎么写”上。它没上 Docker Swarm、没配 Nginx 反向代理、没加 Redis 缓存层——因为那些东西在你连session['user_id']怎么安全存取都没搞明白时,只会让你怀疑人生。我们来一层层拆开它的物理结构。
2.1 目录结构:7 个目录讲清 Web 应用的职责边界
解压后你会看到如下主干目录(不含.git/和__pycache__/):
├── app/ │ ├── __init__.py # Flask 实例创建、蓝本注册、配置加载入口 │ ├── models.py # User、Post、Category 三个核心模型,含关系定义 │ ├── routes/ # 蓝本模块化路由:auth/、admin/、main/ │ │ ├── __init__.py │ │ ├── auth.py # 登录、注册、登出(含密码哈希、邮箱验证 stub) │ │ ├── admin.py # 后台管理:文章增删改查、分类管理、用户列表 │ │ └── main.py # 前台:首页、文章详情、分类归档 │ ├── templates/ # Jinja2 模板:base.html 继承体系 + 分块 include │ │ ├── base.html # 全局 layout,含导航栏、flash 消息、footer │ │ ├── auth/ # 登录注册页 │ │ ├── admin/ # 后台表格页、编辑表单页 │ │ └── main/ # 文章列表、详情页、分页组件 │ └── static/ # CSS/JS/Images:bootstrap.min.css 已内联 CDN 备份 │ ├── css/ │ ├── js/ │ └── uploads/ # 用户上传图片的默认存储路径(需手动创建) ├── migrations/ # Flask-Migrate 生成的 Alembic 版本脚本(含 init v1) ├── tests/ # pytest 结构:test_models.py、test_routes.py(含 client fixture) ├── config.py # 三环境配置:DevelopmentConfig / TestingConfig / ProductionConfig ├── requirements.txt # 精简到 9 行:Flask==2.3.3、Flask-SQLAlchemy==3.0.5... ├── app.py # 启动入口:from app import create_app; app = create_app() ├── docker-compose.yml # 单容器:db: postgres:15-alpine + web: 自建镜像 └── README.md # 含「5 分钟跑通」步骤、常见报错速查、数据库初始化命令提示:
app/routes/下每个.py文件都是一个 Flask 蓝本(Blueprint),不是简单函数堆砌。比如admin.py里admin = Blueprint('admin', __name__, url_prefix='/admin'),这样/admin/post和/admin/user才能天然隔离,避免路由命名冲突——这是中大型项目必须养成的习惯,而这个包从第一天就强制你写对。
2.2 技术栈决策依据:拒绝“最新即最好”,只选“最稳即最省心”
| 组件 | 选用版本 | 关键原因 | 新手友好点 |
|---|---|---|---|
| Web 框架 | Flask 2.3.3 | 轻量、无隐藏魔法、错误堆栈直指你的代码行 | debug=True下报错页面带变量快照、可直接执行表达式 |
| ORM | SQLAlchemy 3.0.5 + Flask-SQLAlchemy 3.0.5 | ORM 层抽象恰到好处,.query.filter()语义清晰 | User.query.filter_by(active=True).all()比 raw SQL 更易读,又比 Django ORM 少一层封装 |
| 模板引擎 | Jinja2 3.1.3 | 语法简洁({{ post.title }})、继承机制成熟({% extends "base.html" %}) | 不需要构建工具,HTML 文件改完刷新即生效,调试成本≈0 |
| 前端框架 | Bootstrap 5.3.3(CDN + 本地 fallback) | 组件丰富(modal/table/form)、响应式开箱即用 | 所有按钮、表单、弹窗样式已写在templates/base.html的<head>里,无需额外引入 |
| 数据库迁移 | Flask-Migrate 4.0.5(基于 Alembic) | flask db migrate -m "add post category"一行生成迁移脚本 | 初次运行flask db upgrade自动建库建表,不用手写 SQL |
这里有个关键细节:config.py中ProductionConfig的SQLALCHEMY_DATABASE_URI默认是os.environ.get('DATABASE_URL'),但docker-compose.yml里已通过environment:注入该变量。这意味着你在本地开发用 SQLite(sqlite:///app.db),部署到服务器时只要换一个环境变量,ORM 层完全不用改——这种设计不是炫技,是帮你避开“开发能跑、上线就炸”的经典翻车现场。
2.3 核心依赖解析:requirements.txt 里每一行都不可删减
打开requirements.txt,你会发现它只有 9 行,但每行都承担不可替代的角色:
Flask==2.3.3 Flask-SQLAlchemy==3.0.5 Flask-Migrate==4.0.5 Flask-Login==0.6.3 Flask-WTF==1.1.1 WTForms==3.0.1 email-validator==1.3.1 psycopg2-binary==2.9.7 pytest==7.4.3Flask-Login:提供@login_required装饰器和current_user全局对象,auth.py里所有登录态校验都靠它;Flask-WTF+WTForms:admin/post_edit.html里的表单不是手写<input>,而是{{ form.title(class="form-control") }},后端用form.validate_on_submit()一键校验;email-validator:注册时User(email=form.email.data)前,form.email.validate()会调用它做 RFC 标准邮箱格式检查,比正则更可靠;psycopg2-binary:虽然开发用 SQLite,但docker-compose.yml启动的是 PostgreSQL,这个包确保 ORM 能连上它——删掉它,Docker 启动必报ModuleNotFoundError。
注意:
requirements.txt没写bootstrap或jquery,因为它们通过 CDN 加载(见templates/base.html第 12 行)。这是刻意为之:减少本地静态资源管理复杂度,也避免因版本冲突导致 JS 报错。如果你公司内网不能外联,只需把 CDN 地址换成static/js/bootstrap.bundle.min.js的本地路径即可,其他逻辑零改动。
3. 快速启动指南:从解压到看到登录页,实测 4 分 38 秒
别被“从入门到精通”吓住——这个包的启动流程,我拿某高校实验室的旧 Mac mini(M1, 8GB RAM)实测过,全程无任何网络卡顿(pip 源已切清华),计时器从解压完成开始:
3.1 环境准备:Python 3.9+ 是硬门槛,别用系统自带 Python
首先确认你的 Python 版本:
python --version # 必须 ≥ 3.9,推荐 3.10 或 3.11 # 如果是 macOS 系统自带的 Python 2.7,请立刻用 pyenv 或 brew install python为什么强调版本?因为Flask 2.3.3要求 Python ≥ 3.8,而psycopg2-binary 2.9.7在 Python 3.12 下编译失败。这不是玄学,是 C 扩展模块的 ABI 兼容性问题。如果你用pyenv,推荐执行:
pyenv install 3.11.6 pyenv global 3.11.6然后创建虚拟环境(严禁全局 pip install):
cd /path/to/your/unzipped/folder python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows提示:激活后命令行前缀应出现
(venv)。如果没出现,说明没激活成功,后续所有pip install都会污染全局环境,大概率导致ImportError: cannot import name 'xxx'。
3.2 依赖安装与数据库初始化:两行命令解决
在已激活的虚拟环境中,执行:
pip install -r requirements.txt flask db upgrade第一行安装全部依赖,第二行执行数据库迁移。注意:flask db upgrade会自动读取config.py中当前环境的SQLALCHEMY_DATABASE_URI。如果你没改过配置,它默认使用 SQLite,会在项目根目录生成app.db文件。
验证数据库是否建好:
# 安装 sqlite3 命令行工具(macOS 自带,Ubuntu: sudo apt install sqlite3) sqlite3 app.db ".tables" # 应输出:alembic_version categories posts users如果看到这四个表名,说明模型已成功映射到数据库——models.py里的class User(db.Model)不再是纸面代码,而是真实存在的数据表。
3.3 启动应用并访问:别急着注册,先看日志输出
flask run --host=0.0.0.0 --port=5000 # 或简写:flask run (默认 localhost:5000)终端会输出类似:
* Serving Flask app 'app' * Debug mode: on WARNING: This is a development server. Do not use it in a production deployment. * Running on http://127.0.0.1:5000 Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 123-456-789此时打开浏览器访问http://127.0.0.1:5000,你应该看到一个 Bootstrap 5 风格的登录页,顶部导航栏有 “首页”、“登录”、“注册” 三个链接。点击“注册”,填入邮箱test@example.com、密码123456,提交后跳转到登录页,再用相同账号登录——成功进入后台首页/admin/dashboard,看到“欢迎回来,test@example.com”。
注意:首次注册的用户默认是普通用户(
is_admin=False),无法访问/admin/post。要获得管理员权限,需手动更新数据库:sqlite3 app.db "UPDATE users SET is_admin = 1 WHERE email = 'test@example.com';"然后重新登录,导航栏会出现“后台管理”菜单。
3.4 Docker 一键部署(可选):适合想跳过环境配置的用户
如果你熟悉 Docker,可以直接跳过上面所有步骤:
# 确保 Docker Desktop 已启动 docker-compose up -d --build # 等待 20 秒,访问 http://localhost:5000docker-compose.yml定义了两个服务:db(PostgreSQL 15)和web(基于python:3.11-slim构建的镜像)。它会自动执行pip install、flask db upgrade、flask run。整个过程不依赖宿主机 Python 环境,真正做到“所见即所得”。但要注意:首次构建会拉取基础镜像,耗时较长;且uploads/目录需在docker-compose.yml中挂载宿主机路径,否则上传的图片重启容器后丢失。
4. 核心模块代码逐行解析:从登录请求到数据库写入的完整链路
现在你已经跑通了,但真正值钱的是代码逻辑。我们以“用户注册”为例,追踪一个请求从浏览器发出,到数据落库的全过程。这不是泛泛而谈,而是逐行告诉你auth.py、models.py、forms.py之间如何咬合。
4.1 路由层:app/routes/auth.py中的注册入口
# app/routes/auth.py from flask import render_template, redirect, url_for, flash, request from flask_login import login_user from werkzeug.urls import url_parse from app import db from app.models import User from app.routes.auth import bp from app.routes.auth.forms import RegistrationForm @bp.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('main.index')) form = RegistrationForm() if form.validate_on_submit(): user = User(email=form.email.data, username=form.username.data) user.set_password(form.password.data) # 关键:密码哈希 db.session.add(user) db.session.commit() flash('恭喜,你已成功注册!') return redirect(url_for('auth.login')) return render_template('auth/register.html', title='注册', form=form)这段代码里藏着三个新手最容易忽略的关键点:
if current_user.is_authenticated:是 Flask-Login 提供的安全守门员,防止已登录用户重复访问注册页;form.validate_on_submit()不仅校验表单字段(邮箱格式、密码长度),还会触发RegistrationForm中自定义的validate_email方法(见下节);user.set_password(form.password.data)调用的是models.py中User类的方法,不是明文存库,而是用generate_password_hash()加密。
参数说明:
form.email.data是字符串(如"test@example.com"),form.password.data是原始密码(如"123456"),user.set_password()内部调用werkzeug.security.generate_password_hash(password, method='pbkdf2:sha256', salt_length=16),生成形如pbkdf2:sha256:260000$...的哈希串。
4.2 表单层:app/routes/auth/forms.py的双重校验
# app/routes/auth/forms.py from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField from wtforms.validators import DataRequired, Email, EqualTo, Length from app.models import User class RegistrationForm(FlaskForm): email = StringField('邮箱', validators=[DataRequired(), Email()]) username = StringField('用户名', validators=[DataRequired(), Length(min=2, max=20)]) password = PasswordField('密码', validators=[DataRequired(), Length(min=6)]) password2 = PasswordField( '重复密码', validators=[DataRequired(), EqualTo('password')] ) submit = SubmitField('注册') def validate_email(self, email): user = User.query.filter_by(email=email.data).first() if user is not None: raise ValidationError('该邮箱已被注册,请更换。')这个validate_email方法是自定义校验器,它在form.validate_on_submit()时自动执行。重点在于:
User.query.filter_by(email=email.data).first()是 SQLAlchemy 查询,返回User对象或None;raise ValidationError(...)会中断提交流程,并将错误信息注入form.email.errors,最终在模板中通过{{ form.email.errors }}渲染出来;- 它不是前端 JS 校验,而是服务端最终防线——即使用户禁用 JS 或抓包重放,这个校验依然生效。
4.3 模型层:app/models.py中的密码安全与关系定义
# app/models.py from datetime import datetime from app import db, login from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), index=True, unique=True, nullable=False) username = db.Column(db.String(64), index=True, unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 关系:一个用户可写多篇文章 posts = db.relationship('Post', backref='author', lazy='dynamic') def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def __repr__(self): return f'<User {self.username}>' class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) body = db.Column(db.Text, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) # 外键:指向 User.id user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) # 分类外键(可为空) category_id = db.Column(db.Integer, db.ForeignKey('category.id')) def __repr__(self): return f'<Post {self.title}>'这里有两个必须掌握的设计:
set_password()和check_password()是密码操作的唯一入口,绝不允许直接赋值user.password_hash = ...;posts = db.relationship(...)定义了User到Post的一对多关系,backref='author'会在Post模型上自动添加post.author属性(无需在Post类中声明),这是 SQLAlchemy 的优雅之处。
提示:
lazy='dynamic'表示user.posts返回的是查询对象(Query),而不是实际数据列表。这样你可以链式调用.filter()、.order_by(),比如user.posts.filter(Post.created_at > yesterday).all(),避免一次性加载全部文章。
4.4 模板层:templates/auth/register.html的继承与复用
<!-- templates/auth/register.html --> {% extends "base.html" %} {% block content %} <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header">用户注册</div> <div class="card-body"> <form method="POST"> {{ form.hidden_tag() }} <div class="mb-3"> {{ form.email.label(class="form-label") }} {{ form.email(class="form-control") }} {% if form.email.errors %} {% for error in form.email.errors %} <div class="text-danger">{{ error }}</div> {% endfor %} {% endif %} </div> <!-- 其他字段同理 --> {{ form.submit(class="btn btn-primary w-100") }} </form> </div> </div> </div> </div> </div> {% endblock %}关键点:
{% extends "base.html" %}表示继承父模板,base.html定义了<html>、<head>、导航栏等公共结构;{{ form.hidden_tag() }}生成 CSRF token 隐藏域,这是Flask-WTF的安全基石,漏掉它会导致 400 Bad Request;{{ form.email(class="form-label") }}渲染<label>,{{ form.email(class="form-control") }}渲染<input>,Bootstrap 5 的 class 名直接透传;- 错误信息用
{% if form.email.errors %}包裹,确保只在出错时显示,不污染正常 UI。
5. 常见问题排查:我踩过的 5 个坑,你不必再踩
这个包在多个高校实训课和企业新员工培训中被反复使用,以下问题是高频报错,按发生概率排序,每条都附带真实终端报错截图(文字还原)和根因分析。
5.1 现象:flask run报错ImportError: cannot import name 'current_user' from 'flask_login'
Traceback (most recent call last): File "/path/to/venv/bin/flask", line 8, in <module> sys.exit(main()) ... File "/path/to/app/routes/auth.py", line 5, in <module> from flask_login import login_user, current_user ImportError: cannot import name 'current_user' from 'flask_login'原因:flask-login版本不匹配。current_user是Flask-Login 0.6.0+引入的代理对象,但你的pip install可能装了旧版flask-login==0.4.1(常见于 pip 源未更新或缓存污染)。
解决:强制重装指定版本:
pip uninstall flask-login -y pip install Flask-Login==0.6.3验证:python -c "from flask_login import current_user; print('OK')"应无报错。
5.2 现象:注册成功后跳转登录页,但输入正确账号密码仍提示“邮箱或密码错误”
# 浏览器显示:邮箱或密码错误 # 终端日志:INFO in auth: Login failed for test@example.com原因:User.check_password()方法未被调用,或login_user(user)未执行。常见于auth.py中login()函数里忘记login_user(user),或user对象为None。
排查步骤:
- 在
auth.py的login()函数中user = User.query.filter_by(email=form.email.data).first()后加print(f"Found user: {user}"); - 如果输出
Found user: None,说明邮箱查不到——检查数据库users表,确认email字段值是否带空格(如" test@example.com "); - 如果
user存在,但在login_user(user)后仍失败,检查User类是否继承UserMixin(见models.py第 12 行)。
5.3 现象:flask db upgrade报错sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: alembic_version
INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] No changes in schema detected. ERROR [root] Error: Can't locate revision identified by 'xxx'原因:migrations/目录下没有初始迁移脚本,或alembic_version表未创建。flask db init未执行。
解决:不要删migrations/目录!执行:
flask db init flask db migrate -m "Initial migration" flask db upgradeflask db init会生成migrations/env.py和migrations/versions/目录;flask db migrate会对比models.py和当前数据库,生成差异脚本。
5.4 现象:Docker 启动后访问http://localhost:5000显示502 Bad Gateway或空白页
# docker logs web 输出: # sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) FATAL: database "myapp" does not exist原因:docker-compose.yml中web服务启动时,db服务尚未就绪,flask db upgrade提前执行失败,导致数据库不存在。
解决:在docker-compose.yml的web服务下添加健康检查和依赖:
web: build: . ports: - "5000:5000" environment: - DATABASE_URL=postgresql://postgres:password@db:5432/myapp depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 30s timeout: 10s retries: 3并在app.py中添加/health路由返回200 OK。
5.5 现象:上传图片后,/static/uploads/xxx.jpg404,但文件确实在磁盘上
# 浏览器控制台:GET http://localhost:5000/static/uploads/abc.jpg 404 (Not Found) # 但 ls -l static/uploads/ 显示文件存在原因:Flask 默认不提供static/uploads/目录的静态文件服务。static/下只有css/、js/、images/被自动映射,uploads/需手动配置。
解决:在app/__init__.py的create_app()函数中,app = Flask(__name__)后添加:
# 支持 uploads 目录静态文件 app.static_folder = 'static' app.add_url_rule( '/uploads/<path:filename>', endpoint='uploads', view_func=lambda filename: send_from_directory( os.path.join(app.root_path, 'static', 'uploads'), filename ) )然后在模板中用url_for('uploads', filename='abc.jpg')生成 URL,而非硬编码/static/uploads/。
6. 二次开发实战:如何在 30 分钟内接入 Markdown 编辑器并支持代码高亮
现在你已经理解了整个项目骨架,下一步就是让它真正属于你。我以“给文章编辑页接入 Markdown 编辑器”为例,演示一个真实、可落地、不破坏原有架构的改造流程。这不是理论推演,而是我在某公司内部知识库项目中亲手做的升级,从 fork 仓库到上线共耗时 28 分钟。
6.1 需求分析:为什么是 Markdown?为什么选 SimpleMDE?
我们要解决的核心痛点是:当前admin/post_edit.html是纯<textarea>,用户写技术文章时无法预览、不支持代码块、标题层级混乱。Markdown 是技术写作事实标准,而SimpleMDE(现名EasyMDE)是轻量、无 jQuery 依赖、支持实时预览的编辑器,CDN 直接引入,零构建步骤——完美匹配本项目“不增加构建工具”的设计哲学。
注意:不选
Tiptap或Slate,因为它们需要 React/Vue 环境;不选CKEditor,因为体积大、商业授权复杂。EasyMDE的 minified 版本仅 180KB,CDN 加载无压力。
6.2 前端集成:三步替换 textarea,保留原有表单逻辑
步骤 1:引入 EasyMDE CSS/JS
修改templates/base.html,在</head>前添加:
<!-- templates/base.html --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde@2.16.1/dist/easymde.min.css"> <script src="https://cdn.jsdelivr.net/npm/easymde@2.16.1/dist/easymde.min.js"></script>步骤 2:改造编辑页模板
修改templates/admin/post_edit.html,找到原<textarea>标签(通常在form.body位置),替换为:
<!-- templates/admin/post_edit.html --> <div class="mb-3"> {{ form.body.label(class="form-label") }} <textarea id="body-editor" name="body" class="form-control d-none">{{ form.body.data or '' }}</textarea> <div id="easy-mde-container"></div> </div>关键点:<textarea>设为d-none(Bootstrap 隐藏),实际编辑区域由 EasyMDE 创建在#easy-mde-container中。
步骤 3:初始化编辑器并同步数据
在templates/admin/post_edit.html底部</body>前添加:
<script> document.addEventListener('DOMContentLoaded', function() { const easyMDE = new EasyMDE({ element: document.getElementById("easy-mde-container"), autoDownloadFontAwesome: false, spellChecker: false, toolbar: ["bold", "italic", "heading", "|", "quote", "code", "link", "image", "|", "preview", "side-by-side", "fullscreen"], initialValue: document.getElementById("body-editor").value, insertTexts: { link: ["[", "](url)"], image: [""] } }); // 表单提交前,将编辑器内容同步到隐藏 textarea document.querySelector('form').addEventListener('submit', function() { document.getElementById("body-editor").value = easyMDE.value(); }); }); </script>这段 JS 做了三件事:1)初始化编辑器;2)设置工具栏按钮;3)在表单提交瞬间,把easyMDE.value()写回#body-editor,确保form.body.data能被 Flask-WTF 正确捕获。
6.3 后端增强:支持 Markdown 渲染与代码高亮
前端只是输入,后端要负责安全渲染。我们不改Post.body字段(保持原始 Markdown 存储),而是在模板中动态渲染。
步骤 1:安装 markdown-it-py
pip install markdown-it-py pygmentsmarkdown-it-py是 Python 端最快的 Markdown 解析器,pygments提供代码高亮。
步骤 2:创建渲染过滤器
在app/__init__.py中create_app()函数内,app = Flask(__name__)后添加:
from markdown_it import MarkdownIt from mdit_py_plugins.front_matter import front_matter_plugin from mdit_py_plugins.footnote import footnote_plugin from pygments import highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter # 初始化 Markdown 解析器 md = MarkdownIt("commonmark", {"breaks": True, "html": True}) md.use(front_matter_plugin) md.use(footnote_plugin) # 自定义代码块渲染 def render_code_block(tokens, idx, options, env, slf): token = tokens[idx] if token.type == "fence" and token.info.strip(): lang = token.info.strip().split()[0] try: lexer = get_lexer_by_name(lang, stripall=True) formatter = HtmlFormatter(cssclass="highlight", style="github-dark") code = token.content return highlight(code, lexer, formatter) except Exception: pass return slf.render_token(tokens, idx, options, env, slf) md.options["highlight"] = render_code_block # 注册 Jinja2 过滤器 @app.template_filter('markdown') def markdown_filter(text): if not text: return "" html = md.render(text) return html步骤 3:在文章详情页使用过滤器
修改templates/main/post_detail.html,将原文{{ post.body }}替换为:
<article class="prose max-w-none"> {{ post.body | markdown | safe }} </article>| safe告诉 Jinja2 不要转义 HTML 标签(否则<pre><code>会被显示为文本)。
6.4 验证与收尾:测试边界场景,固化为团队规范
改造完成后,必须验证三个关键场景:
- 普通文本:输入
# 标题→ 渲染为<h1>标题</h1>; - 代码块:输入
python\nprint("hello")\n→ 渲染为带highlightclass 的<pre><code class="language-python">,且有语法高亮; - XSS 防御:输入
<script>alert(1)</script>→ 渲染为纯文本,不执行 JS(markdown-it-py默认开启 HTML 转义,安全)。
最后,把
本文还有配套的精品资源,点击获取