Python Web是Python开发的一个就业方向,这一章我们就介绍一下Python Web开发相关的一些内容。
Web开发是一个系统性的工程,它涉及到前端、后端、数据库、运维等多个领域。我们来简单说一下需要掌握哪些相关的内容。
前端我们需要熟悉HTML、CSS、JavaScript,实际开发中还需要掌握一些前端框架,比如Vue、React等。
后端我们需要熟悉Python的核心语法,如果你已经从第一章阅读到了这一章,这一点应该不成问题,还要熟悉一些框架,比如Flask、Django、FastAPI等,API调试工具也要有所了解,比如Postman。数据库方面应该有一定的SQL基础,熟悉SQLite、MySQL、PostgreSQL、Redis、MongoDB等数据库的基本使用。运维方面应该熟悉基本的Linux命令、Gunicorn、Nginx、Docker等。
无论前端还是后端,对HTTP协议都应该有一定的了解,要熟悉常用的HTTP方法,比如GET、POST、PUT、DELETE、PATCH等;熟悉常见的状态码,比如2xx表示成功、3xx表示重定向、4xx表示客户端错误、5xx表示服务器错误。
对常见代码管理工具,如Git,也要会基本的使用。
不要被列出的这些内容吓倒,只要自己参与一些项目,就会逐渐对这些内容熟悉。我们以一个简单的Demo来逐渐熟悉Python Web的开发,这个Demo我想实现一个简单用户管理系统,我们将其命名为niuma-sys,实现以下一些功能:
1)新用户可以注册账户2)已注册的用户可以通过账号和密码进行登录3)能够查看所有注册用户4)用户登录后可以修改密码5)可以删除用户
我们niuma-sys系统要实现的功能比较简单,选择的框架也尽可能的简单。Flask是Python中比较轻量的一个Web框架,是入门的首选,所以在后端开发框架上我们选择Flask。
数据的存储我们选择使用SQLite,Python中自带SQLite操作的模块,处理起来会比较容易,为了便于我们操作数据库,我们选择使用SQLAlchemy工具,它是Python中一款非常流行的ORM(Object-Relational Mapper,对象关系映射)工具,可以让我们用面向对象的方式操作数据库,让我们少写SQL。
前端我们就使用原生的HTML、CSS、JavaScript,只要具备基本的知识就能够写的出,看得懂。
上来就写代码固然很爽,但先设计再编码更容易让我们想清楚怎么做,提前发现难点与风险,刚开始不熟练可能想不清楚该怎么设计,甚至会耽误不少时间,这都不要紧,经过多次实战就会越来越清楚该怎么设计。
下面我们就对niuma-sys系统涉及到的数据库、服务端接口、前端UI做简单的设计,实际开发中会稍微复杂些。
我们的niuma-sys系统只是实现了对用户增删改查的操作,所以只需要存储用户的信息就可以了,通过对上面功能的简单分析可以了解到,至少要包含用户名和密码这两个信息,为了让信息丰富一点,我们可以再加一个邮箱信息,id作为唯一标识也是需要的,表名就叫users,那么我们可以这样设计用户表:
我们有5个功能需要实现,计划设计5个接口来对应,它们分别是:
上面是接口的概览,方便我们从整体上了解功能对应的接口,接下来我们细化一下每个接口的请求头、请求体和响应结果:
路径:POST /api/register请求头:Content-Type: application/json请求体(JSON):
{ "username": "zhangsan", // 用户名 "password": "123456", // 密码 "email": "zhangsan@example.com" // 邮箱}响应:
{ "message": "用户注册成功"}路径:POST /api/login请求头:Content-Type: application/json请求体:
{ "username": "zhangsan", //用户名 "password": "123456" // 密码}响应:
{ "message": "登录成功"}路径:GET /api/users请求头:无请求参数:无响应:
{ "message": "查询成功", "data": [ { "id": 1, "username": "zhangsan", "email": "zhangsan@example.com" } ]路径:PATCH /api/users/{id}password请求头:无请求参数:id,路径参数响应:
{ "message": "密码修改成功"}路径:DELETE /api/users/{id}请求头:无请求参数:id,唯一标识,路径参数响应:
{ "message": "用户删除成功"}需要解释的是,响应结果中的code我们使用的是HTTP的状态码,即2xx表示成功,4xx表示客户端错误,5xx表示服务端错误。
UI设计一般是有单独的设计人员来做的,也有一些比较专业的流行软件,比如Figma、Sketch、墨刀等,但如果我们是自己开发小型项目需要稍微了解点设计,尤其是现在在AI的加持下,通过画草图或者输入提示词,做点设计也不是难事。
我们的niuma-sys现在要实现的功能比较简单,就算自己动手画也是一件很简单的事情。通过上述的功能分析,我们大致需要设计三个页面就可以了,一个页面用于用户登录,一个页面用于用户注册,一个页面用于列出所有用户信息,并且在这个页面中还可以实现用户删除和密码修改,我们画一下草图:
login.html


要做什么,我们已经比较清楚了,接下来就先搭建一个项目,为我们编写代码做准备。
先打开VSCode,选择一个工作目录,在VSCode控制台(TERMINAL)中使用uv来初始化我们的项目,执行如下命令:
uv init niuma-syscd niuma-syscode .这样就可以在VSCode中打开niuma-sys这个工程了,如果你使用code .这个命令不能帮你打开一个新的窗口,可能是因为安装VSCode时没有允许加入Path环境变量,问题也不大,可以手动选择路径打开。
初始化完工程,我们就可以添加这个项目的依赖库了,在控制台中执行如下命令:
uv add flask flask-sqlalchemy python-dotenv执行完这个命令我们会发现,它帮我们创建了一个.venv的文件夹,这个自然是创建的虚拟环境,在pyproject.toml这个文件中的dependencies下也可以看到添加的依赖和版本。
此时项目的目录结构是这样的:
niuma-sys/├── .venv├── .gitignore├── .python-version├── main.py├── pyproject.toml├── README.md└── uv.lockmain.py只是一个示例文件,删掉即可,我们创建一个demo.py用来验证一下我们的web工程是否能正常运行,在demo.py中写下这样的代码:
from flask import Flaskapp = Flask(__name__)@app.route('/api/hello', methods = ['GET'])def hello_python(): return "Hello,Python"if __name__ == "__main__": app.run(debug=True)然后在控制台中执行uv run demo.py,控制台中输出如下这些内容:
* Serving Flask app 'demo' * Debug mode: onWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Running on http://127.0.0.1:5000Press CTRL+C to quit * Restarting with stat * Debugger is active! * Debugger PIN: 132-913-686我们在浏览器地址栏中输入:http://localhost:5000/api/hello,能够返回Hello,Python,说明我们的Web服务是正常的,如果不能输出,则需要排查问题了。
现在我们可以用CTRL C来停止服务,继续完善项目目录,我们计划创建一个.env文件用于存放配置信息,创建一个app.py用于提供功能接口,创建一个sql.py来创建和初始化我们的数据库,创建一个models.py用于存储数据库表模型及操作,创建一个templates用于存储HTML文件,目录结构是这样的:
niuma-sys/├── .venv├── templates├── .env├── .gitignore├── .python-version├── app.py├── db.py├── demo.py├── models.py├── pyproject.toml├── README.md└── uv.lock接下来我们就可以开始创建数据库表了,用它来记录用户的信息。首先我们需要在.env文件中写一下这个项目的配置信息:
# 应用配置FLASK_APP=app.pyFLASK_ENV=developmentSECRET_KEY=your-secret-123456 # 可自行替换为随机字符串DATABASE_URI=sqlite:///niuma.db # SQLite 数据库路径然后我们在models.py中定义模型,这个是和数据库表的字段对应的:
from flask_sqlalchemy import SQLAlchemyfrom sqlalchemy import Column, Integer, String# 初始化db对象db = SQLAlchemy()# 定义User模型class User(db.Model): id = Column(Integer, primary_key=True) username = Column(String(50), unique=True, nullable=False) password = Column(String(128), nullable=False) email = Column(String(255), unique=True, nullable=False) def to_dict(self): """转换为字典,可以用于接口""" return { 'id': self.id, 'username': self.username, 'email': self.email }我们定义了一个User对象,它继承db.Model,这样定义的User就对应数据库表中的一张表,里面的变量对应表中的字段。
然后我们在db.py中编写代码,让它来帮助我们生成数据库和表:
import osfrom dotenv import load_dotenvfrom flask import Flaskfrom models import db# 加载环境变量load_dotenv()# 初始化应用app = Flask(__name__)app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') # 安全密钥(会话、表单加密用)app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI') # 数据库连接地质app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 关闭不必要的警告# 将models中的db对象绑定到当前Flask应用db.init_app(app)def init_db(): """初始化数据库""" with app.app_context(): db.create_all() print('数据初始化完成,表已创建')if __name__ == '__main__': init_db()dotenv模块中的load_dotenv()方法可以帮助我们自动加载.env中的配置信息,我们写了一个初始化数据库的方法,执行这个脚本,会发现在项目的目录下创建了一个instance文件夹,在这个文件夹中有一个niuma.db,这就是创建的数据库,我们可以通过一些数据库软件(如DBeaver)来查看数据中的信息。
我们在app.py中编写注册用户、登录、修改密码、删除密码、查询所有用户、退出登录这些接口的代码:
import osfrom dotenv import load_dotenvfrom flask import Flask, request, jsonify, session, render_templatefrom sqlalchemy.exc import SQLAlchemyErrorfrom models import db, User# 加载环境变量load_dotenv()# 初始化Flask应用app = Flask(__name__)app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URI')app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False# 将db对象绑定到当前Flask应用db.init_app(app)@app.route('/api/register', methods=['POST'])def register(): """注册""" data = request.get_json() # 参数校验 if not all(key in data for key in['username', 'password', 'email']): return jsonify({'message': 'username,password,email参数缺一不可'}), 400 # 检查用户名或邮箱是否存在 if User.query.filter_by(username=data['username']).first(): return jsonify({'message':'用户名已存在'}), 409 if User.query.filter_by(email=data['email']).first(): return jsonify({'message': '邮箱已存在'}), 409 # 创建新用户 new_user = User(username=data['username'], email=data['email'], password=data['password']) # 保存到数据库 try: db.session.add(new_user) db.session.commit() return jsonify({ 'message': '用户注册成功' }), 201 except SQLAlchemyError as e: db.session.rollback() print(e) return jsonify({'message': '用户注册失败'}), 500@app.route('/api/login', methods=['POST'])def login(): """登录""" data = request.get_json() # 参数校验 if not all(key in data for key in ['username', 'password']): return jsonify({'message': '用户名和密码均不能为空'}), 400 user = User.query.filter_by(username=data['username']).first() if not user or user.password != data['password']: return jsonify({'message': '用户名或密码错误!'}), 401 # 记录登录状态 session['user_id'] = user.id session['username'] = user.username return jsonify({'message': '登陆成功!'}), 200@app.route('/api/users', methods=['GET'])def query_all_users(): """查询所有用户""" if 'user_id' not in session: return jsonify({'message': '请先登录'}), 401 users = User.query.all() return jsonify({ 'message': '查询成功', 'data': [user.to_dict() for user in users] }), 200@app.route('/api/users/<int:uid>/password', methods=['PATCH'])def change_password(uid): """修改密码""" if 'user_id' not in session: return jsonify({'message': '请先登录'}) data = request.get_json() # 参数校验 if not all(key in data for key in ['old_password', 'new_password']): return jsonify({'message': '旧密码和新密码都必须提供'}), 400 user = db.session.get(User, uid) if user.password != data['old_password']: return jsonify({'message': '旧密码不正确'}), 401 # 提交修改密码 user.password = data['new_password'] try: db.session.commit() return jsonify({'message': '密码修改成功'}), 200 except SQLAlchemyError as e: db.session.rollback() print(e) return jsonify({'message': '密码修改失败'}), 500@app.route('/api/users/<int:user_id>', methods=['DELETE']) def delete_user(user_id): """删除用户""" if 'user_id' not in session: return jsonify({'message': '请先登录'}), 401 # 禁止删除自己 if user_id == session['user_id']: return jsonify({'message': '不能删除当前登录用户'}), 403 user = db.session.get(User, user_id) if not user: return jsonify({'message': '用户不存在'}), 404 try: db.session.delete(user) db.session.commit() return jsonify({'message': '用户删除成功'}), 200 except SQLAlchemyError as e: db.session.rollback() print(e) return jsonify({'message': '删除失败'}), 500@app.route('/api/logout', methods=['POST']) def logout(): """退出登录""" session.clear() return jsonify({'message': '退出成功'}), 200if __name__ == '__main__': app.run(debug=True, port=5000)app.py中基本的实现思路是加载环境变量,初始化Flask应用,将数据库对象绑定到Flask应用上,然后实现我们设计的几个接口。
实现的这几个接口,基本覆盖到了日常开发使用的场景,使用@app.route()装饰器来设置接口路径和请求方法,虽然我们的功能简单,但像GET/POST/DELETE/PATCH这些增删查改的方法我们也都用到了,还有一个比较常用的PUT方法我们这里没有用到。
使用jsonify()方法来处理返回结果(JSON),在jsonify()方法后紧跟的是状态码,这些状态码都是HTTP中标准的状态码。
最后我们使用app.run(debug=True, port=5000)来作为服务启动的入口,在控制台中我们可以执行uv run app.py即可启动我们的服务了,然后我们可以用一些接口测试工具,如Postman、Apifox等来测试接口功能是否正常。
实际的开发中多使用前后端分离的方式,使用一些前端框架,但我们的功能比较简单,也是为了熟悉web开发的基本流程,我们就直接在templates目录下编写前端代码,实现注册、登录、用户列表、删除、修改密码这些功能。
我们需要三个页面即可,一个页面用于注册,一个页面用于登录,一个页面用于展示所有用户并且提供删除和修改功能,接下来我们逐个看这些页面的代码。
register.html用于用户注册:
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <title>用户注册</title> <style> body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 0 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; box-sizing: border-box; } button { background: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer; width: 100%; } button:hover { background: #45a049; } .message { margin-top: 15px; padding: 10px; border-radius: 4px; } .success { background: #dff0d8; color: #3c763d; } .error { background: #f2dede; color: #a94442; } .link { text-align: center; margin-top: 15px; } a { color: #4CAF50; text-decoration: none; }</style></head><body> <h2>用户注册</h2> <form id="registerForm"> <div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" name="username" required> </div> <div class="form-group"> <label for="email">邮箱</label> <input type="email" id="email" name="email" required> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" name="password" required> </div> <button type="submit">注册</button> </form> <div id="message" class="message" style="display: none;"></div> <div class="link"> 已有账号?<a href="/">立即登录</a> </div> <script> document.getElementById('registerForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const email = document.getElementById('email').value; const password = document.getElementById('password').value; const messageDiv = document.getElementById('message'); try { const response = await fetch('/api/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, email, password }) }); const result = await response.json(); if (response.status == 201) { messageDiv.className = 'message success'; messageDiv.textContent = result.message; messageDiv.style.display = 'block'; // 注册成功后跳转到登录页 setTimeout(() => { window.location.href = '/'; }, 500); } else { messageDiv.className = 'message error'; messageDiv.textContent = result.message; messageDiv.style.display = 'block'; } } catch (error) { messageDiv.className = 'message error'; messageDiv.textContent = '网络错误,请重试'; messageDiv.style.display = 'block'; } });</script></body></html>login.html用于用户登录:
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <title>用户登录</title> <style> body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 0 20px; } .form-group { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input { width: 100%; padding: 8px; box-sizing: border-box; } button { background: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer; width: 100%; } button:hover { background: #45a049; } .message { margin-top: 15px; padding: 10px; border-radius: 4px; } .success { background: #dff0d8; color: #3c763d; } .error { background: #f2dede; color: #a94442; } .link { text-align: center; margin-top: 15px; } a { color: #4CAF50; text-decoration: none; }</style></head><body> <h2>用户登录</h2> <form id="loginForm"> <div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" name="username" required> </div> <div class="form-group"> <label for="password">密码</label> <input type="password" id="password" name="password" required> </div> <button type="submit">登录</button> </form> <div id="message" class="message" style="display: none;"></div> <div class="link"> 还没有账号?<a href="/register">立即注册</a> </div> <script> document.getElementById('loginForm').addEventListener('submit', async function(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; const messageDiv = document.getElementById('message'); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const result = await response.json(); if (response.status == 200) { messageDiv.className = 'message success'; messageDiv.textContent = result.message; messageDiv.style.display = 'block'; // 登录成功后跳转到用户列表页 setTimeout(() => { window.location.href = '/users'; }, 500); } else { messageDiv.className = 'message error'; messageDiv.textContent = result.message; messageDiv.style.display = 'block'; } } catch (error) { messageDiv.className = 'message error'; messageDiv.textContent = '网络错误,请重试'; messageDiv.style.display = 'block'; } });</script></body></html>users.html用户展示用户列表、修改密码和删除用户:
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <title>用户列表</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 0 20px; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #f2f2f2; } tr:hover { background-color: #f5f5f5; } .action-btn { padding: 5px 10px; margin: 0 5px; border: none; cursor: pointer; border-radius: 3px; } .delete-btn { background: #f44336; color: white; } .password-btn { background: #2196F3; color: white; } .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.4); } .modal-content { background: white; margin: 15% auto; padding: 20px; width: 300px; border-radius: 5px; } .modal-content button {background: #2196F3; color: white; margin: 10px auto 0 auto; display: block; border: none; width: 80px; padding: 2px 10px;} .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .close:hover { color: black; } .message { margin-top: 15px; padding: 10px; border-radius: 4px; } .success { background: #dff0d8; color: #3c763d; } .error { background: #f2dede; color: #a94442; } .logout {text-align: right;}</style></head><body> <h2>用户管理</h2> <div id="logout" class="logout"> <button onclick="logout()">退出</button> </div> <div id="message" class="message" style="display: none;"></div> <table id="usersTable"> <thead> <tr> <th>ID</th> <th>用户名</th> <th>邮箱</th> <th>操作</th> </tr> </thead> <tbody id="usersBody"> <!-- 用户数据会动态加载到这里 --> </tbody> </table> <!-- 修改密码弹窗 --> <div id="passwordModal" class="modal"> <div class="modal-content"> <span class="close" onclick="closeModal()">×</span> <h3>修改密码</h3> <input type="hidden" id="modalUserId"> <div class="form-group"> <label for="oldPassword">旧密码:</label> <input type="password" id="oldPassword" required> </div> <div class="form-group"> <label for="newPassword">新密码:</label> <input type="password" id="newPassword" required> </div> <button onclick="saveNewPassword()">保存</button> </div> </div> <script> // 页面加载时自动加载用户列表 window.onload = loadUsers; // 加载用户列表 async function loadUsers() { try { const response = await fetch('/api/users'); const result = await response.json(); if (response.status == 200) { const usersBody = document.getElementById('usersBody'); usersBody.innerHTML = ''; result.data.forEach(user => { const row = document.createElement('tr'); row.innerHTML = ` <td>${user.id}</td> <td>${user.username}</td> <td>${user.email}</td> <td> <button class="action-btn password-btn" onclick="openPasswordModal(${user.id})">修改密码</button> <button class="action-btn delete-btn" onclick="deleteUser(${user.id})">删除</button> </td> `; usersBody.appendChild(row); }); showMessage('用户列表加载成功', 'success'); } else { showMessage('加载用户列表失败', 'error'); } } catch (error) { showMessage('网络错误,请重试', 'error'); } } // 删除用户 async function deleteUser(userId) { if (!confirm('确定要删除这个用户吗?')) return; try { const response = await fetch(`/api/users/${userId}`, { method: 'DELETE' }); const result = await response.json(); if (response.status == 200) { showMessage('用户删除成功', 'success'); loadUsers(); // 重新加载列表 } else { showMessage(result.message || '删除用户失败', 'error'); } } catch (error) { showMessage('网络错误,请重试', 'error'); } } // 打开修改密码弹窗 function openPasswordModal(userId) { document.getElementById('modalUserId').value = userId; document.getElementById('newPassword').value = ''; document.getElementById('passwordModal').style.display = 'block'; } // 关闭弹窗 function closeModal() { document.getElementById('passwordModal').style.display = 'none'; } // 保存新密码 async function saveNewPassword() { const userId = document.getElementById('modalUserId').value; const oldPassword = document.getElementById('oldPassword').value; const newPassword = document.getElementById('newPassword').value; if(!oldPassword) { showMessage('请输入旧密码', 'error') return; } if (!newPassword) { showMessage('请输入新密码', 'error'); return; } try { const response = await fetch(`/api/users/${userId}/password`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ old_password: oldPassword, new_password: newPassword }) }); const result = await response.json(); if (response.status == 200) { showMessage('密码修改成功', 'success'); closeModal(); } else { showMessage(result.message || '密码修改失败', 'error'); } } catch (error) { showMessage('网络错误,请重试', 'error'); } } // 显示消息提示 function showMessage(text, type) { const messageDiv = document.getElementById('message'); messageDiv.textContent = text; messageDiv.className = `message ${type}`; messageDiv.style.display = 'block'; // 3秒后自动隐藏 setTimeout(() => { messageDiv.style.display = 'none'; }, 1000); } // 点击弹窗外部关闭 window.onclick = function(event) { const modal = document.getElementById('passwordModal'); if (event.target == modal) { closeModal(); } } //退出登录 async function logout() { try { const response = await fetch(`/api/logout`, { method: 'POST' }); const result = await response.json(); if (response.status == 200) { window.location.href = '/' } else { showMessage(result.message || '退出失败', 'error'); } } catch (error) { showMessage('网络错误,请重试', 'error'); } }</script></body></html>这些功能涉及到的代码比较少,所以我们将CSS和JavaScript的代码都写在了HTML文件中,如果功能比较复杂,还是要将其分离,可以在这个项目下创建一个单独的目录来存储。
HTML/CSS/JavaScript如果讲起来也需要很多的篇幅,这不是我们这篇文章需要讨论的重点,所以我们在这里就点到为止。
前端代码完成之后,我们直接在浏览器中访问http://localhost:5000并不能返回我们编写的页面,比如login.html。我们还需要将前端和服务端联系起来,所以还需要在app.py中增加一些代码。我们可以在logout()函数之后追加以下代码:
@app.route('/')def index(): """首页""" return render_template('login.html')@app.route('/register')def to_register(): """注册页面""" return render_template('register.html')@app.route('/users')def to_users(): """用户列表页面""" return render_template('users.html')我们使用flask模块中的render_template()方法,将HTML文件返回给浏览器渲染。分别将登录页面、注册页面、用户列表页面返回给浏览器,这里我们将系统访问的首页指定为了登录页面。
将前后端连接起来,我们就可以在本地测试一下程序的功能是否正常了,在实际开发中我们的接口是需要写单元测试的,对于Python Web的接口测试可以用pytest+requests库编写测试用例。
虽然多数开发人员也不怎么写,但写单元测试,以后代码发生了变更容易维护,测试一般也会有专门的测试人员,我们做的功能比较简单,自己走一遍也很容易验证功能是否符合预期,就不再编写测试用例了(其实也是懒得写)。
我们已经完成了功能的开发,但只是在自己的开发环境下运行,开发完成之后我们需要将其部署到服务器上,这样才能让更多人使用,这里我们介绍两种环境下的部署,一个是Windows环境下,一个是Linux环境下。
我使用的是Windows 11专业版,在Windows下部署我们使用waitree,首先需要添加这个依赖库:
uv add waitress然后需要创建一个用于服务器下启动应用的脚本,我们将其命名为wsgi.py,代码也非常的简单:
from waitress import servefrom app import appif __name__ == '__main__': serve(app, port=5000)然后将我们的项目复制到部署环境的某个目录下,我们只需要复制用到的源代码和配置文件就可以了,像虚拟环境(.venv)、开发环境下的数据库(instance/niuma.db)、缓存文件等不需要复制,不过需要保证所部署的机器是有Python运行环境和uv管理工具。
Windows环境下安装Python,我们在《第1章:Python入门第一课》中有聊到,uv工具的安装在《第5章:模块与包》中有聊到,如果不了解,可以返回去看一下。
在部署目录下,打开niuma-sys,也就是我们项目的目录,可以在这个窗口的地址栏中输入cmd命令然后回车,进入命令行窗口,在命令行中依次执行下面三行命令:
uv venv # 创建虚拟环境uv sync # 安装依赖.venv\Scripts\activate # 激活虚拟环境激活虚拟环境后,在命令行的开头会有(niuma-sys)的显示,这个时候我们就可以初始化数据库了,执行python db.py,然后可以启动应用,执行python wsgi.py命令,看到光标一直在闪烁说明服务开始运行了,这个时候可以打开浏览器在地址栏中输入http://localhost:5000就能看到登录页面了。如果想停止应用,使用CTRL C命令即可。
多数情况下,Web应用都是部署在Linux环境的,Linux有多个发行版本,我们这里使用Ubuntu Server 20.04 LTS版本。
在Linux下我们使用Gunicorn作为Web服务容器,由于和Windows使用的Web服务容器不同,所以在启动的脚本上也有点区别,我们先写一个适用于Linux下的脚本,将其命名为wsgi_linux.py,代码如下:
from app import appif __name__ == '__main__': app.run()登录我们的Ubuntu Server服务器,先来安装Python相关的组件:
sudo apt install python3 python3-pip python3-venv -ycurl -LsSf https://astral.sh/uv/install.sh | shsource $HOME/.local/bin/env然后将目录切换到niuma-sys下,开始创建虚拟环境和安装依赖:
uv venv # 创建虚拟环境uv sync # 安装依赖uv add gunicorn # 安装 Gunicorn接下来我们切换到虚拟环境,初始化数据,启动服务:
# 激活虚拟环境source .venv/bin/activate# 初始化数据库python db.py# 用 Gunicorn启动gunicorn -w 4 -b 0.0.0.0:5000 wsgi_linux:app启动完成之后niuma-sys就可以正常访问了。
我们来简单解释一下gunicorn启动命令,其中:
如果我们想将niuma-sys设置为后台运行或者随系统自动重启,可以在Ubuntu中创建系统服务,这里就不再详细说明了。
至此,Python Web开发的基本知识已经介绍完毕,掌握了这些内容加上前面一些章节的学习已经足以让我们开发出一些像样的Web功能。
当然,在这章的例子中也存在一些问题,比如密码直接用了明文存储、没有日志记录、没有进行跨域处理等等,这可以作为本章的练习,如果你有兴趣可以尝试完成,或者你有自己想解决问题更好。
要说明的是,从这章开始,如果有练习将不会再给出示例,因为从这一章开始其实都属于实战内容了,这些都是对于前面基础知识的运用了。