我的问题
在我的工作中,我经常需要一些信息才能保持在埃及之外。我尝试了可能的类型的Stickit Notes和其他程序,但它们似乎都太大了,脚本不容易。我还需要来自家庭网络中多个系统的信息。因此,我在NW.js中创建了原始的BulletinBoard,其效果很好,但是现在我需要使其更小。另外,旧代码有点骇客。 NW.JS是使用HTML,CSS和JavaScript构建应用程序的好方法。由于它非常易于使用,因此我使用它来创建首次通过演示应用程序。问题在于它是一个完整的Chrome Web浏览器。对于一个小应用程序,这是太多浪费的记忆。因此,我在Wails中进行了全面的重新设计以使其足迹。
哀号
wails是一个go language程序,可以使用GO语言后端创建HTML,CSS和JavaScript应用程序。它创建了非常小的构建,因为它使用了尺寸最小的系统HTML浏览器(因此,最小的API)。这使程序可以在Windows,Linux和MacOS上使用,并且占地面积仍然很小。我将几个程序转换为哀号:Modal File Manager,EmailIt和ScriptBar。
为了通过原始Bulletinboard程序获得相同的功能,我需要内置完整的Web服务器。原始版本允许我使用Node.js Backend做到这一点。现在,我需要弄清楚如何对lang做同样的事情并仍然与哭声一起工作。
wails已经运行了Web服务器后端(我怀疑它是基于Web插座的),可以与程序的前端合作。由于我可以回到它上,因此我必须有一个单独的Web服务器后端。这是必要的,因为前端JavaScript不支持执行服务器。
问题变成:您可以在同一go后端运行单独的Web服务器吗?
当前哭泣的事情没有做
在撰写本文时,wails 2仍在开发中,并且没有此应用程序所需的所有功能。例如,它可以使用不显示的码头图标和梅纳巴尔图标来运行。这些仍在工作中。但是,它可以控制程序的隐藏和显示。它也可以使窗口始终在顶部。因此,大多数功能已准备就绪。
公告板设计
GO语言有几个用于运行Web服务器的后端。我决定使用gin后端,因为它非常易于使用,并且似乎是最受欢迎的后端。我以前只完成了一个项目,但这是一种简单的语言。
设计是基于API后端,该后端在:message
上接收到路由:message
的get请求是要显示的消息。同一消息以JSON格式在体内与:
{
msg: message
}
其中message
在网址中是相同的文本。这样做之所以这样做,是因为原始设计仅基于网址,但需要较长的消息需要身体。这是一个黑客,但是它奏效了,没有时间改变一切!另外,在脚本语言中易于实现get请求(例如使用wget
或curl
命令行程序)。
公告板通信 - 用OneModel制造
后端收到此请求,并使用信号将其传递给前端。 wails有一个很棒的基于事件的消息发送框架。前端是使用Svelte完成的。原件是简单的JavaScript和HTML,不太漂亮(好吧,这是一个快速的黑客)。现在,我使用一个不错的框架来构建一个易于扩展的模块化系统。
原始设计也允许对话框发送信息。对于这个项目,我只是在执行消息系统,以查看是否可以使用wails和Go Language Server进行工作。
让我开始
要开始,您需要安装nodejs with npm,go language和Wails 2。我在本文中使用了wails 2的43版。
为您的项目创建目录,并运行以下命令行,以使用Svelte初始化wails项目:
wails -n “BulletinBoard” -d . -t “svelte"
这将创建一个使用普通JavaScript的Svelte Frontend Wails项目。如果要使用TypeScript,请使用模板 - svelte-ts。
首先是设置main.go
文件,如下所示:
package main
import (
"embed"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
//go:embed frontend/dist
var assets embed.FS
//go:embed build/appicon.png
var icon []byte
func main() {
// Create an instance of the app structure
app := NewApp()
// Create application with options
err := wails.Run(&options.App{
Title: "BulletinBoard",
Width: 100,
Height: 60,
Assets: assets,
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 0},
DisableResize: true,
Fullscreen: false,
Frameless: false,
StartHidden: true,
AlwaysOnTop: true,
HideWindowOnClose: true,
OnStartup: app.startup,
OnDomReady: app.domReady,
OnShutdown: app.shutdown,
Bind: []interface{}{
app,
},
Mac: &mac.Options{
TitleBar: mac.TitleBarHiddenInset(),
Appearance: mac.NSAppearanceNameDarkAqua,
WebviewIsTransparent: true,
WindowIsTranslucent: true,
About: &mac.AboutInfo{
Title: "BulletinBoard",
Message: "© 2022 Richard Guay <raguay@customct.com>",
Icon: icon,
},
},
})
if err != nil {
println("Error:", err.Error())
}
}
此设置一个具有透明背景的典型程序,并在启动上隐藏。 OnStartup
条目设置为app.startup
,这是我们将启动服务器的地方。在此位置启动服务器将在一个地方具有所有自定义代码,并允许首先初始化前端的wails服务器。
接下来,我们需要在app.go
文件中创建我们的应用程序逻辑。添加以下信息:
package main
import (
"context"
"github.com/gin-gonic/gin” // webserver framework
rt "github.com/wailsapp/wails/v2/pkg/runtime”. // Wails runtime
"net/http"
"net/url"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
func (a *App) domReady(ctx context.Context) {
}
func (a *App) shutdown(ctx context.Context) {
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
//
// We need to start the backend and setup the signaling.
//
go backend(ctx)
}
这主要是设置代码,除了启动功能。 ctx
变量是整个应用程序的上下文,服务器需要运行。它使用该行传递给服务器:
go backend(ctx)
后端函数名称正面的go
告诉GO以Go Coroutine运行服务器。这是在没有干扰其他前端代码的单独线程上运行的。这允许服务器运行而不会打扰其他任何东西并使此项目起作用。
然后将以下内容添加到app.go
文件的底部:
type Msg struct {
Message string `json:"msg" xml:"user" binding:"required"`
}
func backend(ctx context.Context) {
//
// This will have the web server backend for BulletinBoard.
//
r := gin.Default()
r.Use(gin.Recovery())
//
// Define the message route. The message is given on the URI string and in the body.
//
r.GET("/api/message/:message", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "okay",
})
var json Msg
if err := c.ShouldBindJSON(&json); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
message := c.Param("message")
messageBody := json.Message
if messageBody != message {
message = messageBody
}
message, err := url.QueryUnescape(message)
if err != nil {
// An error in decoding.
message = ""
}
//
// Send it to the frontend.
//
rt.EventsEmit(ctx, "message", message)
})
//
// Run the server.
//
r.Run(":9697")
}
这是Gin Web服务器框架中单个路由的基本设置。现在是非常基本的,但是非常有用。我需要添加进行安全检查以使程序更安全的中间件。但是,我们只是想解释这种方法是可能的。
路由的功能从URI和Body JSON结构中获取消息。然后,它将其从URI编码中解码并检查它们是否相同。如果是这样,只需从URL发送一个即可。否则,它将从身体发送。
有趣的部分是使用wails Runtime rt.EventsEmit()
函数告诉前端我们有一个新消息要显示的下一行。该事件的名称````消息''将收到和设置以显示消息。这对于将来扩展其他类型很重要。
在frontend/src/main.js
文件中,添加此信息:
import BulletinBoard from './BulletinBoard.svelte'
const app = new BulletinBoard({
target: document.body
})
export default BulletinBoard
这是用于启动Svelte应用程序的简单样板代码。 BulletinBoard.svelte
文件是主要程序。
现在用于主程序。使用frontend/src
目录创建BulletinBoard.svelte
文件,并使用以下代码:
<script>
import { onMount, afterUpdate } from "svelte";
import Message from "./components/Message.svelte";
import { state } from "./stores/state.js";
import { theme } from "./stores/theme.js";
import { message } from "./stores/message.js";
import * as rt from "../wailsjs/runtime/runtime.js"; // the runtime for Wails2
let containerDOM = null;
let minWidth = 300;
let minHeight = 60;
onMount(() => {
$state = "nothing";
getTheme();
//
// Set a function to run when a event (signal) is sent from the webserver.
//
rt.EventsOn("message", (msg) => {
if (msg.trim().length !== 0) {
//
// Set the message state and save the message in the store.
//
$state = "message";
$message = msg;
//
// Show window in case it's off.
//
rt.WindowShow();
} else {
//
// An empty message send by having just a space turns off the BulletinBoard.
//
rt.WindowHide();
}
});
});
afterUpdate(() => {
//
// The nothing state should force a window hiding.
//
if($state === "nothing") {
rt.WindowHide();
}
//
// Figure out the width and height of the new canvas.
//
if (containerDOM !== null) {
let width = minWidth;
let height = minHeight;
if (height < containerDOM.clientHeight) height = containerDOM.clientHeight;
if (width < containerDOM.clientWidth) width = containerDOM.clientWidth;
rt.WindowSetSize(width, height);
}
});
function getTheme(callback) {
//
// This would read the theme from a file. It currently just sets a typical theme.
// I love the Dracula color theme.
//
$theme = {
font: "Fira Code, Menlo",
fontSize: "12pt",
textAreaColor: "#454158",
backgroundColor: "#22212C",
textColor: "#80ffea",
borderColor: "#1B1A23",
Cyan: "#80FFEA",
Green: "#8AFF80",
Orange: "#FFCA80",
Pink: "#FF80BF",
Purple: "#9580FF",
Red: "#FF9580",
Yellow: "#FFFF80",
};
}
</script>
<div
id=“closure"
bind:this={containerDOM}
style="background-color: {$theme.backgroundColor}; color:
{$theme.textColor}; font-family: {$theme.font}; font-size:
{$theme.fontSize};"
>
<div id="header" data-wails-drag>
<h3>Bulletin Board</h3>
</div>
<div id="main">
{#if $state === "message"}
<Message />
{/if}
</div>
</div>
<style>
:global(body) {
margin: 0px;
padding: 0px;
overflow: hidden;
border: transparent solid 1px;
border-radius: 10px;
background-color: transparent;
}
#closure {
display: flex;
flex-direction: column;
margin: 0px;
padding: 0px;
border-radius: 10px;
overflow: hidden;
}
#header {
height: 20px;
margin: 0px;
padding: 5px;
-webkit-user-select: none;
user-select: none;
cursor: default;
}
#main {
display: flex;
flex-direction: column;
margin: 0px 0px 0px 20px;
padding: 0px;
min-width: 100px;
}
h3 {
text-align: center;
margin: 0px;
padding: 0px;
cursor: default;
font-size: 1 em;
}
</style>
onMount
函数告诉Svelte编译器在安装此组件时运行代码。该代码初始化了存储在$theme
State Store中的主题。然后,该函数设置事件接收器,以使用rt.EventsOn
函数接收消息事件。收到消息时,它将为其设置$ Message Store变量,并将$ State Store变量设置为消息。这告诉HTML代码显示Message
组件并使用rt.ShowWindow()
显示窗口。
bind:this={containerDOM}
将将DOM节点放入变量containerDOM
中。然后可以查询此信息以查找有关包含消息的div
节点的信息。
下一个有趣的代码位于afterUpdate
函数调用中。在设置所有组件并可见后,设置了要调用的函数。在此中,如果状态不等于“没有”,它首先将窗口设置为隐藏。因此,它可以创建一种简单的方法,可以简单地设置状态变量来隐藏任何地方。其次,它使用containerDOM
变量根据应用程序div
大小来调整窗口大小。它还可以防止它变得太小。
其余代码只是为创建主窗口的HTML和CSS设置。现在,我们可以继续使用消息组件。
现在创建frontend/src/components/
目录,并使用此信息创建Message.svelte
文件:
<script>
import { message } from "../stores/message.js";
</script>
<div id="message">
<span>{$message}</span>
</div>
<style>
#message {
display: flex;
flex-direction: column;
margin: 0px;
padding: 10px;
}
</style>
这是一个简单的组件,因为它只是显示一条消息。您可能会认为我在HTML中显示了使用{$message}
的HTML中给定的信息。括号告诉Svelte获得JavaScript表达式的结果,掩盖它并显示。在这里,我们将信息显示在$ Message Store变量中。由于Svelte进行了消毒,我不必担心。
这看起来非常小,可以制作完整的组件,但这是模块化的设计。我只是添加了更多用于执行不同类型显示的组件。其他显示类型将在其中包含更多代码。
其余部分是该项目中使用的不同Svelte商店。不同的商店位于frontend/src/stores
目录中,可以在github BulletinBoard项目页面中看到。这是一个正在进行的项目,此repro反映出,每周都会编写更多功能。
该应用程序是使用命令行构建的:
wails build --platform "darwin/universal”
在build/bin
目录中创建MacOS通用二进制文件并将其全部捆绑在称为BulletinBoard.app
的应用程序捆绑包中。
如果您在MacOS系统上进行访问,则只需使用:
wails build
当应用程序运行时,请求者对话框询问您是否希望它允许程序接受传入的网络连接(仅MACOS)。只需允许它并且程序正在运行。但是等等,什么都没有出现。那是因为该应用程序是在隐藏状态下启动的。为了使程序变得可见,必须收到消息请求。
为了向其发送消息,您必须创建一个消息发送程序。这是用一个小的红宝石脚本完成的:
#!/usr/bin/env ruby
require 'net/http'
require 'json'
def uri_encode(str)
str.gsub(URI::UNSAFE) do |match|
match.each_byte.map { |c| sprintf('%%%02X', c.ord) }.join
end
end
if ARGV[0] == '-' then
message = ''
else
message = uri_encode(ARGV[0])
end
uri = URI("http://localhost:9697/api/message/#{message}")
http = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Get.new(uri.path, 'Content-Type' => 'application/json')
req.body = {msg: "#{message}"}.to_json
res = http.request(req)
puts "response #{res.body}”
如果将其放在名为“ sendmsg”的文件中并将其设置为可执行文件,则可以输入以下命令行:
sendmsg “hello”
公告板应用程序将像这样显示:
结论
本教程刚刚证明,由于GO语言对Coroutines的使用,单个Wails应用程序内部可以有多个Web服务器。这很棒,现在我可以完成其他功能。我希望能够向用户显示脚本提供的对话框,并以结果返回JSON数据结构。
如果您有兴趣跟上此项目或只是想自己下载源,则可以在BulletinBoard的GitHub页面上找到它。由于它是一个正在进行的项目,所以它已经看起来不像在这里一样。它有一个discussion board,以提出问题或只是对此发表评论。
开始下一步!
注意:文章图片来自Envato Elements。