在線markdown平臺搭建
前言
我打算把我的域名用于圖床了,網(wǎng)站后面可能訪問就不太行了
【系統(tǒng)已經(jīng)升級啦,快看這里 】
所謂天下代碼一大抄,抄來抄去有提高,用來描述編程再合適不過了,今天我也抄了一波。我通過開源+借鑒的方式,自己搞了一個在線的markdown
編輯器,沒錯這篇文章就是在上面寫的。
話不多說,先上圖,下面就是我抄的成果:
目的
我之前一直都是使用vscode
敲各種代碼的,我非常喜歡這個工具,主要是顏值把住了我,其次通過插件可以支持非常多的語言,通用性非常高,上一個被我這么寵幸的IDE還是eclipse
。
我寫文章使用的是markdown
,之前也用過富文本編輯器,相比于markdown
,富文本編輯器更多樣,這是優(yōu)勢也是劣勢。主要的缺點是寫出來的文章比較花哨(對我來說,有很多讀者都喜歡這種),而且非常容易造成自己寫的文章格式風(fēng)格不統(tǒng)一。
我一直用vscode
編寫markdown
,Markdown All in One
這個插件非常的神器,基本上能用到的功能都有涉及。
問題在于代碼的同步,最初都是用Gitee
,因為GitHub
老是打不開。我這人有一個毛病,不喜歡同步代碼,這就導(dǎo)致家里和公司的代碼出現(xiàn)了不匹配,很煩。
當(dāng)然,代碼同步只是一個方面,最主要的是,如果在公司打開一個黑乎乎的vscode
很引人注意(我的崗位不需要敲代碼),這就有了劃水的嫌疑。
另外呢,我買的還有兩臺服務(wù)器,域名也收藏了好多,正好用上。其實用vscode
連接遠(yuǎn)端服務(wù)器也蠻好的,但是問題還是在工位上打開vscode
不合適~~
(把vscode
改成light
主題??哈哈)
話說我還買了好幾個中文域名,太費(fèi)錢了
需求
為了解決我遇到的困擾,我收集了一下我的主要矛盾:
- 代碼自動同步;
- 界面簡潔低調(diào);
- 良好的markdown編輯體驗
我目前了解到的、喜歡的開源在線編輯工具主要有兩個:
- Editor.md
- CKEditor
Editor.md
是一個網(wǎng)頁版的markdown
編輯器,界面風(fēng)格非常簡潔,Demo
也非常豐富,也是本文的選擇。遺憾的是代碼庫停止更新了。
CKEditor
是一個富文本編輯器,就能力上來說,更強(qiáng),但是是一個富文本編輯器,雖然支持markdown
,對我來說有那么一奈奈的功能過剩。
這兩款編輯工具都非常優(yōu)秀,我非常喜歡,只恨自己不是開發(fā)者~~
設(shè)計
原計劃只做一個頁面,其他功能以彈窗的方式實現(xiàn),但是Editor
和bootstrap
等前端框架有沖突,自己前端水平有限,做不出好看的界面,就能簡則簡。
前端頁面設(shè)計
頁面包括三個:
- 登錄/注冊頁,登錄注冊二合一;
- 文章列表頁,展示編輯過的文章;
- 編輯頁面,使用
Editor.md
實現(xiàn);
后端框架選擇
所謂,人生苦短,我用Python
,順理成章的就選擇了Flask
作為后端框架。
Flask框架
簡單介紹一下Flask
,Python
服務(wù)器開發(fā)的流行框架,非常的輕量,同時插件很豐富,文檔也齊全,有興趣的童鞋可以訪問官網(wǎng),或則訪問我之前寫的文章《我用Python寫網(wǎng)站》,文章寫的比較粗,但是基本的注意事項都提到了。
數(shù)據(jù)庫選擇
sqlite
是常用的單機(jī)數(shù)據(jù)庫解決方案,完全能夠滿足我當(dāng)前的需求,就不折騰MySQL
了。也非常推薦簡單玩玩的童鞋使用,MySQL
如果不是老鳥,太難了~??
我之前的文章使用的是MySQL
,詳細(xì)介紹了如何連接數(shù)據(jù)庫,使用起來都差不多。
連接數(shù)據(jù)庫的工具是flask-sqlalchemy
,SQLAlchemy
是一個ORM
(Object Relational Mapper
)框架,簡單來講,就是可以在不寫sql
的情況下完成各種數(shù)據(jù)庫操作。
圖床sm.ms
因為貧窮,只能使用免費(fèi)的圖床平臺,這里我用的是sm.ms
。
市面上有很多圖床可以選擇,一般都有免費(fèi)空間贈送,sm.ms
有5GB
的免費(fèi)空間,支持API
上傳,不過訪問速度一般,可能因為我是白嫖的。
關(guān)鍵是不需要注冊就能使用,直接上傳圖片就可以獲得鏈接。
實現(xiàn)
下面是抄襲教程:
數(shù)據(jù)庫設(shè)計
數(shù)據(jù)庫使用flask-sqlalchemy
連接,詳細(xì)操作在《我用Python寫網(wǎng)站》中都有講解。
下面的代碼涉及了flask-sqlalchemy
的使用方法、flask-cli
命令行的使用。
可以簡單的使用flask db-init
、flask db-rebuild
等命令操作數(shù)據(jù)庫。
話不多說,上代碼:
# db.py
from email.policy import default
from flask_sqlalchemy import SQLAlchemy
import sqlite3
import click
from flask.cli import with_appcontext
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
db = 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_appcontext
def reb_db_command():
del_db()
init_db()
click.echo('Rebuild the database.')
@click.command('db-clean')
@with_appcontext
def del_db_command():
del_db()
click.echo('Cleared the database.')
@click.command('db-init')
@with_appcontext
def 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 result
class 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)
登錄注冊
首先,去csdn
上搜個登錄注冊頁面源代碼!
我選擇的是《好看實用的HTML登錄界面》
稍微改改里面的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>
后端使用flask-login
插件完成登錄,如果不會用這個插件的,可以訪問我之前的文章《我用Python寫網(wǎng)站》。
@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) #插入數(shù)據(jù)庫
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')
以上代碼寫的非常粗糙,基本上沒有異常的處理,而且登錄失敗沒有頁面跳轉(zhuǎn)??,可以稍微改下。
文章編輯
主要是Editor.md
的引入,同樣的,《我用Python寫網(wǎng)站》里面也都有,我這里直接上代碼:
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="請輸入文章標(biāo)題">
</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('\n','\\n')}}{% else %}{% endif %}",
codeFold: true,
//syncScrolling : false,
saveHTMLToTextarea: true, // 保存 HTML 到 Textarea
searchReplace: true,
//watch : false, // 關(guān)閉實時預(yù)覽
htmlDecode: "style,script,iframe|on*", // 開啟 HTML 標(biāo)簽解析,為了安全性,默認(rèn)不開啟
//toolbar : false, //關(guān)閉工具欄
//previewCodeHighlight : false, // 關(guān)閉預(yù)覽 HTML 的代碼塊高亮,默認(rèn)開啟
emoji: true,
taskList: true,
tocm: true, // Using [TOCM]
tex: true, // 開啟科學(xué)公式TeX語言支持,默認(rèn)關(guān)閉
flowChart: true, // 開啟流程圖支持,默認(rèn)關(guān)閉
sequenceDiagram: true, // 開啟時序/序列圖支持,默認(rèn)關(guān)閉,
//dialogLockScreen : false, // 設(shè)置彈出層對話框不鎖屏,全局通用,默認(rèn)為true
//dialogShowMask : false, // 設(shè)置彈出層對話框顯示透明遮罩層,全局通用,默認(rèn)為true
//dialogDraggable : false, // 設(shè)置彈出層對話框不可拖動,全局通用,默認(rèn)為true
//dialogMaskOpacity : 0.4, // 設(shè)置透明遮罩層的透明度,全局通用,默認(rèn)值為0.1
//dialogMaskBgColor : "#000", // 設(shè)置透明遮罩層的背景顏色,全局通用,默認(rèn)為#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 %}
簡單解釋一下,下載Editor.md
壓縮包,解壓后放在static
文件夾下面,重命名為editor.md
,頁面中的css
和js
文件都可以直接抄editor.md/expample/full.html
的引用方式,然后換成jinja
的格式就可以了。
需要注意的是:
-
自動保存
自動保存功能使用onchange
實現(xiàn),Editor.md
留的有接口,我在這里使用了一個防抖動的技術(shù),說白了就是在文章修改后的第一時間不上傳,而是等停止改動后3秒再上傳,這樣可以有效的降低服務(wù)器壓力。 -
圖片上傳
圖片上傳使用imageUploadURL
指定上傳路徑。我這里沒有把圖片保存在自己的服務(wù)器,而是轉(zhuǎn)手把圖片上傳到了sm.ms
,下面會有詳細(xì)的實現(xiàn)代碼。
文章保存后端代碼
@bp.route('/edit/<int:id>', methods=['POST', 'GET'])
@login_required
def edit(id=0):
target = Post.query.filter_by(id=id).first()
if not target:
return {'msg': '服務(wù)器沒有查詢到當(dāng)前文章的信息!'}, 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_required
def all():
post_list = current_user.posts
return render_template('posts.html', post_list=post_list)
@bp.route('/upload', methods=['POST'])
@login_required
def upload():
img = request.files.get('editormd-image-file')
if not img:
return {'success': 0, 'message': 'null'}
headers = {'Authorization': '這里需要寫自己的授權(quán)碼'}
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}
edit
方法用于更新文章,upload
方法用于圖片上傳。
可以看到代碼寫的非常脆弱,大佬不要嘲諷我~~
圖片上傳里面有一個需要注意的地方,就是headers
變量中的Authorization
值。
這個值需要自己注冊sm.ms
才能獲得,獲得方法如下圖:
文章列表
列表頁面也是我自己寫的唯一一個頁面,主要沒得抄,當(dāng)然也是非常簡單的。
功能就是展示所有的文章,也沒有使用分頁功能。
后端代碼就是上面代碼的all()
函數(shù),這里不再重復(fù),簡單貼一下前端代碼:
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 %}
這里沒啥好解釋的,還是那句話,如果有興趣可以看我之前的文章《我用Python寫網(wǎng)站》。
歡迎大家留言討論,這點我還算熟悉~~~
還有圖標(biāo)呢~~~
??
代碼壓縮包下載
文章pdf下載文章來源:http://www.zghlxwxcb.cn/news/detail-428533.html
【系統(tǒng)已經(jīng)升級啦,快看這里 】文章來源地址http://www.zghlxwxcb.cn/news/detail-428533.html
到了這里,關(guān)于用Python基礎(chǔ)知識實現(xiàn)了一個在線的markdown編輯工具、基于Editor.md、Flask、Flask_SQLAlchemy、sm.ms的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!