手把手教你使用Go和HTML开发客户端软件的详细教程

2026-02-08 0 397

科技进步带动了程序员开发桌面客户端软件的需求。Go语言在这一领域表现突出。今天,我们将探讨如何利用Go和HTML,通过开源项目打造跨平台桌面客户端。这既实用又充满乐趣。

Go语言的优势

Go语言设计简洁,效率高,拥有众多第三方库,使得开发变得更为顺畅。众多程序员因它强大的功能而喜爱。例如,在构建大型网络应用时,Go能高效快速地处理大量网络请求。在不少知名企业的桌面客户端开发中,Go语言的简洁性也帮助团队节省了大量开发时间。它之所以被广泛应用,还因为它能适配多种操作系统。

Go语言在桌面应用开发领域的影响力日益增强。有这样一个例子,某公司先前尝试用其他编程语言打造一款桌面小工具,结果开发过程漫长,问题不断。后来,他们改用Go语言,开发效率大幅提高,产品性能也显著增强。

多平台适用的重要性

go version

现在,用户们操作着各式各样的操作系统,桌面客户端若想受欢迎,必须能在多个平台上运行。一个出色的客户端软件,必须支持Windows、macOS以及Linux等主流操作系统。以某办公软件为例,它起初仅限于Windows系统,这让许多使用Mac和Linux的用户无法使用,限制了其用户基础。后来,该软件转变为跨平台软件,用户数量随之显著增长。

多平台兼容并非仅仅为了扩充用户规模,它还能增强软件的综合竞争力。若软件想在市场上表现出色,忽视任何主流平台都是不可取的。

安装Go的要点

开展开发任务前,需确保Go语言已正确安装。通常情况下,它应当已经安装好。若未安装,请从Go的官方网站下载。安装完成后,可以通过特定命令来检验安装是否成功。特别提醒,由于涉及cgo开发,用户还需额外安装gcc。举例来说,不少初学者可能会在这个步骤出错,忽视gcc的安装,这可能导致后续开发中遇到编译无法通过等问题。

go get github.com/sciter-sdk/go-sciter

安装Go在不同操作系统上都有技巧。比如在Windows上,安装时路径中不能含有中文字符,否则可能会出现未知的错误。至于Linux系统,有时候权限问题也得特别小心处理。

安装SDK的细节

构建项目时,SDK的安装是不可或缺的一步。需要访问官方网站,挑选与操作系统相匹配的SDK版本进行下载。下载完毕后,需要将解压出的动态库文件加入到系统环境变量,或者与执行文件放在一起。新版本的SDK可能并不兼容,建议优先使用4.4.8版。许多开发者往往忽略版本差异,导致使用新SDK时出现不少奇怪的兼容性问题。

各个操作系统在这一阶段需注意的点各不相同。以MacOS为例,其动态库文件的存放位置与Windows系统存在区别。若操作不当,程序运行可能会遭受重大影响。

安装go – 的步骤

接下来,我们需使用Go命令来安装那个名为go的包。这个包在开发过程中扮演着连接Go语言与HTML的关键角色。比如,在数据传输的模块里,go包能够让Go的后端逻辑与基于HTML制作的界面实现无缝对接。若安装不当,这种对接将无法完成,程序的功能也将因此不完整。

package main
import (
	\"embed\"
	\"encoding/json\"
	\"fmt\"
	\"github.com/ncruces/zenity\"
	\"github.com/sciter-sdk/go-sciter\"
	\"github.com/sciter-sdk/go-sciter/window\"
	\"github.com/skratchdot/open-golang/open\"
	\"log\"
	\"os\"
	\"strconv\"
	\"strings\"
	\"time\"
)
// 为了让生成的可执行文件包含了界面文件,直接把views文件夹嵌入到可执行文件中
//go:embed all:views
var views embed.FS
// 定义一个Map类型的数据结构
type Map map[string]interface{}
func main() {
	w, err := window.New(sciter.SW_TITLEBAR|sciter.SW_RESIZEABLE|sciter.SW_CONTROLS|sciter.SW_MAIN|sciter.SW_ENABLE_DEBUG, &sciter.Rect{
		Left:   100,
		Top:    50,
		Right:  1100,
		Bottom: 660,
	})
	if err != nil {
		log.Fatal(err)
	}
  // 定义一个回调函数,用于处理加载资源,home 是自定义的Scheme
	w.SetCallback(&sciter.CallbackHandler{
		OnLoadData: func(params *sciter.ScnLoadData) int {
			if strings.HasPrefix(params.Uri(), \"home://\") {
				fileData, err := views.ReadFile(params.Uri()[7:])
				if err == nil {
					w.DataReady(params.Uri()[7:], fileData)
				}
			}
			return 0
		},
	})
  // 这里定义一些与前端交互的函数
	w.DefineFunction(\"openUrl\", openUrl)
	w.DefineFunction(\"getIndexingTasks\", getIndexingTasks)
	w.DefineFunction(\"getIndexingTask\", getIndexingTask)
	w.DefineFunction(\"getIndexingUrls\", getIndexingUrls)
	w.DefineFunction(\"openAccountJson\", openAccountJson)
	w.DefineFunction(\"loadIndexingSitemap\", loadIndexingSitemap)
	w.DefineFunction(\"createGoogleIndexing\", createGoogleIndexing)
	w.DefineFunction(\"startGoogleIndexing\", startGoogleIndexing)
	w.DefineFunction(\"stopGoogleIndexing\", stopGoogleIndexing)
	w.DefineFunction(\"deleteGoogleIndexing\", deleteGoogleIndexing)
  // 加载主页面
	mainView, err := views.ReadFile(\"views/main.html\")
	if err != nil {
		fmt.Print(\"nofile\", err)
		os.Exit(0)
	}
	w.LoadHtml(string(mainView), \"\")
	w.SetTitle(\"谷歌推送\")
	w.Show()
	w.Run()
}
func openUrl(args ...*sciter.Value) *sciter.Value {
	link := args[0].String()
	_ = open.Run(link)
	return nil
}
func getIndexingTasks(args ...*sciter.Value) *sciter.Value {
	//tasks := service.GetIndexingTasks()
  var task = []Map{}
	// 返回Json格式
	return jsonValue(tasks)
}
func getIndexingTask(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	//task := service.GetIndexingTask(index)
  task := Map{}
	// 返回Json格式
	return jsonValue(task)
}
func getIndexingUrls(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	page, _ := strconv.Atoi(args[1].String())
	if page < 1 {
		page = 1
	}
	//urls, totalPage := service.GetIndexingUrls(index, page)
  urls := []string{}
  totalPage := 0
	// 返回Json格式
	return jsonValue(Map{\"urls\": urls, \"page\": page, \"totalPage\": totalPage})
}
func openAccountJson(args ...*sciter.Value) *sciter.Value {
	accountPath, err := zenity.SelectFile(zenity.Title(\"选择Account Json文件\"), zenity.FileFilter{
		Name:     \"Json file\",
		Patterns: []string{\"*.json\"},
		CaseFold: false,
	})
	if err != nil || accountPath == \"\" {
		fmt.Println(err)
		return nil
	}
	return sciter.NewValue(accountPath)
}
func createGoogleIndexing(args ...*sciter.Value) *sciter.Value {
	accountPath := args[0].String()
	domain := args[1].String()
	tmpNum := args[2].String()
	dailyNum, _ := strconv.Atoi(tmpNum)
	if dailyNum == 0 {
		dailyNum = 200
	}
	if !strings.HasPrefix(domain, \"http\") {
		return sciter.NewValue(\"网址填写错误\")
	}
	// err := service.CreateIndexing(accountPath, domain, dailyNum)
	// if err != nil {
	// 	return sciter.NewValue(err.Error())
	// }
	return nil
}
func loadIndexingSitemap(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	// err := service.LoadIndexingSitemap(index, false)
	// if err != nil {
	// 	return sciter.NewValue(err.Error())
	// }
	return nil
}
func startGoogleIndexing(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	// err := service.StartGoogleIndexing(index)
	// if err != nil {
	// 	return sciter.NewValue(err.Error())
	// }
	return nil
}
func stopGoogleIndexing(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	//service.StopGoogleIndexing(index)
	return nil
}
func deleteGoogleIndexing(args ...*sciter.Value) *sciter.Value {
	index, _ := strconv.Atoi(args[0].String())
	// 需要先stop
	// service.StopGoogleIndexing(index)
	// // 最后删除
	// service.DeleteIndexingTask(index)
	return nil
}
func jsonValue(val interface{}) *sciter.Value {
	buf, err := json.Marshal(val)
	if err != nil {
		return nil
	}
	return sciter.NewValue(string(buf))
}

在安装过程中,常常会遇到一些普遍的问题,比如因网络问题导致的下载失败。多数情况下,通过重新尝试可以解决问题。但有时,我们必须重新核查网络和开发环境的配置。

编写客户端程序过程

<html resizeable>
<head>
    <style src=\"home://views/style.css\" />
    <meta charSet=\"utf-8\" />
</head>
<body>
<div class=\"layout\">
    <div class=\"aside\">
        <h1 class=\"soft-title\"><a href=\"home://views/main.html\">谷歌<br/>推送助手</a></h1>
        <div class=\"aside-menus\">
            <a href=\"home://views/task.html\" class=\"menu-item\">推送任务</a>
            <a href=\"home://views/help.html\" class=\"menu-item\">使用教程</a>
        </div>
    </div>
    <div class=\"container\">
        <div class=\"home\">
            <div>欢迎使用 谷歌推送助手</div>
            <div class=\"start-control\">
                <a href=\"home://views/task.html\" class=\"start-btn\">开始使用</a>
            </div>
        </div>
    </div>
</div>
</body>
</html>

在编写客户端程序时,我们选取了一个案例,即自动将网站URL提交给推送服务器。完成项目创建后,需要将相应的.dll或.dylib文件存放在项目的主目录中,并依据实际需求复制数据库相关的动态库文件。

编写代码时,main.go文件和各个视图的HTML文件各有其独特之处。main.go承载着程序的核心逻辑,而HTML文件则负责构建用户界面。以views目录下的task.html为例,它需要处理列表展示等任务,这就要求开发者必须根据具体的功能需求来编写代码。

<html resizeable>
<head>
    <style src=\"home://views/style.css\" />
    <meta charSet=\"utf-8\" />
</head>
<body>
<div class=\"layout\">
    <div class=\"aside\">
        <h1 class=\"soft-title\"><a href=\"home://views/main.html\">谷歌<br/>推送助手</a></h1>
        <div class=\"aside-menus\">
            <a href=\"home://views/task.html\" class=\"menu-item active\">推送任务</a>
            <a href=\"home://views/help.html\" class=\"menu-item\">使用教程</a>
        </div>
    </div>
    <div class=\"container\">
        <div class=\"task-head\">
            <button #newTask>新建任务</button>
        </div>
        <table class=\"task-list\" #taskList>
                <colgroup>
                    <col width=\"30%\">
                    <col width=\"15%\">
                    <col width=\"15%\">
                    <col width=\"15%\">
                    <col width=\"30%\">
                </colgroup>
                <thead>
                    <tr>
                        <th>站点域名</th>
                        <th>URL数量</th>
                        <th>已推送/每日推送</th>
                        <th>状态</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                <tr>
                    <td colspan=\"5\">加载中</td>
                </tr>
                </tbody>
            </table>
    </div>
    <form class=\"control-form\" #taslForm>
            <div class=\"form-header\">
                <a class=\"form-close\" #resultClose>关闭</a>
                <h3>创建/编辑任务</h3>
            </div>
        <div class=\"form-content\">
            <div class=\"form-item\">
                <div class=\"form-label\">网址或Sitemap地址:</div>
                <div class=\"input-block\">
                    <input(domain) class=\"layui-input\" type=\"text\" placeholder=\"http://或https://开头的网站地址或Sitemap地址\" />
                    <div class=\"text-muted\">说明:如果填写了Sitemap地址,将自动获取Sitemap中的所有URL推送,<br/>否则将抓取推送网址下的所有链接。</div>
                </div>
            </div>
            <div class=\"form-item\">
                <div class=\"form-label\">选择AccountJson:</div>
                <div class=\"input-block text-left\">
                    <div>
                        <button #selectAccountJson>选择.json文件</button>
                        <span #accountJson></span>
                    </div>
                    <div class=\"text-muted\">说明:需要上传谷歌账号的json文件,用于授权。</div>
                </div>
            </div>
            <div class=\"form-item\">
                <div class=\"form-label\">每天推送数量:</div>
                <div class=\"input-block\">
                    <input(daily_num) class=\"layui-input\" type=\"text\" placeholder=\"默认200\" />
                    <div class=\"text-muted\">说明:请根据你的接口限制,填写每天推送的量。</div>
                </div>
            </div>
            <div>
                <button type=\"default\" #formClose>返回</button>
                <button type=\"default\" #taskSubmit>提交</button>
            </div>
        </div>
    </form>
        <div class=\"result-list\" #resultList>
            <div class=\"form-header\">
                <a class=\"form-close\" #resultClose>关闭</a>
                <h3>查看结果</h3>
            </div>
            <div class=\"form-content\">
                <table>
                    <colgroup>
                        <col width=\"40%\">
                        <col width=\"60%\">
                    </colgroup>
                    <tbody>
                    <tr>
                        <td>网站网站</td>
                        <td #resultDomain></td>
                    </tr>
                    <tr>
                        <td>每日推送数量</td>
                        <td #resultDailyNum>0条</td>
                    </tr>
                    <tr>
                        <td>执行状态</td>
                        <td #resultStatus>waiting</td>
                    </tr>
                    <tr>
                        <td>已发现URL</td>
                        <td #resultUrlCount>0条</td>
                    </tr>
                    <tr>
                        <td>已推送</td>
                        <td #resultDailyFinished>0条</td>
                    </tr>
                    <tr>
                        <td>推送结果</td>
                        <td class=\"text-left\" #resultResult>
                            /* <div><span>https://www.anqicms.com</span><span>失败</span></div> */
                        </td>
                    </tr>
                    <tr>
                        <td></td>
                        <td>
                            <div>
                                <span class=\"pate-item\">页码:<span #resultPage>1</span>/<span #resultTotalPage>1</span></span>
                                <button #resultPrev>上一页</button>
                                <button #resultNext>下一页</button>
                            </div>
                        </td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
</div>
</body>
</html>
<script type=\"text/tiscript\">
    function syncTasks() {
        let res = view.getIndexingTasks()
        let result = JSON.parse(res)
        // 重置 #taskList
        let tb = $(#taskList>tbody)
        tb.html = \"\"
        if (!result) {
            return;
        }
        for (let i = 0; i < result.length; i++) {
            let task = result[i];
            let tr = new Element(#tr)
            tr.append(new Element(#td, task.domain))
            tr.append(new Element(#td, task.url_count + \"\"))
            tr.append(new Element(#td, task.daily_finished + \"/\" + task.daily_num))
            tr.append(new Element(#td, task.status + \"\"))
            let td = new Element(#td)
            td.@#class = \"control-btns\"
            td.attributes[\"id\"] = \"task-\" + task.id
            addControlBtn(td, \"结果\", \"task-result\")
            if (task.status == \"running\") {
                addControlBtn(td, \"停止\", \"task-stop\")
            } else {
                addControlBtn(td, \"启动\", \"task-start\")
            }
            if (task.status != \"running\") {
                addControlBtn(td, \"编辑\", \"task-edit\")
                addControlBtn(td, \"删除\", \"task-delete\")
            }
            tr.append(td)
            tb.append(tr)
        }
    }
    function addControlBtn(el, str, cls) {
        let bt = new Element(#button, str)
        bt.@#class = cls
        el.append(bt)
    }
    self.on(\"click\",\".task-start\", function() {
        let id = this.$p(td).attributes['id'].replace(\"task-\", \"\")
        let result = view.startGoogleIndexing(id)
        //view.msgbox(#alert, result || \"启动成功\");
    });
    self.on(\"click\",\".task-stop\", function() {
        let id = this.$p(td).attributes['id'].replace(\"task-\", \"\")
        let result = view.stopGoogleIndexing(id)
        //view.msgbox(#alert, result || \"停止成功\");
    });
    self.on(\"click\",\".task-edit\", function() {
        let id = this.$p(td).attributes['id'].replace(\"task-\", \"\")
        showEditWindow(id)
    });
    self.on(\"click\",\".task-result\", function() {
        let id = this.$p(td).attributes['id'].replace(\"task-\", \"\")
        stdout.println(this.$p(td).attributes['id'])
        showResultWindow(id, 1)
    });
    self.on(\"click\",\".task-delete\", function() {
        let id = this.$p(td).attributes['id'].replace(\"task-\", \"\")
        let result = view.deleteGoogleIndexing(id)
        //view.msgbox(#alert, result || \"删除成功\");
    });
    // 新建任务
    event click $(#newTask){
        showEditWindow(\"-1\")
    }
    function showEditWindow(id) {
        let res = view.getIndexingTask(id);
        let result = JSON.parse(res) || {};
        // 回填表单
        $(#taslForm).value=result;
        $(#taslForm).@.addClass(\"active\");
    }
    // 表单
    let accountPath = '';
    event click $(#selectAccountJson){
        let filePath = view.openAccountJson()
        self#accountJson.text = filePath
        accountPath = filePath;
    }
    event click $(#formClose){
        $(#taslForm).@.removeClass(\"active\");
    }
    event click $(#taskSubmit){
        // 第一步,先保存授权信息
        // 第二步,抓取Sitemap
        // 第三步,开始推送
        let result = view.createGoogleIndexing(accountPath, $(#taslForm).value.domain, $(#taslForm).value.daily_num)
        stdout.println(result)
        view.msgbox(#alert, result || \"保存成功\");
        if (!result) {
            $(#taslForm).@.removeClass(\"active\");
        }
        // 同步结果
        syncTasks();
    }
    let curId = 0;
    let curPage = 1;
    let totalPage = 1;
    function showResultWindow(id, curp) {
        curId = id;
        let res = view.getIndexingTask(curId);
        let result = JSON.parse(res) || {};
        $(#resultList).@.addClass(\"active\");
        $(#resultDomain).text = result.domain;
        $(#resultDailyNum).text = result.daily_num + \"条\";
        $(#resultStatus).text = result.status;
        $(#resultUrlCount).text = result.url_count + \"条\";
        $(#resultDailyFinished).text = \"累计:\" + result.total_finished + \"条\" + \" / 今日:\" + result.daily_finished + \"条\" + (result.daily_finished >= result.daily_num ? ' / 今日已完成' : '');
        
        let res2 = view.getIndexingUrls(curId, curp)
        let result2 = JSON.parse(res2) || {};
        $(#resultPage).text = result2.page + \"\";
        $(#resultTotalPage).text = result2.totalPage + \"\";
        curPage = result2.page
        totalPage = result2.totalPage
        $(#resultResult).html = '';
        for (let val in result2.urls) {
            $(#resultResult).append(\"\" + val.url + \"<span class='item-status' title='"+(val.msg || (val.status == 0 ? '未开始' :''))+\"'>\" + (val.status == 0 ? '-' : val.status != 200 ? \"\"+val.status+\"\" : val.status)+\"\")
        }
    }
    event click $(#resultPrev) {
        if(curPage <= 1) {
            curPage = 1;
            return;
        }
        curPage = curPage - 1;
        showResultWindow(curId, curPage);
    }
    event click $(#resultNext) {
        if(curPage >= totalPage) {
            curPage = totalPage;
            return;
        }
        curPage = curPage + 1;
        showResultWindow(curId, curPage);
    }
    event click $(.item-status) {
        let title = this.attributes['title'];
        if (title) {
            view.msgbox(#error, title);
        }
    }
    event click $(#resultClose){
        $(#resultList).@.removeClass(\"active\");
        $(#taslForm).@.removeClass(\"active\");
    }
     // 进来的时候先执行一遍
    syncTasks();
    // 加载tasklist,2秒钟刷新一次
    self.timer(2000ms, function() {
        syncTasks();
        return true;
    });
</script>

您是否曾用Go编写过桌面应用程序?欢迎在评论区分享您的使用心得。同时,别忘了为这篇文章点赞并转发。

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

申明:本文由第三方发布,内容仅代表作者观点,与本网站无关。对本文以及其中全部或者部分内容的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。本网发布或转载文章出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,也不代表本网对其真实性负责。

左子网 开发教程 手把手教你使用Go和HTML开发客户端软件的详细教程 https://www.zuozi.net/79394.html

常见问题
  • 1、自动:拍下后,点击(下载)链接即可下载;2、手动:拍下后,联系卖家发放即可或者联系官方找开发者发货。
查看详情
  • 1、源码默认交易周期:手动发货商品为1-3天,并且用户付款金额将会进入平台担保直到交易完成或者3-7天即可发放,如遇纠纷无限期延长收款金额直至纠纷解决或者退款!;
查看详情
  • 1、描述:源码描述(含标题)与实际源码不一致的(例:货不对板); 2、演示:有演示站时,与实际源码小于95%一致的(但描述中有”不保证完全一样、有变化的可能性”类似显著声明的除外); 3、发货:不发货可无理由退款; 4、安装:免费提供安装服务的源码但卖家不履行的; 5、收费:价格虚标,额外收取其他费用的(但描述中有显著声明或双方交易前有商定的除外); 6、其他:如质量方面的硬性常规问题BUG等。 注:经核实符合上述任一,均支持退款,但卖家予以积极解决问题则除外。
查看详情
  • 1、左子会对双方交易的过程及交易商品的快照进行永久存档,以确保交易的真实、有效、安全! 2、左子无法对如“永久包更新”、“永久技术支持”等类似交易之后的商家承诺做担保,请买家自行鉴别; 3、在源码同时有网站演示与图片演示,且站演与图演不一致时,默认按图演作为纠纷评判依据(特别声明或有商定除外); 4、在没有”无任何正当退款依据”的前提下,商品写有”一旦售出,概不支持退款”等类似的声明,视为无效声明; 5、在未拍下前,双方在QQ上所商定的交易内容,亦可成为纠纷评判依据(商定与描述冲突时,商定为准); 6、因聊天记录可作为纠纷评判依据,故双方联系时,只与对方在左子上所留的QQ、手机号沟通,以防对方不承认自我承诺。 7、虽然交易产生纠纷的几率很小,但一定要保留如聊天记录、手机短信等这样的重要信息,以防产生纠纷时便于左子介入快速处理。
查看详情

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务