介绍两个用前端语言搭建客户端的工具-pywebview

简介:如何通过前端语言来设计客户端页面,而又能实现后端语言跟操作系统交互,IO操作,数据处理等的特点。

前言

很多从控制台到客户端的朋友,总会被一些复杂的客户端组件挡在门外,因为门槛过于高以至于,要么觉得组件过多一头乱麻而直接放弃,要么费了很大力气搭建了一个贼土的界面而只能自娱自乐。因此今天介绍了两个工具就是能够像搭建web一样,搭建出网页一样更新颖的画面的客户端,而不需要学习更多windows客户端的那些GUI方法。

pywebview

有接触过的朋友可能会知道,WebView是一个基于webkit引擎、展现web页面的控件。它能够将Web页面嵌入到原生应用中,使得开发者可以在应用内展示网页内容,并且提供与原生应用的无缝集成,说白了就是它能够解析前端的语言脚本文件,比如html,js,css等,换句话说就是他类似于浏览器核心引擎,能够解析前端文件并渲染展示。

pywebview,就是把webview封装,使得我们可以用python语言操作这个组件,因而可以向写前端语言文件一样去开发客户端,这样事情变得简单了许多。

这个项目的github地址:https://github.com/r0x0r/pywebview 项目的介绍文档:https://pywebview.flowrl.com/

安装

首先做的是拉取库,其实非常简单,你首先要有python环境,你已经配置好了pip源,然后就正常的用pip来安装即可:

1
pip install pywebview

我的环境: windows10,Python 3.12.3

我个人觉得不会有太多影响,正常的执行安装都能比较顺利,如果真的有什么问题,可以参照上文的文档比对是否缺少依赖项。

大概说下我理解的这个pywebview的运行过程,可以先看下面这个很粗糙的图,这很类似于前后端分离的感觉,前端文件就跟web系统的前端一样,你可以任意搭建,后端就是用python语言来实现,你可以用python做很多运算,以及跟操作系统的接触。同时,前后端还可以通信来实现类似于前后端API调用和资源的获取等。

2

配置和使用

既然知道了pywebview的结构,我们就可以按照这个结构简单的建立一个案例尝试下,首先,建立一个python文件,我就直接用vscode,毕竟免费的哦,而且用起来也很方便。

建立一个app.py的文件,然后再同级建立一个front的文件夹,用作装前端文件

为了让项目效果更好看,我们就建立一个vue项目

vue项目建立

为了实际操作者也能从零开始,我们把vue项目建立过程从头到尾也说下:

1. 安装node.js

到官方网站直接下载,https://nodejs.org/,直接可以下载的windows安装包文件,然后安装即可

1
2
3
4
//安装好之后,命令行输入
node -v

npm -v

显示版本号即可,这样有了npm就可以用它来帮助建立vue项目以及打包了。

2. 建立一个vue项目

因为npm需要换源,设定淘宝镜像之类的,具体不是本次范畴聚不在这里追溯,大家可以自行搜索。 进入前端文件夹,然后开始一个命令行(终端)

tip:可以直接用vscode来 菜单栏-->终端-->新建终端,选择git(如果你有安装git哈)

在界面底部显示了终端界面,进入前端文件夹,输入命令:

1
npm create vue@latest

在输入的过程中,命令行会有交互的过程,第一次是要你给项目起一个名字,也就是要在front文件夹里新建一个项目名的文件夹,我输入了:pywebview-test

然后问你是否要配置一些东西,不用理他,一路回车即可。当结束之后,front文件夹会多出一个文件夹,打开文件夹里会有一些文件夹和文件,在终端进入front里的这个文件中,然后输入

1
npm run build

最后你的文件夹会出现如下结构:

3

其中,npm打包构建成功之后的文件都在dist里,vue的架构就是单一文件,最终都是在一个html里,所以dist文件夹里的index.html就是你的前端文件。

编写python文件

python文件就是真正使用pywebview了,如果你已经都安装好了pywebview,执行完接下来的步骤,就能立竿见影的看见效果,接着在app.py里输入如下代码:

1
2
3
4
5
6
7
8
9
import webview

if __name__ == "__main__":
    # 创建一个API实例
    api = Api()

    # 创建一个窗口,加载HTML文件
    webview.create_window("PyWebView Example", "./front/pywebview-test/dist/index.html")
    webview.start()

然后直接在终端执行代码或者右键run code

1
python app.py

紧接着就能看到一个很清晰的窗口出现,以及vue的默认初始项目的界面:

4

其他API

除了基本的页面,也可以利用自身提供的API来做一些简单的扩展,比如菜单: 在官方给的案例中有关于菜单的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import webview
import webview.menu as wm

def change_active_window_content():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>You changed this window!</h1>')

def click_me():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>You clicked me!</h1>')

def do_nothing():
    pass

def say_this_is_window_2():
    active_window = webview.active_window()
    if active_window:
        active_window.load_html('<h1>This is window 2</h2>')

def open_file_dialog():
    active_window = webview.active_window()
    active_window.create_file_dialog(webview.SAVE_DIALOG, directory='/', save_filename='test.file')

if __name__ == '__main__':
    window_1 = webview.create_window(
        'Application Menu Example', 'https://pywebview.flowrl.com/hello'
    )
    window_2 = webview.create_window(
        'Another Window', html='<h1>Another window to test application menu</h1>'
    )

    menu_items = [
        wm.Menu(
            'Test Menu',
            [
                wm.MenuAction('Change Active Window Content', change_active_window_content),
                wm.MenuSeparator(),
                wm.Menu(
                    'Random',
                    [
                        wm.MenuAction('Click Me', click_me),
                        wm.MenuAction('File Dialog', open_file_dialog),
                    ],
                ),
            ],
        ),
        wm.Menu('Nothing Here', [wm.MenuAction('This will do nothing', do_nothing)]),
    ]

    webview.start(menu=menu_items)

在vscode中新建文件,重新运行下,就会看到入下图所示,不同菜单以及多级菜单的效果。

5

增加调试功能

毕竟是浏览器,要是没有f12可还行,那必然少了很多方便:

1
webview.start(debug=True)

python与js 的交互

当然,数据的处理以及和操作系统的通信可以用python,页面显示和渲染可以用js,但是,我们更关心的是js如何才能和python交互。个人认为,这个是pywebview最重要的,毕竟能够让js把它干不了的活给python干是一个很重要的事情。

官方给的文档里有一个很清晰的例子,可以看这个地址,https://pywebview.flowrl.com/guide/usage.html#communication-between-javascript-and-python

我们尝试在app.py文件里,建立一个类,并创建一个方法:

1
2
3
4
class Api:
    def callPythonFunction(self):
        # 这是一个Python函数,可以被JavaScript调用
        return "Hello from Python!"

然后把类的实例作为参数传入到create_window中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

if __name__ == "__main__":
    # 创建一个API实例
    api = Api()

    # 创建一个窗口,加载HTML文件,并传入API实例
    webview.create_window(
        "PyWebView Example", "./front/pywebview-test/dist/index.html", js_api=api
    )

    webview.start()

然后是前端文件的编写:

前端文件调用的时候可以通过DOM对象去获取这个函数: 我们在建立好了vue文件中新建一个script和函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
function call_python() {
  window.pywebview.api.callPythonFunction().then(function (data) {
    alert(data);
  });
}
</script>

/*
window.pywebview.api.callPythonFunction().then(function (data) {
    alert(data);
  });
注意:这是一个通过js调用底层python函数的范式,
api是类的名,
callPythonFunction是python函数的名,如果有参数可以传参
then里包含的是处理返回值的回调函数(这是一个闭包),里面可以写自己的处理函数

这是promise对象的基本处理方法
promise对象是一个有时间状态的对象,就向上面,调用,等待结果,有结果,处理结果
就这么简单理解吧
*/

然后template里面建立一个button:

1
<button @click="call_python">Click me</button>

然后执行npm run build重新打包 如果你没有关闭窗口直接在窗口右键刷新即可,如果你关闭了,就用python app.py重新启动即可。

在窗口新出现的button点击,即可看到如下内容:

6

我们看到,js成功调用python方法,并从python发送的数据成功传递到了上层js中来。

一个示例

既然可以实现交互,我们做一个完整的小尝试,做一个完整的小工具

**工具内容:**实现对不同域名的证书进行解析并判断是否已经在有效期内

为了方便快速搭建出效果还不错的页面而又不需要花多少力气,我们用选一个vue的组件工具:naive-ui 地址在这里https://www.naiveui.com/

安装和配置naive-ui

直接在pywebview-test文件夹的命令行用npm去安装即可:

1
2
3
4
5
6
7
8
9
npm i -D naive-ui

$ npm i -D naive-ui

added 22 packages in 5s

43 packages are looking for funding
  run `npm fund` for details
安装完成

然后是配置引入的组件,我们为了方便,一股脑全部引入:

把scr目录下的main.js修改下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import './assets/main.css'

import naive from 'naive-ui'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.use(naive)

app.mount('#app')

然后把App.vue改换成以下的样子,就是乱码七糟的都删掉,只保留刚刚写的测试文件,然后再把componets文件夹里的文件也删掉就好(或者愿意保留着等会方便复制粘贴也行)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<template>

</template>
<script setup>
function call_python() {
  window.pywebview.api.callPythonFunction().then(function (data) {
    alert(data);
  });
}
</script>

<style scoped>
</style>

然后再这个组件的地址里,复制对应button的代码template里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
  <n-space>
    <n-button>Default</n-button>
    <n-button type="tertiary">
      Tertiary
    </n-button>
    <n-button type="primary">
      Primary
    </n-button>
    <n-button type="info">
      Info
    </n-button>
    <n-button type="success">
      Success
    </n-button>
    <n-button type="warning">
      Warning
    </n-button>
    <n-button type="error">
      Error
    </n-button>
  </n-space>
</template>

还是执行npm run build,然后刷新页面,就是如下效果

7

同理,你也可以配置上对应的函数测试下:

1
<n-button @click="call_python">Default</n-button>

8

按照这样的方式搭建客户端,会变得很简答,很轻松。

获取域名及解析信息

用python获取域名对应的证书,并解析证书各参数的值,其实我也不知道怎么写,但是我们不是有AI么儿,丢到kimi里面,获取一段代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

import ssl
import socket
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import serialization
from datetime import datetime


def get_domain_cert(domain):
    """
    获取域名证书
    :param domain: str
    :return: bytes
    """
    context = ssl.create_default_context()
    with socket.create_connection((domain, 443)) as sock:
        with context.wrap_socket(sock, server_hostname=domain) as ssock:
            der_cert = ssock.getpeercert(binary_form=True)
    return der_cert


def parse_cert(der_cert):
    """
    解析证书
    :param der_cert: bytes
    :return: dict
    """
    cert = x509.load_der_x509_certificate(der_cert, default_backend())
    cert_info = {
        "证书版本": cert.version.name,
        "证书序列号": hex(cert.serial_number),
        "证书中使用的签名算法": cert.signature_algorithm_oid._name,
        "颁发者": cert.issuer.rfc4514_string(),
        ## 这用python测试,显示方法已失效,所以更换了推荐的方法
        # "有效期从": cert.not_valid_before.strftime("%Y-%m-%d %H:%M:%S"),
        "有效期从": cert.not_valid_before_utc,
        # "有效期到": cert.not_valid_after.strftime("%Y-%m-%d %H:%M:%S"),
        "有效期到": cert.not_valid_after_utc,
        "证书是否已经过期": cert.not_valid_after < datetime.now(),
        "主体信息": cert.subject.rfc4514_string(),
        "公钥长度": cert.public_key().key_size,
        "公钥": cert.public_key()
        .public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo,
        )
        .decode("utf-8"),
    }
    for ext in cert.extensions:
        ext_name = ext.oid._name
        ext_value = ext.value
        if isinstance(ext_value, x509.SubjectAlternativeName):
            ext_value = ", ".join([str(name) for name in ext_value])
        cert_info[f"扩展信息-{ext_name}"] = ext_value
    return cert_info


def print_cert_info(cert_info):
    """
    格式化打印证书信息
    :param cert_info: dict
    """
    for key, value in cert_info.items():
        print(f"{key}: {value}")


if __name__ == "__main__":
    domain = "www.example.com"
    der_cert = get_domain_cert(domain)
    cert_info = parse_cert(der_cert)
    print_cert_info(cert_info)

这个是更换为www.baidu.com域名后得到的结果

9

由于扩展信息,我们在前端暂且用不到就可以先拿掉,然后再通过前端获取下这个信息,首先这段代码复制到一个文件,并把对应扩展信息和打印信息的方法都注释掉。

然后再app.py引入两个函数即,获取证书和解析证书的函数

1
from getssl import get_domain_cert, parse_cert

然后定义一个新的给前端传递证书信息的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Api:
    def callPythonFunction(self):
        # 这是一个Python函数,可以被JavaScript调用
        return "Hello from Python!"

    # 定义一个名为getDomainCert的JavaScript函数,它接受一个域名作为参数,然后返回该域名的证书信息
    def getDomainCert(self, domain):
        cert = get_domain_cert(domain)
        if not cert:
            return None
        return parse_cert(cert)

然后在前端添加这个按钮:

1
<n-button type="tertiary" @click="call_python_get_cert">

并添加这个获取内容的方法:

1
2
3
4
5
6
function call_python_get_cert() {
  var domain = "www.baidu.com"
  window.pywebview.api.getDomainCert(domain).then(function (data) {
    alert(data);
  });
}

然后我们添加一个卡片,让卡片上展示获取到的这个内容:

1
2
3
  <n-card title="证书信息">
    {{ cert_info }}
  </n-card>

同时,注意在这个script scope中添加下面引入的组件,这个ref组件,是vue提供的非常重要的组件,用途就是可以让变量的值动态的展示,具体可以看vue文档,这里不多赘述。

1
2
3
import { ref } from 'vue'

const cert_info = ref('')

然后,python app.py重启画面,然后npm run build重新构建,然后刷新页面即可。 然后点击那个按钮,就可以看到如下效果了

10

因为篇幅有限,今天就把pywebview这个工具介绍好,下一次我们再来介绍另一个工具。

以上就是今天的内容,笔者算是开了一个小头,根据这样的一个方法和框架可以做很多的更加扩展的内容,比如,喜欢前端的同学,对页面有追求的同学,可以利用这个方法进一步把页面做到极致。而对底层数据处理,web通讯,或者更底层驱动更擅长的话就可以用这个框架把下层的数据实现可视化的效果。

updatedupdated2025-01-122025-01-12