Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE

Posted web前端项目案例实战

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE相关的知识,希望对你有一定的参考价值。

基于electron25+vite4+vue3仿制chatgpt客户端聊天模板ElectronChatGPT

electron-chatgpt 使用最新桌面端技术Electron25.x结合Vite4.x全家桶技术开发跨端模仿ChatGPT智能聊天程序模板。支持经典+分栏两种布局、暗黑+明亮主题模式,集成electron封装多窗口及通讯功能。

技术栈

  • 编码工具:vscode
  • 框架技术:electron25+vite4+vue3+pinia2
  • 组件库:veplus (基于vue3自定义组件库)
  • 打包工具:electron-builder^23.6.0
  • 调试工具:electron-devtools-installer^3.2.0
  • 代码高亮:highlight.js^11.7.0
  • markdown组件:vue3-markdown-it
  • 本地缓存:pinia-plugin-persistedstate^3.1.0
  • electron结合vite插件:vite-plugin-electron^0.11.2

项目结构

基于electron最新版本融合vite4.x技术搭建模仿chatgpt桌面端程序。

如果对electron+vite4创建跨端应用及多开窗口感兴趣,可以去看看之前的这两篇分享文章。

https://www.cnblogs.com/xiaoyan2017/p/17436076.html

https://www.cnblogs.com/xiaoyan2017/p/17442502.html

随着electron快速迭代更新,加上vite极速编译,二者配合创建的应用运行速度超快。

Vue3桌面UI组件库

考虑到项目比较轻量级,所以采用自研vue3组件库ve-plus

关于veplus组件库这里不作过多介绍,之前有过一篇分享文章,大家可以去看看。

https://www.cnblogs.com/xiaoyan2017/p/17170454.html

项目布局

项目整体大致分为顶部导航工具栏+左侧会话记录/操作链接+右侧会话区/编辑框等模块。

<template>
    <div class="vegpt__layout flexbox flex-col">
        <!-- //顶部工具栏 -->
        <Toolbar />
        
        <div class="ve__layout-body flex1 flexbox">
            <!-- //侧边栏 -->
            <div class="ve__layout-menus flexbox" :class="\'hidden\': store.config.collapse">
                <aside class="ve__layout-aside flexbox flex-col">
                    <ChatNew />
                    <Scrollbar class="flex1" autohide size="4" gap="1">
                        <ChatList />
                    </Scrollbar>
                    <ExtraLink />
                    <Collapse />
                </aside>
            </div>

            <!-- //主体区域 -->
            <div class="ve__layout-main flex1 flexbox flex-col">
                <Main />
            </div>
        </div>
    </div>
</template>

Electron主进程入口

根目录下新建 electron-main.js 作为主进程入口文件。

/**
 * 主进程入口
 * @author YXY
 */

const  app, BrowserWindow  = require(\'electron\')

const MultiWindow = require(\'./src/multiwindow\')

// 屏蔽安全警告
// ectron Security Warning (Insecure Content-Security-Policy)
process.env[\'ELECTRON_DISABLE_SECURITY_WARNINGS\'] = \'true\'

const createWindow = () => 
    let win = new MultiWindow()
    win.createWin(isMainWin: true)


app.whenReady().then(() => 
    createWindow()
    app.on(\'activate\', () => 
        if(BrowserWindow.getAllWindows().length === 0) createWindow()
    )
)

app.on(\'window-all-closed\', () => 
    if(process.platform !== \'darwin\') app.quit()
)

使用electron的vite插件,在vite.config.js中配置入口。

import  defineConfig, loadEnv  from \'vite\'
import vue from \'@vitejs/plugin-vue\'
import electron from \'vite-plugin-electron\'
import  resolve  from \'path\'
import  parseEnv  from \'./src/utils/env\'

export default defineConfig(( command, mode ) => 
  const viteEnv = loadEnv(mode, process.cwd())
  const env = parseEnv(viteEnv)

  return 
    plugins: [
      vue(),
      electron(
        // 主进程入口文件
        entry: \'electron-main.js\'
      )
    ],
    
    /*构建选项*/
    build: 
      /* minify: \'esbuild\', // 打包方式 esbuild(打包快)|terser
      chunkSizeWarningLimit: 2000, // 打包大小警告
      rollupOptions: 
          output: 
              chunkFileNames: \'assets/js/[name]-[hash].js\',
              entryFileNames: \'assets/js/[name]-[hash].js\',
              assetFileNames: \'assets/[ext]/[name]-[hash].[ext]\',
          
       */
      
      // 如果打包方式是terser,则配置如下
      /* minify: "terser",
      terserOptions: 
        compress: 
          // 去掉所有console和debugger
          // drop_console: true,
          // drop_debugger: true,

          drop_console: command !== \'serve\',
          drop_debugger: command !== \'serve\',
          //pure_funcs:[\'console.log\'] // 移除console.log
        
       */
    ,
    esbuild: 
      // 打包去除 console.log 和 debugger
      drop: env.VITE_DROP_CONSOLE && command === \'build\' ? ["console", "debugger"] : []
    ,

    /*开发服务器选项*/
    server: 
      // 端口
      port: env.VITE_PORT,
      // ...
    ,

    resolve: 
      // 设置别名
      alias: 
        \'@\': resolve(__dirname, \'src\'),
        \'@assets\': resolve(__dirname, \'src/assets\'),
        \'@components\': resolve(__dirname, \'src/components\'),
        \'@views\': resolve(__dirname, \'src/views\')
      
    
  
)

需要注意:由于目前Electron 尚未支持 "type": "module",需要在package.json中去掉,并且配置 "main": "electron-main.js", 入口。

Electron自定义无边框窗口工具栏

创建窗口的时候配置 frame: false 参数,创建的窗口则没有系统顶部导航栏及边框。拖拽区域/最大化/最小化及关闭按钮均需要自定义操作。

通过设置css3属性 -webkit-app-region: drag ,则可对自定义区域进行拖拽操作,设置后按钮/链接点击则会失效,这时通过对按钮或链接设置-webkit-app-region: no-drag就可恢复事件响应。

不过设置-webkit-app-region: drag,点击鼠标右键,会出现上图系统菜单,经过一番调试,windows下可以暂时通过如下方法屏蔽右键菜单。

// 屏蔽系统右键菜单
win.hookWindowMessage(278, () => 
    win.setEnabled(false)
    setTimeout(() => 
        win.setEnabled(true)
    , 100)

    return true
)

components/titlebar目录自定义工具栏条。

control.vue自定义最大化/最小化/关闭按钮

<template>
    <div class="vegpt__control ve__nodrag">
        <div class="vegpt__control-btns" :style="\'color\': color">
            <slot />
            <div v-if="isTrue(minimizable)" class="btn win-btn win-min" @click="handleMin"><i class="iconfont ve-icon-minimize"></i></div>
            <div v-if="isTrue(maximizable) && winCfg.window.resizable" class="btn win-btn win-maxmin" @click="handleRestore">
                <i class="iconfont" :class="isMaximized ? \'ve-icon-maxrestore\' : \'ve-icon-maximize\'"></i>
            </div>
            <div v-if="isTrue(closable)" class="btn win-btn win-close" @click="handleQuit"><i class="iconfont ve-icon-close"></i></div>
        </div>
    </div>
</template>
<template>
    <div class="vegpt__control ve__nodrag">
        <div class="vegpt__control-btns" :>
            <slot />
            <div v-if="isTrue(minimizable)" class="btn win-btn win-min" @click="handleMin"><i class="iconfont ve-icon-minimize"></i></div>
            <div v-if="isTrue(maximizable) && winCfg.window.resizable" class="btn win-btn win-maxmin" @click="handleRestore">
                <i class="iconfont" :class="isMaximized ? \'ve-icon-maxrestore\' : \'ve-icon-maximize\'"></i>
            </div>
            <div v-if="isTrue(closable)" class="btn win-btn win-close" @click="handleQuit"><i class="iconfont ve-icon-close"></i></div>
        </div>
    </div>
</template>


<script setup>
    import  onMounted, ref  from \'vue\'
    import  winCfg, setWin  from \'@/multiwindow/actions\'
    import  appStore  from \'@/pinia/modules/app\'
    import  isTrue  from \'@/utils\'

    const appState = appStore()

    const props = defineProps(
        // 标题颜色
        color: String,

        // 窗口是否可以最小化
        minimizable:  type: [Boolean, String], default: true ,
        // 窗口是否可以最大化
        maximizable:  type: [Boolean, String], default: true ,
        // 窗口是否可以关闭
        closable:  type: [Boolean, String], default: true 
    )

    // 是否最大化
    let isMaximized = ref(false)

    onMounted(() => 
        window.electronAPI.invoke(\'win__isMaximized\').then(data => 
            console.log(data)
            isMaximized.value = data
        )
        window.electronAPI.receive(\'win__hasMaximized\', (e, data) => 
            console.log(data)
            isMaximized.value = data
        )
    )

    // 最小化
    const handleMin = () => 
        window.electronAPI.send(\'win__minimize\')
    
    // 最大化/还原
    const handleRestore = () => 
        window.electronAPI.invoke(\'win__max2min\').then(data => 
            console.log(data)
            isMaximized.value = data
        )
    
    // 关闭窗体
    const handleQuit = () => 
        if(winCfg.window.isMainWin) 
            MessageBox.confirm(\'应用提示\', \'是否最小化到托盘, 不退出程序?\', 
                type: \'warning\',
                cancelText: \'最小化至托盘\',
                confirmText: \'残忍退出\',
                confirmType: \'danger\',
                width: 300,
                callback: action => 
                    if(action == \'confirm\') 
                        appState.$reset()
                        setWin(\'close\')
                    else if(action == \'cancel\') 
                        setWin(\'hide\', winCfg.window.id)
                    
                
            )
        else 
            setWin(\'close\', winCfg.window.id)
        
    
</script>

在 index.vue 中引入 control.vue 操作按钮,并支持自定义左侧、标题等功能。

<template>
    <div class="vegpt__titlebar" :class="\'fixed\': isTrue(fixed), \'transparent fixed\': isTrue(transparent)">
        <div class="vegpt__titlebar-wrapper flexbox flex-alignc ve__drag" :style="\'background\': bgcolor, \'color\': color, \'z-index\': zIndex">
            <slot name="left">
                <img src="/logo.png" height="20" style="margin-left: 10px;" />
            </slot>
            <div class="vegpt__titlebar-title" :class="\'center\': isTrue(center)">
                <slot name="title"> title || winCfg.window.title || env.VITE_APPTITLE </slot>
            </div>

            <!-- 控制按钮 -->
            <Control :minimizable="minimizable" :maximizable="maximizable" :closable="closable">
                <slot name="btn" />
            </Control>
        </div>
    </div>
</template>

Electron创建系统托盘图标

// 创建系统托盘图标
createTray() 
    console.log(\'——+——+——Start Create Tray!\')
    console.log(__dirname)
    console.log(join(process.env.ROOT, \'resource/tray.ico\'))
    
    const trayMenu = Menu.buildFromTemplate([
        
            label: \'打开主界面\',
            icon: join(process.env.ROOT, \'resource/home.png\'),
            click: () => 
                try 
                    for(let i in this.group) 
                        let win = this.getWin(i)
                        if(!win) return
                        // 是否主窗口
                        if(this.group[i].isMainWin) 
                            if(win.isMinimized()) win.restore()
                            win.show()
                        
                    
                 catch (error) 
                    console.log(error)
                
            
        ,
        
            label: \'设置中心\',
            icon: join(process.env.ROOT, \'resource/setting.png\'),
            click: () => 
                for(let i in this.group) 
                    let win = this.getWin(i)
                    if(win) win.webContents.send(\'win__ipcData\',  type: \'CREATE_WIN_SETTING\', value: null )
                
            ,
        ,
        
            label: \'锁屏\',
            icon: join(process.env.ROOT, \'resource/lock.png\'),
            click: () => null,
        ,
        
            label: \'关闭托盘闪烁\',
            click: () => 
                this.flashTray(false)
            
        ,
        type: \'separator\',
        /* 
            label: \'重启\',
            click: () => 
                // app.relaunch( args: process.argv.slice(1).concat([\'--relaunch\']) )
                // app.exit(0)
            
        , */
        
            label: \'关于\',
            click: () => 
                for(let i in this.group) 
                    let win = this.getWin(i)
                    if(win) win.webContents.send(\'win__ipcData\',  type: \'CREATE_WIN_ABOUT\', value: null )
                
            
        ,
        
            label: \'关闭应用并退出\',
            icon: join(process.env.ROOT, \'resource/quit.png\'),
            click: () => 
                dialog.showMessageBox(this.main, 
                    title: \'询问\',
                    message: \'确定要退出应用程序吗?\',
                    buttons: [\'取消\', \'最小化托盘\', \'退出应用\'],
                    type: \'error\',
                    noLink: false,  // true传统按钮样式  false链接样式
                    cancelId: 0
                ).then(res => 
                    console.log(res)

                    const index = res.response
                    if(index == 0) 
                        console.log(\'取消\')
                    if(index == 1) 
                        console.log(\'最小化托盘\')
                        for(let i in this.group) 
                            let win = this.getWin(i)
                            if(win) win.hide()
                        
                    else if(index == 2) 
                        console.log(\'退出应用\')

                        try 
                            for(let i in this.group) 
                                let win = this.getWin(i)
                                if(win) win.webContents.send(\'win__ipcData\',  type: \'WIN_LOGOUT\', value: null )
                            
                            // app.quit 和 app.exit(0) 都可退出应用。
                            // 前者可以被打断并触发一些事件,而后者将强制应用程序退出而不触发任何事件或允许应用程序取消操作。
                            app.quit()
                         catch (error) 
                            console.log(error)
                        
                    
                )
            
        
    ])
    this.tray = new Tray(this.trayIco1)
    this.tray.setContextMenu(trayMenu)
    this.tray.setToolTip(app.name)
    this.tray.on(\'double-click\', () => 
        console.log(\'double clicked\')
    )

    // 开启托盘闪烁
    // this.flashTray(true)

托盘图标、右键菜单图标及打包图标均在resource目录下。

Electron打包脚本electron-builder

在根目录新建一个electron打包配置文件electron-builder.json。


    "productName": "Electron-ChatGPT",
    "appId": "com.yxy.electron-chatgpt-vue3",
    "copyright": "Copyright © 2023-present Andy",
    "compression": "maximum",
    "asar": true,
    "directories": 
        "output": "release/$version"
    ,
    "nsis": 
        "oneClick": false,
        "allowToChangeInstallationDirectory": true,
        "perMachine": true,
        "deleteAppDataOnUninstall": true,
        "createDesktopShortcut": true,
        "createStartMenuShortcut": true,
        "shortcutName": "ElectronVite4Vue3"
    ,
    "win": 
        "icon": "./resource/shortcut.ico",
        "artifactName": "$productName-v$version-$platform-$arch-setup.$ext",
        "target": [
            
                "target": "nsis",
                "arch": ["ia32"]
            
        ]
    ,
    "mac": 
        "icon": "./resource/shortcut.icns",
        "artifactName": "$productName-v$version-$platform-$arch-setup.$ext"
    ,
    "linux": 
        "icon": "./resource",
        "artifactName": "$productName-v$version-$platform-$arch-setup.$ext"
    

Electron主渲染进程通讯传值

由于electron主渲染进程一般都是单窗口之间进行传值。如果需要在多个窗口间传值,如切换主题功能,则需要在渲染进程发送请求,主进程监听后再发送请求给渲染进程(App.vue中监听)。

<div
    class="toolbar__item"
    :title="`切换 暗黑/明亮 模式(当前 $appState.config.isDark ? \'暗黑\' : \'明亮\'模式)`"
    @click="changeMode"
>
    <Icon :name="appState.config.isDark ? \'ve-icon-sunny\' : \'ve-icon-yewan\'" />
</div>

// 主题切换
const changeMode = () => 
    appState.config.isDark = !appState.config.isDark
    ipcRenderer.send(\'win__postData\', appState.config.isDark)

在主进程中使用ipcMain.on监听。

// 主/渲染进程传参
ipcMain.on(\'win__postData\', (event, args) => 
    mainWin.webContents.send(\'win__postData\', args)
)

然后在渲染进程App.vue页面监听并处理通讯传值。

/**
 * 接收主进程发送的事件
 */
ipcRenderer.on(\'win__postData\', (e, data) => 
    console.log(\'——+——+——receive multiwin data:\', data)

    switch(data.type) 
        // 退出登录
        case \'WIN_LOGOUT\':
            appState.$reset()
            break;
        // 布局切换
        case \'CHANGE_LAYOUT\':
            appState.config.layout = data.value
            break;
        // 切换主题
        case \'CHANGE_MODE\':
            appState.config.isDark = data.value
            appState.changeDark()
            break;
        // 侧边栏收缩
        case \'CHANGE_COLLAPSE\':
            appState.config.collapse = data.value
            break;
      
)

这样就能简单实现多窗口传值了。如果大家有其他方法,欢迎一起交流学习哈~

Ok,基于electron25+vue3开发桌面端仿chatgpt聊天实例就先分享到这里,希望对大家有所帮助

转桌面端开发的感受

最近由Java服务端开发转到了桌面端开发,之前的项目暂时不需要更新维护,保持原样。

 

不仅仅是我转向了桌面端开发(主要针对VsCode二次开发),连安卓方面的那两个人也是如此。

 

之前我和另外JAVA的两个小伙伴是公司的前后台开发,而我就多兼任一个运维,至于测试,人人都是测试,彼此互测对方开发的软件。

 

转向桌面端开发(主要对VsCode二次开发),目前仅仅也只是看懂部分相关的javascript、css及其typescript的。

 

从来没有接触过桌面端开发。前端、运维、后台、自动化测试我都没怕过,这次我居然有点小恐惧。

 

第一周(也就是本周)为了达到某个需求,加班加点弄了两天,总还算是达到领导要求的那样。

 

关于对VsCode二次开发,由于之前对此一无所知,为此通过官网来梳理对应的信息(官方是最权威的,初学者最好还是通过官网来学习)。

 

注意:关于链接不能直接点击进入,需要手动复制到浏览器上打开

 

VsCode官网:

https://code.visualstudio.com/

 

VsCode源码:

https://github.com/Microsoft/vscode

 

VsCode架构梳理:

https://www.c-sharpcorner.com/article/vscode-architecture-and-overview/

 

 

对于梳理VsCode源码相关的,个人觉得这篇文章的参考价值还是比较高的:

http://ju.outofmemory.cn/entry/345080

 

但是不能否认由于VsCode处于不断的更新当中,有些博客确实过实了,有些信息就会对不上。

 

所以还是那句话,每当参考某篇博文时,比如修改源码,最好把文章整体看一遍,然后对着自己的源码,比较下,切勿看个大概,然后就开始一顿改,我之前就喜欢这样做,以至于被坑了不少。所以大家不要再重蹈覆辙了。

 

关于如何编译运行VsCode,最好是参考官网:

https://github.com/Microsoft/vscode/wiki/How-to-Contribute

 

同时一定要按照官网上面所要求的环境来,否则会出现很多问题。

 

当然了,也可以参考我的这篇博文

VsCode源码编译运行

 

地址为:

https://www.cnblogs.com/youcong/p/10230091.html

 

说了有点偏题了,不过之所以在前面说那么多,我觉得应该能对将要研究或者是对VsCode感兴趣的朋友们会有一定的帮助。

 

感触如下:

 

1.除了前面提到的恐惧之外,还有就是兴奋,因为这个领域我没有接触过,借此机会可以学习扩展自己的知识面和技能树;

 

2.经过这一周的研究,还是有不少启发的,比如VsCode的插件开发等,与我之前研究的wordpress其实相差并不大,要说的区别的话,也就是编程语言的不同及其端不同(一个是桌面端,一个是web端)。

 

3.眼界算是大开吧,我没有想到TypeScript和Electron及其NodeJs在一起居然能开发一个桌面应用,当然了,最主要还是TypeScript,但是掌握好Eletron对于理解VsCode会更好,这也是我哥哥对我说的,但是我目前没有去验证这一点,用百度去搜索,得来的electron的资源确实很有限,另外我不得不说w3cschool的electron教程没有其他教程详细全面有一个可以运行的地方(比如java就可以直接运行);

 

4.我觉得接触一项没有接触过的,或者是学习新的技术,一定不能认为自己学不会,可以从简单的入手,比如将源码跑起来,或者是改改VsCode布局以此来熟悉VsCode一部分代码或者是之前阅读官方文档不是那么仔细,现在可以再仔细看看,顺便将它上面可以跑起来的例子,自己在本地跑起来。比如插件开发,我就跑了一个hello world的例子,虽然说不是那么的实用,但是让我对此的熟读度越来越高了,当熟悉度越来越高时,你就可以尝试大幅度改代码。

 

有一点在此还是要强调一下,项目代码应该和试验项目代码进行分离。比如我本地有两个VsCode源码,一个是正式开发,一个是做试验的,不管是正式还是做试验我都会对其进行版本控制(使用Git),毕竟改着改着人的记忆是有限的,到时过了一段日子,突然忘记改了那部分就不好了,所以需要及时提交做好相关的记录(版本控制就能帮你做这些事情)。

 

 

也许会有人问,转桌面端开发是不是意味着以后你都做桌面端开发,我的回答是,当然不是。

 

我的核心还是以Java为主,也就是服务端开发。只不过由于目前桌面端那边人手不足我需要顶着,如果服务端需要我,我就去服务端,桌面端需要我,我就支援桌面端。

 

不过目前的话,我是两边都要沾点。主要还是以桌面端为主。

 

目前不仅仅是我这边需要转,原来的两个Java小伙伴他们也要转,只不过还是服务端,但是用的编程语言不一样,服务端目前用的是Python。

 

总而言之,大家接触的都算是新的技术,不过经过这一周我们还是克服了不少困难实现对应的目标。

 

由此我觉得所处创业公司带给我的一个最大收获就是,没有什么困难是克服不了的,只有想不到没有做不到(我想这就是创业精神)。

 

回顾我们的项目历程,从酒店管理系统、智能门锁、资源系统到现在的VsCode二次开发。基本上都遇到困难了,但是我们都解决了。

 

目前分给我的任务是算是插件开发,这一个让我有点头痛。插件开发目前不算是紧急,有一个相对比较紧急的就是修改原有的插件,但是这个插件跟我之前改的其实区别不算大。

 

目前我主要改造的是VsCode中的cpp,代码结构如下:

 

技术图片

 

 

明天可能要去公司加班弄这个玩意,虽头痛但是还是得把它给搞定。

 

 

关于研究数据结构与算法

最近下班回来后,阅读《数据结构与算法》这本书,这本书不得不承认不好啃。难读看不太难。于是为了让我自己注意力集中,不得不将上面一部分相关信息录入到我的学习笔记中,然而阅读看一遍。不过好在上面有代码示例,可以运行跑起来,这样使我不至于被催眠。

 

学习笔记放在我的博客园,代码示例我放在我的Github上。感兴趣的朋友可以去看看。

 

学习笔记:

https://www.cnblogs.com/youcong/category/1369746.html

 

代码示例:

https://github.com/youcong1996/The-Data-structures-and-algorithms

 

 

 

关于知识拓展

最近看的文章和书挺多的,阅读对我来说就是算是一种放松。

 

最近看罗振宇的知识就是力量。

 

同时也在看这三本书《孙子兵法》、《论语》、《黑客与画家》。

 

《论语》之前还听了会书,不过我觉得那个人讲的不好,还不如我看书来的思考快些(说到这,说不定我以后转职业去做计算机老师也是个不错的。

 

《孙子兵法》这本书我在高中的时候读过一遍,这个时候再看一遍纯属个人兴趣消遣一下,小时候一直喜欢历史和军事方面的。

 

《黑客与画家》每个程序员或许都有一个黑客梦,当然了,这本书并不是讲黑客,这本书是我的计算机偶像阮一峰先生翻译的,主要是冲他去的。

 

其实这三本书,我也是有选择的看(挑自己感兴趣的看,除了最后一本《黑客与画家》从头读到尾。

 

最近关于个税方面的改了很多,这个我了解不深,仅仅只是发现比之前多了点。

 

所以写完这篇文章后,我就去看关于个税方面的资料。

看完之后就去leetCode做两道算法题,这一天也就这样充实地度过了。

 

 

 

Docker教程

这个教程是我6月份到7月份的时候写的,那个时候正好买了一本叫《Docker入门到实战》的书,然后跟着上这本书学,记录相关的笔记,顺便运行相关命令看看效果。当然了,在看这本书之前,我就已经将菜鸟教程上面的教程玩了一遍。也算是对Docker有一个大致的了解吧。

 

目前的软件工程模式devops挺流行的。特别是其中的自动化技术,比如自动化部署之类的。目前主要用Docker比较多,再上升的话也就是k8s。但是k8s还是有一定的难度,最好还是将Docker用到一定的熟练度时再去学会更好,特别是Docker三剑客是一定要掌握的。

 

Docker相关教程(我这个关于Docker三剑客没有详细讲,等到到时用到的时候会补充的):

https://www.cnblogs.com/youcong/category/1222243.html

 

 

 

 

 

最后,继续朝着我的2019年目标前进。

 

2019 努力成为更好的自己

 

以上是关于Electron-ChatGPT桌面端ChatGPT实例|electron25+vue3聊天AI模板EXE的主要内容,如果未能解决你的问题,请参考以下文章

转桌面端开发的感受

修改远程桌面端口号.bat

Flutter 条件导入移动端 vs 桌面端

Windows远程桌面:端口号更改&指定IP连接

Java桌面端程序开发

2019-11-29-浅谈-Windows-桌面端触摸架构演进