1. 程式人生 > 程式設計 >如何使用 Flask 做一個評論系統

如何使用 Flask 做一個評論系統

因為我部落格使用的Disqus代理服務下線,部落格的評論系統可能有一陣子沒有工作了。慚愧的是我竟然最近才發現,我的工作環境一直是沒有GFW存在的,發現是因為有個朋友為了留言給我不惜通過讚賞1元錢的方式。讚賞功能也是我最近才上的功能,但我怎麼是這麼一個無良的博主呢,我認為一個好的評論交流環境還是非常有必要的。但是自建評論還是換用其他牆內友好的評論系統,我還是糾結了一陣的,大致上我有這麼幾個要求:

  1. 主要服務牆內,Disqus雖香但牆內用不了啊
  2. 顏值,要能匹配當前部落格的主色調,或者能方便地自定義面板
  3. 評論要支援markdown語法
  4. 評論資料要有地方可管理、歸檔、匯入匯出等
  5. 外部使用者使用評論的門檻要低
  6. 使用者收到回覆時能通過他「常用的」聯絡方式收到通知

評論系統大致有這麼幾個選擇方向:一是使用類似Disqus這樣的三方平臺,這樣資料託管不用操心,但服務隨時有掛掉的風險,而且外觀上也不夠自由;二是使用Github Issue作為後端的評論系統,比如Gitment,utterances 好處是你不必擔心Github掛掉,而且不用收錢。但不方便後續打包遷移,而且我一直反對過度利用Github;那麼剩下的選擇就是自己擼一個了,簡單的構思評估以後我列出以下列功能大綱:

  • 評論資料模型
  • 評論展示
  • 評論管理
  • 匯入disqus評論
  • 新評論通知
  • 第三方登入
  • 評論匯出(低優先)

類比Workpress提供的評論功能,使用者只需要填使用者姓名和電子郵件這兩個資訊就夠了,前者用來顯示作者名,後者用來接收通知,個人網站用來推廣自己,但不是必填的。我在這個基礎上,希望增加第三方登入的功能,這樣使用者就不用填寫這些資訊,點一個按鈕就好了。關於第三方登入的開發實現,我會留到下一篇文章中。

評論資料模型

首先是評論資料模型的設計,我的理念是夠用就好,不用太多太複雜的東西,畢竟我的文章平均0.2條評論。所以,點贊什麼的就不要了,評論刪除直接刪資料就好了,也不需要什麼狀態。

如何使用 Flask 做一個評論系統

其中分別有一個外來鍵指向作者使用者以及文章記錄,User裡面會記錄這個使用者的Email,名稱,頭像資訊。另外會有一個parent_id指向評論回覆的物件(也是一條評論),這裡有一個指向自身的外來鍵,使用Flask-SQLAlchemy寫起來是這樣的:

class Comment(db.Model):
  id = db.Column(db.Integer,primary_key=True)
  post_id = db.Column(db.Integer,db.ForeignKey("post.id"))
  author_id = db.Column(db.Integer,db.ForeignKey("user.id"))
  floor = db.Column(db.Integer)
  content = db.Column(db.Text())
  html = db.Column(db.Text())
  create_at = db.Column(db.DateTime(),default=datetime.utcnow)
  parent_id = db.Column(db.Integer,db.ForeignKey("comment.id"))
  replies = db.relationship(
    "Comment",backref=db.backref("parent",remote_side=[id]),lazy="dynamic"
  )

  __table_args__ = (db.UniqueConstraint("post_id","floor",name="_post_floor"),)

floor表明評論是「第幾樓」,注意這裡有個限制,每篇文章樓層不能重複。

評論展示

接下來看看如何展示評論。每條評論都可能有若干回覆,回覆評論又有回覆,所以這是一個樹形的結構,最極端的,如果把所有樹形都巢狀顯示出來,就會像網易新聞評論蓋樓那樣。另一個極端,是把所有評論都展平,按回復時間排序顯示,這樣又會失去回覆的上下文資訊。還是那句話,夠用就好,我選擇了一條折中的方式:兩層樹形展示。直接評論的是第一層節點,然後回覆這些評論的,和回覆這些回覆的,都展平成一層節點,算作這條評論的子節點。外層評論和子節點都按時間排序顯示,但只有外層評論具有樓層屬性。且子節點應該展示回覆的是哪位作者,這樣就大大減小了上下文混淆的可能(雖然我覺得我這評論的量,全顯示成一層也不會怎樣)。

評論框編輯器使用的是simple-mde,使用起來非常簡單:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" rel="external nofollow" >
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
...
<textarea name="content"></textarea>
...
<script>
var simplemde = new SimpleMDE();
</script>

完事!後續可能會考慮加上emoji選擇器。markdown儲存,後端渲染html,前端取出展示。最後結果非常漂亮令我滿意,大家可以在本篇文章下面看到效果。

評論管理

對應的,在管理員頁面也加上一個評論管理頁面,以及開啟內建評論的開關。因為最初設計的是評論一經發出,只能刪除,不能修改,所以這種頁面對我這樣的CRUD程式設計師來說不在話下。

現在就到了激動人心的時刻了,把Disqus的評論資料遷移過來!我到Disqus頁面上去看,發現Disqus支援匯出評論資料為特定的結構,是一個xml,只要是結構化的資料,那就問題不大了。主要分為兩個部分,前半部分是thread的列表,表示有哪些文章開啟了Disqus的評論,包含文章的url等資訊(取決於你如何開啟的Disqus),後半部分是評論列表,每條評論有評論內容、作者資訊、回覆的上級評論ID,還好資料模型設計得好,這些都在射程範圍內。於是寫了一個函式解析,匯入這些資料,注意有些已刪除的或者垃圾評論直接過濾掉即可,函式放在這裡了。

如何使用 Flask 做一個評論系統

上傳檔案,匯入,成功,Disqus的評論就完美遷移過來了!

評論通知

評論通知需要拿到使用者的聯絡方式,所以表單中電子郵件是必填的,接入第三方登入時,我也要考慮哪些服務是可以獲得聯絡方式的,目前決定是用Github,Google兩種方式,至於新浪微博,雖然國人常用,但好像沒有誰會在微博上留聯絡方式,所以排除,微信倒是很好,但微信的第三方登入好像很麻煩的樣子,暫不考慮。所以最後就是郵件通知。那就簡單了,用Flask的擴充套件Flask-Mail全都搞定,但在使用中我遇到兩個坑:

如果在後臺任務中做傳送郵件的操作,注意獲取g物件需要應用上下文,獲取請求資訊需要請求上下文,而光用Flask提供的copy_current_request_context只複製請求上下文,而會建立新的應用上下文,我寫了兩個函式,一個是新增應用和請求上下文到一個函式,另一個是將函式轉換成後臺任務:

def with_app_context(f):
  ctx = _app_ctx_stack.top
  req_ctx = _request_ctx_stack.top.copy()

  def wrapper(*args,**kwargs):
    with ctx:
      with req_ctx:
        return f(*args,**kwargs)
  return update_wrapper(wrapper,f)


def background_task(f):
  def wrapper(*args,**kwargs):
    future = gevent.spawn(with_app_context(f),*args,**kwargs)

    def callback(result):
      exc = result.exception
      current_app.log_exception((type(exc),exc,exc.__traceback__))

    future.link_exception(with_app_context(callback))
    return future

  return update_wrapper(wrapper,f)

這裡後臺任務用了gevent,如果用執行緒方式,則改成

from concurrent.futures import ThreadPoolExecutor

def background_task(f):
  def wrapper(*args,**kwargs):
    with ThreadPoolExecutor() as pool:
      future = pool.submit(with_app_context(f),**kwargs)

    def callback(result):
      exc = result.exception()
      if exc is not None:
        current_app.log_exception((type(exc),exc.__traceback__))

    future.add_done_callback(with_app_context(callback))
    return future

  return update_wrapper(wrapper,f)

騰訊雲的主機預設禁掉了25埠,害我找了半天原因,只要自己在控制檯解禁一下即可立刻生效。

參考連結

原始碼倉庫: https://github.com/frostming/Flog
Implementing User Comments with SQLAlchemy - miguelgrinberg.com

以上就是如何使用 Flask 做一個評論系統的詳細內容,更多關於flask 做評論系統的資料請關注我們其它相關文章!