3步手写实现网站建设文章系统告别只会语法
刚毕业那会儿,我盯着IDE里满屏的绿色代码,心里直打鼓。语法都背熟了,正则表达式能默写,设计模式能讲出个一二三,但真要让我从零搭一个能上线的网站,手就开始抖。这种学会语法却不知怎么搭项目的无力感,是太多应届生的通病。很多人觉得,只要把API调通了,项目就成了。大错特错。真正的工程化能力,往往藏在那些你看不见的细节里。
今天咱们不玩虚的,直接上手手写实现一个最小可用的网站建设文章管理系统。不依赖庞大的框架,不追求花哨的功能,只用最基础的Web知识,把“文章发布”这个核心闭环跑通。你会发现,当你亲手把每一行代码敲进终端,看着浏览器里显示出自己写的页面时,那种掌控感,是看一百篇教程都换不来的。
项目目标与边界界定
很多新手一上来就想做“大而全”的CMS,结果写到一半就崩了。咱们先定规矩:这个项目只做核心闭环。数据持久化:文章存哪里?为了降低门槛,我们先用JSON文件模拟数据库。这能让你聚焦于业务逻辑,而不是去啃SQL或Redis的配置。
接口规范:提供两个核心API:POST /api/articles(新增文章)和 GET /api/articles(获取列表)。
前端展示:一个极简的HTML页面,包含一个表单和一个列表区域。
技术栈:Python + Flask(轻量级Web框架,适合快速验证逻辑)+ Vanilla JS(原生JavaScript,不引Vue或React,减少干扰)。为什么选Python? 对于应届生来说,Python的语法亲和力最高,能让你在调试时少被语法糖绊倒,多关注逻辑本身。而且,CSDN上大量的Python Web实战案例,都能让你随时找到类似问题的解法,降低排错成本。
边界很重要。我们不做用户登录,不做权限管理,不做分页。这些功能在实际项目中当然需要,但在“手写实现”的学习阶段,它们会掩盖核心逻辑。就像学开车,先学会离合油门配合,再谈并线变道。
目录结构:工程化的第一步
很多初学者喜欢把所有代码塞在一个app.py里。这是大忌。一旦文件超过200行,维护起来就是噩梦。咱们从一开始就养成模块化的习惯。
article-system/
├── app.py # 主入口,Flask实例化与路由挂载
├── config.py # 配置文件,管理JSON文件路径
├── models/
│ └── article.py # 数据模型,定义Article类
├── services/
│ └── article_service.py # 业务逻辑层,处理读写JSON
├── templates/
│ └── index.html # 前端页面
├── static/
│ └── js/
│ └── main.js # 前端交互逻辑
└── data/└── articles.json # 模拟数据库文件这个结构虽然简单,但体现了分层架构的思想:Models:定义数据结构,类似ORM中的实体。
Services:处理业务逻辑,比如验证数据格式、读写文件。
App:只负责接收HTTP请求,调用Service,返回JSON响应。关键细节:data/articles.json 初始内容为 []。在config.py中,我们要用os.path动态获取路径,而不是硬编码相对路径,这样无论你在哪个目录下运行脚本,都能找到数据文件。这是很多新手忽略的“坑”,导致换台电脑就报文件不存在。
核心代码实现:逐行拆解
1. 数据模型与业务层
先看models/article.py。我们定义一个Article类,它不是数据库模型,而是数据载体。
from dataclasses import dataclass, asdict
from datetime import datetime@dataclass
class Article:id: inttitle: strcontent: strcreated_at: strdef to_dict(self):转换为字典,便于JSON序列化return asdict(self)使用dataclass是Python 3.7+的优雅写法,自动生成了__init__等方法。to_dict方法是为了方便Flask的jsonify直接处理。
接着看services/article_service.py,这是项目的“大脑”。
import json
import os
import threading
from config import DATA_FILE
from models.article import Articleclass ArticleService:_lock = threading.Lock() # 简单的线程锁,防止并发写入冲突def __init__(self):self._ensure_data_file()def _ensure_data_file(self):确保数据文件存在if not os.path.exists(DATA_FILE):with open(DATA_FILE, 'w', encoding='utf-8') as f:json.dump([], f)def _read_all(self) - list:读取所有文章with open(DATA_FILE, 'r', encoding='utf-8') as f:data = json.load(f)return [Article(**item) for item in data]def _write_all(self, articles: list):写入所有文章,加锁保证原子性with self._lock:with open(DATA_FILE, 'w', encoding='utf-8') as f:json.dump([a.to_dict() for a in articles], f, ensure_ascii=False, indent=2)def create_article(self, title: str, content: str) - Article:创建新文章articles = self._read_all()# 生成唯一ID:简单用时间戳毫秒+随机数,生产环境应使用UUIDimport time, randomnew_id = int(time.time() * 1000) + random.randint(1000, 9999)new_article = Article(id=new_id,title=title,content=content,created_at=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))articles.append(new_article)self._write_all(articles)return new_articledef get_all_articles(self) - list:获取所有文章列表,按时间倒序articles = self._read_all()articles.sort(key=lambda a: a.created_at, reverse=True)return articles# 全局单例,避免重复初始化
article_service = ArticleService()逐行解析重点:线程锁 _lock:JSON文件不是并发安全的。如果两个用户同时提交文章,可能会覆盖彼此的数据。虽然Flask默认是单线程调试模式,但养成加锁习惯,能避免你在生产环境踩大坑。
ensure_ascii=False:JSON序列化时,如果不设置这个,中文会被转成\uXXXX格式,导致前端显示乱码或难以阅读。这是很多新手容易忽略的细节。
ID生成策略:这里用了时间戳+随机数。在生产环境中,务必使用UUID或数据库自增ID。但在这个Demo中,它足以保证唯一性,且代码简单。2. Web路由与前端交互
app.py 是连接前后端的桥梁。
from flask import Flask, request, jsonify, render_template
from services.article_service import article_serviceapp = Flask(__name__)@app.route('/')
def index():return render_template('index.html')@app.route('/api/articles', methods=['POST'])
def create_article():data = request.get_json()# 简单校验if not data or not data.get('title') or not data.get('content'):return jsonify({'error': 'Title and content are required'}), 400article = article_service.create_article(data['title'], data['content'])return jsonify({'message': 'Article created', 'article': article.to_dict()}), 201@app.route('/api/articles', methods=['GET'])
def get_articles():articles = article_service.get_all_articles()return jsonify([a.to_dict() for a in articles])if __name__ == '__main__':app.run(debug=True)注意201状态码。200表示成功,201表示资源创建成功。这是RESTful API的规范细节。很多面试官会问:“为什么创建资源不返回200?”如果你能答出201,说明你不仅会用,还懂规范。
前端static/js/main.js 负责交互:
document.addEventListener('DOMContentLoaded', () = {const form = document.getElementById('article-form');const listContainer = document.getElementById('article-list');// 加载文章列表async function loadArticles() {try {const response = await fetch('/api/articles');const articles = await response.json();renderArticles(articles);} catch (error) {console.error('Failed to load articles:', error);}}// 渲染文章列表function renderArticles(articles) {listContainer.innerHTML = '';articles.forEach(article = {const item = document.createElement('div');item.className = 'article-item';item.innerHTML = `h3${article.title}/h3p${article.content.substring(0, 100)}.../psmall${article.created_at}/small`;listContainer.appendChild(item);});}// 处理表单提交form.addEventListener('submit', async (e) = {e.preventDefault();const title = document.getElementById('title').value;const content = document.getElementById('content').value;try {const response = await fetch('/api/articles', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ title, content })});if (response.ok) {form.reset();loadArticles(); // 重新加载列表} else {const error = await response.json();alert(error.error || 'Failed to create article');}} catch (error) {alert('Network error');}});loadArticles();
});前端避坑:fetch 的 then vs async/await:async/await 让异步代码看起来像同步代码,逻辑更清晰。但要注意,fetch 默认不会在HTTP错误状态码(如400, 500)时抛出异常,必须手动检查 response.ok。
XSS风险:innerHTML 直接插入用户输入的内容,存在跨站脚本攻击风险。生产环境中,必须对title和content进行HTML转义。在这个Demo中,为了简化,我们假设输入是安全的,但你要知道这个风险。运行与测试:从0到1的闭环
现在,打开终端,执行以下步骤:安装依赖:
pip install flask启动服务:
python app.py访问页面:
浏览器打开 http://127.0.0.1:5000。
测试功能:输入标题“测试文章1”,内容“这是第一行内容”,点击发布。
刷新页面,看列表是否更新。
打开data/articles.json,看文件内容是否正确写入。常见问题排查:中文乱码:检查json.dump是否加了ensure_ascii=False,前端fetch是否正确解析JSON。
文件路径错误:检查config.py中的路径是否绝对路径。推荐写法:
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_FILE = os.path.join(BASE_DIR, 'data', 'articles.json')并发写入冲突:如果快速多次提交,可能出现数据丢失。这是线程锁的局限,生产环境应使用SQLite或MySQL。优化扩展:从Demo到生产
这个Demo能跑,但离生产还差得远。以下是几个进阶方向,也是你面试时可以聊的“亮点”:数据库替换:
将JSON文件替换为SQLite。使用sqlite3标准库或SQLAlchemy ORM。这一步能体现你对数据持久化的深入理解。
# 伪代码
import sqlite3
def init_db():conn = sqlite3.connect('app.db')conn.execute('CREATE TABLE IF NOT EXISTS articles (id INTEGER PRIMARY KEY, title TEXT, content TEXT, created_at TEXT)')conn.commit()输入验证与安全:使用marshmallow库进行数据验证,防止过长输入或恶意字符。
对title和content进行HTML转义,防止XSS。分页查询:
当文章数量增多,全量加载会拖慢页面。添加page和per_page参数,实现分页。
@app.route('/api/articles', methods=['GET'])
def get_articles():page = request.args.get('page', 1, type=int)per_page = request.args.get('per_page', 10, type=int)# 实现分页逻辑日志与监控:
使用logging模块记录请求日志和错误信息。生产环境中,日志是排错的生命线。
部署与Docker:
编写Dockerfile,将应用容器化。这是现代开发的标准技能,也是晋升路径中的关键一环。职业发展视角:
应届生入职后,第一年的核心任务是熟悉业务+夯实基础。能手写实现一个完整的小系统,比只会调API更有说服力。它证明你理解请求生命周期、数据流向、错误处理。在面试中,当面试官问“你做过什么项目”,你能指着这个Demo说:“我用Python从零搭建了一个文章系统,解决了并发写入问题,并实现了前端交互”,这比“我用了Spring Boot做了一个管理系统”更有记忆点。
小结与互动
回顾整个过程,我们从一个空白目录,一步步搭建了文章模型、业务逻辑、Web接口和前端交互。没有依赖复杂的框架,却完整覆盖了Web开发的核心要素。
手写实现的价值,不在于代码有多高效,而在于你对每一行代码的掌控感。你知道数据从哪里来,到哪里去;你知道为什么加锁,为什么用201状态码。这种底层认知,是你在职业生涯中不断进阶的基石。
晋升与职业发展,从来不是一蹴而就的。它始于每一个你愿意亲手敲下的代码,始于你对每一个技术细节的好奇与深挖。培训机构的选择,也往往决定了你能否接触到这种“从零到一”的训练,还是只停留在“调包侠”的层面。
这个知识点你面试被问过吗? 比如“为什么JSON文件写入需要加锁?”或者“如何防止前端XSS攻击?”留言说说你的经历,或者你遇到的坑。我们一起聊聊,怎么把这些细节变成你的面试加分项。
