我有時在 Web 上瀏覽信息時,會瀏覽 Github Trending, Hacker News 和 稀土掘金 等技術社區的資訊或文章,但以爲逐個去看很費時又不靈活。後來我發現國外有一款叫 Panda 的產品,它聚合了互聯網大多數領域的信息,使用起來確實很不錯,惟一的遺憾就是沒有互聯網中文領域的信息,因而我就萌生了一個想法:寫個爬蟲,把常常看的網站的資訊爬下來,並顯示出來。css
有了想法,接下來就是要怎麼實現的問題了。雖然有很多解決方法,但後來爲了嘗試使用 React,就採用了 Flask + React + Redux 的技術棧。其中:html
Flask 用於在後臺提供 api 服務前端
React 用於構建 UIpython
Redux 用於數據流管理react
目前項目已經實現了基本功能,項目源碼:Github 地址。目前界面大概以下:nginx
前端的開發主要涉及兩大部分:React 和 Redux,React 做爲「顯示層」(View layer) 用,Redux 做爲「數據層」(Model layer) 用。git
咱們先整體瞭解一下 React+Redux 的基本工做流程,一圖勝千言(該說的基本都在圖裏面了):github
咱們能夠看到,整個數據流是單向循環的:npm
Store(存放狀態) -> View layer(顯示狀態) -> Action -> Reducer(處理動做) ^ | | | --------------------返回新的 State-------------------------
其中:json
React 提供應用的 View 層,表現爲組件,分爲容器組件(container)和普通顯示組件(component);
Redux 包含三個部分:Action,Reducer 和 Store:
Action 本質上是一個 JS 對象,它至少須要一個元素:type,用於標識 action;
Middleware(中間件)用於在 Action 發起以後,到達 Reducer 以前作一些操做,好比異步 Action,Api 請求等;
Reducer 是一個函數:(previousState, action) => newState
,可理解爲動做的處理中心,處理各類動做並生成新的 state,返回給 Store;
Store 是整個應用的狀態管理中心,容器組件能夠從 Store 中獲取所須要的狀態;
項目前端的源碼在 client 目錄中,下面是一些主要的目錄:
client ├── actions # 各類 action ├── components # 普通顯示組件 ├── containers # 容器組件 ├── middleware # 中間間,用於 api 請求 ├── reducers # reducer 文件 ├── store # store 配置文件
React 部分的開發主要涉及 container 和 component:
container 負責接收 store 中的 state 和發送 action,通常和 store 直接鏈接;
component 位於 container 的內部,它們通常不和 store 直接鏈接,而是從父組件 container 獲取數據做爲 props,全部操做也是經過回調完成,component 通常會屢次使用;
在本項目中,container 對應的原型以下:
而 component 則主要有兩個:一個是選擇組件,一個是信息顯示組件,以下:
這些 component 會被屢次使用。
下面,咱們主要看一下容器組件 (對應 App.js) 的代碼(只顯示部分重要的代碼):
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import Posts from '../../components/Posts/Posts'; import Picker from '../../components/Picker/Picker'; import { fetchNews, selectItem } from '../../actions'; require('./App.scss'); class App extends Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } componentDidMount() { for (const value of this.props.selectors) { this.props.dispatch(fetchNews(value.item, value.boardId)); } } componentWillReceiveProps(nextProps) { for (const value of nextProps.selectors) { if (value.item !== this.props.selectors[value.boardId].item) { nextProps.dispatch(fetchNews(value.item, value.boardId)); } } } handleChange(nextItem, id) { this.props.dispatch(selectItem(nextItem, id)); } render() { const boards = []; for (const value of this.props.selectors) { boards.push(value.boardId); } const options = ['Github', 'Hacker News', 'Segment Fault', '開發者頭條', '伯樂頭條']; return ( <div className="mega"> <main> <div className="desk-container"> { boards.map((board, i) => <div className="desk" style={{ opacity: 1 }} key={i}> <Picker value={this.props.selectors[board].item} onChange={this.handleChange} options={options} id={board} /> <Posts isFetching={this.props.news[board].isFetching} postList={this.props.news[board].posts} id={board} /> </div> ) } </div> </main> </div> ); } } function mapStateToProps(state) { return { news: state.news, selectors: state.selectors, }; } 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 部分的開發主要包含:action,reducer 和 store,其中,store 是應用的狀態管理中心,當收到新的 state 時,會觸發組件從新渲染,reducer 是應用的動做處理中心,負責處理動做併產生新的狀態,將其返回給 store。
在本項目中,有兩個 action,一個是站點選擇(如 Github,Hacker News),另外一個是信息獲取,action 的部分代碼以下:
export const FETCH_NEWS = 'FETCH_NEWS'; export const SELECT_ITEM = 'SELECT_ITEM'; export function selectItem(item, id) { return { type: SELECT_ITEM, item, id, }; } export function fetchNews(item, id) { switch (item) { case 'Github': return { type: FETCH_NEWS, api: `/api/github/repo_list`, method: 'GET', id, }; case 'Segment Fault': return { type: FETCH_NEWS, api: `/api/segmentfault/blogs`, method: 'GET', id, }; default: return {}; } }
能夠看到,action 就是一個普通的 JS 對象,它有一個屬性 type
是必須的,用來標識 action。
reducer 是一個含有 switch 的函數,接收當前 state 和 action 做爲參數,返回一個新的 state,好比:
import { SELECT_ITEM } from '../actions'; import _ from 'lodash'; const initialState = [ { item: 'Github', boardId: 0, }, { item: 'Hacker News', boardId: 1, } ]; export default function reducer(state = initialState, action = {}) { switch (action.type) { case SELECT_ITEM: return _.sortBy([ { item: action.item, boardId: action.id, }, ...state.filter(element => element.boardId !== action.id ), ], 'boardId'); default: return state; } }
再來看一下 store:
import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; import api from '../middleware/api'; import rootReducer from '../reducers'; const finalCreateStore = compose( applyMiddleware(thunk), applyMiddleware(api) )(createStore); export default function configureStore(initialState) { return finalCreateStore(rootReducer, initialState); }
其中,applyMiddleware()
用於告訴 redux 須要用到那些中間件,好比異步操做須要用到 thunk 中間件,還有 api 請求須要用到咱們本身寫的中間件。
後端的開發主要是爬蟲,目前的爬蟲比較簡單,基本上是靜態頁面的爬蟲,主要就是 HTML 解析和提取。若是要爬取稀土掘金和知乎專欄等網站,可能會涉及到登陸驗證,抵禦反爬蟲等機制,後續也將進一步開發。
後端的代碼在 server 目錄:
server ├── __init__.py ├── app.py # 建立 app ├── configs.py # 配置文件 ├── controllers # 提供 api 服務 └── spiders # 爬蟲文件夾,幾個站點的爬蟲
後端經過 Flask 以 api 的形式給前端提供數據,下面是部分代碼:
# -*- coding: utf-8 -*- import flask from flask import jsonify from server.spiders.github_trend import GitHubTrend from server.spiders.toutiao import Toutiao from server.spiders.segmentfault import SegmentFault from server.spiders.jobbole import Jobbole news_bp = flask.Blueprint( 'news', __name__, url_prefix='/api' ) @news_bp.route('/github/repo_list', methods=['GET']) def get_github_trend(): gh_trend = GitHubTrend() gh_trend_list = gh_trend.get_trend_list() return jsonify( message='OK', data=gh_trend_list ) @news_bp.route('/toutiao/posts', methods=['GET']) def get_toutiao_posts(): toutiao = Toutiao() post_list = toutiao.get_posts() return jsonify( message='OK', data=post_list ) @news_bp.route('/segmentfault/blogs', methods=['GET']) def get_segmentfault_blogs(): sf = SegmentFault() blogs = sf.get_blogs() return jsonify( message='OK', data=blogs ) @news_bp.route('/jobbole/news', methods=['GET']) def get_jobbole_news(): jobbole = Jobbole() blogs = jobbole.get_news() return jsonify( message='OK', data=blogs )
本項目的部署採用 nginx+gunicorn+supervisor
的方式,其中:
nginx 用來作反向代理服務器:經過接收 Internet 上的鏈接請求,將請求轉發給內網中的目標服務器,再將從目標服務器獲得的結果返回給 Internet 上請求鏈接的客戶端(好比瀏覽器);
gunicorn 是一個高效的 Python WSGI Server,咱們一般用它來運行 WSGI (Web Server Gateway Interface,Web 服務器網關接口) 應用(好比本項目的 Flask 應用);
supervisor 是一個進程管理工具,能夠很方便地啓動、關閉和重啓進程等;
項目部署須要用到的文件在 deploy 目錄下:
deploy ├── fabfile.py # 自動部署腳本 ├── nginx.conf # nginx 通用配置文件 ├── nginx_geekvi.conf # 站點配置文件 └── supervisor.conf # supervisor 配置文件
本項目採用了 Fabric 自動部署神器,它容許咱們不用直接登陸服務器就能夠在本地執行遠程操做,好比安裝軟件,刪除文件等。
fabfile.py
文件的部分代碼以下:
# -*- coding: utf-8 -*- import os from contextlib import contextmanager from fabric.api import run, env, sudo, prefix, cd, settings, local, lcd from fabric.colors import green, blue from fabric.contrib.files import exists env.hosts = ['deploy@111.222.333.44:12345'] env.key_filename = '~/.ssh/id_rsa' # env.password = '12345678' # path on server DEPLOY_DIR = '/home/deploy/www' PROJECT_DIR = os.path.join(DEPLOY_DIR, 'react-news-board') CONFIG_DIR = os.path.join(PROJECT_DIR, 'deploy') LOG_DIR = os.path.join(DEPLOY_DIR, 'logs') VENV_DIR = os.path.join(DEPLOY_DIR, 'venv') VENV_PATH = os.path.join(VENV_DIR, 'bin/activate') # path on local PROJECT_LOCAL_DIR = '/Users/Ethan/Documents/Code/react-news-board' GITHUB_PATH = 'https://github.com/ethan-funny/react-news-board' @contextmanager def source_virtualenv(): with prefix("source {}".format(VENV_PATH)): yield def build(): with lcd("{}/client".format(PROJECT_LOCAL_DIR)): local("npm run build") def deploy(): print green("Start to Deploy the Project") print green("=" * 40) # 1. Create directory print blue("create the deploy directory") print blue("*" * 40) mkdir(path=DEPLOY_DIR) mkdir(path=LOG_DIR) # 2. Get source code print blue("get the source code from remote") print blue("*" * 40) with cd(DEPLOY_DIR): with settings(warn_only=True): rm(path=PROJECT_DIR) run("git clone {}".format(GITHUB_PATH)) # 3. Install python virtualenv print blue("install the virtualenv") print blue("*" * 40) sudo("apt-get install python-virtualenv") # 4. Install nginx print blue("install the nginx") print blue("*" * 40) sudo("apt-get install nginx") sudo("cp {}/nginx.conf /etc/nginx/".format(CONFIG_DIR)) sudo("cp {}/nginx_geekvi.conf /etc/nginx/sites-enabled/".format(CONFIG_DIR)) # 5. Install python requirements with cd(DEPLOY_DIR): if not exists(VENV_DIR): run("virtualenv {}".format(VENV_DIR)) with settings(warn_only=True): with source_virtualenv(): sudo("pip install -r {}/requirements.txt".format(PROJECT_DIR)) # 6. Config supervisor sudo("supervisord -c {}/supervisor.conf".format(CONFIG_DIR)) sudo("supervisorctl -c {}/supervisor.conf reload".format(CONFIG_DIR)) sudo("supervisorctl -c {}/supervisor.conf status".format(CONFIG_DIR)) sudo("supervisorctl -c {}/supervisor.conf start all".format(CONFIG_DIR))
其中,env.hosts
指定了遠程服務器,env.key_filename
指定了私鑰的路徑,這樣咱們就能夠免密碼登陸服務器了。根據實際狀況修改上面的相關參數,好比服務器地址,用戶名,服務器端口和項目路徑等,就可使用了。注意,在部署以前,咱們應該先對前端的資源進行加載和構建,在 deploy 目錄使用以下命令:
$ fab build
固然,你也能夠直接到 client 目錄下,運行命令:
$ npm run build
若是構建沒有出現錯誤,就能夠進行部署了,在 deploy 目錄使用以下命令進行部署:
$ fab deploy
本項目前端使用 React+Redux
,後端使用 Flask
,這也算是一種比較典型的開發方式了,固然,你也可使用 Node.js
來作後端。
前端的開發須要知道數據的流向:
後端的開發主要是爬蟲,Flask 在本項目只是做爲一個後臺框架,對外提供 api 服務;
本文由 funhacks 發表於我的博客,採用 Creative Commons BY-NC-ND 4.0(自由轉載-保持署名-非商用-禁止演繹)協議發佈。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人。
本文標題爲: 實例講解基於 Flask+React 的全棧開發和部署
本文連接爲: https://funhacks.net/2016/12/...