Flask+React 的全栈开发和部署

Posted Python之美

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flask+React 的全栈开发和部署相关的知识,希望对你有一定的参考价值。


作者:  FunHacks

原文链接:

已授权发布


简介

我有时在 Web 上浏览信息时,会浏览 ,  和  等技术社区的资讯或文章,但觉得逐个去看很费时又不灵活。后来我发现国外有一款叫  的产品,它聚合了互联网大多数领域的信息,使用起来确实很不错,唯一的遗憾就是没有互联网中文领域的信息,于是我就萌生了一个想法:写个爬虫,把经常看的网站的资讯爬下来,并显示出来。

有了想法,接下来就是要怎么实现的问题了。虽然有不少解决方法,但后来为了尝试使用 React,就采用了 Flask + React + Redux 的技术栈。其中:

  • Flask 用于在后台提供 api 服务

  • React 用于构建 UI

  • Redux 用于数据流管理

目前项目已经实现了基本功能,项目源码:。目前界面大概如下:

Flask+React 的全栈开发和部署

前端开发

前端的开发主要涉及两大部分:React 和 Redux,React 作为「显示层」(View layer) 用,Redux 作为「数据层」(Model layer) 用。

我们先总体了解一下 React+Redux 的基本工作流程,一图胜千言(该说的基本都在图里面了):

Flask+React 的全栈开发和部署

我们可以看到,整个数据流是单向循环的:

 
   
   
 
  1. Store -> View layer -> Action -> Reducer

  2. ^                                   |

  3. |                                   |

  4. --------- 返回新的 State -------------

Store表示「存放状态」;view layer:表示「显示状态」;Reducer:表示「处理动作」。

其中:

  1. React 提供应用的 View 层,表现为组件,分为容器组件(container)和普通显示组件(component);

  2. Redux 包含三个部分:Action,Reducer 和 Store:

    • Action 本质上是一个 JS 对象,它至少需要一个元素:type,用于标识 action;

    • Middleware(中间件)用于在 Action 发起之后,到达 Reducer 之前做一些操作,比如异步 Action,Api 请求等;

    • Reducer 是一个函数:(previousState, action) => newState,可理解为动作的处理中心,处理各种动作并生成新的 state,返回给 Store;

    • Store 是整个应用的状态管理中心,容器组件可以从 Store 中获取所需要的状态; 项目前端的源码在 client 目录中,下面是一些主要的目录:

 
   
   
 
  1. client

  2.    ├── actions        # 各种 action

  3.    ├── components     # 普通显示组件

  4.    ├── containers     # 容器组件

  5.    ├── middleware     # 中间间,用于 api 请求

  6.    ├── reducers       # reducer 文件

  7.    ├── store          # store 配置文件

React 开发

React 部分的开发主要涉及 container 和 component:

  • container 负责接收 store 中的 state 和发送 action,一般和 store 直接连接;

  • component 位于 container 的内部,它们一般不和 store 直接连接,而是从父组件 container 获取数据作为 props,所有操作也是通过回调完成,component 一般会多次使用;

在本项目中,container 对应的原型如下:

Flask+React 的全栈开发和部署

而 component 则主要有两个:一个是选择组件,一个是信息显示组件,如下:

Flask+React 的全栈开发和部署

这些 component 会被多次使用。

下面,我们主要看一下容器组件 (对应 App.js) 的代码(只显示部分重要的代码):

 
   
   
 
  1. import React, { Component, PropTypes } from 'react';

  2. import { connect } from 'react-redux';

  3. import Posts from '../../components/Posts/Posts';

  4. import Picker from '../../components/Picker/Picker';

  5. import { fetchNews, selectItem } from '../../actions';

  6. require('./App.scss');

  7. class App extends Component {

  8.  constructor(props) {

  9.    super(props);

  10.    this.handleChange = this.handleChange.bind(this);

  11.  }

  12.  componentDidMount() {

  13.    for (const value of this.props.selectors) {

  14.      this.props.dispatch(fetchNews(value.item, value.boardId));

  15.    }

  16.  }

  17.  componentWillReceiveProps(nextProps) {

  18.    for (const value of nextProps.selectors) {

  19.      if (value.item !== this.props.selectors[value.boardId].item) {

  20.        nextProps.dispatch(fetchNews(value.item, value.boardId));

  21.      }

  22.    }

  23.  }

  24.  handleChange(nextItem, id) {

  25.    this.props.dispatch(selectItem(nextItem, id));

  26.  }

  27.  render() {

  28.    const boards = [];

  29.    for (const value of this.props.selectors) {

  30.      boards.push(value.boardId);

  31.    }

  32.    const options = ['Github', 'Hacker News', 'Segment Fault', '开发者头条', '伯乐头条'];

  33.    return (

  34.      <div className="mega">

  35.        <main>

  36.          <div className="desk-container">

  37.            {

  38.              boards.map((board, i) =>

  39.                <div className="desk" style={{ opacity: 1 }} key={i}>

  40.                  <Picker value={this.props.selectors[board].item}

  41.                    onChange={this.handleChange}

  42.                    options={options}

  43.                    id={board}

  44.                  />

  45.                  <Posts

  46.                    isFetching={this.props.news[board].isFetching}

  47.                    postList={this.props.news[board].posts}

  48.                    id={board}

  49.                  />

  50.                </div>

  51.              )

  52.            }

  53.          </div>

  54.        </main>

  55.      </div>

  56.    );

  57.  }

  58. }

  59. function mapStateToProps(state) {

  60.  return {

  61.    news: state.news,

  62.    selectors: state.selectors,

  63.  };

  64. }

  65. export default connect(mapStateToProps)(App);

其中,

  • constructor(props) 是一个构造函数,在创建组件的时候会被调用一次;

  • componentDidMount() 这个方法在组件加载完毕之后会被调用一次;

  • componentWillReceiveProps() 这个方法在组件接收到一个新的 prop 时会被执行;

上面这几个函数是组件生命周期(react component lifecycle)函数,更多的组件生命周期函数可在此查看。

  • react-redux 这个库的作用从名字就可看出,它用于连接 react 和 redux,也就是连接容器组件和 store;

  • mapStateToProps 这个函数用于建立一个从(外部的)state 对象到 UI 组件的 props 对象的映射关系,它会订阅 Store 中的 state,每当有 state 更新时,它就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染;

Redux 开发

上文说过,Redux 部分的开发主要包含:action,reducer 和 store,其中,store 是应用的状态管理中心,当收到新的 state 时,会触发组件重新渲染,reducer 是应用的动作处理中心,负责处理动作并产生新的状态,将其返回给 store。

在本项目中,有两个 action,一个是站点选择(如 Github,Hacker News),另一个是信息获取,action 的部分代码如下:

 
   
   
 
  1. export const FETCH_NEWS = 'FETCH_NEWS';

  2. export const SELECT_ITEM = 'SELECT_ITEM';

  3. export function selectItem(item, id) {

  4.  return {

  5.    type: SELECT_ITEM,

  6.    item,

  7.    id,

  8.  };

  9. }

  10. export function fetchNews(item, id) {

  11.  switch (item) {

  12.    case 'Github':

  13.      return {

  14.        type: FETCH_NEWS,

  15.        api: `/api/github/repo_list`,

  16.        method: 'GET',

  17.        id,

  18.      };

  19.    case 'Segment Fault':

  20.      return {

  21.        type: FETCH_NEWS,

  22.        api: `/api/segmentfault/blogs`,

  23.        method: 'GET',

  24.        id,

  25.      };

  26.    default:

  27.      return {};

  28.  }

  29. }

可以看到,action 就是一个普通的 JS 对象,它有一个属性 type 是必须的,用来标识 action。

reducer 是一个含有 switch 的函数,接收当前 state 和 action 作为参数,返回一个新的 state,比如:

 
   
   
 
  1. import { SELECT_ITEM } from '../actions';

  2. import _ from 'lodash';

  3. const initialState = [

  4.  {

  5.    item: 'Github',

  6.    boardId: 0,

  7.  },

  8.  {

  9.    item: 'Hacker News',

  10.    boardId: 1,

  11.  }

  12. ];

  13. export default function reducer(state = initialState, action = {}) {

  14.  switch (action.type) {

  15.    case SELECT_ITEM:

  16.      return _.sortBy([

  17.        {

  18.          item: action.item,

  19.          boardId: action.id,

  20.        },

  21.        ...state.filter(element =>

  22.            element.boardId !== action.id

  23.        ),

  24.      ], 'boardId');

  25.    default:

  26.      return state;

  27.  }

  28. }

再来看一下 store:

 
   
   
 
  1. import { createStore, applyMiddleware, compose } from 'redux';

  2. import thunk from 'redux-thunk';

  3. import api from '../middleware/api';

  4. import rootReducer from '../reducers';

  5. const finalCreateStore = compose(

  6.  applyMiddleware(thunk),

  7.  applyMiddleware(api)

  8. )(createStore);

  9. export default function configureStore(initialState) {

  10.  return finalCreateStore(rootReducer, initialState);

  11. }

其中,「applyMiddleware() 」用于告诉 redux 需要用到那些中间件,比如异步操作需要用到 thunk 中间件,还有 api 请求需要用到我们自己写的中间件。

后端开发

后端的开发主要是爬虫,目前的爬虫比较简单,基本上是静态页面的爬虫,主要就是 html 解析和提取。如果要爬取稀土掘金和知乎专栏等网站,可能会涉及到登录验证,抵御反爬虫等机制,后续也将进一步开发。

后端的代码在 server 目录:

 
   
   
 
  1.    ├── __init__.py

  2.    ├── app.py            # 创建 app

  3.    ├── configs.py        # 配置文件

  4.    ├── controllers       # 提供 api 服务

  5.    └── spiders           # 爬虫文件夹,几个站点的爬虫

后端通过 Flask 以 api 的形式给前端提供数据,下面是部分代码:

 
   
   
 
  1. # -*- coding: utf-8 -*-

  2. import flask

  3. from flask import jsonify

  4. from server.spiders.github_trend import GitHubTrend

  5. from server.spiders.toutiao import Toutiao

  6. from server.spiders.segmentfault import SegmentFault

  7. from server.spiders.jobbole import Jobbole

  8. news_bp = flask.Blueprint(

  9.    'news',

  10.    __name__,

  11.    url_prefix='/api'

  12. )

  13. @news_bp.route('/github/repo_list', methods=['GET'])

  14. def get_github_trend():

  15.    gh_trend = GitHubTrend()

  16.    gh_trend_list = gh_trend.get_trend_list()

  17.    return jsonify(

  18.        message='OK',

  19.        data=gh_trend_list

  20.    )

  21. @news_bp.route('/toutiao/posts', methods=['GET'])

  22. def get_toutiao_posts():

  23.    toutiao = Toutiao()

  24.    post_list = toutiao.get_posts()

  25.    return jsonify(

  26.        message='OK',

  27.        data=post_list

  28.    )

  29. @news_bp.route('/segmentfault/blogs', methods=['GET'])

  30. def get_segmentfault_blogs():

  31.    sf = SegmentFault()

  32.    blogs = sf.get_blogs()

  33.    return jsonify(

  34.        message='OK',

  35.        data=blogs

  36.    )

  37. @news_bp.route('/jobbole/news', methods=['GET'])

  38. def get_jobbole_news():

  39.    jobbole = Jobbole()

  40.    blogs = jobbole.get_news()

  41.    return jsonify(

  42.        message='OK',

  43.        data=blogs

  44.    )

部署

本项目的部署采用 nginx+gunicorn+supervisor 的方式,其中:

  1. nginx 用来做反向代理服务器:通过接收 Internet 上的连接请求,将请求转发给内网中的目标服务器,再将从目标服务器得到的结果返回给 Internet 上请求连接的客户端(比如浏览器);

  2. gunicorn 是一个高效的 Python WSGI Server,我们通常用它来运行 WSGI (Web Server Gateway Interface,Web 服务器网关接口) 应用(比如本项目的 Flask 应用);

  3. supervisor 是一个进程管理工具,可以很方便地启动、关闭和重启进程等; 项目部署需要用到的文件在 deploy 目录下:

 
   
   
 
  1. deploy

  2.    ├── fabfile.py          # 自动部署脚本

  3.    ├── nginx.conf          # nginx 通用配置文件

  4.    ├── nginx_geekvi.conf   # 站点配置文件

  5.    └── supervisor.conf     # supervisor 配置文件

本项目采用了 Fabric 自动部署神器,它允许我们不用直接登录服务器就可以在本地执行远程操作,比如安装软件,删除文件等。

fabfile.py 文件的部分代码如下:

 
   
   
 
  1. # -*- coding: utf-8 -*-

  2. import os

  3. from contextlib import contextmanager

  4. from fabric.api import run, env, sudo, prefix, cd, settings, local, lcd

  5. from fabric.colors import green, blue

  6. from fabric.contrib.files import exists

  7. env.hosts = ['deploy@111.222.333.44:12345']

  8. env.key_filename = '~/.ssh/id_rsa'

  9. # env.password = '12345678'

  10. # path on server

  11. DEPLOY_DIR = '/home/deploy/www'

  12. PROJECT_DIR = os.path.join(DEPLOY_DIR, 'react-news-board')

  13. CONFIG_DIR = os.path.join(PROJECT_DIR, 'deploy')

  14. LOG_DIR = os.path.join(DEPLOY_DIR, 'logs')

  15. VENV_DIR = os.path.join(DEPLOY_DIR, 'venv')

  16. VENV_PATH = os.path.join(VENV_DIR, 'bin/activate')

  17. # path on local

  18. PROJECT_LOCAL_DIR = '/Users/Ethan/Documents/Code/react-news-board'

  19. GITHUB_PATH = 'https://github.com/ethan-funny/react-news-board'

  20. @contextmanager

  21. def source_virtualenv():

  22.    with prefix("source {}".format(VENV_PATH)):

  23.        yield

  24. def build():

  25.    with lcd("{}/client".format(PROJECT_LOCAL_DIR)):

  26.        local("npm run build")

  27. def deploy():

  28.    print green("Start to Deploy the Project")

  29.    print green("=" * 40)

  30.    # 1. Create directory

  31.    print blue("create the deploy directory")

  32.    print blue("*" * 40)

  33.    mkdir(path=DEPLOY_DIR)

  34.    mkdir(path=LOG_DIR)

  35.    # 2. Get source code

  36.    print blue("get the source code from remote")

  37.    print blue("*" * 40)

  38.    with cd(DEPLOY_DIR):

  39.        with settings(warn_only=True):

  40.            rm(path=PROJECT_DIR)

  41.        run("git clone {}".format(GITHUB_PATH))

  42.    # 3. Install python virtualenv

  43.    print blue("install the virtualenv")

  44.    print blue("*" * 40)

  45.    sudo("apt-get install python-virtualenv")

  46.    # 4. Install nginx

  47.    print blue("install the nginx")

  48.    print blue("*" * 40)

  49.    sudo("apt-get install nginx")

  50.    sudo("cp {}/nginx.conf /etc/nginx/".format(CONFIG_DIR))

  51.    sudo("cp {}/nginx_geekvi.conf /etc/nginx/sites-enabled/".format(CONFIG_DIR))

  52.    # 5. Install python requirements

  53.    with cd(DEPLOY_DIR):

  54.        if not exists(VENV_DIR):

  55.            run("virtualenv {}".format(VENV_DIR))

  56.        with settings(warn_only=True):

  57.            with source_virtualenv():

  58.                sudo("pip install -r {}/requirements.txt".format(PROJECT_DIR))

  59.    # 6. Config supervisor

  60.    sudo("supervisord -c {}/supervisor.conf".format(CONFIG_DIR))

  61.    sudo("supervisorctl -c {}/supervisor.conf reload".format(CONFIG_DIR))

  62.    sudo("supervisorctl -c {}/supervisor.conf status".format(CONFIG_DIR))

  63.    sudo("supervisorctl -c {}/supervisor.conf start all".format(CONFIG_DIR))

 
   
   
 
  1. $ fab build

当然,你也可以直接到 client 目录下,运行命令:

 
   
   
 
  1. $ npm run build

如果构建没有出现错误,就可以进行部署了,在 deploy 目录使用如下命令进行部署:

 
   
   
 
  1. $ fab deploy

总结

本项目前端使用 React+Redux,后端使用 Flask,这也算是一种比较典型的开发方式了,当然,你也可以使用 Node.js 来做后端。

前端的开发需要知道数据的流向:

Flask+React 的全栈开发和部署

后端的开发主要是爬虫,Flask 在本项目只是作为一个后台框架,对外提供 api 服务;

参考资料

广告时间:欢迎参与我的首次知乎Live:「Python 工程师的入门和进阶」 https://www.zhihu.com/lives/789840559912009728, 可以通过下面的二维码直接进入:




以上是关于Flask+React 的全栈开发和部署的主要内容,如果未能解决你的问题,请参考以下文章

一款基于SSM框架技术的全栈Java web项目(已部署可直接体验)

2019,全栈开发者应该学些什么

revel golang的全栈开发框架

mk-js,一个基于reactnodejs的全栈框架

flask全栈开发3 模板

我的全栈之路-Java基础之Java概述与开发环境搭建