截止到目前為止我們的 django blog 文章展示部分,已經實現的“八九不離十”了。你以為本系列文章就要結束了嗎?不能夠!新的征程才剛剛開始,HelloDjango 系列文章剛剛過半,后面的文章你將接觸更多博客系統的細節。向著一個小而全的博客系統前進、前進、前進,你定會收獲頗多。 今天我們就來開啟博客的評論功能,建起和讀者的溝通橋梁。 創建評論應用 相對來說,評論是另外一個比較獨立的功能。Django 提倡,如果功能相對比較獨立的話,最好是創建一個應用,把相應的功能代碼組織到這個應用里。我們的第一個應用叫 blog,它里面放了展示博客文章列表和詳情等相關功能的代碼。而這里我們再創建一個應用,名為 comments 這里面將存放和評論功能相關的代碼。首先進入到項目根目錄,然后輸入如下命令創建一個新的應用: > pipenv run python manage.py startapp comments 可以看到生成的 comments 應用目錄結構和 blog 應用的目錄是類似的(關于創建應用以及應用的目錄結構在 "空空如也"的博客應用 中已經有過詳細介紹)。 創建新的應用后一定要記得在 settings.py 里注冊這個應用,django 才知道這是一個應用。 blogproject/settings.py ... INSTALLED_APPS = [ ... 'blog.apps.BlogConfig', # 注冊 blog 應用 'comments.apps.CommentsConfig', # 注冊 comments 應用 ]v ... 注意這里注冊的是 CommentsConfig 類,在 博客從“裸奔”到“有皮膚” 中曾經講過如何對應用做一些初始化配置,例如讓 blog 應用在 django 的 admin 后臺顯示中文名字。這里也對評論應用做類似的配置: comments/app.py from django.apps import AppConfig class CommentsConfig(AppConfig): name = 'comments' verbose_name = '評論' 設計評論的數據庫模型 用戶評論的數據必須被存儲到數據庫里,以便其他用戶訪問時 django 能從數據庫取回這些數據然后展示給訪問的用戶,因此我們需要為評論設計數據庫模型,這和設計文章、分類、標簽的數據庫模型是一樣的,如果你忘了怎么做,再回顧一下 創建 Django 博客的數據庫模型 中的做法。我們的評論模型設計如下(評論模型的代碼寫在 comments\models.py 里): comments/models.py from django.db import models from django.utils import timezone class Comment(models.Model): name = models.CharField('名字', max_length=50) email = models.EmailField('郵箱') url = models.URLField('網址', blank=True) text = models.TextField('內容') created_time = models.DateTimeField('創建時間', default=timezone.now) post = models.ForeignKey('blog.Post', verbose_name='文章', on_delete=models.CASCADE) class Meta: verbose_name = '評論' verbose_name_plural = verbose_name def __str__(self): return '{}: {}'.format(self.name, self.text[:20]) 評論會保存評論用戶的 name(名字)、email(郵箱)、url(個人網站,可以為空),用戶發表的內容將存放在 text 字段里,created_time 記錄評論時間。最后,這個評論是關聯到某篇文章(Post)的,由于一個評論只能屬于一篇文章,一篇文章可以有多個評論,是一對多的關系,因此這里我們使用了 ForeignKey。關于 ForeignKey 我們前面已有介紹,這里不再贅述。 此外,在 博客從“裸奔”到“有皮膚” 中提過,所有模型的字段都接受一個 verbose_name 參數(大部分是第一個位置參數),django 在根據模型的定義自動生成表單時,會使用這個參數的值作為表單字段的 label,我們在后面定義的評論表單時會進一步看到其作用。 創建了數據庫模型就要遷移數據庫,遷移數據庫的命令也在前面講過。在項目根目錄下分別運行下面兩條命令: > pipenv run python manage.py makemigrations > pipenv run python manage.py migrate 注冊評論模型到 admin 既然已經創建了模型,我們就可以將它注冊到 django admin 后臺,方便管理員用戶對評論進行管理,如何注冊 admin 以及美化在 博客從“裸奔”到“有皮膚” 有過詳細介紹,這里給出相關代碼: comments/admin.py from django.contrib import admin from .models import Comment class CommentAdmin(admin.ModelAdmin): list_display = ['name', 'email', 'url', 'post', 'created_time'] fields = ['name', 'email', 'url', 'text', 'post'] admin.site.register(Comment, CommentAdmin) 設計評論表單 這一節我們將學習一個全新的 django 知識:表單。那么什么是表單呢?基本的 HTML 知識告訴我們,在 HTML 文檔中這樣的代碼表示一個表單: <form action="" method="post"> <input type="text" name="username" /> <input type="password" name="password" /> <input type="submit" value="login" /> </form> 為什么需要表單呢?表單是用來收集并向服務器提交用戶輸入的數據的??紤]用戶在我們博客網站上發表評論的過程。當用戶想要發表評論時,他找到我們給他展示的一個評論表單(我們已經看到在文章詳情頁的底部就有一個評論表單,你將看到表單呈現給我們的樣子),然后根據表單的要求填寫相應的數據。之后用戶點擊評論按鈕,這些數據就會發送給某個 URL。我們知道每一個 URL 對應著一個 django 的視圖函數,于是 django 調用這個視圖函數,我們在視圖函數中寫上處理用戶通過表單提交上來的數據的代碼,比如驗證數據的合法性并且保存數據到數據庫中,那么用戶的評論就被 django 處理了。如果通過表單提交的數據存在錯誤,那么我們把錯誤信息返回給用戶,并在前端重新渲染表單,要求用戶根據錯誤信息修正表單中不符合格式的數據,再重新提交。 django 的表單功能就是幫我們完成上述所說的表單處理邏輯,表單對 django 來說是一個內容豐富的話題,很難通過教程中的這么一個例子涵蓋其全部用法。因此我們強烈建議你在完成本教程后接下來的學習中仔細閱讀 django 官方文檔關于 表單 的介紹,因為表單在 Web 開發中會經常遇到。 下面開始編寫評論表單代碼。在 comments?目錄下(和 models.py 同級)新建一個 forms.py 文件,用來存放表單代碼,我們的表單代碼如下: comments/forms.py from django import forms from .models import Comment class CommentForm(forms.ModelForm): class Meta: model = Comment fields = ['name', 'email', 'url', 'text'] 要使用 django 的表單功能,我們首先導入 forms 模塊。django 的表單類必須繼承自 forms.Form 類或者 forms.ModelForm 類。如果表單對應有一個數據庫模型(例如這里的評論表單對應著評論模型),那么使用 ModelForm 類會簡單很多,這是 django 為我們提供的方便。之后我們在表單的內部類 Meta 里指定一些和表單相關的東西。model = Comment 表明這個表單對應的數據庫模型是 Comment 類。fields = ['name', 'email', 'url', 'text'] 指定了表單需要顯示的字段,這里我們指定了 name、email、url、text 需要顯示。 關于表單進一步的解釋 django 為什么要給我們提供一個表單類呢?為了便于理解,我們可以把表單和前面講過的 django ORM 系統做類比?;叵胍幌?,我們使用數據庫保存創建的博客文章,但是從頭到尾沒有寫過任何和數據庫有關的代碼(要知道數據庫自身也有一門數據庫語言),這是因為 django 的 ORM 系統內部幫我們做了一些事情。我們遵循 django 的規范寫的一些 Python 代碼,例如創建 Post、Category 類,然后通過運行數據庫遷移命令將這些代碼反應到數據庫。 django 的表單和這個思想類似,正常的前端表單代碼應該是和本文開頭所提及的那樣的 HTML 代碼,但是我們目前并沒有寫這些代碼,而是寫了一個 CommentForm 這個 Python 類。通過調用這個類的一些方法和屬性,django 將自動為我們創建常規的表單代碼,接下來的教程我們就會看到具體是怎么做的。 展示評論表單 表單類已經定義完畢,現在的任務是在文章的詳情頁下方將這個表單展現給用戶,用戶便可以通過這個表單填寫評論數據,從而發表評論。 那么怎么展現一個表單呢?django 會根據表單類的定義自動生成表單的 HTML 代碼,我們要做的就是實例化這個表單類,然后將表單的實例傳給模板,讓 django 的模板引擎來渲染這個表單。 那怎么將表單類的實例傳給模板呢?因為表單出現在文章詳情頁,一種想法是修改文章詳情頁 detail 視圖函數,在這個視圖中實例化一個表單,然后傳遞給模板。然而這樣做的一個缺點就是需要修改 detail 視圖函數的代碼,而且 detail 視圖函數的作用主要就是處理文章詳情,一個視圖函數最好不要讓它做太多雜七雜八的事情。另外一種想法是使用自定義的模板標簽,我們在 頁面側邊欄:使用自定義模板標簽 中詳細介紹過如何自定義模板標簽來渲染一個局部的 HTML 頁面,這里我們使用自定義模板標簽的方法,來渲染表單頁面。 和 blog 應用中定義模板標簽的老套路一樣,首先建立評論應用模板標簽的文件結構,在 comments?文件夾下新建一個 templatetags 文件夾,然后創建 __init__.py 文件使其成為一個包,再創建一個 comments_extras.py 文件用于存放模板標簽的代碼,文件結構如下: ... blog\ comments\ templatetags\ __init__.py comments_extras.py ... 然后我們定義一個 inclusion_tag 類型的模板標簽,用于渲染評論表單,關于如何定義模板標簽,在 頁面側邊欄:使用自定義模板標簽 中已經有詳細介紹,這里不再贅述。 from django import template from ..forms import CommentForm register = template.Library() @register.inclusion_tag('comments/inclusions/_form.html', takes_context=True) def show_comment_form(context, post, form=None): if form is None: form = CommentForm() return { 'form': form, 'post': post, } 從定義可以看到,show_comment_form 模板標簽使用時會接受一個 post(文章 Post 模型的實例)作為參數,同時也可能傳入一個評論表單 CommentForm 的實例 form,如果沒有接受到評論表單參數,模板標簽就會新創建一個 CommentForm 的實例(一個沒有綁定任何數據的空表單)傳給模板,否則就直接將接受到的評論表單實例直接傳給模板,這主要是為了復用已有的評論表單實例(后面會看到其用法)。 然后在 templates/comments/inclusions 目錄下(沒有就新建)新建一個 _form.html 模板,寫上代碼: <form action="{% url 'comments:comment' post.pk %}" method="post" class="comment-form"> {% csrf_token %} <div class="row"> <div class="col-md-4"> <label for="{{ form.name.id_for_label }}">{{ form.name.label }}:</label> {{ form.name }} {{ form.name.errors }} </div> <div class="col-md-4"> <label for="{{ form.email.id_for_label }}">{{ form.email.label }}:</label> {{ form.email }} {{ form.email.errors }} </div> <div class="col-md-4"> <label for="{{ form.url.id_for_label }}">{{ form.url.label }}:</label> {{ form.url }} {{ form.url.errors }} </div> <div class="col-md-12"> <label for="{{ form.text.id_for_label }}">{{ form.text.label }}:</label> {{ form.text }} {{ form.text.errors }} <button type="submit" class="comment-btn">發表</button> </div> </div> <!-- row --> </form> 這個表單的模板有點復雜,一一講解一下。 首先 HTML 的 form 標簽有 2 個重要的屬性,action 和 method。action 指定表單內容提交的地址,這里我們提交給 comments:comment 視圖函數對應的 URL(后面會創建這個視圖函數并綁定對應的 URL),模板標簽 url 的用法在 分類、歸檔和標簽頁 教程中有詳細介紹。method 指定提交表單時的 HTTP 請求類型,一般表單提交都是使用 POST。 然后我們看到 {% csrf_token %},這個模板標簽在表單渲染時會自動渲染為一個隱藏類型的 HTML input 控件,其值為一個隨機字符串,作用主要是為了防護 CSRF(跨站請求偽造)攻擊。{% csrf_token %} 在模板中渲染出來的內容大概如下所示: <input type="hidden" name="csrfmiddlewaretoken" value="KH9QLnpQPv2IBcv3oLsksJXdcGvKSnC8t0mTfRSeNIlk5T1G1MBEIwVhK4eh6gIZ"> CSRF 攻擊是一種常見的 Web 攻擊手段。攻擊者利用用戶存儲在瀏覽器中的 cookie,向目標網站發送 HTTP 請求,這樣在目標網站看來,請求來自于用戶,而實際發送請求的人卻是攻擊者。例如假設我們的博客支持登錄功能(目前沒有),并使用 cookie(或者 session)記錄用戶的登錄狀態,且評論表單沒有 csrf token 防護。用戶登錄了我們的博客后,又去訪問了一個小電影網站,小電影網站有一段惡意 JavaScript 腳本,它讀取用戶的 cookie,并構造了評論表單的數據,然后腳本使用這個 cookie 向我們的博客網站發送一條 POST 請求,django 就會認為這是來自該用戶的評論發布請求,便會在后臺創建一個該用戶的評論,而這個用戶全程一臉懵逼。 CSRF 的一個防范措施是,對所有訪問網站的用戶頒發一個令牌(token),對于敏感的 HTTP 請求,后臺會校驗此令牌,確保令牌的確是網站頒發給指定用戶的。因此,當用戶訪問別的網站時,雖然攻擊者可以拿到用戶的 cookie,但是無法取得證明身份的令牌,因此發過來的請求便不會被受理。 以上是對 CSRF 攻擊和防護措施的一個簡單介紹,更加詳細的講解請使用搜索引擎搜索相關資料。 show_comment_form 模板標簽給模板傳遞了一個模板變量 form,它是 CommentForm 的一個實例,表單的字段 {{ form.name }}、{{ form.email }}、{{ form.url }} 等將自動渲染成表單控件,例如 <input> 控件。 注意到表單的定義中并沒有定義 name、email、url 等屬性,那它們是哪里來的呢?看到 CommentForm 中 Meta 下的 fields,django 會自動將 fields 中聲明的模型字段設置為表單的屬性。 {{ form.name.errors }}、{{ form.email.errors }} 等將渲染表單對應字段的錯誤(如果有的話),例如用戶 email 格式填錯了,那么 django 會檢查用戶提交的 email 的格式,然后將格式錯誤信息保存到 errors 中,模板便將錯誤信息渲染顯示。 {{ form.xxx.label }} 用來獲取表單的 label,之前說過,django 根據表單對應的模型中字段的 verbose_name 參數生成。 然后我們就可以在 detail.html 中使用這個模板標簽來渲染表單了,注意在使用前記得先 {% load comment_extras %} 這個模塊。而且為了避免可能的報錯,最好重啟一下開發服務器。 {% extends 'base.html' %} {% load comment_extras %} ... <h3>發表評論</h3> {% show_comment_form post %} 這里當用戶訪問文章詳情頁面時,我們給他展示一個空表單,所以這里只傳入了 post 參數需要的值,而沒有傳入 form 參數所需的值??梢钥吹奖韱武秩境鰜淼慕Y果了: 評論視圖函數 當用戶提交表單中的數據后,django 需要調用相應的視圖函數來處理這些數據,下面開始寫我們視圖函數處理邏輯: from blog.models import Post from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_POST from .forms import CommentForm @require_POST def comment(request, post_pk): # 先獲取被評論的文章,因為后面需要把評論和被評論的文章關聯起來。 # 這里我們使用了 django 提供的一個快捷函數 get_object_or_404, # 這個函數的作用是當獲取的文章(Post)存在時,則獲??;否則返回 404 頁面給用戶。 post = get_object_or_404(Post, pk=post_pk) # django 將用戶提交的數據封裝在 request.POST 中,這是一個類字典對象。 # 我們利用這些數據構造了 CommentForm 的實例,這樣就生成了一個綁定了用戶提交數據的表單。 form = CommentForm(request.POST) # 當調用 form.is_valid() 方法時,django 自動幫我們檢查表單的數據是否符合格式要求。 if form.is_valid(): # 檢查到數據是合法的,調用表單的 save 方法保存數據到數據庫, # commit=False 的作用是僅僅利用表單的數據生成 Comment 模型類的實例,但還不保存評論數據到數據庫。 comment = form.save(commit=False) # 將評論和被評論的文章關聯起來。 comment.post = post # 最終將評論數據保存進數據庫,調用模型實例的 save 方法 comment.save() # 重定向到 post 的詳情頁,實際上當 redirect 函數接收一個模型的實例時,它會調用這個模型實例的 get_absolute_url 方法, # 然后重定向到 get_absolute_url 方法返回的 URL。 return redirect(post) # 檢查到數據不合法,我們渲染一個預覽頁面,用于展示表單的錯誤。 # 注意這里被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。 context = { 'post': post, 'form': form, } return render(request, 'comments/preview.html', context=context) 這個評論視圖相比之前的一些視圖復雜了很多,主要是處理評論的過程更加復雜。具體過程在代碼中已有詳細注釋,這里僅就視圖中出現了一些新的知識點進行講解。 首先視圖函數被 require_POST 裝飾器裝飾,從裝飾器的名字就可以看出,其作用是限制這個視圖只能通過 POST 請求觸發,因為創建評論需要用戶通過表單提交的數據,而提交表單通常都是限定為 POST 請求,這樣更加安全。 另外我們使用了 redirect 快捷函數。這個函數位于 django.shortcuts 模塊中,它的作用是對 HTTP 請求進行重定向(即用戶訪問的是某個 URL,但由于某些原因,服務器會將用戶重定向到另外的 URL)。redirect 既可以接收一個 URL 作為參數,也可以接收一個模型的實例作為參數(例如這里的 post)。如果接收一個模型的實例,那么這個實例必須實現了 get_absolute_url 方法,這樣 redirect 會根據 get_absolute_url 方法返回的 URL 值進行重定向。 如果用戶提交的數據合法,我們就將評論數據保存到數據庫,否則說明用戶提交的表單包含錯誤,我們將渲染一個 preview.html 頁面,來展示表單中的錯誤,以便用戶修改后重新提交。preview.html 的代碼如下: {% extends 'base.html' %} {% load comment_extras %} {% block main %} {% show_comment_form post form %} {% endblock main %} 這里還是使用 show_comment_form 模板標簽來展示一個表單,然而不同的是,這里我們傳入由視圖函數 comment 傳來的綁定了用戶提交的數據的表單實例 form,而不是渲染一個空表單。因為視圖函數 comment 中的表單實例是綁定了用戶提交的評論數據,以及對數據進行過合法性校驗的表單,因此當 django 渲染這個表單時,會連帶渲染用戶已經填寫的表單數據以及數據不合法的錯誤提示信息,而不是一個空的表單了。例如下圖,我們提交的數據中 email 格式不合法,表單校驗了數據格式,然后渲染錯誤提示: 綁定 URL 視圖函數需要和 URL 綁定,這里我們在 comment 應用中再建一個 urls.py 文件,寫上 URL 模式: from django.urls import path from . import views app_name = 'comments' urlpatterns = [ path('comment/<int:post_pk>', views.comment, name='comment'), ] 別忘了給這個評論的 URL 模式規定命名空間,即 app_name = 'comments'。 最后要在項目的 blogproject?目錄的 urls.py 里包含 comments\urls.py 這個文件: blogproject/urls.py urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'', include('blog.urls')), url(r'', include('comments.urls')), ] 可以測試一下提交評論的功能了,首先嘗試輸入非法格式的數據,例如將郵箱輸入為 xxx@xxx,那么評論視圖在校驗表單數據合法性時,發現郵箱格式不符,就會渲染 preview 頁面,展示表單中的錯誤,將郵箱修改為正確的格式后,再次點擊發表,頁面就跳轉到了被評論文章的詳情頁,說明視圖正確執行了保存表單數據到數據庫的邏輯。 不過這里有一點不好的地方就是,評論成功后頁面直接跳轉到了被評論文章的詳情頁,沒有任何提示,用戶也不知道評論究竟有沒有真的成功。這里我們使用 django 自帶的 messages 應用來給用戶發送評論成功或者失敗的消息。 發送評論消息 django 默認已經為我們做好了 messages 的相關配置,直接用即可。 兩個地方需要發送消息,第一個是當評論成功,即評論數據成功保存到數據庫后,因此在 comment 視圖中加一句。 from django.contrib import messages if form.is_valid(): ... # 最終將評論數據保存進數據庫,調用模型實例的 save 方法 comment.save() messages.add_message(request, messages.SUCCESS, '評論發表成功!', extra_tags='success') return redirect(post) 這里導入 django 的 messages 模塊,使用 add_message 方法增加了一條消息,消息的第一個參數是當前請求,因為當前請求攜帶用戶的 cookie,django 默認將詳細存儲在用戶的 cookie 中。第二個參數是消息級別,評論發表成功的消息設置為 messages.SUCCESS,這是 django 已經默認定義好的一個整數,消息級別也可以自己定義。緊接著傳入消息的內容,最后 extra_tags 給這條消息打上額外的標簽,標簽值可以在展示消息時使用,比如這里我們會把這個值用在模板中的 HTML 標簽的 class 屬性,增加樣式。 同樣的,如果評論失敗了,也發送一條消息: # 檢查到數據不合法,我們渲染一個預覽頁面,用于展示表單的錯誤。 # 注意這里被評論的文章 post 也傳給了模板,因為我們需要根據 post 來生成表單的提交地址。 context = { 'post': post, 'form': form, } messages.add_message(request, messages.ERROR, '評論發表失??!請修改表單中的錯誤后重新提交。', extra_tags='danger') 發送的消息被緩存在 cookie 中,然后我們在模板中獲取顯示即可。顯示消息比較好的地方是在導航條的下面,我們在模板 base.html 的導航條代碼下增加如下代碼: <header> ... </header> {% if messages %} {% for message in messages %} <div class="alert alert-{{ message.tags }} alert-dismissible" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button> {{ message }} </div> {% endfor %} {% endif %} 這里 django 會通過全局上下文自動把 messages 變量傳給模板,這個變量里存儲我們發送的消息內容,然后就是循環顯示消息了。這里我們使用了 bootstrap 的一個 alert 組件,為其設置不同的 class 會顯示不同的顏色,所以之前添加消息時傳入的 extra_tags 就派上了用場。比如這里 alert-{{ message.tags }},當傳入的是 success 時,類名就為 alert-success,這時顯示的消息背景顏色就是綠色,傳入的是 dangerous,則顯示的就是紅色。 評論發布成功和失敗的消息效果如下圖: 顯示評論內容 為了不改動已有的視圖函數的代碼,評論數據我們也使用自定義的模板標簽來實現。模板標簽代碼如下: @register.inclusion_tag('comments/inclusions/_list.html', takes_context=True) def show_comments(context, post): comment_list = post.comment_set.all().order_by('-created_time') comment_count = comment_list.count() return { 'comment_count': comment_count, 'comment_list': comment_list, } 我們使用了 post.comment_set.all() 來獲取 post 對應的全部評論。 Comment 和Post 是通過 ForeignKey 關聯的,回顧一下我們當初獲取某個分類 cate 下的全部文章時的代碼:Post.objects.filter(category=cate)。這里 post.comment_set.all() 也等價于 Comment.objects.filter(post=post),即根據 post 來過濾該 post 下的全部評論。但既然我們已經有了一個 Post 模型的實例 post(它對應的是 Post 在數據庫中的一條記錄),那么獲取和 post 關聯的評論列表有一個簡單方法,即調用它的 xxx_set 屬性來獲取一個類似于 objects 的模型管理器,然后調用其 all 方法來返回這個 post 關聯的全部評論。 其中 xxx_set 中的 xxx 為關聯模型的類名(小寫)。例如 Post.objects.filter(category=cate) 也可以等價寫為 cate.post_set.all()。 模板 _list.html 代碼如下: <h3>評論列表,共 <span>{{ comment_count }}</span> 條評論</h3> <ul class="comment-list list-unstyled"> {% for comment in comment_list %} <li class="comment-item"> <span class="nickname">{{ comment.name }}</span> <time class="submit-date" datetime="{{ comment.created_time }}">{{ comment.created_time }}</time> <div class="text"> {{ comment.text|linebreaks }} </div> </li> {% empty %} 暫無評論 {% endfor %} </ul> 要注意這里 {{ comment.text|linebreaks }} 中對評論內容使用的過濾器 linebreaks,瀏覽器會將換行以及連續的多個空格合并為一個空格。如果用戶評論的內容中有換行,瀏覽器會將換行替換為空格,從而顯示的用戶評論內容就會擠成一堆。linebreaks 過濾器預先將換行符替換為 br HTML 標簽,這樣內容就能換行顯示了。 然后將 detail.html 中此前占位用的評論模板替換為模板標簽渲染的內容: <h3>發表評論</h3> {% show_comment_form post %} <div class="comment-list-panel"> {% show_comments post %} </div> 訪問文章詳情頁,可以看到已經發表的評論列表了: 大功告成! 歡迎關注 HelloGitHub 公眾號,獲取更多開源項目的資料和內容
轉載自://www.cnblogs.com/xueweihan/p/11486510.html
国产无遮挡无码视频在线观看