likes
comments
collection
share

从Java springboot到Python flask项目的迁移(C端)

作者站长头像
站长
· 阅读数 12

最近在写一些桌面应用程序。之前使用的是Springboot+Vite+Vue 3的前后端分离方法,打包使用electron打包。但这样有几个问题:

  1. 包体积太大(通常100MB起步)
  2. 打包后资源容易丢失且配置麻烦
  3. 前后端分离的方式在打包后并不能很好地运行后台程序。

在上网时找到了一篇使用Python flask开发的方法,此方法有以下优点:

  1. 体积小(基本在20-30MB左右)
  2. 打包后资源不易丢失(配置简单)
  3. 可直接访问后台程序,并不需要先启动后台服务然后再启动前台服务,对于开发者和用户而言体验都很棒。

但也有缺点,那就是用户必须安装至少一款基于Chrome内核的浏览器(Chrome,Edge均可)

准备工作

  1. 安装Python 3.11

  2. 安装Pycharm

  3. 配置Pip国内镜像源

(下面内容默认读者已经完成上述步骤)

开始

第一部分:创建Flask项目

  1. 在Pycharm中选择新建项目,新建项目时解释器选自己安装的Python,输入好项目名称、确定好项目路径后点击确定,待项目创建完毕并且flask安装完毕后进行下一步。(Pycharm在项目创建好后会自动安装Flask,此时确保网络畅通)

  2. 安装Pywebview

pip install pywebview

第二部分:编码

  1. 修改入口

app.py中找到'__main__',删除掉原先的app.run(),改成下面的样子:

if __name__ == '__main__':
  # 在main函数下写入这两个
    webview.create_window(title="窗口标题", url=app, confirm_close=True,
                                   http_port=10005, width=1024, height=768)
    webview.start()
    # app.run() <==删掉这个

(注意导入webview,import webview

参数说明

参数名含义
title窗口的标题,可以是项目的名称。可中文。
url默认地址,这个必须是类Flask的实例
confirm_close在程序关闭时是否弹出确认窗口
http_port程序通讯用的端口,前端获取后端数据也通过此端口。
width屏幕默认宽度(单位:像素,下同)
height屏幕默认高度
  1. 解决跨域问题

安装flask_cors

pip install flask_cors

app=Flask(__name__)后面加上CORS(app, resources={r'/*': {'origins': '*'}})

  1. 编写数据库功能部分

    编写数据库连接类。这个项目使用了sqlite数据库,下面介绍如何使用sqlite数据库

     安装SQL Alchemy(相当于Mybatis)
    
       ```Python
       pip install sqlAlchemy
       ```
    

    在项目文件夹下新建util

    在util文件夹下新建sqlUtil并编写下面的代码

    import datetime
    
    from sqlalchemy import (
        create_engine,
        Column,
        Integer,
        String,
        Enum,
        DECIMAL,
        DateTime,
        Boolean,
        UniqueConstraint,
        Index, ForeignKey, event, Engine, Table, MetaData, insert
    )
    from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session
    
    engine=create_engine("sqlite:///data") # sqlite数据库的本地文件链接方式,通过这种方式创建数据库链接引擎。
    Base=declarative_base()# 表实体类的父类,表示数据表的实体类必须继承该类。
    class Books(Base): # 实体类
        __tablename__="books" # 表名
        # Id,name,Commons都是表的列名以及对象的属性名
        id=Column(String(16),primary_key=True)
        name=Column(String(50),nullable=False)
        commons=Column(String(150))
        def to_json(self):
          # 便于JSON序列化,SQLAlchemy默认返回的对象或者列表无法直接转换为JSON,需要先转化为字典再转化为JSON
            dict = self.__dict__
            if "_sa_instance_state" in dict:
                del dict["_sa_instance_state"]
            return dict
    # 绑定引擎
    Session = sessionmaker(bind=engine)
    # 创建数据库链接池,直接使用session即可为当前线程拿出一个链接对象conn
    # 内部会采用threading.local进行隔离
    session = scoped_session(Session)
    
    Base.metadata.create_all(engine) # 创建数据表(一定要有这句话)
    
  2. 编写controller

    在项目下创建一个controller文件夹,再在controller文件夹下创建一个book_controller.py文件并写入以下代码:

    import uuid
    
    from flask import Blueprint, request
    from sqlalchemy.exc import SQLAlchemyError
    
    import sqlConnect
    #我自己写的一个工具类,其主要目的是向前端返回一个JSON数据
    from util.MessageUtil import send_ok_message, send_fail_message
    
    book_api=Blueprint('book_api',url_prefix="/books",import_name=__name__)# 创建一个Flask蓝图,这里面的url_prefix就相当于Springboot里面类上面的@RequestMapping
    @book_api.route("/add",methods=["POST"])# 这个就相当于是Springboot中方法上的@RequestMapping,如果要访问这个API,输入地址localhost:xxxx/books/add即可。不写methods参数默认只接受Get请求
    def add():
        newAdd=request.json#request是flask提供的获取请求的东西,json方法会按照JSON字符串获取参数内容,常用于POST传输方式
        newAdd["id"]=str(uuid.uuid1())[0:15]
        try:
            sqlConnect.session.add(sqlConnect.Books(**newAdd))# 通过Session完成数据库的插入操作
            sqlConnect.session.commit() # 别忘了提交!
            return send_ok_message(newAdd, "message.book.createSuccess")
        except SQLAlchemyError:# 捕获数据库错误
            return send_fail_message(500,"message.fail.db")
    @book_api.route("/update/<id>")
    def update(id):
        newAdd=request.json
        try:
            sqlConnect.session.query(sqlConnect.Books).filter(sqlConnect.Books.id == id).update(newAdd) #更新操作,newAdd是新的Book
            sqlConnect.session.commit()
            return send_ok_message(newAdd, "message.book.updateSuccess")
    except SQLAlchemyError:
            return send_fail_message(500,"message.fail.db")
    @book_api.route("/delete/<id>",methods=["POST"])
    def delete(id):
        try:
            sqlConnect.session.query(sqlConnect.Books).filter(sqlConnect.Books.id == id).delete()
            sqlConnect.session.commit()
            return send_ok_message("ok", "message.book.deleteSuccess")
    except SQLAlchemyError:
            return send_fail_message(500,"message.fail.db")
    @book_api.route("/list")
    def list():
        try:
            list=[]
            result=sqlConnect.session.query(sqlConnect.Books).all()# 获取列表
            for r in result:
                list.append(r.to_json())# 将列表中的元素JSON化(变为字典)
            return send_ok_message(list,"message.book.listSuccess")
        except SQLAlchemyError:
            return send_fail_message(500,"message.fail.db")
    @book_api.route("/get/<id>")
    def get(id):
        try:
            result=sqlConnect.session.query(sqlConnect.Books).filter(sqlConnect.Books.id == id).first()# 获取第一个符合条件的数据
            return send_ok_message(result.to_json(),"message.book.listSuccess")
        except SQLAlchemyError:
            return send_fail_message(500,"message.fail.db")
    

    这段代码借助Flask蓝图功能来完成类似于Java中的Controller组织模式。

  3. 载入蓝图并设置启动页面为前端的主页面

    app.py文件中加入app.register_blueprint(book_api, url_prefix="/books")并在@app.route("/")方法中将return "xxx"删掉,改为return render_template("index.html")。最后的app.py文件应该是这个样子的;

    import os
    
    from flask import Flask, render_template
    from flask_cors import CORS
    
    from controller.BookController import book_api
    from controller.IniController import ini_api
    import webview
    
    from controller.PasswordController import password_api
    
    app = Flask(__name__)
    app.register_blueprint(book_api, url_prefix="/books")
    CORS(app, resources={r'/*': {'origins': '*'}})
    @app.route('/')
    def hello_world():  # put application's code here
        return render_template("index.html")
    
    
    if __name__ == '__main__':
        webview.create_window(title="密码本", url=app, confirm_close=True,
                                       http_port=10005, width=1024, height=768)
        webview.start()
    

第三部分:导入Vue文件

  1. 修改路由模式为WebHashHistory并且路径改为"/"

    const router = createRouter({
      history: createWebHashHistory("/"),//原先是createHashHistory(import.meta.env.BASE_URL)
      routes: [
        {
          path: '/',
          name: 'home',
          component: HomeView
        },{
          path:'/book',
          name:'book',
          component:BookView
        }
      ]
    })
    
  2. 运行vite build(或者npm run build)后会在项目目录中生成一个dist文件夹,将该文件夹中的index.html放入到flask项目中的template文件夹中,将其他的文件放到flask项目中的static文件夹中。

    注意:如果项目中存在图片需要将图片的引用地址改为../static/assets/xxx/xxx同时确保在打包时该图片被放在了assets文件夹下

  3. 修改template中的index.html

    <script type="module" crossorigin src="/assets/index-xxxx.js"></script>改为<script type="module" crossorigin src="../static/assets/index-xxxx.js"></script>

    <link rel="stylesheet" href="/assets/index-xxxx.css">改为<link rel="stylesheet" href="../static/assets/index-xxx.css">

第四部分:打包

  1. 安装auto-py-to-exe

    pip install auto-py-to-exe
    
  2. 执行auto-py-to-exe

    项目目录中输入下面的命令

    auto-py-to-exe
    

    在弹出的窗口中,脚本位置选择本项目的app.py文件,然后依次选择单文件→基于窗口的,在附加文件处点击“添加目录”将static文件夹和template文件夹添加进去,最后点击最下面的生成exe文件等待生成完毕。

  3. 运行打包的程序。

可能遇到的问题

  1. 运行时提示xxxx has no attribute aaa

    • 确保该类是否有属性aaa

    • 如果该类有属性aaa,请确保该属性是不是公有的

  2. 桌面窗口弹出后什么都没有

    • 确保Vue使用的是WebHash路由模式并更改基本路径为"/"

    • 确保index.htmltemplate文件夹内

    • 如果你修改了index.html的文件名,那么@app.route("/")(后面统一称呼为根节点)路由方法的返回值应该是render_template("[你修改的文件名].html")

    • 确保根节点方法的return后面是render_template(xxx)而不是template_rendered(xxx)

  3. 桌面窗口弹出后有些图标(或样式)消失(或失效)

    • 确保vue打包后的assets文件夹和其他文件都放在了static文件夹中

    • 如果上面一条满足那么确保在打包前有将这些样式表或者图标的引用地址改为../static/assets/xxxxxx

    • 如果某个组件库导致这样的情况建议查看对应组件库中关于“静态资源配置”的文档

  4. 数据插入失败

    情形1:插入或者其他操作时莫名其妙少了一些字段导致事务失败

    这种情形请确保在声明字段时语法正确。

    举例:

    下面的代码是错误的

    class BookPwd(Base):
        __tablename__="book_pwd"
        recordId=Column(Integer,primary_key=True,name="record_id",autoincrement=True)
        bookId=Column(String(16),ForeignKey("books.id",ondelete="CASCADE"),name="book_id"),# 这里多加了一个逗号
        passwordId=Column(String(16),ForeignKey("passwords.id",ondelete="CASCADE"),name="password_id")
        def to_json(self):
            dict = self.__dict__
            if "_sa_instance_state" in dict:
                del dict["_sa_instance_state"]
            return dict
    

    这种语法错误在程序运行时是不会被检查的,只有对book_pwd数据表进行操作时才会报错,此时查看日志会发现少了bookId这列,只需要将多加的逗号去掉,然后重新运行就好。

    情形2:There is no column named "xxx"

    这种情形经常发生在前端向后端传值时。导致这个问题产生的原因是前端传递了多余的属性给后端,而后端没有处理便直接使用传递过来的信息。此时可能会有一些多余的列,那么在插入时便报错。

    解决方法是重新封装该数据,将多余的属性剔除。可以在前端进行也可以在后端进行。

  5. xxx不能转换为JSON

    这种情况往往发生在从数据库取值后返回给前端时。解决办法是使用每个实体类中的to_json方法将对象转换为字典,然后直接发送。对于多条数据,需要新建一个数组然后通过循环将变为字典的数据放入新数组中,将新数组返回。

    password=session.query(Passwords).filter(Passwords.id.in_(pwIdList)).all()
         pwList=[]
         for pl in password:
             pwList.append(pl.to_json())
         return send_ok_message_only_data(pwList)
    
  6. 为什么不使用“单目录”方式打包?

    使用单目录方式打包也是可以运行的,但是需要注意的是,这种方法打包出来的文件是不能进行压缩的。一旦压缩就会出现文件损坏的现象进而导致程序无法运行。因此,如果需要在网站上发布这个项目,建议使用单文件的方式,这样做易于分发。如果已经以“单目录”方式打包,那么建议将这些文件做成安装包,不要进行压缩操作。

  7. 打包时可以使用UPX压缩吗?

    可以,auto-py-to-exe提供UPX压缩,但有一些问题需要注意。

    使用UPX压缩后会导致文件VCRUNTIME140.dll损坏,因此在压缩时要排除这个文件。