☰
Flask + Bulma 模板实战:无需前端构建的后端页面开发指南
2026/10/14 9:56:55 网站建设 项目流程

简介:这份资源是面向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 或自己的 bundle,Flask 项目里引入这些会让静态资源目录变重。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 lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}默认标题{% endblock %}</title> <!-- Bulma CSS 本地引入,避免 CDN 在离线环境失效 --> <link rel="stylesheet" href="{{ url_for('static', filename='css/bulma.min.css') }}"> <!-- 自定义样式覆盖区,放在 Bulma 之后保证优先级 --> {% block extra_css %}{% endblock %} </head> <body> <!-- 导航栏组件,所有页面共享 --> <nav class="navbar is-primary" role="navigation" aria-label="main navigation"> <div class="navbar-brand"> <a class="navbar-item" href="{{ url_for('index') }}"> <strong>项目名称</strong> </a> <!-- 移动端汉堡菜单按钮,需要配合 JS 切换 is-active --> <a role="button" class="navbar-burger" aria-label="menu"># app.py from flask import Flask app = Flask(__name__, static_url_path='/assets', static_folder='static')

这样所有静态资源会走/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.js

requirements.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 class="columns is-multiline"> <!-- Bulma 的栅格系统:每个 column 占 4 列,一行三个 --> <div class="column is-4"> <div class="card"> <div class="card-content"> <p class="title is-5">模块一</p> <p class="subtitle is-6">状态:运行中</p> </div> </div> </div> <div class="column is-4"> <div class="card"> <div class="card-content"> <p class="title is-5">模块二</p> <p class="subtitle is-6">状态:待配置</p> </div> </div> </div> <div class="column is-4"> <div class="card"> <div class="card-content"> <p class="title is-5">模块三</p> <p class="subtitle 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', modules=modules)

逻辑说明:render_template把modules列表传给模板,模板里可以用{% for %}循环渲染。参数方面,render_template的第一个参数是模板文件名(相对于templates/目录),后面的关键字参数会变成模板里的变量。Bulma 的columns和column is-4是栅格 class,is-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 method="POST" action="{{ url_for('config_form') }}"> <!-- CSRF token 必须放在表单里,否则 Flask-WTF 会拒绝请求 --> {{ form.hidden_tag() }} <div class="field"> <label class="label">{{ form.name.label.text }}</label> <div class="control"> {{ form.name(class="input", placeholder="输入配置名称") }} </div> </div> <div class="field"> <div class="control"> {{ form.submit(class="button 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', form=form)

参数说明:SECRET_KEY是 Flask-WTF 生成和验证 CSRF token 的密钥,生产环境必须换成随机字符串并从环境变量读取。form.hidden_tag()渲染隐藏的 CSRF token 字段。form.name(class="input")里的class参数会把 Bulma 的inputclass 加到生成的<input>标签上,这样样式才能生效。flash消息需要在 base 模板里渲染出来,通常放在 content 块上方:

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} <div class="notification 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_folder='templates') @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_folder='templates'告诉蓝图去哪里找模板,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 模板、静态资源路径和一个最小路由跑通,确认页面能正常渲染再往上堆功能。这个习惯帮我省掉了无数次“样式怎么又没了”的排查时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询