博客更新日志

2023-10-17 12:25:10 博客开发

开篇简介

本篇文章主要是对于我利用flask框架开发的个人博客的一些后续优化实现,对于此前博客遗留问题进行解决,并增加实现一些自己的idea

当然,里面有些精彩的内容我也会单独做一个专题文章,比如集成Markdown编辑器、实现flash消息提示时显示不同的样式等等

总之,后续额外注意点也会在本章节更新

下面是更新情况

  1. 版本2.0:取消使用富文本编辑器,并实现封装markdown编辑器——

重构博客图片文件上传函数

在最开始的图片处理函数中,如下所示:

# _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

通过增加一些测试片段,如:

# 在update_filename函数中
print(f.filename)           # 打印文件名

extension = os.path.splitext(f.filename)            # 获取拓展名
print()                     # 打印拓展名

print(names, name[0], name[1])              # 打印通过secure_filename处理后的文件名及后缀名


# 在upload_file_path函数中
print(file_path / filename, filename)       # 打印处理好后的图片文件完整路径和文件名

测试结果:

头像.png
.png
['f74785b3f1044cfe8603158fd7de8769', ''] f74785b3f1044cfe8603158fd7de8769 
D:\Python\flask_blog\App\admin\static\markdown_picture\f74785b3f1044cfe8603158fd7de8769 f74785b3f1044cfe8603158fd7de8769

阅读后可以知道,在最开始为了加密,是没有保存图片后缀的,后面想了想,还是保存的好,不然有时候图片操作也挺麻烦的

所以我们只需要稍加改变,取消secure_filename的过滤就好了

重构updata_filename函数中的:

names = list(os.path.splitext((secure_filename(f.filename))))

重构后代码为:

names = list(os.path.splitext(f.filename))

其余代码保持不变,这里贴出来看看:

# _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就包括了2部分,names[0]:文件名,name[1]:文件后缀
    names = list(os.path.splitext(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

然后再执行同样的测试,发现输出为:

靓仔照片.jpg
.jpg
['613511ae052942258dcb8b5528a84c50', '.jpg'] 613511ae052942258dcb8b5528a84c50 .jpg
D:\Python\flask_blog\App\admin\static\markdown_picture\613511ae052942258dcb8b5528a84c50.jpg 613511ae052942258dcb8b5528a84c50.jpg .jpg

不放心?查看保存图片的文件夹,发现所有的图片文件都是有后缀的,完美!

1696578527878

博客更新日志/Flask实现Markdown在线编辑与解析

思路

  • 前端加载Markdown编辑器,数据通过POST传数据到后端并存入数据库
  • 后端取出数据,在模板中将Markdown解析为HTML再输出给用户

在线Markdown编辑器Editor

集成的Markdown编辑器是基于GitHub上开源的一个15年就停止维护的Markdown产品。虽然时间久远,但是胜在功能还是比较齐全的,所以我们基于此编辑器实现在Flask博客项目中封装Markdown编辑器

下载地址:Editor.md

下载完成后,将压缩包解压至项目static文件下,并重名名为editormd(你也可以根据自己的喜好重命名,不影响,不过后续引用路径时需要注意)

基于个人博客项目的正式实现

实现思路:

  • 我们通过Markdown语法新增/编辑文章内容时,找到Editor.MD里面将Markdown语法转换的HTML文章内容,然后截取前端这部分HTML内容内容传递至后端
  • 将原本的文章内容转换为html文件内容,存在自定义的数据模型PostForm中content(管理文章内容)里。然后查看文章时,通过safe过滤器,将html内容渲染为我们需要的Markdown内容

文本内容的实现

css样式和div块的实现

首先,现在我们的文章有关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"则没有过多要求

此时,无论是点击添加文章还是编辑文章,我们可以看见在原本文本框下面多了一个文本框

1696421284884

当然,此时我们还需要进一步完善Markdown编辑器的样式和功能

Markdown编辑器js实体化

首先先在{% 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,
                /admin/static/markdown_pictureUpload: true,    //开启本地图片上传
                /admin/static/markdown_pictureFormats: ["jpg", "jpeg", "gif", "png"],    //设置上传图片的格式
                /admin/static/markdown_pictureUploadURL: "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文本框内

1696422037688

问题解决

但是,此时我们会遇见几个问题:

  1. 添加文章时,未隐藏原来的文本框,且必须要原来的文本编辑器和Markdown文本编辑器都有内容才能保存文章
  2. 编辑文章时,未隐藏原来的文本框,且只有修改原文本框才能修改文章内容,在Markdown文本框内修改,不会影响原内容
  3. 阅读文章时,文本内容并没有以Markdown语法显示
  4. 编辑或添加文章时,回显内容是html,而不是最开始我们写文章的格式

在实现Markdown编辑器的js代码中,有一句:saveHTMLToTextarea : true // 开启自动转换HTM文件

这就是为解决上面几个问题所预留的一个解决方案

我们可以通过获取Editor.MD将Markdown语法转换为HTML内容的块的内容,并将HTML内容上传至数据库文章内容部分,在我们阅读文章时通过safe|过滤渲染,解决上面的问题

PS:如果想节省时间,建议先看问题4的解决,然后再看按顺序看其余问题的解决

问题1:添加文章article_add的重构

首先找到我们项目里用于实现添加文章的视图函数,路径: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块

1696425015766

里面有个属性叫做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)

此时,我们就已经实现添加文章了,当然还是需要两个文本框都有内容才能保存文章内容

1696427045652

但是,点击保存后,我们发现原本的文本框添加的内容不会存放在数据库中,所以后续可以直接把原本的文本框隐藏了

同样在编辑文章的视图函数中也需要进行修改

问题2:修改文章内容article_edit重构

重构前:

# 实现修改博客文章内容——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文本框里能否增加新内容

1696427384814

点击保存后,在编辑此文章,发现完全可以

1696427438396

现在我们已经解决了问题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文本框了

新增文章

1696427621382

编辑文章

1696427629122

问题3:没有以Markdown语法显示

回到我们文章浏览界面,我们不难发现这样式是不是有点怪…

1696427903913

和其他的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文件前:

1696437628836

这是注释掉jquery.min.js文件后:

1696437478694

问题4:解决编辑文章时原文章格式问题

其实这个问题很好解决,我们只需要在数据库模型Post模型里新增一个字段content_html就行

Post和article_add的重构

首先更新一下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的重构

然后就是重构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内容
  • 当我们在网页上通过Markdown编辑器修改原文章内容时,产生的新数据可以分为两类,一类是原来的文章数据,还有一类是新多出来的HTML数据,但是获取的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>

问题解决后的最终效果预览

添加文章界面:

1696440872558

编辑文章界面:

1696440960361

文章详细界面:

1696440980029

实现利用Editor.MD进行本地图片上传

在我们实现了文件上传功能后,我们自然要开始考虑实现上传图片的功能了

Editor.MD中,实现图片上传有两种方法

  • 同域上传:将上传保存图片至本地目录
  • 跨域上传:将图片保存在某些服务器里

本文章主要实现本地上传图片文件,跨域上传可以看看别的博主的相关文章,这里贴几篇吧,也方便自己以后看

基于Editor.MD的Flask图片实现

Flask集成Markdown文本编辑器

实现上传图片前的须知内容

上传图片的处理方法

首先就是先提一下我之前构建的处理图片的方法,这些是前面用来处理头像等图片的方法

# _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
前端网页markdown的js代码必须新增内容

回到我们的: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", {
                ...
                省略部分代码
                ...

                /admin/static/markdown_pictureUpload: true,    //开启本地图片上传
                /admin/static/markdown_pictureFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp" ],    //设置上传图片的格式
                /admin/static/markdown_pictureUploadURL: "{{url_for('admin.markdown_upload')}}", //必须设置上传图片请求路径(也就是处理图片上传的视图函数)

                ...
                省略部分代码
                ...
            });
        });

</script>
{% endblock vue_script %}

缺一个都不行!!!

后端实现

markdown_upload上传图片函数实现

首先,我们要上传图片,那么就得先有储存图片的地方,所以我们现在对于的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-/admin/static/markdown_picture-file'
    f = request.files.get('editormd-/admin/static/markdown_picture-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-/admin/static/markdown_picture-file里保存的图片数据,并将图片数据通过upload_file_path进行切片,切片为图片文件完整路径:markdown_picture_path和图片名称:filename

这样我们就可以将每一个图生成唯一的uuid存放在markdown_picture文件里了

但是,我们通过Markdown编辑器自带的图片上传功能上传图片,会发现文本框是没有图片链接插入的

原因是因为我们没有从本地读取图片文件数据,并反馈给前端进行渲染实现实时渲染反馈

1696521760882

markdown_picture图片处理

这个路由函数的作用就是处理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

这段代码的作用是读取指定名称的图片文件,并将其作为响应返回给客户端。以下是对每行代码的详细解释:

  1. img = f'App/admin/static/markdown_picture/{name}': 这一行构建了图片文件的完整路径。它使用了 f 字符串格式,其中 {name} 是一个占位符,用于插入图片文件的名称,即传递给函数的 name 参数。这行代码将路径存储在变量 img 中,如 “markdown_picture/filename.jpg”。

  2. with open(img, 'rb') as f:: 这一行使用 Python 的 open 函数打开了指定路径的图片文件。参数 'rb' 表示以二进制只读模式打开文件,这是因为图片文件通常是二进制文件。

  3. resp = Response(f.read()): 这一行创建了一个名为 resp 的响应对象。它通过 f.read() 从打开的文件对象 f 中读取文件内容,并将其设置为响应的主体内容。这意味着响应的内容将是图片文件的二进制数据。

  4. return resp: 最后一行将响应对象 resp 返回给客户端,从而将图片文件作为响应发送给浏览器。浏览器可以解析该响应并显示图片。

现在我们就可以通过markdown编辑器的图片上传功能上传本地图片了,并且会自动回显保存路径

1696580898393

1696580931539

回到阅读文章的界面,可以看见图片正常回显,赞!

1696581011443

拓展:实现复制粘贴上传图片

细心的朋友可能在前面就注意到了,我一直强调的是利用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('/admin/static/markdown_picture') !== -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-/admin/static/markdown_picture-file', file, fileName);
    $.ajax({
        url: Editor.settings./admin/static/markdown_pictureUploadURL,
        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 %}

让我们来看一看效果吧

编写文章的界面

1696601231480

阅读文章界面

1696601368954

至此,我们markdown编辑器基本已经集成完毕了,剩下的就是些许微调,后面会专门开一个专栏说部分微调内容

实现上传图片时自动生成目录

文章已有标题时

在我们实际过程中,我们更希望将文章的图片分文章保存,比如我创建了A文章,那么就希望生成一个专门用于存放A文章的图片目录

实现逻辑:通过某些方法,获取到当前的文章名称,然后再在markdown_picture目录里,生成对应文章名称的文件夹,最后我们上传图片时就会将图片上传至markdown_picture/文章名下,实现归类保存了

通过按F12阅读网页源码,我们要想办法获取下面红框里的标题内容

1696611783537

那么该在那个视图函数里实现获取文章标题,然后创建文件夹呢?

让我们再捋一下思路吧,我们最开始文章是没有图的,当我们上传第一张图到后端并保存前,我们就要创建一个文件夹来保存

我们实现上传图片的路由函数是markdown_upload,获取图片文件是f = request.files.get('editormd-/admin/static/markdown_picture-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就可以了

找到:/admin/static/markdown_pictureUploadURL

从前端多传入一个参数给后端视图函数即可,重构代码如下:

//必须设置上传图片请求路径(也就是处理图片上传的视图函数),并再返回文章标题用以创建对应的图片保存点
/admin/static/markdown_pictureUploadURL: "{{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 -

如果一切正确,那么就能从正确的路径中准确找到图片回显

1696658882647

文章无标题时(暂未完全解决)

本章节bug没有完全解决,而是成立一个中转站,每天文章的第一个图会被存放在temp_pic文件夹里

这里在测试上传新文章时,我们遇见了一个bug,如下图所示:

1696612826800

这里新文章由于没有没有提交表单,所以标题就没有更新,为默认空标题,让我们通过测试代码看看是不是这样

<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="">

很显然,得到的是个空内容,所以这里需要进行一定修改,通过路径也可以看出,跳过了根据文章标题创建文章图片存放目录的过程

1696659264626

解决思路: 如果标题为空,也就是新建文章,那么就创建一个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语句就可以实现添加新文章时创建一个临时图片保存目录了,并且可以正常加载图片回显至页面

1696666052464

找到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编辑器撰写文章时,编辑器会有个默认文本内容

1696648545638

现在我们要删除它,通过F12在浏览器中查看内容发现,有两个部分是关于这个文本的,删去第一个无影响,删去第二个就会让原来的文本框消失

1696648645481

所以我们开始在项目中用关键词Enjoy Markdown! coding now...定位

通过逐一排查,最后找到是在:editormd.js里删除该语段后浏览器默认语段消失

1696648819648

1696648868484

修改信息展示,取消ID信息

无论是分类,还是文章,亦或者是用户,当数量增多时,这id显示还是有点突兀

1696683195582

所以找到这些地方,统统注释!

article.htmlbanner.htmlcategory.htmltag.htmluser.html文件里找到以下字段并注释掉

......
<th>ID</th>
......
<td>{{ xx.id }}</td>

ID消失!

1696683607548

待更新

实现侧边栏目录

实现文章上传浏览时显示上传用户

更改flask中flash消息闪现样式

解决文章分类过多时顶部显示问题