建筑公告板
#javascript #html #svelte #wails

BulletinBoard

我的问题

在我的工作中,我经常需要一些信息才能保持在埃及之外。我尝试了可能的类型的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 ManagerEmailItScriptBar

为了通过原始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请求(例如使用wgetcurl命令行程序)。

BulletinBoard Communications

公告板通信 - 用OneModel制造

后端收到此请求,并使用信号将其传递给前端。 wails有一个很棒的基于事件的消息发送框架。前端是使用Svelte完成的。原件是简单的JavaScript和HTML,不太漂亮(好吧,这是一个快速的黑客)。现在,我使用一个不错的框架来构建一个易于扩展的模块化系统。

原始设计也允许对话框发送信息。对于这个项目,我只是在执行消息系统,以查看是否可以使用wails和Go Language Server进行工作。

让我开始

要开始,您需要安装nodejs with npmgo languageWails 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”

公告板应用程序将像这样显示:

BulletinBoard.png

结论

本教程刚刚证明,由于GO语言对Coroutines的使用,单个Wails应用程序内部可以有多个Web服务器。这很棒,现在我可以完成其他功能。我希望能够向用户显示脚本提供的对话框,并以结果返回JSON数据结构。

如果您有兴趣跟上此项目或只是想自己下载源,则可以在BulletinBoard的GitHub页面上找到它。由于它是一个正在进行的项目,所以它已经看起来不像在这里一样。它有一个discussion board,以提出问题或只是对此发表评论。

开始下一步!

注意:文章图片来自Envato Elements