Python + React

原文链接

背景

研究了开源项目 superset 已经有一段时间了,突然想自己搭建一个类似的 Python + React 类型的项目,搭建的过程中产生了各种问题,这篇文章记录了搭建的整个过程以及遇到的问题和相关解决方法。

基本环境

系统环境: macOS系统

使用python等版本如下

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

虚拟环境和相关安装包配置

进入项目目录创建 requirements.txt 文件,配置相关依赖包(见文末)

创建虚拟环境(python3直接创建)并激活环境:

python3 -m venv venv
source venv/bin/activate

使用如下命令安装相关依赖包

pip3 install -r requirements.txt

创建项目

本项目以 Flask-AppBuilder 为基础搭建,相关文档和地址:
GitHub 地址 文档地址

按照文档使用如下命令创建项目

flask fab create-app
36-1.png

我们看到安装出现了错误,我从 Flask-AppBuilder 2.1.10 升级到 2.1.13 还是有这个错,搜了下issues 发现已经有这个问题,并且这个问题已经被修复,最新版本应该是还没有发布。我们直接复制地址到浏览器下载: https://github.com/dpgaspar/Flask-AppBuilder-Skeleton/archive/master.zip 把下载压缩包解压,移动文件中的内容到项目文件目录下。如下所示

36-2.png

相关目录创建和文件配置

删除 views.py 和 models.py

新增目录 static、assets、views、models

1. 软连接 assets 到 static 目录下(使用绝对路径)
ln -s /Users/xx/app/assets /Users/xx/app/static/assets

2. models 文件目录是相关模型
3. views 文件目录是相关视图
4. templates 文件目录是相关模版文件(见React部分)
5. assets 文件目录是react相关配置(见React部分)

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

36-3.png

配置 config.py 文件

数据库地址配置: SQLALCHEMY_DATABASE_URI
并新增 SQLALCHEMY_TRACK_MODIFICATIONS = False 配置项
应用名称配置: APP_NAME
应用icon配置: APP_ICON

models 模块配置

为了方便 model 统一管理我们创建了 models,该文件下主要存放各种模型文件和模型辅助文件,结构目录如下所示:

36-4.png

1、__init__.py 文件内容为:

from . import core

2、core.py 我们定义的一个log模型

from datetime import datetime
import functools
import json
    
from flask import request, g
from sqlalchemy import (
    Boolean, Column, DateTime, ForeignKey, Integer, String, Text,
)
from flask_appbuilder import Model
    
class Log(Model):
    
    """ORM object used to log Superset actions to the database"""
    
    __tablename__ = 'logs'
    
    id = Column(Integer, primary_key=True)
    action = Column(String(512))
    blog_id = Column(Integer)
    json = Column(Text)
    timestamp = Column(DateTime, default=datetime.utcnow)
    duration_ms = Column(Integer)
    referrer = Column(String(1024))
    user_id = Column(Integer, ForeignKey('ab_user.id'))
    
    def __repr__(self):
        return self.user_id if self.user_id else self.action
    
    @classmethod
    def log_this(cls, f):
        """Decorator to log user actions"""
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            """自定义记录内容"""
        return wrapper

views 模块配置

同理为了方便视图的统一管理我们创建了 views,结构目录如下所示:

36-5.png

1、__init__.py 文件内容同model模块

2、base.py 定义继承于 BaseView 的公共基类和获取用户的基本信息

from flask import Response, get_flashed_messages, g
from flask_appbuilder import BaseView
import simplejson as json
from flask_appbuilder.security.sqla import models as ab_models
from app import db


def bootstrap_user_data(username=None, include_perms=False):
    """获取用户信息"""
    if username:
        username = username
    else:
        username = g.user.username
    user = db.session.query(ab_models.User).filter_by(username=username).one()
    payload = {
        'username': user.username,
        'firstName': user.first_name,
        'lastName': user.last_name,
        'userId': user.id,
        'isActive': user.is_active,
        'email': user.email,
        'createdOn': user.created_on.isoformat()
    }
    if include_perms:
        """"""
    return payload
    
    
class BaseDDBlogView(BaseView):
    
    def json_response(self, obj, status=200):
        return Response(
            json.dumps(obj, ignore_nan=True),
            status=status,
            mimetype='application/json'
        )
    
    def common_bootstrap_payload(self):
        """common bootstrap"""
        messages = get_flashed_messages(with_categories=True)
        return {
            'flash_messages': messages
        }

3、core.py

from flask import (
    Response, request, url_for, redirect, render_template, flash, g
)
from flask_appbuilder import expose
import simplejson as json
from .base import (
    BaseDDBlogView, bootstrap_user_data
)
from app import (app, appbuilder, db)
    
    
class DDBlog(BaseDDBlogView):
    """"""
    @expose('/welcome')
    def welcome(self):
        if not g.user or not g.user.get_id():
            return redirect(appbuilder.get_url_for_login)
        payload = {
            'common': self.common_bootstrap_payload(),
            'user': bootstrap_user_data()
        }
        return self.render_template(
            'blog/basic.html',
            entry='welcome',
            bootstrap_data=json.dumps(payload)
        )
    
    
appbuilder.add_view_no_menu(DDBlog)

修改 run.py 文件

from app import db, app
from app.models.core import Log
    
    
@app.shell_context_processor
def make_shell_context():
    """
    此处引入model中已经创建好的Log模型,用于migrate创建是自动加载
    """
    return dict(app=app, db=db, Log=Log)
    
    
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080, debug=True)
    

配置 app/__init__.py 文件

1、新增 migrate 配置

APP_DIR = os.path.dirname(__file__)
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")

2、继承并重定向 IndexView

class MyIndexView(IndexView):
    @expose("/")
    def index(self):
        return redirect("/ddblog/welcome")
    
    
with app.app_context():
    appbuilder = AppBuilder(
        app,
        db.session,
        base_template="blog/base.html",
        indexview=MyIndexView,
    )

3、配置启动处理 assets 模块 manifest

"""Handling manifest file logic at app start"""
MANIFEST_FILE = APP_DIR + "/static/assets/dist/manifest.json"
manifest = {}
    
    
def parse_manifest_json():
    global manifest
    try:
        with open(MANIFEST_FILE, "r") as f:
            full_manifest = json.load(f)
            manifest = full_manifest.get("entrypoints", {})
    except Exception as e:
        print(str(e))
        pass
    
    
def get_js_manifest_files(filename):
    if app.debug:
        parse_manifest_json()
    entry_files = manifest.get(filename, {})
    return entry_files.get("js", [])
    
    
def get_css_manifest_files(filename):
    if app.debug:
        parse_manifest_json()
    entry_files = manifest.get(filename, {})
    return entry_files.get("css", [])
    
    
def get_unloaded_chunks(files, loaded_chunks):
    filtered_files = [f for f in files if f not in loaded_chunks]
    for f in filtered_files:
        loaded_chunks.add(f)
    return filtered_files
    
    
parse_manifest_json()
    
    
@app.context_processor
def get_manifest():
    return dict(
        loaded_chunks=set(),
        get_unloaded_chunks=get_unloaded_chunks,
        js_manifest=get_js_manifest_files,
        css_manifest=get_css_manifest_files,
)

以上配置完成后创建并初始化数据库

1、 进入到到当前项目,启动虚拟环境,执行如下命令:

export FLASK_APP=run.py
flask db init

如下图所示:

36-6.png

2、 执行下面命令生成数据库版本并更新

flask db migrate
flask db upgrade

到这里数据库已经完成创建

3、 创建管理员账号 flask fab create-admin 如下图

36-7.png

如果以上都能执行完成,Python 模块基本配置完成,这里总结上面出现的几种常见错误

1. 项目的目录结构层级错误
2. 目录 app 名称被修改,执行命令会出现找不到 app 等错误
3. run.py 文件中需要配置一个自己定义的模型如 Log,模型才能被初始化合并到项目中

React 配置

此模块主要记录了项目中 webpack 配置过程中所产生的各种问题和相关解决方法。 该模块主要包含 templates 和 assets 两部分配置,assets 部分配置是重点。

templates 模块配置

该模块主要参考 superset 内容,里面有两个文件,一个 appbuilder, 一个是自定义的模块bog,该文件名称和文章前面 __init__.py 中 apppbuilder 中 base_template 配置相同。

assets 模块配置(容易出错的地方)

以下操作在文件目录 assets 下执行

生成 package.json 文件

第一次进入assets目录下改文件夹内容是空的我们按照下面过程操作

  1. 执行 npm init 命令 一直回车生成 package.json 文件

  2. 编辑 package.json 文件内容,这时文件目录如下所示

  3. 执行 npm install 安装 node_modules 模块

以上执行完成后该文件夹内容如下所示

36-8.png

assets目录下新增 src、images 和 stylesheets目录

  • src 目录中主要存放 react 编写的 js 文件,这里我把 superset 中 welcome 模块修改了部分内容后直接使用
  • stylesheets 存放的是项目中需要使用的 css less样式,这里我也套用 superset 模块内容
  • images 存放是项目中使用的图片

创建并配置 webpack.config.js

webpack 各个参数配置和说明可以参考中文文档

在我们创建并配置好 webpack.config.js 后,当前文件目录如下所示

36-9.png

1、 以上配置完成后我尝试的运行了下打包命令 npm run dev,第一个错误出现

36-10.png

该错误提示比较明显,提示我们缺少一个 tsconfig 文件,因此我们新增加了一个 tsconfig.config 文件内容参考 superset 配置,修改后的文件目录如下所示

36-11.png

2、 我又尝试的运行了下打包命令 npm run dev ,第二个错误出现

36-12.png

从错误里面看到好像和 babel-core 有关,于是我找到下面这篇文章, 从该文章我知道 babel、babel-loader 版本需要和 webpack 对应,于是我将这三个模块都回退到了7执行以下命令

npm install -D babel-loader@7 babel-core babel-preset-env webpack

3、我又尝试的运行了下打包命令 npm run dev ,第三个错误出现

36-13.png

这次错误倒是少了提示 babel-loader 失败,从字面理解应该是 babel 模块加载失败,但是从这里很难找到问题,于是我又查找了一通,终于从发下了下面这篇文章 webpack配置 babel,从这篇文章我突然发现我少了一个 .babelrc 文件,于是我又查看了 superset 果然有这个文件,于是我又参考了它配置了一个如下

{
  "presets": ["react", "env", "airbnb"],
  "plugins": ["lodash", "syntax-dynamic-import", "react-hot-loader/babel"],
  "env": {
    "test": {
      "plugins": [
        "babel-plugin-dynamic-import-node"
      ]
    }
  }
}

4、我又尝试的运行了下打包命令 npm run dev ,第四个错误出现

36-14.png

这个错误还是比较明显的提示我们 aribnb 没有找到,回到package.json 中发现我配置的是 babel-preset-airbnb,修改presets内容后再次运行

36-15.png

终于在经过上面n多次修改后,我的项目终于编译成功!附上该模块最终目录截图

36-16.png

小结

事后我又直接找到 babel 中文网,发现原来Babel 是一个工具链主要用于语法转换的,它的核心库包含 babel-core、babel-cli、babel-preset-env和babel-polyfill等,关于他的一些配置事项该网站中都有介绍,如果我早查看该文章可能会少走些弯路。总的来说过程是痛苦的但是结果是好的

以上内容参考以下文章

superset

Flask-AppBuilder文档地址

webpack中文文档

babel 中文网

webpack配置 babel

webpack.config.js配置遇到Error: Cannot find module '@babel/core'&&Cannot find module '@babel/plugin-transform-react-jsx' 问题

webpack.config.js 文件内容:

const path = require('path');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const WebpackAssetsManifest = require('webpack-assets-manifest');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

// Parse command-line arguments
const parsedArgs = require('minimist')(process.argv.slice(2));

// input dir
const APP_DIR = path.resolve(__dirname, './');

// output dir
const BUILD_DIR = path.resolve(__dirname, './dist');

const {
  mode = 'development',
  devPort = 9000,
  proPort = 8088,
  measure = false,
  analyzeBundle = false,
} = parsedArgs;

const isDevMode = mode !== 'production';

const plugins = [
  // creates a manifest.json mapping of name to hashed output used in template files
  new WebpackAssetsManifest({
    publicPath: true,
    entrypoints: true,
    writeToDisk: isDevMode,
  }),

  // create fresh dist/ upon build
  new CleanWebpackPlugin(['dist']),

  // expose mode variable to other modules
  new webpack.DefinePlugin({
    'process.env.WEBPACK_MODE': JSON.stringify(mode),
  }),

  // runs type checking on a separate process to speed up the build
  new ForkTsCheckerWebpackPlugin({
    checkSyntacticErrors: true,
  }),
];

if (isDevMode) {
  // Enable hot module replacement
  plugins.push(new webpack.HotModuleReplacementPlugin());
} else {
  // text loading (webpack 4+)
  plugins.push(new MiniCssExtractPlugin({
    filename: '[name].[chunkhash].entry.css',
    chunkFilename: '[name].[chunkhash].chunk.css',
  }));
  plugins.push(new OptimizeCSSAssetsPlugin());
}

const output = {
  path: BUILD_DIR,
  publicPath: '/static/assets/dist/', // necessary for lazy-loaded chunks
};

if (isDevMode) {
  output.filename = '[name].[hash:8].entry.js';
  output.chunkFilename = '[name].[hash:8].chunk.js';
} else {
  output.filename = '[name].[chunkhash].entry.js';
  output.chunkFilename = '[name].[chunkhash].chunk.js';
}

const PREAMBLE = [
  'babel-polyfill',
  path.join(APP_DIR, '/src/preamble.js'),
];

function addPreamble(entry) {
  return PREAMBLE.concat([path.join(APP_DIR, entry)]);
}

const config = {
  node: {
    fs: 'empty',
  },
  entry: {
    theme: path.join(APP_DIR, '/src/theme.js'),
    preamble: PREAMBLE,
    welcome: addPreamble('/src/welcome/index.jsx'),
  },
  output,
  optimization: {
    splitChunks: {
      chunks: 'all',
      automaticNameDelimiter: '-',
      minChunks: 2,
      cacheGroups: {
        default: false,
        major: {
          name: 'vendors-major',
          test: /[\\/]node_modules\/(brace|react[-]dom|core[-]js)[\\/]/,
        }
      }
    }
  },
  resolve: {
    alias: {
      src: path.resolve(APP_DIR, './src'),
    },
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
  context: APP_DIR, // to automatically find tsconfig.json
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        include: APP_DIR,
        loader: 'babel-loader',
      },
      {
        test: /\.css$/,
        include: APP_DIR,
        use: [
          isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
        ],
      },
      {
        test: /\.less$/,
        include: APP_DIR,
        use: [
          isDevMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
        ],
      },
      /* for css linking images */
      {
        test: /\.png$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: '[name].[hash:8].[ext]',
        },
      },
      {
        test: /\.(jpg|gif)$/,
        loader: 'file-loader',
        options: {
          name: '[name].[hash:8].[ext]',
        },
      },
      /* for font-awesome */
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/font-woff',
      },
      {
        test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'file-loader',
      },
    ],
  },
  externals: {
    cheerio: 'window',
    'react/lib/ExecutionEnvironment': true,
    'react/lib/ReactContext': true,
  },
  plugins,
  devtool: isDevMode ? 'cheap-module-eval-source-map' : false,
  devServer: {
    historyApiFallback: true,
    hot: true,
    index: '', // This line is needed to enable root proxying
    inline: true,
    stats: { colors: true },
    overlay: true,
    port: devPort,
    // Only serves bundled files from webpack-dev-server
    // and proxy everything else to backend
    proxy: {
      context: () => true,
      '/': `http://localhost:${proPort}`,
      target: `http://localhost:${proPort}`,
    },
    contentBase: path.join(process.cwd(), '../static/assets/dist'),
  },
};

if (!isDevMode) {
  config.optimization.minimizer = [
    new TerserPlugin({
      cache: '.terser-plugin-cache/',
      parallel: true,
      extractComments: true,
    }),
  ];
}

// Bundle analyzer is disabled by default
// Pass flag --analyzeBundle=true to enable
// e.g. npm run build -- --analyzeBundle=true
if (analyzeBundle) {
  config.plugins.push(new BundleAnalyzerPlugin());
}

// Speed measurement is disabled by default
// Pass flag --measure=true to enable
// e.g. npm run build -- --measure=true
const smp = new SpeedMeasurePlugin({
  disable: !measure,
});

module.exports = smp.wrap(config);

package.json 文件内容:

{
  "name": "ddblog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack --mode=development --colors --progress --debug --watch",
    "dev-server": "webpack-dev-server --mode=development --progress",
    "build": "NODE_ENV=production webpack --mode=production --colors --progress"
  },
  "keywords": [
    "blog",
    "python",
    "react"
  ],
  "author": "wushenchao",
  "license": "MIT",
  "dependencies": {
    "abortcontroller-polyfill": "^1.1.9",
    "bootstrap": "^3.3.6",
    "bootstrap-slider": "^10.0.0",
    "jquery": "3.4.1",
    "lodash": "^4.17.11",
    "moment": "^2.20.1",
    "prop-types": "^15.6.0",
    "postcss": "6.0.20",
    "react": "^16.4.1",
    "react-ace": "^5.10.0",
    "react-addons-shallow-compare": "^15.4.2",
    "react-bootstrap": "^0.31.5",
    "react-bootstrap-slider": "2.1.5",
    "react-bootstrap-table": "^4.3.1",
    "react-hot-loader": "^4.3.6",
    "react-dom": "^16.4.1",
    "react-redux": "^5.0.2",
    "reactable": "1.0.2",
    "redux": "^3.5.2",
    "redux-localstorage": "^0.4.1",
    "redux-thunk": "^2.1.0",
    "redux-undo": "^1.0.0-beta9-9-7",
    "shortid": "^2.2.6"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.10.4",
    "babel-eslint": "^8.2.2",
    "babel-jest": "^25.0.0",
    "babel-loader": "^7.1.4",
    "babel-plugin-css-modules-transform": "^1.1.0",
    "babel-plugin-dynamic-import-node": "^1.2.0",
    "babel-plugin-lodash": "^3.3.4",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "babel-polyfill": "^6.23.0",
    "babel-preset-airbnb": "^2.1.1",
    "babel-preset-env": "^1.7.0",
    "cache-loader": "^1.2.2",
    "clean-webpack-plugin": "^0.1.19",
    "css-loader": "^1.0.0",
    "enzyme": "^3.3.0",
    "enzyme-adapter-react-16": "^1.1.1",
    "exports-loader": "^0.7.0",
    "fetch-mock": "^7.0.0-alpha.6",
    "file-loader": "^1.1.11",
    "fork-ts-checker-webpack-plugin": "^1.5.0",
    "ignore-styles": "^5.0.1",
    "imports-loader": "^0.7.1",
    "less": "^3.10.0",
    "less-loader": "^4.1.0",
    "mini-css-extract-plugin": "^0.4.0",
    "minimist": "^1.2.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "speed-measure-webpack-plugin": "^1.2.3",
    "style-loader": "^0.21.0",
    "transform-loader": "^0.2.3",
    "ts-loader": "^5.2.0",
    "typescript": "^3.1.3",
    "url-loader": "^1.0.1",
    "webpack": "^4.19.0",
    "webpack-assets-manifest": "^3.0.1",
    "webpack-bundle-analyzer": "^3.0.2",
    "webpack-cli": "^3.1.1",
    "webpack-dev-server": "^3.1.7",
    "webpack-sources": "^1.1.0"
  }
}

requirements.txt 文件内容:

Babel==2.6.0
bleach==3.0.2
celery==4.2.0
fake-useragent==0.1.11
Flask==1.1.0
Flask-AppBuilder==2.1.13
Flask-Babel==0.11.1
Flask-Caching==1.4.0
Flask-Compress==1.4.0
Flask-Login==0.4.1
Flask-Migrate==2.5.0
Flask-Moment==0.9.0
Flask-OpenID==1.2.5
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.0
Flask-WTF==0.14.2
flower==0.9.2
future==0.16.0
numpy==1.15.2
pandas==0.23.4
requests==2.21.0
selenium==3.141.0
simplejson==3.15.0
six==1.11.0
SQLAlchemy==1.2.2
SQLAlchemy-Utils==0.32.21

你可能感兴趣的:(Python + React)