ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Flask集成UEditor:文件上传与action分发源码解析

Flask集成UEditor:文件上传与action分发源码解析 简介这是一份基于Flask框架的Ueditor富文本编辑器Python源码面向需要快速接入富文本编辑与文件上传能力的Web开发者也适合Flask中高级学习者研究前后端交互与静态资源管理。项目由开源版优化而来核心覆盖图片、视频、附件、涂鸦、远程抓图上传以及在线图片和附件管理能直接服务内容发布、后台管理等场景。压缩包共286个文件约6.91MB其中含87个PNG、45个GIF、27个HTML、20个CSS、76个JavaScript、3个Python脚本等前端样式、交互组件与后端处理逻辑均有涉及。已有408人学习下载说明其具备一定参考价值。通过源码可梳理Flask路由与请求处理、Ueditor配置方式、上传接口设计、templates模板渲染和requirements依赖管理适合作为实际项目改造或功能扩展的起点。1. Flask 集成 Ueditor-for-python先把富文本编辑器的源码地图画出来如果你维护过 2015 年前后的后台系统一定见过那排小图标补齐的富文本工具栏。UEditor 官方停止大版本维护后很多 Flask 项目反而把它的 Ueditor-for-python 后端源码当成“现成能力包”来用图片、视频、附件上传涂鸦、远程抓图在线图片和文件管理这些高频功能不用再重写一遍。这套源码从 coding.net 上的开源 python flask 版本演进而来286 个文件里既有 ueditor.css、ueditor.min.css 这类前端皮肤也有 3 个 Python 脚本负责的上传控制器。适合两类人一是要对接老后台的 flask 开发二是想把编辑器上传 action 流程完整拆开懂的源码学习者。2. 286 个文件的构成静态资源、Python 脚本和部署文件拿到源码包第一件事不是急着跑起来而是先看懂文件构成。这个项目不是那种只有一个 app.py 的纯接口工程它同时带着完整的 UEditor 前端发布目录所以文件数量才会膨胀到 286 个。把文件类型分布列出来目录逻辑马上就清晰了。2.1 文件类型分布表和前端皮肤的关系按类型统计这个源码包的资源构成如下类型数量在编辑器里承担的角色PNG87工具栏图标、对话框里的示例图、表情面板底图JavaScript76UEditor 核心、语言包、video-js 播放器、codemirror 代码高亮GIF45loading 动画、表情 GIF 贴图HTML27dialogs 目录下的上传/涂鸦/附件等对话框模板CSS20ueditor.css、video-js.css、shCoreDefault.css 等样式JPG6默认图片和示例素材SWF5老版本 Flash 上传组件现代浏览器基本用不到PHP5官方发布包自带的其他后端示例Python 侧不调用Python3项目核心逻辑Flask 路由、上传处理、配置Git 忽略文件1控制不上传本地缓存与临时文件这组数字说明了一个典型结构前端静态资源占四分之三以上Python 代码只负责“后端的最后一公里”。SWF 和 PHP 文件是 UEditor 官方包的遗留物我一般处理方式是直接删除或改名避免部署扫描器把 PHP 文件当作可执行入口报风险。真正决定编辑器行为的是那 3 个 Python 脚本它们通常被拆成 app.pyFlask 应用入口、ueditor.py控制器、config.py上传路径与动作配置。2.2 templates 与 static 组成的前端层从 HTML 文件数量能看出UEditor 的对话框不是硬编码在 JavaScript 里而是通过模板文件动态加载。典型目录结构是这样. ├── app.py # Flask 应用入口注册路由 ├── ueditor.py # UEditor 控制器处理 action 分发 ├── config.py # 上传目录、允许的后缀、URL 前缀 ├── templates/ │ └── index.html # 承载编辑器的后台页面 ├── static/ │ ├── ueditor/ │ │ ├── ueditor.config.js │ │ ├── ueditor.all.min.js │ │ ├── dialogs/ # 27 个 HTML 对话框模板 │ │ ├── lang/ │ │ └── themes/ │ └── uploads/ # 图片、视频、附件落盘目录 ├── requirements.txt └── Procfile这里的“前后端分离”不是 React/Vue 那种接口完全独立而是页面骨架由 Flask 的 Jinja2 渲染编辑器初始化后的所有交互都走 ajax JSON。templates 目录下只有一个或少数几个 HTML 页面它们负责输出带编辑器容器的管理后台dialogs 里的 27 个 HTML 才是真正的弹窗 UI。2.3 requirements.txt 与 Procfile 在生产部署里的位置requirements.txt 是这个源码包能一键复现环境的关键。常见内容如下flask2.3.3 itsdangerous2.1.2 Jinja23.1.2 Werkzeug2.3.7 requests2.31.0 gunicorn21.2.0其中 requests 不是给编辑器业务用的而是远程抓图 action 要代用户下载目标图片gunicorn 则是生产级 WSGI 服务器。Procfile 是给 Heroku、Dokku 这类 PaaS 平台识别进程类型的文件里面一行web: gunicorn app:appweb表示这是对外提供 HTTP 服务的进程gunicorn app:app表示从 app.py 文件中导入名为app的 Flask 应用对象。放到自己的 Linux 服务器上这一行也能直接用。如果你的部署环境不是 PaaSProcfile 可以忽略但保留能帮你快速切换部署方式。3. ueditor 控制器的 action 分发图片视频附件上传的实现核心UEditor 的前端和后端之间没有 RESTful 资源概念所有交互都收敛到一个/ueditor/controller地址上通过action参数区分当前要做什么。这是理解这套源码第一道门槛。3.1 action 参数是 UEditor 和后端的“路由器”当你在编辑器里点“上传图片”前端会向 serverUrl 发起一个带actionuploadimage的 multipart/form-data 请求点“涂鸦”则带actionuploadscrawl打开在线管理对话框时带actionlistimage。控制器里最外层就是一个 if/elif 分发# ueditor.py from flask import Blueprint, request, jsonify ueditor_bp Blueprint(ueditor_bp, __name__) ueditor_bp.route(/ueditor/controller, methods[GET, POST]) def ueditor_controller(): action request.args.get(action, ) if action config: return jsonify(get_editor_config()) if action in (uploadimage, uploadvideo, uploadfile): return upload_file(action) if action uploadscrawl: return upload_scrawl() if action catchimage: return catch_image() if action in (listimage, listfile): return list_files(action) return jsonify({state: 请求地址出错})这段代码的逻辑很简单前端每次请求都带action后端把它当作路由表。用 Blueprint 将 UEditor 路由从主应用里拆出来后续不管挂载到哪个 Flask 项目只要app.register_blueprint(ueditor_bp)就能复用这是 flask 框架中比较干净的模块组织方式。configaction 很特殊它是编辑器启动时会请求一次配置项的接口把上传大小、允许类型、URL 前缀一股脑交给前端。3.2 文件上传从 multipart 请求到落盘目录图片、视频、附件三种上传行为差别不大区别只在允许后缀和存储目录。用一个通用函数处理三种 action 是常见做法import os import time import hashlib from werkzeug.utils import secure_filename ALLOW_MAP { uploadimage: { exts: {jpg, jpeg, png, gif, webp, bmp}, dir: image }, uploadvideo: { exts: {mp4, webm, ogg, mp3, wav}, dir: video }, uploadfile: { exts: {zip, rar, 7z, pdf, doc, docx, xls, xlsx, txt}, dir: file } } UPLOAD_ROOT /data/uploads/ def upload_file(action): conf ALLOW_MAP.get(action, {}) file request.files.get(upfile) if not file: return jsonify({state: 未获取到文件}) original secure_filename(file.filename) ext original.rsplit(., 1)[-1].lower() if . in original else if ext not in conf.get(exts, set()): return jsonify({state: 不支持的文件类型}) date_dir time.strftime(%Y%m%d) save_dir os.path.join(UPLOAD_ROOT, conf.get(dir), date_dir) os.makedirs(save_dir, exist_okTrue) rand hashlib.md5(f{time.time()}{original}.encode()).hexdigest()[:6] save_name f{time.strftime(%Y%m%d%H%M%S)}_{rand}.{ext} file.save(os.path.join(save_dir, save_name)) return jsonify({ state: SUCCESS, url: f/uploads/{conf.get(dir)}/{date_dir}/{save_name}, title: original, original: original })这段代码有四个细节值得注意。第一request.files.get(upfile)里的upfile是 UEditor 前端固定的字段名改成别的任何一个名字都会取不到文件。第二secure_filename会过滤掉中文文件名和路径穿越符号但它在某些环境下会把中文全部变成空字符串所以存储名称用“时间戳 时间加原文件名的 MD5 前 6 位”重新生成目录按天切分避免单目录文件过多。第三rsplit(., 1)从右侧分割一个点对a.b.png这种双后缀文件名也能正确取到png。第四返回给前端的url不是绝对地址而是从站点根开始的路径由后续 Flask 静态路由把/uploads/映射到实际目录。UEditor 对返回 JSON 有固定契约主要参数是state、url、title、original。其中state必须是字符串SUCCESS上传失败时用它携带错误信息。动作参数含义如下参数位置含义actionquery string请求类型如 uploadimage / listfileupfilemultipart 表单二进制文件字节流startquery string在线文件列表分页起始下标sizequery string在线文件列表每页数量3.3 涂鸦和远程抓图两种不太一样的写入路径涂鸦功能从浏览器 canvas 拿到的不是二进制文件而是 base64 字符串所以上传逻辑里多了解码步骤def upload_scrawl(): base64_data request.form.get(upfile, ) if not base64_data: return jsonify({state: 涂鸦内容为空}) import base64 try: if , in base64_data: base64_data base64_data.split(,)[1] content base64.b64decode(base64_data) except Exception: return jsonify({state: base64 解码失败}) date_dir time.strftime(%Y%m%d) save_dir os.path.join(UPLOAD_ROOT, image, scrawl, date_dir) os.makedirs(save_dir, exist_okTrue) save_name fscrawl_{time.strftime(%Y%m%d%H%M%S)}.png with open(os.path.join(save_dir, save_name), wb) as fp: fp.write(content) return jsonify({state: SUCCESS, url: f/uploads/image/scrawl/{date_dir}/{save_name}})很多资料里的涂鸦代码直接 b64decode没处理data:image/png;base64,这个前缀。UEditor 某些版本提交的数据会带前缀不带前缀就先 split 再处理兼容两种情况。远程抓图的实现则完全不同它要接收多个图片 URL然后由服务器去下载def catch_image(): source_list request.form.getlist(source[]) saved_urls [] for source in source_list: try: resp requests.get(source, timeout10, headers{ User-Agent: Mozilla/5.0 }) if resp.status_code 200: ext source.rsplit(., 1)[-1].split(?)[0] if . in source else jpg save_name fcatch_{time.strftime(%Y%m%d%H%M%S)}_{len(saved_urls)}.{ext} with open(os.path.join(save_dir, save_name), wb) as fp: fp.write(resp.content) saved_urls.append({state: SUCCESS, url: f/uploads/image/catch/{date_dir}/{save_name}}) else: saved_urls.append({state: 抓取失败, source: source}) except Exception: saved_urls.append({state: 抓取失败, source: source}) return jsonify({state: SUCCESS, list: saved_urls})远程抓图最大的坑是 SSRF如果编辑器可以被任意用户操作攻击者会让服务器去请求内网地址。生产环境里我一般会在抓图函数前面加一个域名白名单校验只允许下载配置过的图片域名并且把requests.get的allow_redirects关掉避免跳转绕过白名单。4. Flask 路由与前端初始化serverUrl、在线管理对话框和后端对接后端控制器写好前端怎么把它喊出来关键在serverUrl配置和在线管理接口的返回格式。这一章把前后端对接的完整链路串起来。4.1 serverUrl 配置与前端初始化在 Jinja2 模板里加载 UEditor 资源并初始化编辑器link relstylesheet href{{ url_for(static, filenameueditor/themes/default/css/ueditor.css) }} script src{{ url_for(static, filenameueditor/ueditor.config.js) }}/script script src{{ url_for(static, filenameueditor/ueditor.all.min.js) }}/script div ideditorContainer/div script var ue UE.getEditor(editorContainer, { serverUrl: /ueditor/controller, initialFrameHeight: 320, autoHeightEnabled: true }); /scriptserverUrl必须和 Flask 蓝图注册的路由完全一致。如果蓝图带了url_prefix/api这里就要写成/api/ueditor/controller。注意不要在初始化参数里手写actionUEditor 内部会按照操作类型自动拼接。ueditor.config.js里的window.UEDITOR_HOME_URL也是容易踩坑的点它决定编辑器到哪里找 dialogs 和 lang 目录当静态资源被 Flask 蓝图托管时这个值要写成{{ url_for(static, filenameueditor/) }}对应的绝对路径。4.2 listimage 与 listfile 的分页契约在线图片管理和在线附件管理是后台用户使用频率很高的功能。UEditor 的对话框会向 controller 发actionlistimage并带上start和size两个分页参数。一个可用的实现def list_files(action): dir_name image if action listimage else file try: start max(int(request.args.get(start, 0)), 0) size min(max(int(request.args.get(size, 20)), 1), 100) except ValueError: start, size 0, 20 base_dir os.path.join(UPLOAD_ROOT, dir_name) all_files [] for root, _, names in os.walk(base_dir): for name in names: rel os.path.relpath(os.path.join(root, name), UPLOAD_ROOT) all_files.append({url: /uploads/ rel.replace(os.sep, /)}) total len(all_files) page all_files[start:start size] return jsonify({ state: SUCCESS, list: page, start: start, total: total })这里的start和size是回显参数UEditor 对话框滚动到列表底部时会再次请求start20、start40所以后端必须原样返回下一次请求用的下标。total用来告诉前端还有没有更多数据。用os.walk遍历是能跑但性能一般的做法文件超过几千个时建议记录最近的几个日期目录按时间倒序取文件避免每次全量扫描。另一个容易忽略的问题是start可能被传负数上面代码用max(int(...), 0)做了下限收敛。UEditor 对话框各种按钮对应的 action 映射关系对话框功能action 值返回关键字段编辑器配置初始化config上传大小、类型等配置项上传图片uploadimagestate, url, title上传视频uploadvideostate, url, title上传附件uploadfilestate, url, title在线图片管理listimagestate, list, start, total在线附件管理listfilestate, list, start, total涂鸦板uploadscrawlstate, url远程抓图catchimagestate, list4.3 模板渲染与静态资源映射Flask 侧要把/uploads/这个 URL 前缀映射到真实目录否则上传成功但编辑器里图片加载 404。在 app.py 里补一段from flask import send_from_directory app.route(/uploads/path:filename) def uploaded_files(filename): return send_from_directory(UPLOAD_ROOT, filename)path:filename转换器允许路径里带斜杠这样才能把日期目录一层层带出来。如果前面有静态文件路由写了/uploads的 catch-all优先级会冲突一般把上传目录路由放在 UEditor 蓝图之后注册即可。Flask 的url_for(static, ...)默认映射到项目根下的static目录UEditor 的 css 里引用图片用的又是相对路径所以不要单独给静态文件再做一层 URL 重写否则图标和背景图全是裂图。5. 部署排错与上传目录改造从 Procfile 到对象存储最后落地部署时最容易出问题的反而是那些 20 行以内的环境配置。5.1 用 gunicorn 把 Flask 应用拉起来依赖安装完直接用 gunicorn 启动pip install -r requirements.txt gunicorn app:app -b 0.0.0.0:8000 --workers 2 --timeout 30app:app的第一个app是模块名第二个app是 Flask 实例名。如果你在 app.py 里定义的是application Flask(__name__)这里就要相应改成application。--workers 2对小型后台够用worker 太多反而会因为 GIL 和内存占用造成浪费。5.2 高频问题定位表现象原因处理方式图片上传成功但编辑器里不显示后端返回的 url 没有对应用户可访问路由注册/uploads/path:filename发送目录上传大视频直接 413Nginx 的 client_max_body_size 默认值太小在 Nginx 配置里调到 100m远程抓图一张都成功不了目标网站拒绝默认 Python User-Agent在 requests.get 里加浏览器 UA在线管理列表为空uploads 目录下面没有预期的 image 或 file 子目录检查 UPLOAD_ROOT 是否和上传保存目录一致中文名文件上传后变成下划线secure_filename 的过滤规则用时间戳加随机数生成存储文件名Flask 自带服务器运行正常gunicorn 下 404蓝图注册顺序或静态目录路径不一致用app.url_map打印路由表核对5.3 把上传动作抽成存储接口源码默认把文件写在本地磁盘但生产环境经常要换成 OSS 或 S3。我一般会在上传函数里抽一层存储接口不直接调用file.saveclass LocalStorage: def save(self, file_storage, save_path): os.makedirs(os.path.dirname(save_path), exist_okTrue) file_storage.save(save_path) return f/uploads/{save_path} class OssStorage: def __init__(self, bucket): self.bucket bucket def save(self, file_storage, save_path): self.bucket.put_object(save_path, file_storage.stream) return fhttps://cdn.example.com/{save_path}然后在上传函数里把file.save(...)替换为storage.save(file, image/ date_dir / save_name)。改造之后前端拿到的 url 仍然是完整可访问地址UEditor 和后续逻辑都不用动。本地存储保留给开发环境对象存储给生产环境两者通过一个 environment 配置切换。本文还有配套的精品资源点击获取
返回列表