Python + React (2)

背景

上篇文章我们已经把 python + react 基本环境搭建出来了,基本环境已有,下面我将仿照某个博客网站来开发一个同样的网站。

基本环境

系统环境: macOS系统

使用python等版本如下

1
2
3
node -v: v11.1.0
npm -v: 6.5.0
python3 --version: 3.6.5

数据库模型配置和升级

模型配置

博客类型网站肯定和用户、文章、评论等相关,flask_appbuilder 已经自带用户模型,因此我们在这基础上添加两个模型 Blog 和 Comment(初版本只为实现基本功能)。其中 Blog 和 Comment 分别关联上用户(ab_user)信息, 除了分别关联上用户信息,Blog 和 Comment 之间实现了一对多关心。如下所示:

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
class Blog(Model, AuditMixinNullable):
"""发布文章"""

__tablename__ = 'blog'
id = Column(Integer, primary_key=True)
title = Column(String(500))
body = Column(Text)
body_html = Column(Text)
user_id = Column(Integer, ForeignKey('ab_user.id'))
owner = relationship(security_manager.user_model, backref='blog', foreign_keys=[user_id])
comments = relationship('Comment', back_populates="blog")

def __repr__(self):
return self.title if self.title else self.id

@property
def simple_json(self):
return {
'id': self.id,
'title': self.title,
'body': self.body,
'body_html': self.body_html,
'timestamp': self.changed_on.isoformat(),
'owner': self.owner.to_json(),
'comments': [comment.simple_json for comment in self.comments]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Comment(Model, AuditMixinNullable):
"""发布文章评论"""

__tablename__ = 'comments'
id = Column(Integer, primary_key=True)
body = Column(Text)
body_html = Column(Text)
user_id = Column(Integer, ForeignKey('ab_user.id'))
owner = relationship(security_manager.user_model, backref='comments', foreign_keys=[user_id])
blog_id = Column(Integer, ForeignKey('blog.id'))
blog = relationship(Blog, back_populates="comments")
disabled = Column(Boolean, default=False)

def __repr__(self):
return self.body if self.body else self.body_html

@property
def simple_json(self):
return {
'id': self.id,
'body': self.body,
'timestamp': self.changed_on.isoformat(),
'owner': self.owner.to_json()
}

这两个模型我们都引入了一个 AuditMixinNullable,该模块继承于 flask_appbuilder 中的 AuditMixin,我们调整了 nullable 字段,使其允许使用空字段 (参考 superset), 这样我们在生成 Blog 和 Comment 模型时会生成 created_on、changed_on、created_by_fk 和changed_by_fk 四个字段。我们在 models 下新建一个helpers.py,将 AuditMixinNullable 放入该文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from datetime import datetime

from flask_appbuilder.models.mixins import AuditMixin
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declared_attr

class AuditMixinNullable(AuditMixin):

created_on = sa.Column(sa.DateTime, default=datetime.now, nullable=True)
changed_on = sa.Column(sa.DateTime, default=datetime.now, onupdate=datetime.now, nullable=True)

@declared_attr
def created_by_fk(self):
return sa.Column(
sa.Integer,sa.ForeignKey("ab_user.id"),default=self.get_user_id,nullable=True,
)

@declared_attr
def changed_by_fk(self):
return sa.Column(
sa.Integer,sa.ForeignKey("ab_user.id"),default=self.get_user_id,onupdate=self.get_user_id,nullable=True,
)

调整后的目录结构如下所示:

模型升级

以上配置都完成后我们开始升级数据库,我们进入到项目对应目录下,启动虚拟环境并设置环境变量,然后执行 flask db migrate 命令,如下图所示:

执行完成后我们会在 migration/versions/ 目录下发现新生成的一个文件,检查该文件内容,确定是否需要作出调整(一般是不需要更改)然后我们执行下面命令

1
flask db upgrade

以上命令都执行完成后,数据库就生成了对应的 blog 和 comment 两张表。

网站建立

样式抓取

用 Google 浏览器打开需要抓取网站的地址,使用 Command + S 快捷键保存该页面内容到本地文件下(其他浏览器保存方式自行查找)。我们发现文件下会出现一个 .htm 结尾的文件和一个 _files 结尾的文件夹。打开文件夹,我们发现里面大多是是图片,删除里面除 .css 和 .js 结尾的其他文件。如下所示:

抓取下来的 .htm 和 .css 文件内容都是错位的,为了方便查看我们使用工具将这两个文件分别格式化。这样就完成了该页面样式的抓取。

页面分析

用浏览器打开 .htm 文件,如下图所示:

分析该网站发现该页面除了导航栏主要分左右两个模块,左侧由头部轮播和文章列表两部分组成,右侧主要由banner和推荐作者两个模块组成。第一个版本我们只做文章列表和推荐作者这两个模块。

页面搭建

首页对应我们的 assets/src/welcome 模块,从上面分析知该模块我们需要的文章列表和推荐用户列表两大数据。

App.js

新增 blogList 和 recommendUsers 两大数据源,并将数据传入到 welcome.jsx 模块 具体代码如下所示:

App.js

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
const container = document.getElementById('app');
const bootstrap = JSON.parse(container.getAttribute('data-bootstrap'));
/** 当前用户信息 **/
const user = { ...bootstrap.user };
/** 博客列表 **/
const blogList = { ...bootstrap.blogList };
/** 推荐用户列表 **/
const recommendUsers = { ...bootstrap.recommendUsers };

const store = createStore(
combineReducers({
messageToasts: messageToastReducer,
}),
{},
compose(
applyMiddleware(thunk),
initEnhancer(false),
),
);

const App = () => (
<Provider store={store}>
<Welcome user={user} blogList={blogList} recommendUsers={recommendUsers}/>
</Provider>
);
export default hot(module)(App);

Welcome.jsx

我们从 App.jsx 获取到数据后,我们进入主页面搭建过程,分析 .htm 文件我们知道我们需要两个模块的数据在 container index 中, 而博客列表数据在画红线的 col-xs-16 main 中,推荐用户列表在画蓝线的 col-xs-7 col-xs-offset-1 aside 中 如下图所示:

我们将博客列表拆分到 BlogList 中并将博客数据传入该模块,同理推荐用户拆分到 RecommendList 中并把推荐用户数据传入该模块,Welcome.jsx 中代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
render() {
const blogList = this.props.blogList;
const recommendUsers = this.props.recommendUsers;
const user = this.props.user;
return (
<div className="container index">
<div className="row">
<BlogList blogList={blogList} user={user} />
<RecommendList recommendUsers={recommendUsers} />
</div>
</div>
);
}

BlogList.jsx、BlogListRow.jsx、BlogListRowContent.jsx

现在我们分析 .htm 中 col-xs-16 main 模块,该模块主要是博客内容列表,这个版本我们不考虑头部的轮播图,因此我们直接找到 list-container 下的 note-list, 该模块由多个 li 组成,观察 li 的内容我们发现,该模块主要有两种类型,一种是带图片的,一种是无图的,核心样式都是一样的,因此我们可以拆分一个 BlogListRow.jsx ,在这个类中去区分有无图片这个问题。将核心的列表样式都放到 BlogListRowContent.jsx,这样blog这个模块的代码基本就可以确定下来了如下所示:

BlogList.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render() {
const blogList = this.props.blogList;
let data = Object.keys(blogList).map( key => blogList[key]);
return (
<div className="col-xs-16 main">
<div className="split-line"> </div>
<div id="list-container">
<ul className="note-list">
{data.map( (blog) => (
<BlogListRow key={blog.id} blog={blog} user={this.props.user} />)
)}
</ul>
</div>
</div>
)
}

BlogListRow.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
render() {
const blog = this.props.blog;
/** list has image **/
const hasImg = false;
if (hasImg) {
return (
<li className="hav-img">
<a className="wrap-img" href="#">
<img src="" className="img-blur" alt="120"/>
</a>
<BlogListRowContent blog={blog} user={this.props.user} />
</li>
)
} else {
return (
<li>
<BlogListRowContent blog={blog} user={this.props.user} />
</li>
)
}
}

BlogListRowContent.jsx

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
render() {
const blog = this.props.blog;
const owner = blog.owner;
return (
<div className="content">
<a className="title" href="">
{blog.title}
</a>
<p className="abstract">
{blog.body}
</p>
<div className="meta">
<a className="nickname" href="">
{owner.username}
</a>
<a href="">
<i className="iconfont ic-list-comments"> </i> 7
</a>
<span>
<i className="iconfont ic-list-like"> </i> 10
</span>
</div>
</div>
)
}

我们只是把 .htm 的样式代码部分复制过来了,但是核心的 css 样式还没有引入到项目中,我们可以在 assets/stylesheets/ 目录下我们新建一个 welcome.css 文件来管理我们的 css 样式,打开我们刚刚保存的网站样式我们看到里面有两个 css 结尾的文件,我们打开以 entry 开头的 css 文件,发现里面有 .note-list.recommended-authors 等一些样式,这些刚好和我们刚刚 blog 里面的代码样式能对应上,因此我们将该内容粘贴到 welcome.css 样式中,在打开 web 开头到文件,里面有 1w+ 行代码,粗略看了下,这里面的样式和页面的基本样式有关,因此我们将该文件内容放入基本样式文件 ddblog.less 中,并在 theme.js文件中引入该文件(后期在做调整)。这样首页的样式就配置成功了,最后我们在 welcome.jsx 文件中引入刚刚配置的 welcome.css 文件。

RecommendList.jsx RecommendRowContent.jsx

推荐用户部分的可以参考上面部分完成具体内容如下:

RecommendList.jsx

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
render() {
const recommendUsers = this.props.recommendUsers;
if (recommendUsers == null || recommendUsers.length == 0) {
return (
<div> </div>
)
}
let data = Object.keys(recommendUsers).map( key => recommendUsers[key]);
return (
<div className="col-xs-7 col-xs-offset-1 aside">
<div className="recommended-author-wrap">
<div className="recommended-authors">
<div className="title">
<span>{"推荐作者"}</span>
<a className="page-change">
<i className="iconfont ic-search-change"> {"换一批"} </i>
</a>
</div>
<ul className="list">
{data.map( (user) => (
<RecommendRowContent key={user.id} user={user} />)
)}
</ul>
<a href="" className="find-more">
{"查看全部"}
</a>
</div>
</div>
</div>
);

RecommendRowContent.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
render() {
const user = this.props.user;
return (
<li>
<a href="" className="avatar">
</a>
<a className="follow">
<i className="iconfont ic-follow"> </i> {"关注"}
</a>
<a href="" className="name">
{user.username}
</a>
<p>
{"2.1k喜欢"}
</p>
</li>
);
}

npm 打包

以上操作都完成后,我们运行以下命令:

1
npm run dev

如果没有出现任何红色警告或者提示,那么恭喜你可以进入下一步。

数据提供

上面我们已经把首页的样式完成,现在我们需要为首页提供数据。首页的入口我们上次配置在 views/core.py 文件中,打开该文件找到 welcome 接口, 新增查找博客和用户列表数据库操作,具体代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@expose('/welcome')
def welcome(self):

blog_list = (
db.session.query(models.Blog).all()
)
if blog_list:
blog_list = [blog.simple_json for blog in blog_list]
recommend_list = (
db.session.query(security_manager.user_model).all()
)
if recommend_list:
recommend_list = [recommend.to_json() for recommend in recommend_list]
payload = {
'common': self.common_bootstrap_payload(),
'user': bootstrap_user_data(),
'blogList': blog_list or [],
'recommendUsers': recommend_list or []
}
return self.render_template(
'blog/basic.html',
entry='welcome',
bootstrap_data=json.dumps(payload)
)

注意 payload 中的 key(blogList,recommendUsers,user) 需要和 App.js 中的相对应。

小结

以上都配置都没出现错误,我们就可以成功启动项目了。附上一张成功启动的图