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.zip
和 backend.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
就此,配置完毕。