2023-10-08 16:22:20 随笔 随笔
集成的Markdown编辑器是基于GitHub上开源的一个15年就停止维护的Markdown产品。虽然时间久远,但是胜在功能还是比较齐全的,所以我们基于此编辑器实现在Flask博客项目中封装Markdown编辑器
下载地址:Editor.md
下载完成后,将压缩包解压至项目static
文件下,并重名名为editormd
(你也可以根据自己的喜好重命名,不影响,不过后续引用路径时需要注意)
因为只是一个示例,所以就没有添加其他的东西,在实际博客项目中,需要根据自己的需求进行相应的修改
我这里就只是在新建的Flask项目自带的app.py
文件里创建相关路由
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def hello_world(): # put application's code here
return 'hello_world'
@app.route('/test', methods=['GET', 'POST'])
def markdown_test():
mkd = '''
# header
## header2
[picture](http://www.example.com)
* 1
* 2
* 3
**bold**
'''
return render_template('test.html', mkd=mkd)
if __name__ == '__main__':
app.run()
其中markdown_test
路由就是我们测试用的,其中的mkd
可以不添加,只是为了模拟从后端传递数据给前端(毕竟后面肯定还要将存储在数据库中笔记取出来,并且在前端以Markdown形式解析)
在templates
目录下创建对应的test.html
文件,然后在里面实现基本的Markdown编辑器样式呈现
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8"/>
<title>Simple example</title>
<link rel="stylesheet" href="{{ url_for('static',filename='editormd/css/editormd.css') }}"/>
</head>
<body>
<h1>nihao</h1>
<div id="test-editormd">
</div>
<script src="{{ url_for('static',filename='editormd/examples/js/jquery.min.js') }}"></script>
<script src="{{ url_for('static',filename='editormd/editormd.min.js') }}"></script>
<script type="text/javascript">
var testEditor;
$(function () {
testEditor = editormd("test-editormd", {
width: "90%",
height: 640,
syncScrolling: "single",
path: "{{ url_for('static',filename='editormd/lib/') }}"
});
});
</script>
</body>
</html>
在这里,需要注意的有:
<script src="{{ url_for('static',filename='js/jquery.min.js') }}"></script>
找不到jquery.min.js
文件从到导致Markdown编辑器无法正常显示的问题。所以为了出现因为找不到路径产生问题,建议所有导入的文件均通过对文件点击鼠标右键,然后找到复制/引用路径,传递文件的根路径jquery.min.js
!!!此时,我们就可以通过访问:127.0.0.1:5000/test
,看到Markdown编辑器了
我们实现Markdown编辑器后,输入有了,但是输出呢?总不能还是以源文件显示吧
所以我们需要将之前的Markdown笔记通过Flask-Markdown
插件解析,从而使文章内容是以Markdown形式展示
安装:pip install flask-markdown
不过这里有件怪事,不知道是pycharm问题还是咋的,下载flask-markdown
后,会提示找不到这个包,让我再安装flask-markdown2
绑定app
from flask_markdown import Markdown
这里不详细展开,因为会需要一些额外的东西。并且不知道为什么我的项目里,jinja2无法通过Markdown转换器转义,即:{{xxx|safe|markdown}}
所以这里不详细展开,后面具体案例综合分析采用获取Editor.MD转义后的HTML文本内容,然后直接传给前端,引用Editor的js和css文件渲染为Markdown内容
实现思路:
- 我们通过Markdown语法新增/编辑文章内容时,找到Editor.MD里面将Markdown语法转换的HTML文章内容,然后截取前端这部分HTML内容内容传递至后端
- 将原本的文章内容转换为html文件内容,存在自定义的数据模型PostForm中content(管理文章内容)里。然后查看文章时,通过safe过滤器,将html内容渲染为我们需要的Markdown内容
首先,现在我们的文章有关html中实现Markdown编辑框呈现
打开article_form.html
,这个html文件是我博客中实现文章详细的界面,编辑和添加文章均是以此界面为基础
然后重构代码,引入Markdown编辑器的css样式,{% block extra_head_style %}{% endblock extra_head_style %}
这个块是我在最开始的base.html
预留的块,就是为了实现文本编辑器的拓展(富文本和Markdown等等)
<!-- 引入Markdown编辑器css -->
{% block extra_head_style %}
<link rel="stylesheet" href="{{ url_for('admin.static', filename='editormd/css/editormd.css') }}">
<script src="{{ url_for('admin.static',filename='editormd/examples/js/jquery.min.js') }}"></script> <!-- 引入 jQuery -->
<script src="{{ url_for('admin.static', filename='editormd/editormd.min.js') }}"></script>
{% endblock extra_head_style %}
然后在文章详细块{% block table_content %}{% endblock table_content %}
内封装一个新的块,用来展示Markdown编辑器
<!-- 文章详情字段 -->
<div class="field">
{{ form.content.label(class='label') }} <!-- 显示文章详情标签 -->
<div class="control">
{{ form.content(class='textarea', rows="10", placeholder='文章详情') }} <!-- 显示文章详情文本域 -->
</div>
</div>
<!-- Markdown编辑器实现 -->
<div class="field">
<div id="fancy-editormd" class="editormd">
{{ form.content(style="display:none;") }}
</div>
</div>
上面这段代码中,第一个文章详细字段是原来的文本框,第二个Markdown编辑器实现是我们需要的Markdown文本框。需要特别注意的是,id="fancy-editormd"
这个字段和后面Markdown实体化的id字段需要一致,否则无法正常显示Markdown编辑器!
class="editormd"
则没有过多要求
此时,无论是点击添加文章还是编辑文章,我们可以看见在原本文本框下面多了一个文本框
当然,此时我们还需要进一步完善Markdown编辑器的样式和功能
首先先在{% block extra_foot_script %}{% endblock extra_foot_script %}
引入js文件,这个块是我在最开始的base.html
预留的块,就是为了实现文本编辑器的拓展(富文本和Markdown等等)
<!-- 引入Markdown的js文件 -->
{% block extra_foot_script %}
<script src="{{ url_for('admin.static', filename='editormd/examples/js/jquery.min.js') }}"></script> <!-- 引入 jQuery -->
<script src="{{ url_for('admin.static',filename='editormd/editormd.min.js') }}"></script>
{% endblock extra_foot_script %}
然后再在{% block vue_script %}{% endblock vue_script %}
实现Markdown编辑器实体化
<!-- 实现Markdown编辑器js代码 -->
{% block vue_script %}
<script type="text/javascript">
// 创建Markdown编辑器实例
var testEditor;
$(function() {
testEditor = editormd("fancy-editormd", {
width: "100%",
height: 600,
syncScrolling: "single",
emoji: true,
path: "{{ url_for('admin.static',filename='editormd/lib/') }}", //依赖lib文件夹路径
taskList: true,
tocm: true,
imageUpload: true, //开启本地图片上传
imageFormats: ["jpg", "jpeg", "gif", "png"], //设置上传图片的格式
imageUploadURL: "admin/static/utils/update_filename", //上传图片请求路径
htmlDecode: "style,script,iframe", //可以过滤标签解码
tex: true, // 默认不解析
flowChart:true, // 默认不解析
sequenceDiagram:true, // 默认不解析
saveHTMLToTextarea : true // 开启自动转换HTM文件
});
});
</script>
{% endblock vue_script %}
此时,无论是添加文章还是编辑文章都能呈现出Markdown编辑器了,并且Markdown编辑器能读取到数据库中传过来的content文章内容,自动同步到Markdown文本框内
但是,此时我们会遇见几个问题:
在实现Markdown编辑器的js代码中,有一句:saveHTMLToTextarea : true // 开启自动转换HTM文件
这就是为解决上面几个问题所预留的一个解决方案
我们可以通过获取Editor.MD将Markdown语法转换为HTML内容的块的内容,并将HTML内容上传至数据库文章内容部分,在我们阅读文章时通过safe|过滤渲染,解决上面的问题
PS:如果想节省时间,建议先看问题4的解决,然后再看按顺序看其余问题的解决
首先找到我们项目里用于实现添加文章的视图函数,路径:App.admin.views.article_add
# 增加文章——article_add
@blue.route('/article/add', methods=['GET', 'POST'])
@login_required
def article_add():
# 创建一个名为 'form' 的表单对象,用于接收用户提交的文章信息,使用时需要先导入PostForm表单模型
form = PostForm()
# 为 'category_id' 和 'tags' 字段设置选项,通过查询数据库获取所有分类和标签的选项
form.category_id.choices = [(v.id, v.name) for v in Category.query.all()]
form.tags.choices = [(v.id, v.name) for v in Tag.query.all()]
# 如果表单验证通过,即用户提交了有效的数据
if form.validate_on_submit():
# 创建一个新的文章对象 'post',并将表单数据赋值给对应的属性
post = Post(
title=form.title.data, # 文章标题
desc=form.desc.data, # 文章描述
has_type=form.has_type.data, # 文章发布状态
category_id=int(form.category_id.data), # 文章分类,一对多保存
content=form.content.data, # 文章内容
)
# 通过标签的 ID 查询标签对象,并将标签对象添加到文章的 'tags' 属性中
post.tags = [Tag.query.get(tag_id) for tag_id in form.tags.data]
# 将文章对象 'post' 添加到数据库会话中,并提交更改
db.session.add(post)
db.session.commit()
# 使用 Flask 的 'flash' 函数将成功消息闪现给用户
flash(f'{form.title.data}文章添加成功')
# 重定向到文章管理页面
return redirect(url_for('admin.article'))
# 如果表单验证不通过或是首次访问该页面,则渲染文章添加页面
return render_template('admin/article_form.html', form=form)
我们需要重构一下部分的内容
首先我们先得找到Editor.MD
转换的HTML内容
由于我们我们先前已经开启了自动转换HTML功能,所以我们可以通过F12打开浏览器控制台,点击元素找到相应的转换div块
里面有个属性叫做name="fancy-editormd-html-code"
,我们需要在后端article_add
视图函数中做的就是截取这部分内容,存放在数据库中,查看文章时通过过滤器{{ post.content| safe }}
过滤渲染为我们想要的Markdown文本
此时开始重构代码:
# 增加文章——article_add
@blue.route('/article/add', methods=['GET', 'POST'])
@login_required
def article_add():
...
省略部分代码
...
# 如果表单验证通过,即用户提交了有效的数据
if form.validate_on_submit():
# 重构代码,通过截取前端Editor.MD转换的HTML内容,实现Markdown文本预览
content_html = request.form['fancy-editormd-html-code']
# 创建一个新的文章对象 'post',并将表单数据赋值给对应的属性
post = Post(
...
省略部分代码
...
# content=form.content.data, # 文章内容
content=content_html # 重构文章内容,以前是传递的表单中文章内容data数据,现在是接受的HTML数据
)
...
省略部分代码
...
# 如果表单验证不通过或是首次访问该页面,则渲染文章添加页面
return render_template('admin/article_form.html', form=form)
此时,我们就已经实现添加文章了,当然还是需要两个文本框都有内容才能保存文章内容
但是,点击保存后,我们发现原本的文本框添加的内容不会存放在数据库中,所以后续可以直接把原本的文本框隐藏了
同样在编辑文章的视图函数中也需要进行修改
重构前:
# 实现修改博客文章内容——article_edit
@blue.route('/article/edit/<int:post_id>', methods=['POST', "GET"])
@login_required
def article_edit(post_id):
# 通过文章的 ID 获取要编辑的文章对象
post = Post.query.get(post_id)
# 获取该文章的标签 ID 列表
tags = [tag.id for tag in post.tags]
# 创建一个表单对象 'form',并将要编辑的文章的信息填充到表单中
form = PostForm(
title=post.title, # 文章标题
desc=post.desc, # 文章描述
category_id=post.category_id, # 文章分类
has_type=post.has_type, # 文章发布状态
content=post.content, # 文章内容
tags=tags # 文章标签
)
# 为表单中的 'tags' 字段设置选项,通过查询数据库获取所有标签的选项
form.tags.choices = [(v.id, v.name) for v in Tag.query.all()]
# 为表单中的'category_id'字段设置选项,通过查询数据库获取所有的文章分类
form.category_id.choices = [(v.id, v.name) for v in Category.query.all()]
if form.validate_on_submit():
# 如果表单验证通过,将表单中的数据更新到文章对象 'post' 中
post.title = form.title.data
post.desc = form.desc.data
post.has_type = form.has_type.data
post.category_id = form.category_id.data ## 这是一处不同的标记
post.content = form.content.data
# 通过标签的 ID 查询标签对象,并将标签对象添加到文章的 'tags' 属性中
post.tags = [Tag.query.get(tag_id) for tag_id in form.tags.data]
# 将更新后的文章对象 'post' 添加到数据库会话中,并提交更改
db.session.add(post)
db.session.commit()
# 使用 Flask 的 'flash' 函数将成功消息闪现给用户
flash(f'{form.title.data}文章修改成功!')
# 重定向到文章管理页面
return redirect(url_for('admin.article'))
# 如果表单验证不通过或是首次访问该页面,则渲染文章编辑页面,显示编辑表单
return render_template('admin/article_form.html', form=form)
重构后:
# 实现修改博客文章内容——article_edit
@blue.route('/article/edit/<int:post_id>', methods=['POST', "GET"])
@login_required
def article_edit(post_id):
...
省略部分代码
...
if form.validate_on_submit():
# 重构代码,通过截取前端Editor.MD转换的HTML内容,实现Markdown文本预览
content_html = request.form['fancy-editormd-html-code']
...
省略部分代码
...
# post.content = form.content.data # 重构前获取更改表单内容
post.content = content_html # 重构后获取Editor.MD转换后的HTML文件
...
省略部分代码
...
# 如果表单验证不通过或是首次访问该页面,则渲染文章编辑页面,显示编辑表单
return render_template('admin/article_form.html', form=form)
点击编辑文章,然后我们试着看在Markdown文本框里能否增加新内容
点击保存后,在编辑此文章,发现完全可以
现在我们已经解决了问题1、2
了,但是打开添加文章或编辑文章的界面,我们还是会出现两个文本框,我们肯定只希望出现Markdown编辑器的文本框,所以我们需要打开App/admin/templates/admin/article_form.html
,找到原文本框块,然后注释/删去即可
<!-- 文章详情字段 -->
<div class="field">
{{ form.content.label(class='label') }} <!-- 显示文章详情标签 -->
<div class="control">
{{ form.content(class='textarea', rows="10", placeholder='文章详情') }} <!-- 显示文章详情文本域 -->
</div>
</div>
此时无论是新增文章还是编辑文章均能实现只显示Markdown文本框了
新增文章
编辑文章
回到我们文章浏览界面,我们不难发现这样式是不是有点怪…
和其他的Markdown编辑器相比,为什么没有代码高亮、颜色区分等功能呢?
那是因为我们在显示详细文章的html文件中并没有添加Editor.MD的css和js文件
,所以就无法正常渲染Editor.MD
的语法
此时,我们找到有关博客文章显示的文件:App/blog/templates/detail.html
<!-- 引入Editor.MD的css文件 -->
{% block extra_head_style %}
<link href="{{ url_for('admin.static', filename='editormd/css/editormd.preview.min.css') }}" rel="stylesheet" />
<link href="{{ url_for('admin.static', filename='editormd/css/editormd.css') }}" rel="stylesheet" />
{% endblock extra_head_style %}
<!--以下是使文章正常显示Editor.MD样式的js文件部分引用 -->
{% block extra_foot_script %}
<script src="{{ url_for('admin.static', filename='editormd/examples/js/jquery.min.js') }}"></script> <!-- 引入jQuery,必须要引入!!!否则会出现某些变量undefined! -->
<script src="{{ url_for('admin.static', filename='editormd/lib/marked.min.js') }}"></script>
<script src="{{ url_for('admin.static', filename='editormd/lib/prettify.min.js') }}"></script>
<script src="{{ url_for('admin.static', filename='editormd/editormd.min.js') }}"></script>
<script type="text/javascript">
editormd.markdownToHTML("fancy-content");
</script>
{% endblock extra_foot_script %}
需要注意的是,一定,千万要在引用Editor.MD的js文件先引用jquery的js文件!
否则就会出现无法正常加载的情况,如下面两个图所示:
这是注释掉jquery.min.js
文件前:
这是注释掉jquery.min.js
文件后:
其实这个问题很好解决,我们只需要在数据库模型Post
模型里新增一个字段content_html
就行
首先更新一下Post
模型,保证编辑文章内容时能正常回显原文本内容,更新完后记得同步数据库模型
路径:App.blog.models.Post
# 创建文章主题模型
class Post(BaseModel):
...
省略部分内容
...
# LONGTEXT需要引入MySQL,长文本
content = db.Column(LONGTEXT, nullable=False)
# 新增Markdown转义HTML文本
content_html = db.Column(LONGTEXT, nullable=False)
...
省略部分内容
...
然后重构一下添加文章视图函数:App.admin.views.article_add
# 增加文章——article_add
@blue.route('/article/add', methods=['GET', 'POST'])
@login_required
def article_add():
...
省略部分代码
...
# 如果表单验证通过,即用户提交了有效的数据
if form.validate_on_submit():
# 重构代码,通过截取前端Editor.MD转换的HTML内容,实现Markdown文本预览
content_html = request.form['fancy-editormd-html-code']
# 创建一个新的文章对象 'post',并将表单数据赋值给对应的属性
post = Post(
...
省略部分代码
...
content=form.content.data, # 文章内容
content=content_html # 重构文章内容,以前是传递的表单中文章内容data数据,现在是接受的HTML数据
)
...
省略部分代码
...
# 如果表单验证不通过或是首次访问该页面,则渲染文章添加页面
return render_template('admin/article_form.html', form=form)
同步数据库模型:
flask db migrate
flask db upgrade
然后就是重构article_edit
,做到回显原文本内容,并且在浏览文章界面时传递HTML文本内容给detail.html
模板渲染
路径:App.admin.views.article_edit
# 实现修改博客文章内容——article_edit
@blue.route('/article/edit/<int:post_id>', methods=['POST', "GET"])
@login_required
def article_edit(post_id):
...
省略部分代码
...
if form.validate_on_submit():
# 重构代码,通过截取前端Editor.MD转换的HTML内容,实现Markdown文本预览
content_html = request.form['fancy-editormd-html-code']
...
省略部分代码
...
post.content = form.content.data # 重构前获取更改的原表单内容
post.content = content_html # 重构后获取Editor.MD转换后的HTML文件内容
...
省略部分代码
...
# 如果表单验证不通过或是首次访问该页面,则渲染文章编辑页面,显示编辑表单
return render_template('admin/article_form.html', form=form)
其实我们能发现,问题四的解决其实就是将问题1、2导致的遗留问题进行处理
为什么我们不更新PostForm表单模型呢?
让我们理一理:
post
数据,然后回显数据至网页,回显的过程不需要用到HTML
内容content_html = request.form['fancy-editormd-html-code']
直接存放在content_html
变量中的,那我们提交表单数据上传至数据库时,自然就不需要PostForm表单里再创建一个content_html字段了PostForm
只是表单模型,也就充当一个临时中转站的角色,又不需要将内容存放在数据库,自然就更没必要更新新增content_html
字段了最后在detail.html
模板渲染content_html
数据即可
路径:App/blog/templates/detail.html
重构前:<div class="content has-text-grey mt-1" id="fancy-content">{{ post.content|safe }}</div>
重构后:<div class="content has-text-grey mt-1" id="fancy-content">{{ post.content_html|safe }}</div>
添加文章界面:
编辑文章界面:
文章详细界面:
在我们实现了文件上传功能后,我们自然要开始考虑实现上传图片的功能了
在Editor.MD
中,实现图片上传有两种方法
本文章主要实现本地上传图片文件,跨域上传可以看看别的博主的相关文章,这里贴几篇吧,也方便自己以后看
首先就是先提一下我之前构建的处理图片的方法,这些是前面用来处理头像等图片的方法
# _file_path函数的作用是判断指定路径是否存在,不存在则创建该路径,传进来的参数为文件夹的名称
def _file_path(directory_name):
# 构建文件路径,BASE_DIR获取项目根目录的路径并存在变量file_path中
file_path = BASE_DIR / f'App/admin/static/{directory_name}'
# 如果文件路径不存在,则创建该路径
if not os.path.exists(file_path):
os.makedirs(file_path)
# 最后返回该文件路径
return file_path
# update_filename函数的作用是修改文件名(不包含中文)
def update_filename(f):
# secure_filename函数用于处理文件中的中文名,如:my头像123.jpg就会被修改为my123.jpg
# 使用secure_filename前需要先引入该模块
names = list(os.path.splitext((secure_filename(f.filename))))
# 生成随机UUID,并将其插入到文件名中
names[0] = ''.join(str(uuid.uuid4()).split('-'))
# 将列表names中的所有元素连接成一个字符串,并返回这个连接后的字符串
return ''.join(names)
# 调用_file_path和update_filename,返回返回文件的完整路径和修改后的文件名
def upload_file_path(directory_name, f):
# 获取文件存储路径,如:D:\Python\flask_blog\App\admin\static\avatar
file_path = _file_path(directory_name)
# 修改文件名为UUID,如:b4018ef85e324c93a2316b50737319b0
filename = update_filename(f)
# 返回文件的完整路径和修改后的文件名
return file_path / filename, filename
回到我们的:App/admin/templates/admin/article_form.html
找到我们实现markdown编辑器的代码块
<!-- 实现Markdown编辑器js代码 -->
{% block vue_script %}
<script type="text/javascript">
// 创建Markdown编辑器实例
var testEditor;
$(function() {
testEditor = editormd("fancy-editormd", {
...
省略部分代码
...
imageUpload: true, //开启本地图片上传
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp" ], //设置上传图片的格式
imageUploadURL: "{{url_for('admin.markdown_upload')}}", //必须设置上传图片请求路径(也就是处理图片上传的视图函数)
...
省略部分代码
...
});
});
</script>
{% endblock vue_script %}
缺一个都不行!!!
首先,我们要上传图片,那么就得先有储存图片的地方,所以我们现在对于的static
目录下创建一个markdown_picture
路径:App/admin/static/markdown_picture
然后我们就要创建后端接口,接受前端传过来的图片数据。
创建一个新的路由函数,路径:App.admin.views.markdown_upload
# 实现上传图片至Editor.MD封装的Markdown编辑器
@blue.route('/markdown_upload', methods=["POST", "GET"])
@login_required
def markdown_upload():
# 获取上传的文件,名字为 'editormd-image-file'
f = request.files.get('editormd-image-file')
# 如果没有文件,返回上传失败的响应
if not f:
res = {
'success': 0,
'message': '上传失败'
}
else:
# 引入上传图片文件的方法
from App.admin.static.utils import upload_file_path
# 传入上传的图片数据文件,并指定上传markdown_picture,没有就由upload_file_path里的_file_path创建文件夹
markdown_picture_path, filename = upload_file_path('markdown_picture', f)
# 保存上传的文件到指定的路径,这里保存的是上传图片的完整路径(包括了图片文件后缀)
f.save(markdown_picture_path)
res = {
'success': 1,
'message': '上传成功',
'url': url_for('admin.markdown_picture', name=filename)
}
# 引入jsonify,返回json信息
from flask import jsonify
return jsonify(res)
这个函数用于获取前端editormd-image-file
里保存的图片数据,并将图片数据通过upload_file_path
进行切片,切片为图片文件完整路径:markdown_picture_path
和图片名称:filename
这样我们就可以将每一个图生成唯一的uuid
存放在markdown_picture
文件里了
但是,我们通过Markdown编辑器自带的图片上传功能上传图片,会发现文本框是没有图片链接插入的
原因是因为我们没有从本地读取图片文件数据,并反馈给前端进行渲染实现实时渲染反馈
这个路由函数的作用就是处理markdown_upload
接受并保存到本地的图片数据,通过markdown_upload
里的name=namefile
字段获取文件名,然后从而获取完整的回显图片文件路径,最后将图片数据传递给前端进行渲染,从而实现回显
代码实现:
@blue.route('/markdown_picture/<name>')
def markdown_picture(name): # 这里传入的参数name就是获取markdown_upload里文件名
# 重新构建完整的图片回显路径
img = f'App/admin/static/markdown_picture/{name}'
# 数'rb'表示以二进制串口模式打开文件,这是因为图片文件通常是二进制文件
with open(img, 'rb') as f:
# 创建了一个名为resp的响应对象,响应的内容将是图片文件的二进制数据,响应需要引入Response
from flask import Response
resp = Response.(f.read())
# 将响应对象resp返回给客户端,从而将图片文件作为响应发送给浏览器
return resp
这段代码的作用是读取指定名称的图片文件,并将其作为响应返回给客户端。以下是对每行代码的详细解释:
img = f'App/admin/static/markdown_picture/{name}'
: 这一行构建了图片文件的完整路径。它使用了 f 字符串格式,其中 {name}
是一个占位符,用于插入图片文件的名称,即传递给函数的 name
参数。这行代码将路径存储在变量 img
中,如 “markdown_picture/filename.jpg”。
with open(img, 'rb') as f:
: 这一行使用 Python 的 open
函数打开了指定路径的图片文件。参数 'rb'
表示以二进制只读模式打开文件,这是因为图片文件通常是二进制文件。
resp = Response(f.read())
: 这一行创建了一个名为 resp
的响应对象。它通过 f.read()
从打开的文件对象 f
中读取文件内容,并将其设置为响应的主体内容。这意味着响应的内容将是图片文件的二进制数据。
return resp
: 最后一行将响应对象 resp
返回给客户端,从而将图片文件作为响应发送给浏览器。浏览器可以解析该响应并显示图片。
现在我们就可以通过markdown编辑器的图片上传功能上传本地图片了,并且会自动回显保存路径
回到阅读文章的界面,可以看见图片正常回显,赞!
细心的朋友可能在前面就注意到了,我一直强调的是利用Editor.MD
自带的图片上传功能上传图片
但是在日常写文章的过程中,我们更多需要的是随时截图,然后ctrl+v
上传图片。
很可惜的是Editor.MD
并没有实现这一功能,所以这就需要我们进行拓展了
首先就是创建一个叫uploadImg.js
的js插件,用来实现读取粘贴内容,并识别出图片的格式
路径:App/admin/static/uploadImg.js
代码:
function initPasteDragImg(Editor){
var doc = document.getElementById(Editor.id)
doc.addEventListener('paste', function (event) {
var items = (event.clipboardData || window.clipboardData).items;
var file = null;
console.log(items)
console.log(items.length)
if (items && items.length) {
// 搜索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
}
else {
console.log("当前浏览器不支持");
return;
}
if (!file) {
console.log("粘贴内容非图片");
return;
}
uploadImg(file,Editor);
});
var dashboard = document.getElementById(Editor.id)
dashboard.addEventListener("dragover", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("dragenter", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("drop", function (e) {
e.preventDefault()
e.stopPropagation()
var files = this.files || e.dataTransfer.files;
uploadImg(files[0],Editor);
})
}
function uploadImg(file,Editor){
var formData = new FormData();
var fileName=new Date().getTime()+"."+file.name.split(".").pop();
formData.append('editormd-image-file', file, fileName);
$.ajax({
url: Editor.settings.imageUploadURL,
type: 'post',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
success: function (msg) {
var success=msg['success'];
if(success==1){
var url=msg["url"];
if(/\.(png|jpg|jpeg|gif|bmp|ico)$/.test(url)){
Editor.insertValue("![]("+msg["url"]+")");
}else{
Editor.insertValue("[下载附件]("+msg["url"]+")");
}
}else{
console.log(msg);
alert("上传失败");
}
}
});
}
然后回到article_form.html
,在引入markdown的js文件块里新增uploadImg.js
的引入
<!-- 引入Markdown的js文件 -->
{% block extra_foot_script %}
...
省略部分代码
...
<script src="{{ url_for('admin.static', filename='uploadImg.js') }}"></script>
{% endblock extra_foot_script %}
最后,在我们的实体化markdown对象处再引用这个图片处理方法即可
<!-- 实现Markdown编辑器js代码 -->
{% block vue_script %}
<script type="text/javascript">
// 创建Markdown编辑器实例
var testEditor;
$(function() {
testEditor = editormd("fancy-editormd", {
...
省略部分代码
...
onload: function () {
initPasteDragImg(this); //必须引入,否则无法通过复制粘贴插入图片
}
});
});
</script>
{% endblock vue_script %}
让我们来看一看效果吧
编写文章的界面
阅读文章界面
至此,我们markdown编辑器基本已经集成完毕了,剩下的就是些许微调,后面会专门开一个专栏说部分微调内容
在我们实际过程中,我们更希望将文章的图片分文章保存,比如我创建了A文章,那么就希望生成一个专门用于存放A文章的图片目录
实现逻辑:通过某些方法,获取到当前的文章名称,然后再在
markdown_picture
目录里,生成对应文章名称的文件夹,最后我们上传图片时就会将图片上传至markdown_picture/文章名
下,实现归类保存了
通过按F12阅读网页源码,我们要想办法获取下面红框里的标题内容
那么该在那个视图函数里实现获取文章标题,然后创建文件夹呢?
让我们再捋一下思路吧,我们最开始文章是没有图的,当我们上传第一张图到后端并保存前,我们就要创建一个文件夹来保存
我们实现上传图片的路由函数是markdown_upload
,获取图片文件是f = request.files.get('editormd-image-file')
,上传并保存图片是:markdown_picture_path, filename = upload_file_path('markdown_picture', f)
那么显而易见,我们只需要在上传并保存图片前,在markdown_upload
视图函数实现创建归类文件夹就行
逻辑解决了,如何通过代码实现呢?
首先是获取网页里标题的内容,在我们调用markdown_upload
路由函数前,我们是通过App/admin/templates/admin/article_form.html
里的实现markdown的js代码部分从前端传递图片信息给markdown_upload
视图函数的。所以我们找到这部分,然后再将标题传给markdown_upload
就可以了
找到:imageUploadURL
从前端多传入一个参数给后端视图函数即可,重构代码如下:
//必须设置上传图片请求路径(也就是处理图片上传的视图函数),并再返回文章标题用以创建对应的图片保存点
imageUploadURL: "{{url_for('admin.markdown_upload', article_title=form.title)}}",
在我们的markdown_upload
里,做以下测试
# 实现上传图片至Editor.MD封装的Markdown编辑器
@blue.route('/markdown_upload', methods=["POST", "GET"])
@login_required
def markdown_upload():
...
省略部分代码
...
# 获取文章标题参数,并将其分割获取我们需要的标题内容
article_title = request.args.get('article_title')
print(article_title, type(article_title))
print(article_title.split())
print(article_title.split()[6])
print(article_title.split()[6].lstrip('value="').rstrip('>').rstrip('"'))
...
省略部分代码
...
通过这部分测试代码,当我们上传图片时,测试出的结果如下:
<input id="title" maxlength="128" name="title" required type="text" value="这是Markdown编辑器demo(测试title抓取)"> <class 'str'>
['<input', 'id="title"', 'maxlength="128"', 'name="title"', 'required', 'type="text"', 'value="这是Markdown编辑器demo(测试title抓取)">']
value="这是Markdown编辑器demo(测试title抓取)">
这是Markdown编辑器demo(测试title抓取)
可以看见,当我们通过一定的过滤规则后,就能获取文章的标题内容了
如何拼接出路径呢?
用一个变量接收标题,然后再指定拼接即可实现,代码案例如下:
title = article_title.split()[6].lstrip('value="').rstrip('>').rstrip('"')
print(title)
path = f"markdown_picture/{new_path}"
print(path)
"""
运行结果:
这是Markdown编辑器demo(测试title抓取)
markdown_picture/这是Markdown编辑器demo(测试title抓取)
"""
那么接下来就是组合技了
在markdown_upload
函数中:
# 获取文章标题参数,并将其分割获取我们需要的标题内容
article_title = request.args.get('article_html')
# 处理返回来的标签信息,得到标题纯文本
title = article_title.split()[6].lstrip('value="').rstrip('>').rstrip('"')
# 拼接出存放文件新路径
new_path = f'markdown_picture/{title}'
# 如果没有文件,返回上传失败的响应
if not f:
......
else:
......
markdown_picture_path, filename = upload_file_path(new_path, f)
# 保存上传的文件到指定的路径,这里保存的是上传图片的完整路径(包括了图片文件后缀)
f.save(markdown_picture_path)
res = {
'success': 1,
'message': '上传成功',
'url': url_for('admin.markdown_picture', title=title, name=filename)
}
......
这样我们就实现了从前端获取标题html内容,然后再加工一下获得文章标题,然后通过拼接获取文章存放路径new_path
,通过upload_file_path
方法加载出对应的文件夹,并将上传的图片存放在文件夹内。再在上传成功时,向markdown_picture
路由传递一个新的文章标题title
参数,这个参数很重要,下面markdown_picture
视图函数需要用上!
回到markdown_picture
视图函数:
# 实现图像反馈至markdown编辑器里
# 文件上传时会返回url,顺序为/admin/markdown_picture/path的内容/name的内容
# 这里path、name的内容取决于markdown_upload传过来的参数
@blue.route('/markdown_picture/<title>/<name>')
def markdown_picture(name, title): # 这里传入的参数name就是获取markdown_upload里文件名
# 重新构成完整的图片上传至前端
img = f'App/admin/static/markdown_picture/{title}/{name}'
# 参数'rb'表示以二进制串口模式打开文件,这是因为图片文件通常是二进制文件
with open(img, 'rb') as f:
# 创建了一个名为resp的响应对象,响应的内容将是图片文件的二进制数据,响应需要引入Response
from flask import Response
resp = Response(f.read())
# 将响应对象resp返回给客户端,从而将图片文件作为响应发送给浏览器
return resp
这里的坑很多很多,首先是如果没有在路由哪里接收前面传过来的title
参数,那么你就无法准确加载存放的图片文件
此外,如果路由顺序不对,那么最后图片加载路径也会错误,加载路径错误,就会导致图片无法回显!
比如下面这个错误提示就是因为没有正确加载<title>
标签导致
127.0.0.1 - - [07/Oct/2023 14:36:42] "GET /admin/markdown_picture//b59394a0f06641bbb26037f04b32c927.png HTTP/1.1" 404 -
如果一切正确,那么就能从正确的路径中准确找到图片回显
本章节bug没有完全解决,而是成立一个中转站,每天文章的第一个图会被存放在
temp_pic
文件夹里
这里在测试上传新文章时,我们遇见了一个bug,如下图所示:
这里新文章由于没有没有提交表单,所以标题就没有更新,为默认空标题,让我们通过测试代码看看是不是这样
<input id="title" maxlength="128" name="title" required type="text" value=""> <class 'str'>
['<input', 'id="title"', 'maxlength="128"', 'name="title"', 'required', 'type="text"', 'value="">']
value="">
很显然,得到的是个空内容,所以这里需要进行一定修改,通过路径也可以看出,跳过了根据文章标题创建文章图片存放目录的过程
解决思路: 如果标题为空,也就是新建文章,那么就创建一个template_pic用来临时存放图片,当提交文章后,在标题验证哪里进行判断。如果存在template_pic目录,就复制删除,然后新建文章标题目录,将图片粘贴进去
回到markdown_upload
视图函数,新增一个判断语句
# 实现上传图片至Editor.MD封装的Markdown编辑器
@blue.route('/markdown_upload', methods=["POST", "GET"])
@login_required
def markdown_upload():
......
# 对于新文章,不存在标题时就将标题命名为temp_pic,最后在提交表单时将临时目录重命名
if not title:
title = 'temp_pic'
# 拼接出存放文件新路径
path = f"markdown_picture/{title}"
......
通过if语句就可以实现添加新文章时创建一个临时图片保存目录了,并且可以正常加载图片回显至页面
找到article_add
视图函数,在里面提交表单数据里新增以下代码
这里出了点问题,不知道为什么一直找不到文件夹,所以也无法利用os.rename重置文件夹名字
这里是一些测试语句:
# 提交表单后,获取title,创建图片保存目录
import shutil
print(f"os.path.abspath('temp_pic'):{os.path.abspath('temp_pic')}")
print(os.getcwd())
from RealProject import BASE_DIR
old_path = BASE_DIR / f'App/admin/static/markdown_picture/temp_pict'
new_name = BASE_DIR / f'App/admin/static/markdown_picture/{post.title}'
# old_path = 'D:\\Python\\flask_blog\\App\\admin\\static\\markdown_picture\\temp_pict'
# new_name = 'D:\\Python\\flask_blog\\App\\admin\\static\\markdown_picture\\测试temp_pic是否更新为post.title'
print(old_path)
print(new_name)
if os.path.isdir(old_path):
print(f"temp_pic找到了!{os.path.isdir(old_path)}")
else:
print(f"temp_pic没找到!{os.path.isdir(os.path.abspath('markdown_picture'))}, {os.path.abspath('markdown_picture')}")
print(f"temp_pic没找到!{os.path.isdir(os.path.abspath('static'))}, {os.path.abspath('static')}")
print(f"temp_pic没找到!{os.path.isdir(os.path.abspath('admin'))}, {os.path.abspath('admin')}")
print(f"temp_pic没找到!{os.path.isdir(os.path.abspath(old_path))}, {os.path.abspath(old_path)}")
# 获取根路径
if os.path.exists(os.path.abspath('temp_pic')):
print("判断成功,根路径存在")
try:
os.rename(old_path, new_name)
print(f'文件夹名称已从 "temp_pic" 修改为 "{post.title}"')
except OSError as e:
print(f'无法修改文件夹名称: {e}')
else:
print("判断不通过,未识别出根路径语句")
在我们使用markdown编辑器撰写文章时,编辑器会有个默认文本内容
现在我们要删除它,通过F12在浏览器中查看内容发现,有两个部分是关于这个文本的,删去第一个无影响,删去第二个就会让原来的文本框消失
所以我们开始在项目中用关键词Enjoy Markdown! coding now...
定位
通过逐一排查,最后找到是在:editormd.js
里删除该语段后浏览器默认语段消失