
简介这份资源是面向Python Web开发初学者与Flask进阶学习者的项目模板将后端框架Flask与前端CSS框架Bulma结合解决从零搭建带用户系统的Web应用时结构混乱、重复造轮子的问题。模板基于Corey Schafer的Flask系列教程整理未采用应用工厂模式代码按功能拆分到独立文件夹与文件中便于按需查阅和二次开发。压缩包共34个文件约536KB包含13个HTML页面模板、8个Python脚本、6张界面截图以及CSS、JavaScript、依赖清单和说明文档等覆盖页面渲染、路由逻辑与静态资源等用途。功能上提供账户注册、登录登出、邮箱确认、密码重置、简单数据库模型与表单处理等模块并附有界面截图辅助理解。目前已有283人学习下载适合希望快速获得一套结构清晰、可直接改造成个人项目的Flask入门者参考使用。1. 从一份 Flask Bulma 模板说起为什么后端工程师需要一个能直接跑的前端壳子如果你写过 Flask大概率经历过这个场景路由跑通了接口返回 200但打开浏览器一看页面是裸的 HTML按钮丑得没法见人。这时候你有两个选择——要么花半天时间配 Webpack、装 Tailwind、调构建流程要么找一个已经搭好的模板把精力留给后端逻辑。这份 flask-bulma-template 就是为第二种场景准备的它把 Flask 的 Jinja2 模板体系和 Bulma 这个纯 CSS 框架拼在一起不需要 Node.js不需要打包工具克隆下来就能跑出一个带导航栏、卡片、表单样式的完整页面骨架。它适合谁适合那些主力写 Python、不想碰前端构建链、但又需要页面能拿得出手的后端开发者。Bulma 本身是纯 CSS 框架只依赖一个 CSS 文件没有 JavaScript 运行时依赖和 Flask 的模板继承机制配合起来非常自然。你不需要理解 Sass 编译也不需要维护 package.json改样式就是改 CSS 变量和 class 名。接下来我会把这份模板的结构、用法、参数配置和几个我实际踩过的坑拆开讲让你拿到手就能改成一个能用的项目起点。2. Flask 与 Bulma 的协作机制模板继承、静态资源与块覆盖2.1 为什么选 Bulma 而不是 Bootstrap 或 Tailwind先说选型理由因为这决定了你后面改样式的手感。Bootstrap 生态最大组件最全但它的 JavaScript 插件依赖 jQuery 或自己的 bundleFlask 项目里引入这些会让静态资源目录变重。Tailwind 是原子化 CSS写起来灵活但它需要构建步骤来 purge 未使用的 class否则生产环境 CSS 文件会膨胀到几 MB这对一个纯 Flask 项目来说是不必要的复杂度。Bulma 的定位在两者之间它提供了完整的组件 class导航栏、卡片、表单、表格、通知但完全是 CSS没有 JS。交互行为比如导航栏折叠、下拉菜单需要你自己写几行原生 JavaScript 或者用它的扩展。对于后端开发者来说这个取舍是划算的——你得到了一套开箱即用的视觉规范代价只是少量交互需要手写。常见做法是在 base 模板里引入 Bulma 的 CDN 链接或者本地 CSS 文件然后在页面模板里通过 Jinja2 的{% block %}覆盖内容区域。2.2 模板继承结构base.html 如何组织这份模板的核心是base.html它定义了整个站点的骨架。下面是一个典型的 base 模板结构我按实际项目里会用的方式写出来!-- templates/base.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}默认标题{% endblock %}/title !-- Bulma CSS 本地引入避免 CDN 在离线环境失效 -- link relstylesheet href{{ url_for(static, filenamecss/bulma.min.css) }} !-- 自定义样式覆盖区放在 Bulma 之后保证优先级 -- {% block extra_css %}{% endblock %} /head body !-- 导航栏组件所有页面共享 -- nav classnavbar is-primary rolenavigation aria-labelmain navigation div classnavbar-brand a classnavbar-item href{{ url_for(index) }} strong项目名称/strong /a !-- 移动端汉堡菜单按钮需要配合 JS 切换 is-active -- a rolebutton classnavbar-burger aria-labelmenu># app.py from flask import Flask app Flask(__name__, static_url_path/assets, static_folderstatic)这样所有静态资源会走/assets前缀模板里的url_for(static, ...)会自动适配不需要改模板。参数static_folder指定物理目录static_url_path指定 URL 前缀两者可以独立配置。3. 从零跑起来环境搭建、页面渲染与表单接入3.1 最小可运行项目的文件结构与启动命令拿到模板后第一步是确认目录结构完整。一个能跑的最小 Flask Bulma 项目通常长这样project/ ├── app.py ├── requirements.txt ├── templates/ │ ├── base.html │ ├── index.html │ └── form_demo.html └── static/ ├── css/ │ ├── bulma.min.css │ └── custom.css └── js/ └── main.jsrequirements.txt里只需要一行Flask因为 Bulma 是纯 CSS不需要 Python 包。安装和启动# 创建虚拟环境并安装依赖 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt # 启动开发服务器 flask --app app run --debug--debug开启调试模式和热重载改模板和 Python 代码后自动重启。注意生产环境不要用这个命令应该用 Gunicorn 或 uWSGI 配合 Nginx。3.2 渲染第一个页面index.html 如何继承 base子模板的写法很直接核心就是{% extends %}和{% block %}!-- templates/index.html -- {% extends base.html %} {% block title %}首页 - 项目名称{% endblock %} {% block content %} div classcolumns is-multiline !-- Bulma 的栅格系统每个 column 占 4 列一行三个 -- div classcolumn is-4 div classcard div classcard-content p classtitle is-5模块一/p p classsubtitle is-6状态运行中/p /div /div /div div classcolumn is-4 div classcard div classcard-content p classtitle is-5模块二/p p classsubtitle is-6状态待配置/p /div /div /div div classcolumn is-4 div classcard div classcard-content p classtitle is-5模块三/p p classsubtitle is-6状态已完成/p /div /div /div /div {% endblock %}对应的 Flask 路由# app.py from flask import Flask, render_template app Flask(__name__) app.route(/) def index(): # 实际项目里这里通常从数据库或 API 取数据 modules [ {name: 模块一, status: 运行中}, {name: 模块二, status: 待配置}, {name: 模块三, status: 已完成}, ] return render_template(index.html, modulesmodules)逻辑说明render_template把modules列表传给模板模板里可以用{% for %}循环渲染。参数方面render_template的第一个参数是模板文件名相对于templates/目录后面的关键字参数会变成模板里的变量。Bulma 的columns和column is-4是栅格 classis-4表示占 12 列中的 4 列也就是三分之一宽度。在移动端 Bulma 会自动堆叠不需要额外写媒体查询。3.3 表单页面与 CSRF 防护的接入方式Flask 项目里表单通常用 Flask-WTF 来处理它自带 CSRF 防护。安装pip install flask-wtf表单类定义# forms.py from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired class ConfigForm(FlaskForm): # DataRequired 确保字段非空否则表单验证不通过 name StringField(配置名称, validators[DataRequired()]) submit SubmitField(保存)模板里渲染!-- templates/form_demo.html -- {% extends base.html %} {% block content %} form methodPOST action{{ url_for(config_form) }} !-- CSRF token 必须放在表单里否则 Flask-WTF 会拒绝请求 -- {{ form.hidden_tag() }} div classfield label classlabel{{ form.name.label.text }}/label div classcontrol {{ form.name(classinput, placeholder输入配置名称) }} /div /div div classfield div classcontrol {{ form.submit(classbutton is-primary) }} /div /div /form {% endblock %}路由处理# app.py from flask import Flask, render_template, flash, redirect, url_for from forms import ConfigForm app Flask(__name__) app.config[SECRET_KEY] dev-key-change-in-production # CSRF 依赖这个密钥 app.route(/config, methods[GET, POST]) def config_form(): form ConfigForm() if form.validate_on_submit(): # 表单验证通过处理数据 flash(f配置 {form.name.data} 已保存, success) return redirect(url_for(config_form)) return render_template(form_demo.html, formform)参数说明SECRET_KEY是 Flask-WTF 生成和验证 CSRF token 的密钥生产环境必须换成随机字符串并从环境变量读取。form.hidden_tag()渲染隐藏的 CSRF token 字段。form.name(classinput)里的class参数会把 Bulma 的inputclass 加到生成的input标签上这样样式才能生效。flash消息需要在 base 模板里渲染出来通常放在 content 块上方{% with messages get_flashed_messages(with_categoriestrue) %} {% if messages %} {% for category, message in messages %} div classnotification is-{{ category }}{{ message }}/div {% endfor %} {% endif %} {% endwith %}4. 避坑与排查模板不渲染、样式丢失、CSRF 报错的真实原因4.1 页面返回 200 但样式全无静态资源 404 排查现象Flask 日志显示 200但浏览器控制台一堆 404页面是裸 HTML。原因通常是静态资源路径写错或者static/目录位置不对。Flask 默认要求static/和templates/在应用根目录同级如果你把app.py放在子目录里Flask 可能找不到。解决确认app Flask(__name__)的__name__对应的包路径下确实有static/目录或者在创建 app 时显式指定static_folder的绝对路径。另一个常见原因是 Bulma 的 CSS 文件没下载完整bulma.min.css只有几十 KB如果文件大小明显不对重新从官方渠道获取。4.2 子模板内容不显示block 名称拼写与 extends 位置现象index.html里写了内容但页面只显示 base 的骨架。原因通常是{% block content %}的名字和 base 里定义的不一致或者{% extends %}没有放在文件第一行。Jinja2 要求extends必须是模板的第一个标签注释除外。解决检查 base 里定义的 block 名确保子模板用的名字完全一致大小写敏感。另外如果你在{% extends %}之前写了任何 HTML 或文本Jinja2 会忽略 extends直接渲染子模板内容。4.3 CSRF token missing 报错SECRET_KEY 与表单提交现象提交表单时返回 400提示 The CSRF token is missing。原因通常是SECRET_KEY没设置或者表单里没有{{ form.hidden_tag() }}。解决确认app.config[SECRET_KEY]已赋值确认模板里表单内包含hidden_tag()。如果用了 AJAX 提交需要手动从 cookie 或 meta 标签读取 CSRF token 并放到请求头里Flask-WTF 默认从表单字段读取。4.4 移动端导航栏点不开Bulma 的 JS 依赖边界现象桌面端导航栏正常移动端汉堡按钮点了没反应。原因Bulma 是纯 CSS 框架navbar-burger的展开逻辑需要自己写 JavaScript。解决在main.js里加一段切换is-active的代码// static/js/main.js document.addEventListener(DOMContentLoaded, () { // 找到所有汉堡按钮 const burgers document.querySelectorAll(.navbar-burger); burgers.forEach(burger { burger.addEventListener(click, () { // 切换按钮自身的 is-active burger.classList.toggle(is-active); // 切换对应菜单的 is-active const target document.getElementById(burger.dataset.target); if (target) target.classList.toggle(is-active); }); }); });参数说明>/* static/css/custom.css */ /* 放在 bulma.min.css 之后引入保证优先级 */ :root { --bulma-primary-h: 210; /* 色相210 是蓝色改成 150 变绿 */ --bulma-primary-s: 70%; /* 饱和度 */ --bulma-primary-l: 50%; /* 亮度 */ --bulma-link-h: 210; --bulma-link-s: 70%; --bulma-link-l: 50%; } /* 覆盖导航栏背景使用自定义色 */ .navbar.is-primary { background-color: hsl(var(--bulma-primary-h), var(--bulma-primary-s), var(--bulma-primary-l)); }逻辑说明Bulma 0.9 之后的版本大量使用 CSS 自定义属性CSS variables这让我们可以在不重新编译的情况下调整主题。--bulma-primary-h控制色相0 是红120 是绿210 是蓝。改这三个值就能换掉主色调。注意这段 CSS 必须在bulma.min.css之后加载否则会被框架默认值覆盖。在 base 模板里extra_css块的位置已经保证了这一点。5.2 用蓝图拆分模板目录多模块项目的组织方式当项目从单页面长到十几个页面时把所有模板堆在templates/根目录会很难维护。Flask 的蓝图Blueprint允许每个模块有自己的模板目录# modules/user/views.py from flask import Blueprint, render_template user_bp Blueprint(user, __name__, template_foldertemplates) user_bp.route(/profile) def profile(): return render_template(user/profile.html)目录结构变成modules/ └── user/ ├── __init__.py ├── views.py └── templates/ └── user/ └── profile.html注册蓝图# app.py from modules.user.views import user_bp app.register_blueprint(user_bp, url_prefix/user)参数说明template_foldertemplates告诉蓝图去哪里找模板url_prefix/user给所有该蓝图下的路由加上/user前缀。子模板里{% extends base.html %}仍然指向全局的 base因为 Flask 的模板查找路径会合并应用级和蓝图级的模板目录。这里有个坑如果两个蓝图有同名模板文件Flask 按注册顺序查找先注册的优先。所以建议在蓝图模板目录下再加一层子目录如user/profile.html避免命名冲突。5.3 验证模板是否被正确加载一个调试路由最后分享一个我常用的调试技巧加一个路由列出当前应用能找到的所有模板文件确认路径没问题。# app.py 里临时加的路由上线前删掉 app.route(/debug/templates) def debug_templates(): if not app.debug: return 仅调试模式可用, 403 # 遍历 Jinja2 的模板加载器列出所有可发现的模板 templates sorted(app.jinja_env.list_templates()) return br.join(templates)访问/debug/templates就能看到 Flask 实际能加载的模板列表。如果某个模板不在列表里说明目录结构或蓝图配置有问题。这个路由只在app.debug为 True 时可用避免生产环境暴露信息。从那以后我每次新建 Flask 项目都会先把 base 模板、静态资源路径和一个最小路由跑通确认页面能正常渲染再往上堆功能。这个习惯帮我省掉了无数次“样式怎么又没了”的排查时间。希望帮到你。本文还有配套的精品资源点击获取