pangao1990 / PPX

基于Python和JavaScript,一键生成macOS、Windows和Linux平台客户端应用程序
https://blog.pangao.vip/docs-ppx/
GNU Affero General Public License v3.0
231 stars 39 forks source link

前言

PPX(曾用名 vue-pywebview-pyinstaller)。第一个 P 表示 Python ,当然,也可以表示 Pangao(潘高,也就是我本人)。第二个 P 表示 Pywebview ,也可以表示 Pyinstaller 。第三个 X 表示无限可能,指视图层可以使用 Vue、React、Angular、HTML 中的任意一种。

搭后语

现如今,要说比较火的编程语言当属 JavaScript 和 Python 了,这两门语言都可以独立编写前端页面、后端服务器、手机 APP、电脑客户端等等,无所不能。不过,不同的编程语言有不同的侧重点。比如 JavaScript 写网页得心应手,Python 处理大数据信手拈来。那么,能不能取两者的优点,构建一个跨平台客户端框架呢?这就有了今天的主角:PPX

应用简介

PPX 基于 pywebview 和 PyInstaller 框架,构建 macOS 、 Windows 和 Linux 平台的客户端。本应用的视图层支持 Vue、React、Angular、HTML 中的任意一种,业务层支持 Python 脚本。考虑到某些生物计算场景数据量大,数据私密,因此将数据上传到服务器计算,并不一定是最优解,采用本地 Python 也是一种不错的选择。不过,如果需要调用远程 API,本应用也是支持的。

应用优势

适用场景

适用人群

熟悉 Python3 和 任意一款前端框架,如 Vue、React、Angular、HTML 编程的程序员。

应用安装

运行环境

应用下载

利用 git(git 安装教程) 下载应用,如下所示:

git clone https://github.com/pangao1990/PPX.git

或者,直接在 github 下载。

# 进入项目
cd PPX

进入项目,项目清单如下所示:

image

初始化

下载完毕后,运行初始化命令,程序会自动下载安装对应操作平台的所需依赖软件,如下所示:

# 初始化 (linux系统中需要输入sudo命令密码)
pnpm run init

没报错信息,则初始化完成,如下所示:

image

image

项目根目录多了一个 node_modules 文件夹和 pnpm-lock.yaml 文件,用于存放 pnpm 下载的包。

应用运行

输入如下命令,即可启动应用:

pnpm run start

终端显示如下:

image

同时,启动一个客户端程序,如下:

image

整体效果如下所示(gif 图片加载可能有点慢):

image

高级用法

客户端引擎介绍

本应用基于 pywebview 构建客户端。而 pywebview 构架构建客户端的原理是利用本地电脑自带的浏览器引擎驱动,模拟生成客户端。本质上还是网页,或者说是一个浏览器,但是感官上和本地客户端没有差别。

那么,基于 pywebview 构架构建客户端的成败或质量,就与本地电脑的浏览器引擎息息相关了。

Windows 系统

在 Windows 系统上,大体上分为两类客户端引擎:正常模式和兼容模式。

正常模式下,按照 edgechromium ,edgehtml, mshtml 的客户端引擎依次检索。如果本地电脑 edge 浏览器支持这些引擎,则客户端可以正常启动。否则,请安装对应的 EdgeWebView2Runtime 浏览器引擎。

如果本地电脑 edge 浏览器不支持这些引擎,同时也不想下载 EdgeWebView2Runtime ,那么就可以使用兼容模式。兼容模式的原理就是利用 CEFPython,嵌入 Chromium 的 Web 浏览器控件。也就是只要本地电脑安装了谷歌浏览器 V66 版及其以上版本,即可正常启动客户端。缺点就是生成的安装包体积会增加大约 60M 左右。

macOS 系统

macOS 系统的浏览器引擎就没有那么多版本了,由于 macOS 系统的封闭性,在 macOS 系统就只有一种 WebKit 引擎可用。

不过,在 macOS 系统却存在另一个问题。那就是苹果自主研发的 M 系列芯片。用 x86_64 芯片打包的应用可以在 x86_64 和 M 芯片电脑上运行,用 M 芯片打包的应用只能在 M 芯片电脑上运行。

构建客户端 API

构建客户端的主程序是 main.py ,如下所示:

image

main.py 里面主要是依靠 webview.create_window 和 webview.start 这两个 API 来构建客户端。其他的一些 API,我也会在后面的教程中详细介绍。或者可以直接查看 pywebview 官网 了解详情。

webview.create_window

webview.create_window(title, url=None, html=None, js_api=None, width=800,
    height=600, x=None, y=None, screen=None, resizable=True, fullscreen=False,
    min_size=(200, 100), hidden=False, frameless=False, easy_drag=True,
    focus=True, minimized=False, maximized=False, on_top=False,
    confirm_close=False, background_color='#FFFFFF', transparent=False,
    text_select=False, zoomable=False, draggable=False,
    server=http.BottleServer, server_args={}, localization=None)

创建一个新的客户端窗口并返回其实例。在 GUI 循环启动之前,窗口不会显示。如果在 GUI 循环期间调用该函数,则会立即显示该窗口。

webview.start

webview.start(func=None, args=None, gui=None, debug=False, menu=[],
              http_server=False, http_port=None, user_agent=None,
              server=http.BottleServer, server_args={}, localization={})

启动 GUI 循环,并显示已经创建的窗口。此函数必须从主线程调用。

域间通信

这里的通信指的是 JavaScript(视图层,前端)和 Python(业务层,后端)的相互访问。

从 Python 调用 Javascript

Python 代码中调用 window.evaluate_js(code, callback=None) 可以执行 JavaScript 代码。

// JavaScript

window['py2js_demo'] = (res) => {
    const resDict = JSON.parse(res)
    console.log('js', resDict)
}
# Python

import webview

def system_py2js(window):
    '''调用js中挂载到window的函数'''
    info = {'appName': 'PPX'}
    infoJson = json.dumps(info)
    window.evaluate_js(f"py2js_demo('{infoJson}')")

window = webview.create_window()
webview.start(system_py2js, window)

从 Javascript 调用 Python

Python 代码中将类的实例传给 create_windowjs_api 。在 JavaScript 代码中调用 pywebview.api.method_name 即可。

# Python

import webview

class Api:
    def system_getAppInfo(self):
        return {'appName': 'PPX'}

if __name__ == '__main__':
    api = Api()
    webview.create_window(js_api=api)
    webview.start()
// JavaScript

window.pywebview.api.system_getAppInfo().then((res) => {
    console.log('js', res)
})

打包客户端

###########
# 简单用法 #
###########

# 初始化
pnpm run init

# 开发模式
pnpm run start

# 正式打包
pnpm run build

# 预打包,带console,方便输出日志信息
pnpm run pre

###########
# 进阶用法 #
###########

# 初始化,cef兼容模式
pnpm run init:cef

# 开发模式,cef兼容模式【仅win系统】
pnpm run start:cef

# 预打包,带console,cef兼容模式【仅win系统】
pnpm run pre:cef

# 预打包,带console,生成文件夹【仅win系统】
pnpm run pre:folder

# 预打包,带console,生成文件夹,cef兼容模式【仅win系统】
pnpm run pre:folder:cef

# 正式打包,单个exe程序【仅win系统】
pnpm run build:pure

# 正式打包,cef兼容模式【仅win系统】
pnpm run build:cef

# 正式打包,生成文件夹【仅win系统】
pnpm run build:folder

# 正式打包,生成文件夹,cef兼容模式【仅win系统】
pnpm run build:folder:cef

应用打包分为两步进行:

打包成可执行文件

基于 Pyinstaller 将项目代码打包成可执行文件。

打包过程,会先由 pyapp/spec/getSpec.py 脚本生成 windows.specmacos.speclinux.spec 打包配置文件,之后基于该配置文件进行打包。

::: tip 注意
这里需要注意一个问题。因 Pyinstaller 的打包机制,可能会造成某些动态库或者 Python 模块并没有被打包进可执行文件。因此,可能出现在生产环境运行没问题。但是打包后,就提示某些动态库或模块丢失。遇到这种情况,就需要在打包配置文件中添加丢失的动态库或模块。
:::

添加动态链接库

也可以简单的理解为, addDll 用于添加文件


# 示例
# 动态库是以元组形式字符串添加
# 以下示例表示将本地包中的 icudtl.dat 和 zh-CN.pak 文件添加到打包中
addDll = """
    ('../../pyapp/pyenv/pyenvCEF/Lib/site-packages/cefpython3/icudtl.dat', './'),
    ('../../pyapp/pyenv/pyenvCEF/Lib/site-packages/cefpython3/locales/zh-CN.pak','./locales')
    """
添加 Python 模块

也可以简单的理解为, addModules 用于添加文件夹


# 模块是以元组形式字符串添加
# 以下示例表示将本地包中的 requests 模块整体添加到打包中
addModules = "('../../gui/dist', 'web'), ('../../static', 'static')"

打包成安装程序

打包成 exe

打包过程,会先由 pyapp/package/exe/getIss.py 脚本生成 InnoSetup.iss 打包配置文件,之后基于该配置文件进行打包。

打包所需的数据均来自于 pyapp/config/config.py 配置文件。该文件几乎不需要修改。

::: warning 注意
值得一提的是,安装包的唯一 GUID 。这个唯一编号取自于 pyapp/config/config.py 配置文件。在 pnpm run init 初始化之前,需要手动把 pyapp/config/config.py 配置文件中的 appISSID 置空,PPX 会自动生成一个唯一 appISSID ,生成后请勿修改!否则安装程序将会重复安装多个应用,而非覆盖安装。
:::

# pyapp/config/config.py

# 初始化之前,请手动将 appISSID 设置为 ''
appISSID = ''    # Inno Setup 打包唯一编号。在执行 pnpm run init 之前,请设置为空,程序会自动生成唯一编号,生成后请勿修改!!!
# pyapp/package/exe/getIss.py

import os
from config.config import Config

appName = Config.appName    # 应用名称
appVersion = Config.appVersion  # 应用版本号
appVersion = appVersion[1:]    # 去掉第一位V
appDeveloper = Config.appDeveloper  # 应用开发者
appBlogs = Config.appBlogs  # 个人博客
rootDir = os.path.dirname(pyappDir)
buildDir = os.path.join(rootDir, 'build')
logoPath = os.path.join(rootDir, 'pyapp', 'icon', 'logo.ico')
appISSID = Config.appISSID    # 安装包唯一GUID
打包成 dmg

打包过程,会先由 pyapp/package/dmg/getDMG.py 脚本生成 dmg.py 打包配置文件,之后基于该配置文件进行打包。

在打包之前,请替换 pyapp/package/dmg/bg.png 背景图片和 pyapp/package/dmg/潘高的小站.webloc 网址文件。

filename = 'PPX'
volume_name = 'PPX.dmg'
format = 'UDBZ'
files = ['~/build/PPX.app', '~/pyapp/package/dmg/潘高的小站.webloc']
symlinks = {'Applications': '/Applications'}
icon_locations = {
    'PPX.app': (160, 120),
    'Applications': (430, 120),
    '潘高的小站.webloc': (450, 243)
}
window_rect = ((200, 200), (590, 416))
icon_size = 60
text_size = 12
badge_icon = '~/pyapp/icon/logo.icns'
background = '~/pyapp/package/dmg/bg.png'
打包成 deb

打包过程,会先由 pyapp/package/deb/makeDeb.py 脚本生成 control postinst PPX.desktop 等打包配置文件,之后基于这些配置文件进行打包。

::: warning 提示
PPX 仅在 Ubuntu 22.04.2 版测试成功,其他 Linux 版本还请开发者自行测试。
:::

跨平台打包

在本机电脑操作,只能打包出本系统对应的程序包。如果想打包出三种系统的程序包,需要借助 Github Action 的能力。

::: warning 提示
这里需要有 Github 操作基础。
:::

PPX 已经预先写好了 .github/workflows/main.yml 文件。

name: build
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [windows-latest, macos-latest, ubuntu-latest]
    steps:
      - name: 拉取项目代码
        uses: actions/checkout@v4

      - name: 安装node环境
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: 安装pnpm
        uses: pnpm/action-setup@v4
        id: pnpm-install
        with:
          version: 9
          run_install: false

      - name: 获取pnpm仓库目录
        id: pnpm-cache
        shell: bash
        run: |
          echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT

      - name: 设置pnpm缓存
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
          key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-store-

      - name: 安装Python3环境
        uses: actions/setup-python@v5
        with:
          python-version: "3.9"
          cache: "pip"

      - name: 初始化打包环境
        run: pnpm run init

      - name: 开始打包
        run: pnpm run build

      - name: 上传打包完成的程序包
        uses: actions/upload-artifact@v4
        with:
          name: Setup_${{ runner.os }}
          retention-days: 1
          path: build/*-*_*.*

将代码提交至 Github 后,在 Actions 下会自动生成三种系统的程序包。

打包后程序白屏的一些解决方案

PPX 显示 GUI 窗口的本质是显示 HTML 页面,因此出现白屏现象极有可能是系统不支持正常显示 HTML 页面。

一般情况下,macOS 不会出现白屏。在 Windows 系统下出现白屏,可以按以下步骤排查:

数据库迁移

在 api/db/models.py 中修改数据库格式后,执行以下命令迁移数据库。

注意:迁移数据库前,需要对 sqlalchemy 数据库对象映射框架有所了解。

# 迁移数据库
m=备注迁移信息 pnpm run alembic

HMR 原理

*注:这里感谢 WnagoiYy 同学的 PR。

注意问题

历史版本

V5.2.1

V5.2.0

V5.1.0

V5.0.0

V4.4.0

V4.3.0

V4.2.2

V4.2.1

V4.2.0

V4.1.0

V4.0.1

V4.0.0

V3.1.1

V3.1.0

V3.0.0

V2.0.0

V1.3.0

V1.0.0



打赏 🥰🥰🥰

如果这款应用对你有帮助,或者想给我微小的工作一点点资瓷,请随意打赏。
潘高 微信支付
微信支付
潘高 支付宝
支付宝



致谢 🥳🥳🥳

本应用自开源以来,获得了很多人的支持。

这离不开各位小伙伴的赞赏、意见和 PR,感谢你们!

我会朝着 加速软件开发向开源运动转变 的理念,继续前进。

🍄 Rewarders

昵称 金额 备注
**亮 100 元 感谢 PPX 项目,小小支持一下 ✊
jackiexiao 16.66 元 PPX yes!
Karinyooo 512 元 天使投资,哈哈!
XXX 100 元
mQ 50 元 PPX 加油!
min 6.66 元
mQ 100 元 帮助了许多东西
icebear 30 元
icebear 100 元
mlw 11 元
veteran 5 元
ly 1 元
漫倦彧翾 1 元
潘多拉的盒子 5 元
Shirley 6.66 元
夏林 2 元
曾姐 5 元

🍄 Stargazers

Stargazers

🍄 Forkers

Forkers


更多编程教学请关注公众号:潘高陪你学编程

image



Back to top