在上一篇博客中,我们实现了用户登录功能,并介绍了如何在angular中使用cookie,以及angular中路由事件的使用。在这期博客中,我们将开始开发我们的核心功能——故事系统与评论系统。
我们的故事系统和评论系统如下图所示:
故事系统由一个简单的表单组成,包括标题栏和内容栏,内容栏我们会使用之前安装好的ngx-quill富文本框插件来实现,以支持富文本功能。
我们的评论系统如下图所示:
用户可以对每篇故事发表评论,且可以对故事下的每篇评论再次评论,形成一个评论树。
下面,就让我们看看该如何实现这两个系统吧。
十一 故事系统的开发
1 编写故事
前端部分
我们会把整个故事系统和评论系统作为一个新的模块来开发,因此我们需要建立一个新的angular模块。
在cmd中输入如下命令,让angular为我们建立一个新的模块:
ng g module story
这样,angluar就会为我们建立一个新的story模块,我们可以把这个模块加到app.module.ts文件中:
//...
import { StoryModule } from './story/story.module';
@NgModule({
declarations: [
//...
],
imports: [
//...
StoryModule,
],
providers: [CookieService],
bootstrap: [AppComponent]
})
export class AppModule { }
接下来,让我们在story模块中建立writestory组件,用于编写故事。在cmd中输入以下命令,在story模块中建立writestory组件:
ng g c -m story
-m story表示我们要在story模块中建立这个组件,而不是在根模块下建立。
现在,让我们把所有需要的组件和模块都导入到story模块中,包括之后要在评论系统中使用的模块:
//story.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NzFormModule } from 'ng-zorro-antd/form';
import { NzInputModule } from 'ng-zorro-antd/input';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { FormsModule } from '@angular/forms';
import { ReactiveFormsModule } from '@angular/forms';
import { NzDividerModule } from 'ng-zorro-antd/divider';
import { NzSpaceModule } from 'ng-zorro-antd/space';
import { NzCardModule } from 'ng-zorro-antd/card';
import { QuillModule } from 'ngx-quill';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { IconDefinition } from '@ant-design/icons-angular';
import { NzCollapseModule } from 'ng-zorro-antd/collapse';
import { NzCommentModule } from 'ng-zorro-antd/comment';
import { NzAvatarModule } from 'ng-zorro-antd/avatar';
import { StarOutline, CommentOutline, LikeOutline } from '@ant-design/icons-angular/icons';
import { NzListModule } from 'ng-zorro-antd/list';
const icons: IconDefinition[] = [ StarOutline, CommentOutline, LikeOutline ];
@NgModule({
declarations: [
],
imports: [
CommonModule,
NzFormModule,
NzInputModule,
NzButtonModule,
FormsModule,
ReactiveFormsModule,
NzDividerModule,
NzSpaceModule,
NzCardModule,
QuillModule.forRoot(),
NzLayoutModule,
NzIconModule.forRoot(icons),
NzCollapseModule,
NzCommentModule,
NzAvatarModule,
NzListModule
],
exports:[]
})
export class StoryModule { }
接下来,我们在writestory中新建storyData.ts,定义故事的接口,以便之后和服务器通信使用:
//storyData.ts
export interface storyData {
author:string;
title: string;
content: string;
}
这个接口很简单,包括作者、标题和内容三部分。
然后,我们打开writestory.component.html,开始编写前端代码:
<form nz-form [formGroup]="storyForm" (ngSubmit)="onSubmit()">
<nz-form-item>
<nz-form-control>
<nz-input-group>
<p><input nz-input placeholder="标题" formControlName="title"/></p>
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<nz-input-group>
<p><quill-editor formControlName="content"></quill-editor></p>
</nz-input-group>
</nz-form-control>
</nz-form-item>
<nz-form-item>
<nz-form-control>
<button nz-button class="login-form-button" [nzType]="'primary'">提交</button>
</nz-form-control>
</nz-form-item>
</form>
这个页面显示一个简单的表单,包括标题、内容和提交按钮。
然后,让我们打开writestory.component.ts文件,编写前端背后的逻辑代码:
//writestory.component.ts
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { CookieService } from 'ngx-cookie-service';
import { StoryService } from '../../service/story.service';
import { storyData } from './storyData';
@Component({
selector: 'app-writestory',
templateUrl: './writestory.component.html',
styleUrls: ['./writestory.component.scss']
})
export class WritestoryComponent implements OnInit {
storyForm = new FormGroup({
title:new FormControl(''),
content:new FormControl(''),
});
newStory:storyData;
author:string;
constructor(private cookie:CookieService,private storyService:StoryService,private router:Router) {
this.author = this.cookie.get('currentuser')
this.newStory = {author:this.author,title:'',content:''}
}
ngOnInit(): void {
}
onSubmit():void{
this.newStory = this.storyForm.value;
this.newStory.author = this.author;
console.log(this.newStory)
this.storyService.writeStory(this.newStory).subscribe((data:any)=>{
if (data)
{
console.log(data['result']);
if (data['result'] == 'Success')
{
this.router.navigateByUrl('/');
}
}
})
}
这里的构造函数包含了一个CookieService对象、一个稍后要实现的StoryService对象和一个Router对象。其中CookieService对象用于获得当前登录的用户名,作为故事的作者;StoryService用于与后端服务器通信,而Router则用来发布故事之后的页面跳转操作。在onSubmit函数中,我们会将表单数据通过service传送到后端服务器,如果服务器返回的结果是Success的话,则重定向到根路径。
现在,让我们编写StoryService服务,它将负责与后端服务器通信。
我们在cmd窗口中输入以下命令,建立storyService:
ng g s service/story
然后,在生成的story.service.ts中输入以下代码:
//story.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { storyData } from '../story/writestory/storyData';
@Injectable({
providedIn: 'root'
})
export class StoryService {
constructor(private http:HttpClient) { }
writeStory(story:storyData):Observable<storyData>{
return this.http.post<storyData>('http://localhost:8000/writestory',story)
}
}
这个服务目前只实现了一个功能:writeStory,其用途是将前端的数据以storyData接口的形式传递到后端服务器,并得到结果。
最后,让我们把writestory组件加入到angular的路由以及首页的菜单中,这样我们就可以通过菜单访问这个页面了:
//app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { WritestoryComponent } from './story/writestory/writestory.component';
const routes: Routes = [
{path:'register',component:RegisterComponent},
{path:'login',component:LoginComponent},
{path:'',component:HomeComponent},
{path:'writestory',component:WritestoryComponent}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
<!--mainlayout.component.html-->
<li nz-menu-item routerLink='/writestory'>书写故事</li>
这样,我们就完成了编写故事的前端部分代码,接下来让我们来看它的后端部分。
后端部分
我们先来处理数据库的部分。我们需要一张新表storys来存储每篇故事,其字段介绍如下:
字段名 | 类型 | 含义 |
---|---|---|
id | Integer | 内部Id,自增 |
author | string | 作者用户名 |
title | string | 标题 |
content | string | 内容 |
publishdate | Date | 发布日期 |
commentCount | Integer | 评论数 |
state | string | 文章状态 |
externalId | string | 外部Id,即别的数据引用该记录的Id |
我们在database目录中建立tblstorys.py文件,输入如下代码:
from database.tablebase import Base
from sqlalchemy import Column,String,Integer,Date,DateTime
class Storys(Base):
__tablename__ = 'storys'
id = Column(Integer,autoincrement=True,primary_key=True)
author = Column(String,nullable=False)
title = Column(String,nullable=False)
content = Column(String,nullable=False)
publishDate = Column(Date)
commentCount = Column(Integer)
state = Column(String)
externalId = Column(String)
def __repr__(self):
return '<storys(author=%s,title=%s,publishDate=%s)>' % (self.auther,self.title,self.publishDate)
再在alembic目录下打开cmd,输入如下命令:
alembic revision -m create storys table
在生成的xxx_create_storys_table.py文件中的upgrade函数中输入以下代码:
# xxx_create_storys_table.py
def upgrade():
op.create_table(
'storys',
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
sa.Column('author', sa.String, nullable=False),
sa.Column('title', sa.String, nullable=False),
sa.Column('content', sa.String, nullable=False),
sa.Column('publishDate', sa.Date),
sa.Column('commentCount', sa.Integer),
sa.Column('state', sa.String, nullable=False),
)
然后再在cmd中执行以下命令,在sqlite数据库中建立storys表:
alembic upgrade head
这样,我们的数据库部分就准备完毕了。
我们在util包下再建立一个story包,再在其中建立storyutil.py文件。我们将在这个文件中实现各种story和comment相关的util函数。
让我们打开storyutil.py文件,实现createStory函数,这个函数将用于编写故事。
# util/story/storyutil.py
from database.dbcore import session
from database.curd import insertdata,deletedata
from database.tblstorys import Storys
from datetime import date,datetime
def createStory(author,title,content):
newstory = Storys(author=author,title=title,content=content,publishDate=date.today(),commentCount=0,state='WaitForApprove')
newstory.externalId = 'doc_' + datetime.now().strftime('%Y%m%d%H%M%S').__str__()
result = insertdata(newstory)
return result
这个函数很简单,使用传入的author、title和content参数在storys表中生成一行记录。其中,publishDate用今天的时间;commentCount默认为0,而state默认为WaitForApprove,为之后的故事审批系统预留好数据;externalId则是用’doc_'加当前时间组成,以确保唯一性。
我们已经实现了编写故事的util函数,现在让我们去实现相关的RequestHandler。我们在apps包下再建立story_comment_app包,再在其中新建story_comment_app.py文件,我们将在这里实现所有与故事和评论相关的RequestHandler。
我们打开story_comment_app.py文件,输入以下代码:
# apps/story_comment_app/story_comment_app.py
from server.apps.basehandler import BaseHandler
from tornado.escape import json_decode
from database.curd import session
import json
from util.story.storyutil import createStory
from database.tblstorys import Storys
class WriteStory(BaseHandler):
def post(self):
ret = self.request.body
req = json_decode(ret)
author = req['author']
title = req['title']
content = req['content']
result = createStory(author,title,content)
returnResult = {'result':result}
returnResult_str = json.dumps(returnResult)
self.write(returnResult_str)
WriteStory只需实现post方法即可,目的是从前端获得author、title和content的值,并传入createStory函数来编写故事。
最后,我们在main.py中将WriteStory加入后端路由:
# main.py
# ...
def make_app():
routelist = [
(r"/register",Register),
(r"/login",Login),
(r"/writestory",WriteStory),
]
# ...
这样,我们就完成了编写故事的后端部分的开发。
在这期博客中,我们建立了一个新模块story,并在其中完成了编写故事功能的开发。在下一期博客中,我们将实现一个故事列表,这个故事列表会将所有故事展示在我们项目的首页;我们还会开始开发评论功能,并最终实现评论树的功能,希望大家继续关注~