
说实话很多人学Python学到一定阶段会卡在一个地方单机脚本写得非常顺爬虫、数据处理、自动化脚本都没问题但一说到Web开发就有点发怵。今天这个主题正好是Python 100天系列里的day53核心内容是前后端分离开发入门讲的是从传统到现代的Web开发演进。这个定位非常好因为它解决的不是怎么写代码而是为什么现在企业里做Web开发要这么搞的问题。这篇文章我会用Flask加一个最朴素的HTML页面带你理解前后端分离到底分的是什么以及前端页面和后端Python服务之间是怎么通过接口对话的。无论你是Python入门没多久的程序员还是想做全栈方向但一直没搞懂前后端协作逻辑的同学这篇都能帮你把那条线打通。1. 为什么前后端分离成了Web开发的主流1.1 传统模式一个Python文件包办到底先回到大家最熟悉的场景。大部分Python教程教Web开发的时候路径都差不多——用Flask或者Django写一个Python文件里面定义几个路由然后用render_template去渲染一个HTML模板。比如Flask里这么写from flask import Flask, render_template app Flask(__name__) app.route(/) def index(): user {name: 张三, age: 25} return render_template(index.html, useruser)看起来挺顺对不对页面也能出来数据也能动态展示。在小型项目、个人博客或者内部工具里这种开发方式完全够用而且开发速度很快。我早期做小项目时也是这么干的确实省事。但随着项目变大这种模式的问题就慢慢浮出水面了。服务端渲染的核心特征是后端把HTML拼好了再返回给浏览器。也就是说页面长什么样、数据怎么展示、用户点击某个按钮后跳转到哪里这些逻辑拿捏在后端手里。前端开发者只能在后端写好的模板里加一点CSS和原生JS想要改个交互逻辑得先找到对应的Python视图函数改完再重启服务效率非常低。更麻烦的是多端复用。你写了一个网页版的后台管理系统第二天产品经理说手机上也要一个。传统模式下你几乎要把一套页面逻辑再写一遍因为同一个接口返回的是带样式的HTML手机App根本没法直接用。数据还是那些数据但被HTML裹住了杂七杂八混在一起复用就成了噩梦。还有团队协作问题。一个项目里如果前后端都堆在一个代码库里前端改一行样式可能会影响后端的Python代码后端调整一下数据结构前端HTML又得跟着改。两个人同时开一个功能代码冲突是家常便饭。做Web开发时间久了你会发现很多项目后期每个月有半个月在改页面细节多半就是因为前后端没有解耦。1.2 前后端分离到底分的是什么前后端分离不是简单地把代码分成两个文件夹而是把职责边界拆得干脆利落前端浏览器端只负责页面展示和用户交互后端服务端只负责业务逻辑和数据存储。两者之间通过一个统一约定好的远程接口来通信最常用的形式就是HTTP请求加JSON数据。打个比方。传统模式像去一家餐厅吃饭你点菜后厨直接把配上酱汁的成品菜端上来。这个成品菜是照着你的口味做好的换个顾客想少放盐后厨得重新做。而前后端分离更像餐厅把厨房和大堂分开厨房只管把半成品准备好大堂负责根据客人的需求切配装盘。客人就是浏览器你传什么参数后端就返回对应的纯数据JSON至于这些数据在网页上显示成表格还是图表后端完全不关心。这个模式带来几个非常实际的红利第一是并行开发。前端和后端可以同时开工只要提前把接口约定好比如登录接口返回什么字段列表接口的分页参数叫什么。两边互不等待开发效率翻倍。第二是接口可以跨平台复用。同一个后端接口Web端可以用手机App可以用小程序也可以用。我平时做一个项目后端只写一套API前端页面、微信小程序、管理后台全部共用好处是一旦后端逻辑有修改所有端同时生效不用改三遍。第三是技术栈解耦。后端随便用Python、Java、Go前端随便用Vue、React、Angular谁也不绑架谁。这个对团队招聘和中期维护都非常友好。你在招人的时候不用再找又会写Python视图又会写前端交互的全栈大牛可以招熟练的Python工程师和后端工程师各一个让他们通过API协同工作。2. 演进路线与核心概念梳理2.1 三代Web开发模式对照把Web开发的历史捋一遍你会发现前后端分离并不是凭空冒出来的而是需求驱动下的必然结果。我用一张表把这几个阶段说明白这样你理解起来会有更清晰的时间线。阶段核心特点Python侧代表技术适用场景痛点纯静态页面内容写在HTML里改内容就是改文件无Nginx/Apache直接托管个人主页、公司官网展示数据是死的维护成本高服务端渲染传统模式Python生成完整HTML数据直接拼在页面里Flask Jinja2、Django Template内容型网站、中小型系统前后端耦合难以复用协作低效前后端分离后端只提供JSON数据接口前端负责渲染Flask/Django构建REST API Vue/React中大型Web应用、小程序、移动App需要前后端约定口径联调成本高从表格里可以看出来前后端分离最核心的转变是页面渲染的职责从后端移到了前端。Python后端不再认识HTML标签它只做一件事——接收请求、查数据或者说算数据、把结果转成JSON返回。这个转变在中小型项目里尤其明显。一个项目经理只需要一个花哨的大屏展示页面数据来自另一个Python爬虫每天抓取入库的MySQL表。如果用传统模式Python得把大屏HTML拼好再返回那大屏上用来做动画的JavaScript和Python代码混在一起想想都头疼。但是用前后端分离Python只需写几个API把数据库里的数据读出来返回大屏前端页面把数据画成图表各司其职互不干扰。2.2 连接前后端的关键RESTful API与JSON聊前后端分离必然绕不开一个词RESTful API。听起来很学术但拆开来看就是一套用URL表达资源用HTTP方法表达操作的接口设计风格。RESTful API设计的基本原则就是把业务里需要操作的对象当成资源用URL去表示它然后用HTTP标准方法来表达要对这个资源做什么操作。以最经典的任务管理为例HTTP方法URL作用语义GET/api/tasks获取任务列表查GET/api/tasks/1获取ID为1的任务查单个POST/api/tasks新建一个任务增PUT/api/tasks/1完整更新ID为1的任务改DELETE/api/tasks/1删除ID为1的任务删这套风格的好处是接口见名知意。前端开发看到这个URL和方法的组合就知道它是要做什么。而请求出去的参数、返回的数据则统一用JSON格式承载。JSON在Python里对应字典在JavaScript里对应对象两边都是天生一对不需要做太多转换。举个最简单的后端返回示例{ code: 0, message: success, data: [ {id: 1, title: 写Python 100天博客, done: false}, {id: 2, title: 学Flask接口开发, done: true} ] }包裹一个code、message、data是实际项目中比较稳妥的做法。code为0表示成功非0表示某种业务错误比如参数错误、未登录。这样前端拿到响应后先看code再决定是渲染data还是弹错误提示。这个约定的好处是和HTTP状态码解耦因为很多时候HTTP返回200但业务逻辑却失败了比如用户名已存在如果不用业务码前端根本没法区分。2.3 HTTP状态码是前后端沟通的暗号除了业务码HTTP本身自带的状态码也非常重要这是浏览器和服务器之间最基础的沟通语言。前后端联调的时候看状态码基本就能快速缩小问题范围。2xx成功200表示请求成功201表示创建成功后端按预期执行了。4xx客户端问题400参数错误、401未认证、403无权限、404接口或资源不存在、405请求方法不对。这时候优先查前端的请求参数和URL有没有写对。5xx服务端问题500服务器内部异常502/504网关超时。这时候问题大概率出在Python代码本身需要看后端日志。我平时排查问题的习惯是先在浏览器开发者工具的Network面板里找到那个请求看状态码看响应内容。如果状态码是4xx基本可以断定问题在前端传参或者URL如果是5xx就切换到后端终端看Traceback。这个方法能节省非常多排查时间。很多新手联调时经常一脸懵就是因为不看状态码凭感觉乱猜。3. 实操Flask 原生JS完成一个最小前后端分离项目理论讲再多不如动手跑一遍。这一部分我们做一个极简但完整的前后端分离小项目一个任务清单页面能显示任务列表、新增任务、删除任务。后端是用Python Flask提供JSON接口前端是一个纯HTML页面用原生fetch和这个接口交互。不引入任何复杂框架目的是把前端页面和后端接口各自的职责看得一清二楚。3.1 准备环境与项目结构环境方面你只需要装好Python 3.8以上的版本然后通过pip安装Flaskpip install flask如果你用的是虚拟环境先创建并激活虚拟环境再装依赖这是Python开发的常规操作可以避免污染全局环境。接着创建项目文件夹。我这里建议按以下结构组织干净也符合实际项目的布局习惯project/ ├── backend/ │ └── app.py └── frontend/ └── index.htmlbackend放Python代码frontend放前端文件。实际开发中往往还分得更细比如backend下面按blueprint组织模块frontend下面有src和public目录。现在这个规模保持简单即可。3.2 后端写出第一个可以返回JSON的接口在backend/app.py中写以下代码from flask import Flask, jsonify, request app Flask(__name__) tasks [] task_id_counter 1 app.route(/api/tasks, methods[GET]) def get_tasks(): return jsonify({ code: 0, message: success, data: tasks }) app.route(/api/tasks, methods[POST]) def create_task(): global task_id_counter data request.get_json(silentTrue) if not data or not data.get(title): return jsonify({ code: 1, message: title不能为空, data: None }), 400 task { id: task_id_counter, title: data[title], done: False } tasks.append(task) task_id_counter 1 return jsonify({ code: 0, message: success, data: task }), 201 app.route(/api/tasks/int:task_id, methods[DELETE]) def delete_task(task_id): global tasks tasks [t for t in tasks if t[id] ! task_id] return jsonify({ code: 0, message: success, data: None }) if __name__ __main__: app.run(host127.0.0.1, port5000, debugTrue)这里有几个细节值得说一下。第一个是request.get_json(silentTrue)的参数。silentTrue的意思是如果请求头里的Content-Type不是application/json或者请求体不是合法JSON后端不会抛异常而是返回None。这样我们可以统一在代码里判断not data集中处理异常情况。如果不加这个参数一旦前端忘记设置Content-Type后端会直接报400错误而且错误信息还不友好联调体验会比较差。第二个是业务码和HTTP状态码的同时使用。返回参数错误时我用的是返回值400加业务码1的组合。前端拿到这两个信息后既能在HTTP层面看到请求有问题也能从业务码里知道具体是哪个业务校验没通过。这是推荐的做法。第三个是tasks这个列表简单模拟了数据库。真实项目中你会换成MySQL或者MongoDB但接口层逻辑是一样的前端传入参数后端做校验和处理返回结果。启动后可以在浏览器访问http://127.0.0.1:5000/api/tasks你会看到返回的JSON数据。这个页面没有任何样式和排版直接在浏览器里以纯文本的形式展示数据结构。这正是接口应该有的样子——纯粹的数据不掺HTML。3.3 前端不依赖框架也能看清数据交互新建frontend/index.html内容如下。为了让你看清核心交互逻辑这里刻意不用任何前端框架!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title任务管理 - 前后端分离演示/title style body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; } .task-item { display: flex; justify-content: space-between; padding: 8px; border-bottom: 1px solid #eee; } .task-item button { margin-left: 12px; } /style /head body h1任务清单/h1 div input idtaskInput typetext placeholder输入新任务 button idaddBtn添加/button /div ul idtaskList/ul script const API_BASE http://127.0.0.1:5000/api/tasks; async function loadTasks() { const res await fetch(API_BASE); const result await res.json(); if (result.code ! 0) { alert(加载任务失败 result.message); return; } renderTasks(result.data); } function renderTasks(tasks) { const list document.getElementById(taskList); list.innerHTML ; tasks.forEach(task { const li document.createElement(li); li.className task-item; li.innerHTML span${task.title}/span button>Access to fetch at http://127.0.0.1:5000/api/tasks from origin null has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource.这就是跨域问题。简单解释就是浏览器为了保证安全默认只允许页面请求同源的接口。同源指的是协议、域名、端口三个都相同。你现在是file://协议访问的页面要去请求http://127.0.0.1:5000的接口协议不同、端口也不同浏览器自然就拦住了。解决方式有两种。第一种是给后端Flask配置CORS支持这是最根本的解决方式。先安装flask-corspip install flask-cors然后修改app.py导入并启用CORSfrom flask_cors import CORS CORS(app)CORS这个扩展会自动在响应头中加上Access-Control-Allow-Origin: *表示任何来源的页面都能访问这个接口。开发阶段这样用完全没问题但生产环境中我会建议把它配置成只允许特定的域名访问比如CORS(app, origins[http://localhost:3000])第二种方式是让前端页面也通过HTTP协议访问不要用file://直接打开。你可以在项目根目录下运行一个静态文件服务cd project python -m http.server 8000然后通过http://127.0.0.1:8000访问frontend/index.html。这种方案适合联调阶段更贴近实际生产环境前端由Web服务器托管后端API单独部署。我自己在开发时习惯用后端直接开CORS因为前后端经常各有各的启动脚本开CORS省心。但是生产部署时CORS涉及安全问题大项目一般交给网关统一管控Python后端不直接处理。做完这一步你的页面应该就能正常展示任务列表、添加任务、删除任务了。至此你已经完整跑通了一个前后端分离的最小闭环。4. 常见问题与排查技巧实录4.1 请求能发出去但前端报错先看Network面板很多同学在联调时遇到页面白屏或者按钮点了没反应第一反应是后端代码哪里写错了。但只要后端能启动问题大概率在前端请求或者数据解析上。浏览器按F12打开开发者工具切到Network网络面板刷新页面。你会看到所有网络请求的列表。找到对应的/api/tasks请求点进去看三个东西状态码是不是200还是401/400/500。响应内容后端返回的JSON长什么样是不是符合约定。请求头Content-Type设置了没有Authorization带上了没有。我在实际工作中用这个方法解决了太多联调问题。有一次前端同事说接口报错了我打开Network一看状态码200响应数据也正常是前端代码里没有对code做判断直接渲染了data字段数据结构不一样就显示空白。问题根本不在后端而是前端没看清返回结构。4.2 状态码200但业务码返回1细看前后端的判断逻辑这点在代码里已经出现过。HTTP状态码200表示请求成功到达服务器并正常处理但业务处理未必成功。我给你举个例子前端发起一个POST请求想添加任务但传的title是空字符串后端返回了{ code: 1, message: title不能为空, data: null }因为请求本身被正确处理了所以HTTP状态码还是200但业务上这是一个失败的请求。我见过不少项目在联调时前端看到HTTP状态码200就认为操作成功直接关闭了loading状态或者刷新页面结果发现数据没变才回头去检查响应体。所以前端逻辑里一定要对code 0做判断而不是只看HTTP状态码。4.3 后端返回数据经常变字段如何减少联调返工前后端分离项目最常见的矛盾就是接口文档变了。昨天后端定的字段叫created_time今天改成createTime前端只改了显示逻辑还好如果用到的地方多那就是一场灾难。我建议从第一天就建立接口约定习惯。最简单的做法是在后端代码里直接写好接口的返回示例并和前端共享。同时约定字段命名风格Python习惯用snake_case下划线风格JavaScript习惯用camelCase驼峰风格。项目里统一用一套就好保留字典序。比如如果后端用create_time前端也接受create_time前端JavaScript里虽然命名习惯是驼峰但读取API返回字段时统一用task.create_time不要做多余的转换这样可以省去一堆麻烦。更稳妥的做法是在返回的数据模型上做严格的字段设计比如这个例子里的task结构业务字段就四个id、title、done。额外信息用message、code携带不要老想着把什么数据都塞进接口返回里。接口返回越精简联调成本越低。4.4 前后端连上了但页面样式乱了分清接口和渲染的边界还有一种情况是接口通了数据也有了但页面效果和预期差得很远。这时候要清楚地认识到渲染层的问题和后端接口已经没关系了。比如你可能在拼装DOM时忘记处理特殊字符或者模板字符串里丢了个引号。这类的排查思路就是回到前端代码逻辑上逐步用console.log打印数据看看渲染到哪一步出了问题。我在实际开发中养成的一个习惯是写前端渲染逻辑时先console.log(result)把后端返回的完整结构打印出来确认数据没问题再动手写DOM渲染。这样可以防止数据已经错了还在调样式的无用功。4.5 接口联调调试小工具推荐除了浏览器自带的开发者工具前后端联调时我还会用Postman或者Apifox这类接口调试工具。这类工具可以帮你脱离前端环境直接测试后端接口发一个POST请求、加一个Header都只是点几个按钮的事尤其适合排查浏览器里被跨域拦截、但在工具里却正常的情况。还有代理抓包工具比如Charles或Whistle等以后做起中大型项目熟悉一个就够了。5. 关于day53之后我的几点心得走到这里你其实已经跨过了Web开发中最重要的一道门槛理解了前后端之间的边界。后面再去学Vue、React这类前端框架你会有一种原来如此的感觉因为框架解决的核心问题就是把页面渲染逻辑组织得更高效而数据来源还是你后端提供的那些JSON接口。我个人的几个体会分享给大家。第一一定要亲手完整跑通一个前后端分离的小项目哪怕只是上面这个任务管理。很多知识点光看教程会觉得这不难但自己动手时你会发现CORS、fetch异步、字段命名这些细节哪一个不踩一遍都不算真正掌握。第二后端接口设计时不用追求一次做到完美但要保证稳定和可读。稳定是说接口返回的结构不要朝令夕改可读是说你定义的字段名、URL路径别人一眼能看懂。把这个意识养成习惯你以后在团队协作中的口碑会完全不一样。第三学会用浏览器开发者工具是前后端联调的基本功。不管你是偏Python后端还是偏前端Network面板、控制台、Sources面板这几个入口用熟了能省下大把查问题的时间。如果这个项目你已经跑通了下一步可以试着把前端换成Vue或者React的脚手架项目把后端拆成多个Blueprint再接一个数据库一个接近真实企业形态的全栈项目就逐渐落地了。day53只是个开始后面需要练的还有很多。