
简介基于 Flask 与 Layui 的完整前后端源码包面向 Python Web 入门与进阶开发者也适合需要快速搭建后台管理界面的项目二次开发。项目实现了一套清晰的增删改查流程Flask 负责路由、请求处理与业务数据操作Layui 提供表格、表单等前端交互组件并已针对本项目做了定制修改代码采用前后端分离思路通过 JSON 完成数据交换配合 SQLAlchemy 模型可帮助理解 Web 应用常见架构。压缩包共含 1760 个文件体积约 17.52MB以 Python 源码py/pyc、HTML 模板、JavaScript、CSS、GIF 演示图、依赖包及虚拟环境配置为主其中大量 py 文件覆盖业务逻辑与工具模块HTML/JS/CSS 组成可运行前端页面。已有 2554 人学习下载。整体目录包含 Flask 主入口、templates、static、数据模型与 requirements 依赖清单等并带有环境激活脚本、配置文件和日志记录便于按模块研读、测试及部署是学习前后端联动与 CRUD 实现的实用参考资料。1. 为什么这套flasklayui源码仍是快速交付的答案选型时最容易出现的分歧是都要做前后端了为什么不用 Spring Boot Vue反而把 Flask 和 Layui 放在一起这个标题所指向的“前后端源码”定位不是要做一个严格的前后端分离工程而是把两者优点拼起来Flask 用 Jinja2 输出主框架页Layui 在前端负责表格、弹层、表单校验业务数据全部走 Ajax 请求 Flask 接口。对用户管理、订单管理、数据报表这类信息管理系统来说这种结构的价值在于改动路径短单人也能在两三周内完整交付。常规情况下后端加一个路由、前端加一段 table.render只需要动两个文件不用维护独立的 Node 工程也不用处理跨域和 token 链路。它的上手成本明显低于 Vue 全家桶同时比纯 Jinja2 服务端渲染更接近接口化开发习惯后续如果真要拆分Flask 接口可以直接保留给移动端复用前端再单独重写。适合内部管理系统、外包交付以及团队对 Python 更熟悉、前端人手不足的场景。下面按实际搭建这类源码项目的顺序展开先把 Flask 端的接口返回结构定好再讲 Layui 怎么对接表格和表单然后处理日期控件和下拉框动态赋值这两个容易返工的点最后落到部署与验证。这套流程走完你拿到的就不只是能跑的源码而是一套可以复用到下一个后台项目的骨架。2. Flask端先定接口返回结构layui表格才不白屏Layui 的 table 模块从接口拿数据默认约定的格式不是普通 JSON 数组而是 code、msg、count、data 四个字段。直接返回[{...}]这种数组表格会直接提示数据格式错误。所以 Flask 端做接口时第一件事不是急着写业务而是把响应包装成统一结构。2.1 项目布局与Flask工厂模式blueprint注册在哪一层从源码角度看目录分工决定后面扩展是否顺畅。常见做法是页面渲染和 JSON 接口分开静态文件按官方包原样存放flask-layui-demo/ ├── app/ │ ├── __init__.py # create_app 工厂 │ ├── models.py # SQLAlchemy 模型 │ ├── utils.py # 统一响应函数 │ ├── api/ │ │ ├── __init__.py │ │ ├── user.py # /api/user 蓝图 │ │ └── order.py │ └── views/ │ └── index.py # 渲染首页 ├── static/ │ └── libs/layui/ # layui 完整包 ├── templates/ │ └── index.html ├── config.py └── requirements.txtFlask 工厂函数在这里承担的是装配角色。用工厂创建 app把数据库和蓝图都注册进去# app/__init__.py from flask import Flask from .models import db def create_app(): app Flask(__name__) app.config.from_object(config.Config) db.init_app(app) from .api.user import user_bp from .views.index import index_bp app.register_blueprint(user_bp, url_prefix/api/user) app.register_blueprint(index_bp) return app逻辑说明工厂函数返回一个完整可运行的 app 实例测试的时候也可以通过create_app()创建多个不同配置的实例避免测试环境互相污染。url_prefix/api/user是给蓝图内所有路由统一加前缀user_bp里只写/list最终接口就是/api/user/listLayui 表格请求时只需要按最终路径填。如果你拿到的源码里没有工厂函数而是在app.py中直接app Flask(__name__)开发没问题但做单元测试和部署多配置时会受限制。我一般会顺手改成工厂模式改动量不大收益却很明显。2.2 用Flask-SQLAlchemy定义模型字段名与to_dict映射模型定义时数据库字段习惯用下划线但前端 Layui 表格里的 field 往往用驼峰。两者之间需要一层显式转换最省事的方式是在模型里放一个to_dict()# app/models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class User(db.Model): __tablename__ t_user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(32), uniqueTrue, nullableFalse) create_time db.Column(db.DateTime, defaultdatetime.now) def to_dict(self): return { id: self.id, username: self.username, createTime: self.create_time.strftime(%Y-%m-%d %H:%M:%S) if self.create_time else }参数说明db.Column(db.DateTime, defaultdatetime.now)里传的是函数对象不是datetime.now()这样才能在每个插入行时取当前时间。strftime把 datetime 转成字符串避免后面 jsonify 时遇到TypeError。如果create_time允许为空需要加一层if判断否则调用strftime会直接抛异常。这里的关键是to_dict返回的 key 就是前端 Layui 表格中cols里field的参考值。前端写createTime后端就必须返回createTime一旦不一致表格不会报错但那一列就是空白。这种问题排查起来非常费时间所以项目一开始就要定下驼峰返回的约定。2.3 分页接口返回code/count/msg/data四字段Layui 的 table 模块开启page: true后请求会自动带上page和limit参数。Flask 端接收参数并返回指定格式# app/utils.py from flask import jsonify def response_ok(dataNone, count0, msg): return jsonify(code0, msgmsg, countcount, datadata or [])# app/api/user.py from flask import Blueprint, request from ..models import User from ..utils import response_ok user_bp Blueprint(user, __name__) user_bp.route(/list) def list_user(): page request.args.get(page, default1, typeint) limit request.args.get(limit, default10, typeint) query User.query count query.count() users query.order_by(User.id.desc()).offset((page - 1) * limit).limit(limit).all() return response_ok(data[u.to_dict() for u in users], countcount)参数说明request.args.get(page, default1, typeint)中typeint会把请求参数强转为 int如果前端传了非数字也不会导致 500而是用默认值兜底。count必须取全表总数不能取当前页长度否则 Layui 的分页条数会错误。offset((page - 1) * limit)是跳过前几页的数据配合limit(limit)每页取指定条数。Layui 表格期望的返回字段如下字段类型说明codeint0 表示成功非 0 时表格弹出 msg 内容msgstring提示信息成功时通常为空countint数据总条数分页组件依赖此值dataarray当前页数据列表如果查询过程中出现异常比如数据库连接断掉Flask 默认返回 HTML 错误页Layui 拿到非 JSON 内容会解析失败。所以接口里最好包一层 try/except异常时返回jsonify(code500, msgstr(e), count0, data[])这样前端至少能看到明确错误信息。统一响应结构的好处是前端永远只判断code是否为 0不需要为每个接口单独写一套解析逻辑。3. Layui的table与form模块如何对接Flask接口Layui 在这套结构里不参与路由判断只负责浏览器端组件。引入方式不走 npm把官方包解压后放到static/libs/layui页面通过模板引擎引用静态文件。3.1 layui.css与layui.js引入use按需加载在templates/index.html中通过 Flask 的url_for生成静态资源路径这样部署到子目录时路径也不会写死link relstylesheet href{{ url_for(static, filenamelibs/layui/css/layui.css) }} script src{{ url_for(static, filenamelibs/layui/layui.js) }}/script逻辑说明libs/layui是官方下载包解压后的目录名必须和实际文件夹完全一致。在 Windows 上开发时大小写不敏感但部署到 Linux 后大小写差异会直接 404。layui.js会在浏览器中注入全局对象layui之后页面里的脚本调用layui.use按需加载模块。模块加载原则是“用哪个加载哪个”。比如页面同时有表格、日期控件和表单就写script layui.use([table, form, laydate, layer], function () { var table layui.table; var form layui.form; var laydate layui.laydate; var layer layui.layer; }); /scripttab 与 layer 模块必须放在数组里回调函数中的变量名称是自定义的不必与模块名保持一致。如果页面不需要某个模块从数组里删掉即可避免一次性加载整个组件库拖慢首屏。3.2 table.render参数url、page、where默认请求形式核心渲染代码table.render({ elem: #userTable, url: /api/user/list, method: get, page: true, limit: 10, limits: [10, 20, 50, 100], where: { keyword: $(#keyword).val() }, cols: [[ { type: checkbox }, { field: id, title: ID, width: 80, sort: true }, { field: username, title: 用户名 }, { field: createTime, title: 创建时间, width: 180 } ]] });这里的参数说明要对应清楚参数作用默认行为elem表格容器选择器必须指向一个存在的 table 元素url数据接口地址请求方式由 method 决定page是否开启分页开启后自动带 page 和 limit 参数where额外的查询条件会以 query string 或 body 形式一起提交cols列配置数组field 必须与后端 JSON 的 key 一致sort: true开启列排序后Layui 会额外提交field和order参数。如果后端没有实现排序逻辑排序功能就是假的点击后数据不会变化。我一般的处理是后端列表接口接收可选的sortField和sortOrder然后用order_by拼接排序字段。如果只是内部管理系统建议直接去掉sort: true避免用户点了没反应影响体验。3.3 form.on提交与table.reload刷新表格表单保存后刷新表格是表单模块最常用的联动场景form.on(submit(saveForm), function (data) { var loadingIndex layer.load(1); $.ajax({ url: /api/user/save, type: POST, data: JSON.stringify(data.field), contentType: application/json, dataType: json, success: function (res) { layer.close(loadingIndex); if (res.code 0) { layer.msg(保存成功); table.reload(userTable); } else { layer.msg(res.msg || 保存失败, { icon: 2 }); } } }); return false; });逻辑说明form.on(submit(saveForm))监听button lay-submit lay-filtersaveForm的点击事件data.field是表单里所有name属性组成的对象。return false是阻止表单默认提交否则页面会跳转刷新。table.reload(userTable)中userTable是table.render时传的id参数不是elem选择器。如果没有设置id重载时就只能传elem对应的属性组合容易写错。这里最容易踩的一个坑是请求体格式不一致。前端用jQuery.ajax且指定contentType: application/json时Flask 后端必须用request.get_json()接收。如果两个项目联调时发现接口通了但取值全是None九成是前端用 JSON 发送、后端却用request.form读取或者反过来。前后端源码是否配套有时候就在这一行 contentType 上。4. 日期控件上限当前日期由layui设置时区由Flask兜底管理系统的表单里日期范围查询和下拉选择是出现频率最高的两个控件。Layui 的 laydate 和 select 都有一些细节处理不好会直接返工。4.1 laydate设置max为当前日期的三种写法热搜词里“layui date 最大日期当前日期”指的是禁选未来日期最常见的写法是laydate.render({ elem: #startDate, max: 0 });在 laydate 中max和min支持三类值具体日期字符串、相对今天的天数、函数。max: 0表示最大就是今天-1表示昨天1表示明天。这个相对天数的写法兼容性最好不需要额外拼字符串。第二类写法是字符串用当前日期直接赋值laydate.render({ elem: #endDate, max: new Date().toISOString().split(T)[0] });字符串必须是yyyy-MM-dd格式。这里推荐不要用这种写法因为toISOString()拿到的是 UTC 时间在 UTC8 时区内凌晨 0 点到 8 点之间会取到前一天导致表单默认把今天禁掉。更稳妥的是由后端渲染模板时把日期作为变量传入script var today {{ today }}; laydate.render({ elem: #endDate, max: today }); /scriptFlask 视图里对应输出from datetime import datetime from flask import render_template index_bp.route(/) def index(): return render_template(index.html, todaydatetime.now().strftime(%Y-%m-%d))这样页面上看到的“今天”与服务器日期一致不受用户本机时区影响。提示laydate 的max也支持函数返回值函数返回格式仍要求是yyyy-MM-dd字符串或时间戳。4.2 Flask处理datetime序列化与MySQL时区Flask 自带 jsonify 对 datetime 对象并不友好会抛出TypeError: Object of type datetime is not JSON serializable。如果在源码中每个接口都要strftime转一遍很容易漏。更好的做法是定义一个全局 JSON Providerfrom flask.json.provider import DefaultJSONProvider from datetime import datetime class AppJSONProvider(DefaultJSONProvider): def default(self, obj): if isinstance(obj, datetime): return obj.strftime(%Y-%m-%d %H:%M:%S) return super().default(obj)然后在工厂函数中指定app.json_provider_class AppJSONProvider app.json AppJSONProvider(app)逻辑说明default方法只在 jsonify 遇到不知道如何序列化的对象时调用。把 datetime 统一转为字符串后Layui 表格里直接显示即可前端不需要再写格式化函数。这样做的代价是返回给前端的时间全部变成字符串如果前端需要做时间范围比较要记得在 JS 里new Date(str)还原。时间处理方式适合场景注意点模型 to_dict 中 strftime返回字段少、结构简单新增字段时容易漏全局 JSON Provider多个模型直接返回所有时间统一成字符串格式如果使用 MySQL时区问题会在部署后显现。比如 Python 写入的 UTC 时间和本机时间差 8 小时。可以通过 SQLAlchemy 连接参数强制会话时区SQLALCHEMY_ENGINE_OPTIONS { connect_args: { init_command: SET time_zone 08:00 } }这里的init_command在每次新建连接时执行保证查询结果和写入时间都按东八区解释。如果数据库本身已经存了正确时间时间差问题就出在连接层这个配置能直接解决。4.3 select远程数据动态赋值后必须form.renderLayui 的 select 渲染后原生option的增删不会自动反映到页面上。从 Flask 接口拿选项数据再赋值必须重新调用form.render$.get(/api/user/options, function (res) { if (res.code 0) { var options option value全部/option; res.data.forEach(function (item) { options option value item.id item.username /option; }); $(#userId).html(options); form.render(select); } });这里如果少了form.render(select)打开页面后下拉框还是空的或者一直是默认的“请选择”但其实现 DOM 已经有数据。这种问题在浏览器里看 HTML 源码会发现 option 存在视觉效果却不变很容易误判为接口问题。如果这个 select 出现在layer.open弹出的表单中赋值时机必须放在弹层内容的渲染完成后。可以在layer.open的success回调里执行$.get而不是在父页面加载时就请求。原因是弹层打开前select 对应的 DOM 节点还不存在$(#userId)选不到元素自然赋值不上去。5. 部署flasklayui源码时三个必查项静态目录、进程和Nginx本地跑通只是第一步部署阶段最容易出问题的不是 Flask 业务代码而是静态路径和反向代理的配合方式。这里按源码目录结构逐项排查。5.1 Flask static目录下Layui路径的404排查Flask 默认的静态目录就是项目根下的static模板中url_for(static, filenamelibs/layui/css/layui.css)生成的路径是/static/libs/layui/css/layui.css。如果文件确实存在还报 404先查大小写和目录层级。快速验证方式curl -I http://127.0.0.1:8000/static/libs/layui/css/layui.css返回200 OK说明静态文件没问题。如果返回 404检查实际目录是否多了一层比如static/static/libs这是 Laravel 或 Django 项目改造过来时常见的毛病。Flask 的构造参数里可以指定静态目录一般不推荐自行改动app Flask(__name__, static_folderstatic)static_folder参数对应的是磁盘上的目录名url_for里的 filename 是相对于该目录的路径。保持默认即可除非你有特殊的安全隔离需求。5.2 Gunicorn与Waitress进程参数选择Flask 自带的app.run()只适合开发并发能力和健壮性都不够。Linux 环境常用 Gunicorngunicorn -w 2 -b 127.0.0.1:8000 --timeout 60 app:create_app()参数说明参数含义建议-wworker 进程数小型系统按 CPU 核数 × 2 1-b监听地址只监听本机由 Nginx 转发--timeout单个请求超时时间默认 30 秒接口慢则调大--access-logfile访问日志位置-表示输出到标准输出app:create_app()是告诉 Gunicorn 从app模块导入并调用create_app工厂。Windows 环境没有 Gunicorn普遍用 Waitress# run.py from waitress import serve from app import create_app serve(create_app(), host0.0.0.0, port8000, threads4)threads4是 Waitress 处理请求的线程数。Python 的 GIL 决定了多线程在 CPU 密集场景下不会线性扩展但这类管理系统的接口大多是数据库查询和 JSON 拼接IO 等待占比高4 到 8 个线程足够。5.3 Nginx反向代理与静态文件分离配置生产环境的常见拆分是/api/开头的请求交给 Gunicorn/static/直接由 Nginx 处理其余路径渲染首页。这样静态资源不经过 Python 进程能明显减轻 Flask 压力server { listen 80; server_name _; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 60s; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; } location /static/ { alias /opt/flask-layui-demo/static/; expires 7d; access_log off; } }配置说明proxy_pass http://127.0.0.1:8000;末尾不带路径时会把原始 URI 原样传给后端所以/api/user/list到 Gunicorn 后仍是这个路径。alias指向磁盘上静态文件的实际目录注意结尾必须有/。expires 7d让浏览器缓存静态资源Layui 的 css/js 体积不小缓存能显著加快二次打开速度。有一个隐含问题值得说明如果 Flask 的index路由是/那么所有非/api/路径都会被 Nginx 转发给 Flask再由 Flask 判断是否匹配路由。如果用户直接访问不存在的页面Flask 会返回 404如果想让所有未知路径统一走首页就要在 Flask 侧加一个app.errorhandler(404)返回首页模板。6. 验证前后端联动三个最快手法源码交付前我会按固定顺序做三次验证每一次都能定位不同层面的问题。6.1 curl直接打接口看code是否等于0curl -s http://127.0.0.1:8000/api/user/list?page1limit10直接看返回的 JSON 结构。确认code为 0、count大于 0、data数组里每行都包含前端需要的字段。这一步不打开浏览器就能先隔离后端问题。如果返回了 HTML 错误页说明路由没注册或数据库连接异常如果code非 0直接看msg字段定位。6.2 浏览器Network核对请求路径和字段命名打开页面后按 F12 进 Network筛选 XHR点表格刷新按钮。看请求的 Query String Parameters 是否有page和limit再看响应 JSON 里的 key 与表格cols中field是否一致。常见问题是后端返回create_time前端表格写的是createTime导致列空白且控制台无报错。保持 Flask 模型字段用下划线JSON 返回用驼峰并在to_dict()中统一转换这个约定能避免 80% 的表格空白问题。6.3 用Flask test_client做冒烟测试最后在项目里放一个测试文件不启动服务器就能验证接口与 Layui 协议匹配# tests/test_api.py import pytest from app import create_app, db pytest.fixture() def app(): app create_app() app.config[TESTING] True app.config[SQLALCHEMY_DATABASE_URI] sqlite:///:memory: with app.app_context(): db.create_all() yield app def test_user_list_protocol(app): client app.test_client() resp client.get(/api/user/list?page1limit10) body resp.get_json() assert body[code] 0 assert count in body assert isinstance(body[data], list)resp.get_json()直接把响应体解析为字典断言code、count、data三个 key 存在说明 Flask 端已经满足 Layui 的表格协议。把tests/test_api.py放进项目根目录后执行pytest -q看到 passed 再去做页面联调问题会隔离得更干净。本文还有配套的精品资源点击获取