在线markdown平台搭建
文章目录
前言
定制网站我打算把我的域名用于了,定制网站网站后面可能访问就不太行了
定制网站所谓天下代码一大抄,定制网站抄来抄去有提高,定制网站用来描述编程再合适不过了,定制网站今天我也抄了一波。定制网站我通过开源+定制网站借鉴的方式,定制网站自己搞了一个在线的markdown
编辑器,定制网站没错这篇文章就是在上面写的。
话不多说,先上图,定制网站下面就是我抄的成果:
目的
定制网站我之前一直都是使用vscode
定制网站敲各种代码的,定制网站我非常喜欢这个工具,定制网站主要是颜值把住了我,定制网站其次通过插件可以支持定制网站非常多的语言,定制网站通用性非常高,定制网站上一个被我这么宠幸的IDE还是eclipse
。
定制网站我写文章使用的是markdown
,定制网站之前也用过富文本编辑器,相比于markdown
,定制网站富文本编辑器更多样,定制网站这是优势也是劣势。定制网站主要的缺点是写出来的定制网站文章比较花哨(对我来说,定制网站有很多读者都喜欢这种),而且非常容易造成自己写的文章格式风格不统一。
我一直用vscode
编写markdown
,Markdown All in One
这个插件非常的神器,基本上能用到的功能都有涉及。
问题在于代码的同步,最初都是用Gitee
,因为GitHub
老是打不开。我这人有一个毛病,不喜欢同步代码,这就导致家里和公司的代码出现了不匹配,很烦。
当然,代码同步只是一个方面,最主要的是,如果在公司打开一个黑乎乎的vscode
很引人注意(我的岗位不需要敲代码),这就有了划水的嫌疑。
另外呢,我买的还有两台服务器,域名也收藏了好多,正好用上。其实用vscode
连接远端服务器也蛮好的,但是问题还是在工位上打开vscode
不合适~~
(把vscode
改成light
主题??哈哈)
话说我还买了好几个中文域名,太费钱了
需求
为了解决我遇到的困扰,我收集了一下我的主要矛盾:
- 代码自动同步;
- 界面简洁低调;
- 良好的markdown编辑体验
我目前了解到的、喜欢的开源在线编辑工具主要有两个:
Editor.md
是一个网页版的markdown
编辑器,界面风格非常简洁,Demo
也非常丰富,也是本文的选择。遗憾的是代码库停止更新了。
CKEditor
是一个富文本编辑器,就能力上来说,更强,但是是一个富文本编辑器,虽然支持markdown
,对我来说有那么一奈奈的功能过剩。
这两款编辑工具都非常优秀,我非常喜欢,只恨自己不是开发者~~
设计
原计划只做一个页面,其他功能以弹窗的方式实现,但是Editor
和bootstrap
等前端有冲突,自己前端水平有限,做不出好看的界面,就能简则简。
前端页面设计
页面包括三个:
- 登录/注册页,登录注册二合一;
- 文章列表页,展示编辑过的文章;
- 编辑页面,使用
Editor.md
实现;
后端框架选择
所谓,人生苦短,我用Python
,顺理成章的就选择了Flask
作为后端框架。
框架
简单介绍一下Flask
,Python
服务器开发的流行框架,非常的轻量,同时插件很丰富,文档也齐全,有兴趣的童鞋可以访问,或则访问我之前写的文章,文章写的比较粗,但是基本的注意事项都提到了。
数据库选择
是常用的单机数据库解决方案,完全能够满足我当前的需求,就不折腾MySQL
了。也非常推荐简单玩玩的童鞋使用,MySQL
如果不是老鸟,太难了~💔
我之前的文章使用的是MySQL
,详细介绍了如何连接数据库,使用起来都差不多。
连接数据库的工具是,SQLAlchemy
是一个ORM
(Object Relational Mapper
)框架,简单来讲,就是可以在不写sql
的情况下完成各种数据库操作。
图床sm.ms
因为贫穷,只能使用免费的图床平台,这里我用的是。
市面上有很多图床可以选择,一般都有免费空间赠送,sm.ms
有5GB
的免费空间,支持API
上传,不过访问速度一般,可能因为我是白嫖的。
关键是不需要注册就能使用,直接上传图片就可以获得链接。
实现
下面是抄袭教程:
数据库设计
数据库使用flask-sqlalchemy
连接,详细操作在中都有讲解。
下面的代码涉及了flask-sqlalchemy
的使用方法、flask-cli
命令行的使用。
可以简单的使用flask db-init
、flask db-rebuild
等命令操作数据库。
话不多说,上代码:
# db.pyfrom email.policy import defaultfrom flask_sqlalchemy import SQLAlchemyimport sqlite3import clickfrom flask.cli import with_appcontextfrom datetime import datetimefrom werkzeug.security import generate_password_hash, check_password_hashfrom flask_login import UserMixindb = SQLAlchemy()def addUser(u): if isinstance(u, User): db.session.add(u) db.session.commit()def updatePost(p): if isinstance(p, Post): db.session.add(p) db.session.commit()def init_app(app): db.init_app(app) app.cli.add_command(init_db_command) app.cli.add_command(reb_db_command) app.cli.add_command(del_db_command)def init_data(): admin = User(username='admin', password='996996', email='666@163.com') db.session.add(admin) db.session.flush() db.session.commit() post = Post(title='第一篇文章', html='# 第一篇文章', markdown='# 第一篇文章') post.author_id = admin.id db.session.add(post) db.session.commit() anonym = User(username='anonym', password='996996', email='666@666.com') db.session.add(anonym) db.session.commit()def init_db(): db.create_all() init_data()def del_db(): db.drop_all()@click.command('db-rebuild')@with_appcontextdef reb_db_command(): del_db() init_db() click.echo('Rebuild the database.')@click.command('db-clean')@with_appcontextdef del_db_command(): del_db() click.echo('Cleared the database.')@click.command('db-init')@with_appcontextdef init_db_command(): init_db() click.echo('Initialized the database.')class ShareField(object): created = db.Column( db.DateTime, nullable=False, default=datetime.utcnow) updated = db.Column(db.DateTime, onupdate=datetime.utcnow) status = db.Column(db.Integer, default=0)class User(db.Model, ShareField, UserMixin): __tablename__ = 't_users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), nullable=False) _password = db.Column(db.String(128), nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) posts = db.relationship('Post', backref='author', lazy=True) def __init__(self, username, password, email): self.username = username self.password = password self.email = email # getter @property def password(self): return self._password # setter @password.setter def password(self, raw_password): self._password = generate_password_hash(raw_password) # 加密 # check def check_password(self, raw_password): result = check_password_hash(self.password, raw_password) return resultclass Post(db.Model, ShareField): __tablename__ = 't_posts' id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(64), nullable=False, default='') html = db.Column(db.String(30000), nullable=False, default='') markdown = db.Column(db.String(30000), nullable=False, default='') author_id = db.Column(db.Integer, db.ForeignKey( 't_users.id'), nullable=False)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
登录注册
首先,去csdn
上搜个登录注册页面源代码!
我选择的是
稍微改改里面的form
代码,以下仅供参考:
<div class="container__form container--signup"> <form action="{{url_for('auth.register')}}" method="post" class="form" id="form1"> <h2 class="form__title">Sign Up</h2> <input type="text" name="username" placeholder="UserName" class="input" /> <input type="email" name="email" placeholder="Email" class="input" /> <input type="password" name="password" placeholder="Password" class="input" /> <input type="submit" class="btn" value="Sign Up"></input> </form> </div> <!-- Sign In --> <div class="container__form container--signin"> <form action="{{url_for('auth.login')}}" method="post" class="form" id="form2"> <h2 class="form__title">Sign In</h2> <input type="email" name="email" placeholder="Email" class="input" /> <input type="password" name="password" placeholder="Password" class="input" /> <a href="#" class="link">Forgot your password?</a> <input type="submit" class="btn" value="Sign In"></input> </form> </div>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
后端使用flask-login
插件完成登录,如果不会用这个插件的,可以访问我之前的文章。
@bp.route('/register', methods=['POST', 'GET'])def register(): if request.method == 'POST': email = request.form.get('email', '') username = request.form.get('username', '') password = request.form.get('password', '') if email == '' or username == '' or password == '': flash('注册信息不完整') return {'msg': '注册信息不完整'}, 201 user = User.query.filter_by(email=email).first() if user: flash('邮箱已注册') return {'msg': '邮箱已注册'}, 201 user = User(email=email,username=username,password=password) addUser(user) #插入数据库 return redirect(url_for('auth.login')) return render_template('sigh.html')@bp.route('/login', methods=['POST', 'GET'])def login(): if request.method == 'POST': email = request.form.get('email', '') password = request.form.get('password', '') print(email, password) if email == '' or password == '': flash('登录信息不完整') return {'msg': '登录信息不完整'}, 201 user = User.query.filter_by(email=email).first() if not user: flash('用户不存在') return {'msg': '用户不存在'}, 404 if not user.check_password(password): flash('密码错误') return {'msg': '密码错误'}, 201 login_user(user) return redirect(url_for('post.all')) return render_template('sign.html')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
以上代码写的非常粗糙,基本上没有异常的处理,而且登录失败没有页面跳转😫,可以稍微改下。
文章编辑
主要是Editor.md
的引入,同样的,里面也都有,我这里直接上代码:
index.html{% extends 'base.html' %}{% block style %}{{super()}}<link rel="stylesheet" href="{{ url_for('static',filename='css/editormd.css')}}" />{% endblock %}{% block content %}<div class="main_content"> <div class="center_content"> <div class="btn-group"> <button id="show-btn">Show editor</button> <button id="hide-btn">Hide editor</button> <button id="get-md-btn">Get Markdown</button> <button id="get-html-btn">Get HTML</button> <button id="show-toolbar-btn">Show toolbar</button> <button id="close-toolbar-btn">Hide toolbar</button> {% if current_user.is_authenticated %} <a class="link-btn" href="{{url_for('auth.logout')}}">Quit</a> <a class="link-btn" href="{{url_for('post.all')}}">Post List</a> {% else %} <a class="link-btn" href="{{url_for('auth.register')}}">Sign Up</a> <a class="link-btn" href="{{url_for('auth.login')}}">Sign In</a> {%endif%} </div> <input id="title" name="title" type="text" value="{{target.title}}" style="width: 100%;" placeholder="请输入文章标题"> </div> <div id="test-editormd"> <textarea style="display: none;">{% if target %}{{ target.markdown}}{% else %}{% endif %}</textarea> </div></div>{% endblock %}{% block script %}{{super()}}<script src="{{ url_for('static',filename='js/editormd.js')}}"></script><script type="text/javascript"> function debounce(func, wait, immediate) { let timeout return function (...args) { clearTimeout(timeout) timeout = setTimeout(() => { timeout = null if (!immediate) func.apply(this, args) }, wait) if (immediate && !timeout) func.apply(this, [...args]) } }; function update() { title = $('#title').val(); html = testEditor.getHTML(); mark = testEditor.getMarkdown(); data = { title: title, html: html, markdown: mark } $.ajax({ url: '{{url_for("post.edit",id=target.id)}}', data: JSON.stringify(data), method: 'post', dataType: 'json', contentType: 'application/json', success: function (data) { console.log(data.msg); } }); } $('#title').on('input', debounce(update, 3000, false)); var testEditor; $(function () { // $.get('test.md', function (md) { testEditor = editormd("test-editormd", { width: "90%", height: 740, path: '{{url_for("static",filename="editor.md/lib/")}}', // theme: "dark", // previewTheme: "dark", // editorTheme: "pastel-on-dark", // markdown: "{% if target %}{{ target.markdown.replace('','\')}}{% else %}{% endif %}", codeFold: true, //syncScrolling : false, saveHTMLToTextarea: true, // 保存 HTML 到 Textarea searchReplace: true, //watch : false, // 关闭实时预览 htmlDecode: "style,script,iframe|on*", // 开启 HTML 标签解析,为了安全性,默认不开启 //toolbar : false, //关闭工具栏 //previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启 emoji: true, taskList: true, tocm: true, // Using [TOCM] tex: true, // 开启科学公式TeX语言支持,默认关闭 flowChart: true, // 开启流程图支持,默认关闭 sequenceDiagram: true, // 开启时序/序列图支持,默认关闭, //dialogLockScreen : false, // 设置弹出层对话框不锁屏,全局通用,默认为true //dialogShowMask : false, // 设置弹出层对话框显示透明遮罩层,全局通用,默认为true //dialogDraggable : false, // 设置弹出层对话框不可拖动,全局通用,默认为true //dialogMaskOpacity : 0.4, // 设置透明遮罩层的透明度,全局通用,默认值为0.1 //dialogMaskBgColor : "#000", // 设置透明遮罩层的背景颜色,全局通用,默认为#fff imageUpload: true, imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"], imageUploadURL: "{{url_for('post.upload')}}", onload: function () { console.log('onload', this); //this.fullscreen(); //this.unwatch(); //this.watch().fullscreen(); //this.setMarkdown("#PHP"); //this.width("100%"); //this.height(480); //this.resize("100%", 640); }, onchange: debounce(update, 3000, false), }); // }); $("#goto-line-btn").bind("click", function () { testEditor.gotoLine(90); }); $("#show-btn").bind('click', function () { testEditor.show(); }); $("#hide-btn").bind('click', function () { testEditor.hide(); }); $("#get-md-btn").bind('click', function () { alert(testEditor.getMarkdown()); }); $("#get-html-btn").bind('click', function () { alert(testEditor.getHTML()); }); $("#watch-btn").bind('click', function () { testEditor.watch(); }); $("#unwatch-btn").bind('click', function () { testEditor.unwatch(); }); $("#preview-btn").bind('click', function () { testEditor.previewing(); }); $("#fullscreen-btn").bind('click', function () { testEditor.fullscreen(); }); $("#show-toolbar-btn").bind('click', function () { testEditor.showToolbar(); }); $("#close-toolbar-btn").bind('click', function () { testEditor.hideToolbar(); }); $("#toc-menu-btn").click(function () { testEditor.config({ tocDropdown: true, tocTitle: "目录 Table of Contents", }); }); $("#toc-default-btn").click(function () { testEditor.config("tocDropdown", false); }); });</script>{% endblock %}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
简单解释一下,下载Editor.md
压缩包,解压后放在static
文件夹下面,重命名为editor.md
,页面中的css
和js
文件都可以直接抄editor.md/expample/full.html
的引用方式,然后换成jinja
的格式就可以了。
需要注意的是:
-
自动保存
自动保存功能使用onchange
实现,Editor.md
留的有接口,我在这里使用了一个防抖动的技术,说白了就是在文章修改后的第一时间不上传,而是等停止改动后3秒再上传,这样可以有效的降低服务器压力。 -
图片上传
图片上传使用imageUploadURL
指定上传路径。我这里没有把图片保存在自己的服务器,而是转手把图片上传到了sm.ms
,下面会有详细的实现代码。
文章保存后端代码
@bp.route('/edit/<int:id>', methods=['POST', 'GET'])@login_requireddef edit(id=0): target = Post.query.filter_by(id=id).first() if not target: return {'msg': '服务器没有查询到当前文章的信息!'}, 404 if request.method == 'POST': data = request.json target.title = data['title'] target.html = data['html'] target.markdown = data['markdown'] print(target.html, target.markdown) updatePost(target) return {'msg': 'success'}, 200 return render_template('index.html', target=target)@bp.route('/all')@login_requireddef all(): post_list = current_user.posts return render_template('posts.html', post_list=post_list)@bp.route('/upload', methods=['POST'])@login_requireddef upload(): img = request.files.get('editormd-image-file') if not img: return {'success': 0, 'message': 'null'} headers = {'Authorization': '这里需要写自己的授权码'} files = {'smfile': img} url = 'https://sm.ms/api/v2/upload' res = requests.post(url, files=files, headers=headers).text import json js = json.loads(res) if js.get('success') == True: url = js.get('data').get('url') else: url = js.get('images') msg = js.get('message') return {'success': 1, 'message': msg, 'url': url}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
edit
方法用于更新文章,upload
方法用于图片上传。
可以看到代码写的非常脆弱,大佬不要嘲讽我~~
图片上传里面有一个需要注意的地方,就是headers
变量中的Authorization
值。
这个值需要自己注册sm.ms
才能获得,获得方法如下图:
文章列表
列表页面也是我自己写的唯一一个页面,主要没得抄,当然也是非常简单的。
功能就是展示所有的文章,也没有使用分页功能。
后端代码就是上面代码的all()
函数,这里不再重复,简单贴一下前端代码:
posts.html{% extends 'base.html' %}{% block style %}{{super()}}<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">{% endblock %}{% block content %}<div class="main_content"> <div class="center_content"> <div> <span style="font-size: 18px;font-weight: bold ;">文章列表</span> {% if current_user.is_authenticated %} <a class="link-btn" href="{{url_for('auth.logout')}}">Quit</a> {% else %} <a class="link-btn" href="{{url_for('auth.register')}}">Sign Up</a> <a class="link-btn" href="{{url_for('auth.login')}}">Sign In</a> {%endif%} </div> <br> <ul class="post_list"> <li><a href="{{url_for('post.add')}}" style="color: rgb(45, 141, 128);"><i class="bi bi-plus-circle"></i> Create a new post</a></li> {% if post_list %} {% for post in post_list | reverse %} <li> <a href="{{url_for('post.edit',id=post.id)}}"> {{ post.created.strftime('%Y-%m-%d %H:%M:%S')}}《{{ post.title}}》 </a> </li> {% endfor %} {% endif %} </ul> </div></div>{% endblock %}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
这里没啥好解释的,还是那句话,如果有兴趣可以看我之前的文章。
欢迎大家留言讨论,这点我还算熟悉~~~
还有图标呢~~~
😍