Python + React (2)

原文地址

背景

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

基本环境

系统环境: macOS系统

使用python等版本如下

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 之间实现了一对多关心。如下所示:

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]
        }
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 放入该文件

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,
        )

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

38-1.png

模型升级

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

38-2.png

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

flask db upgrade

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

网站建立

样式抓取

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

38-3.png

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

页面分析

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

38-4.png

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

页面搭建

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

App.js

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

App.js

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 中 如下图所示:

38-5.png

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

  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

  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

  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

  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

  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

  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 打包

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

npm run dev

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

数据提供

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

    @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 中的相对应。

小结

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

38-6.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容