Pynecone 拔草

附应用的手动部署方法

Pynecone 是一个纯 python 的 Web App 开发框架。它的后端基于 Python 的 FastAPI 框架,前端基于 Node.js 的 Next.js 框架。但使用它不需要书写任何前端代码,可以完全用 Python 一把梭。听上去非常诱人,但实际如何呢?

一个简单 Pynecone 应用的代码如下:

pythonimport pynecone as pc


class State(pc.State):
    text: str = 'Hello, World!'
    
    def goodbye(self):
        self.text = 'Goodbye, World!'

def index():
    return pc.text(State.text, on_click=State.goodbye)

app = pc.App(state=State)
app.add_page(index, route='/')
app.compile()

Pynecone 会为每个 Session 维护一个 State 上下文,这个状态数据是保存在服务器端的,只能通过修改 State 的 Props 来间接改变视图。其原理是, Pynecone 生成的前端代码会通过一个 WebSocket 连接和后端进行交互。当用户进行了某个前端操作后,前端会将该事件通过 WebSocket 连接发送给后端,后端将改变后的 State 返回给前端,最后前端更新视图。由于所有前端操作都需要和后端交互,在网络不佳的情况下,会导致前端操作响应很慢

前端每次调用后端的事件处理器只能获取一次 State ,因此如果一次事件要触发多次 State 改变,需要通过「 Event Chains 」来实现。比如,点击一个按钮,按钮状态变成不可用,当后端处理完毕后,按钮状态再恢复为可用。官方示例代码如下:

pythonimport asyncio


class ChainExampleState(pc.State):
    count = 0
    show_progress = False

    def toggle_progress(self):
        self.show_progress = not self.show_progress

    async def increment(self):
        await asyncio.sleep(0.5)
        self.count += 1


def index():
    return pc.cond(
        ChainExampleState.show_progress,
        pc.circular_progress(is_indeterminate=True),
        pc.heading(
            ChainExampleState.count,
            on_click=[
                ChainExampleState.toggle_progress,
                ChainExampleState.increment,
                ChainExampleState.toggle_progress,
            ],
            _hover={"cursor": "pointer"},
        ),
    )

但是,Event Chains 不是原子的,可能会导致视图停留在「脏」状态。在网络抖动的情况下,事件链中的处理器可能无法全部被成功执行,或者前端没有正确接收到状态的更新。以上面的代码为例, on_click 事件链中第二个 ChainExampleState.toggle_progress 可能没有执行成功, ChainExampleState.show_progress 一直为 True ,导致前端视图一直卡在 circular_progress 状态。即便刷新页面也无法从这种「脏」状态中摆脱出来。

另外,Page 中的代码无法直接访问 State 的 Props 。例如,下面的代码是错误的:

pythonclass State(pc.State):
    is_goodbye: bool = True

def index():
    if State.is_goodbye:
        return pc.text('Goodbye, World!')
    else:
        return pc.text('Hello, World!')

在 Page 代码中,必须使用 pc.cond()pc.foreach() 来处理业务逻辑。上面的代码需要写作:

pythonclass State(pc.State):
    is_goodbye: bool = True

def index():
    return pc.cond(
        State.is_goodbye,
        pc.text('Goodbye, World!'),
        pc.text('Hello, World!')
    )

但是仅凭 pc.cond()pc.foreach() 根本不足以处理复杂逻辑。

由于 Pynecone 无法直接操作网页 DOM ,有些常规开发时稀松平常的业务场景,在 Pynecone 中变得难以实现。比如,一个由一个按钮和一个输入框组成的表单,当用户点击按钮后,提交输入框中的内容并清空输入框。在 Pynecone 框架下,如果需要操作输入框的内容,需要将 Value 绑定到 State 的某个 Var ,并通过 on_change 事件来处理内容修改:

pythonclass State(pc.State):
    text: str = ''
    
    def submit(self):
        text = ''

def index():
    pc.vstack(
        pc.input(value=State.text, on_change=State.set_text),
        pc.button('SUBMIT', on_click=State.submit),
    )

这种解决方案非常荒诞:用户每次修改输入框内容都需要和服务器交互,另外还无法在开启输入法的时候正常输入。

总得来说, Pynecone 适合用来快速搭建原型,但根本不适合用在生产环境。这仅仅是个大号的玩具罢了。


如何部署 Pynecone 应用

首先,需要安装 Nginx 和 Supervisor 。这里以 Ubuntu 系统为例:

shellsudo apt install nginx supervisor

配置以 my.app 作为站点域名为例。

生成代码

修改项目 pcconfig.py 配置:

pythonimport pynecone as pc

config = pc.Config(
    app_name="myapp",
    db_url="sqlite:///pynecone.db",
    env=pc.Env.PROD,
    api_url="http://my.app",    # 修改此处
)

运行命令生成项目的前、后端代码:

shellpc export

该命令会在当前目录下生成 frontend.zipbackend.zip 两个文件。同时,该命令也会在项目的 .web/_static 路径下生成前端静态文件。

配置 Nginx

假设项目代码路径为 /data/myapp 。编辑 Nginx 默认配置文件 /etc/nginx/sites-available/default

nginxmap $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80 default_server;
    listen [::]:80 default_server;

    root /data/myapp/.web/_static;    # 修改此处

    index index.html;

    server_name my.app;    # 修改此处

    location / {
        try_files $uri $uri/ $uri.html @api;
    }
    location @api {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

执行 nginx -s reload 命令使配置生效。

配置 Supervisor

为项目创建 Python 环境:

shellcd /data/myapp
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

新建配置文件 /etc/supervisor/conf.d/myapp.conf

ini[program:myapp]
process_name=myapp-backend
directory=/data/myapp
command=/data/myapp/.venv/bin/pc run --no-frontend --env prod
environment=PATH="/data/myapp/.venv/bin:$PATH"
autostart=true
autorestart=true
startretries=10
exitcodes=0
stopsignal=KILL
stopwaitsecs=10
redirect_stderr=true
stopasgroup=true
killasgroup=true

启动后端服务:

shellsudo supervisorctl reload
sudo supervisorctl update

就此,配置完毕。