使用 Go 和 Wails 构建跨平台桌面应用程序

由于多种原因,Electron 曾经(并且仍然)大受欢迎。首先,其跨平台功能使开发人员能够从单个代码库支持 Linux、Windows 和 macOS。最重要的是,它对于熟悉 Javascript 的开发人员来说有一个精简的学习曲线。

尽管它有其缺点(其中应用程序大小和内存消耗最为突出),但它为创建跨平台桌面应用程序提供了丰富的可能性。

然而,自其发布以来,许多替代品也加入了竞争。本文探讨了这样一种替代方案 - Wails,该项目使得使用 Go 和 Web 技术(例如 React 和 Vue)编写桌面应用程序成为可能。Wails 的一个主要卖点是它不嵌入浏览器,而是使用平台的本机渲染引擎。这使其成为Electron 的轻量级替代品。

为了熟悉 Wails,您将构建一个 GitHub 桌面客户端,它将与GitHub API交互,并提供以下功能:

  1. 查看公共存储库和要点
  2. 查看经过身份验证的用户的私有存储库和要点
  3. 为经过身份验证的用户创建一个新的要点。

后端将用 Go 编写,前端将使用React和Vite 。UI 组件将使用Ant Design (AntD)创建。

怎么运行的
如前所述,Wails 的工作原理是将用 Go 编写的后端与使用 Javascript 库/框架或使用 Vanilla HTML 和 Javascript 编写的前端相结合。即使您的函数和数据类型是在后端声明的,Wails 也可以在前端调用它们。更重要的是,当在后端声明一个结构体时,Wails 能够生成一个TypeScript模型以在前端使用。其结果是前端和后端之间的无缝通信。您可以在此处阅读有关 Wails 如何工作的更多信息。

先决条件
要学习本教程,您将需要以下内容:

  • 对Go和React的基本了解
  • go1.19
  • 新项目管理
  • Wails的最新安装

入门
通过运行以下命令创建一个新的 Wails 项目

wails init -n github_demo -t react

这搭建了一个新项目,后端使用 Go,前端使用 React + Vite。脚手架过程完成后,通过运行以下命令导航到新创建的文件夹并运行项目。

cd github_demo
wails dev

这将运行应用程序,如下图所示。

在这里插入图片描述
关闭应用程序并在您喜欢的编辑器或 IDE 中打开项目目录,开始向应用程序添加功能。

构建后端
添加 API 请求功能
应用程序首先需要具备向 GitHub API 发送 GET 和 POST 请求的能力。在应用程序的根目录中,创建一个名为api.go的新文件。在此文件中,添加以下代码。

package main
import ("bytes""fmt""io""net/http"
)
func makeRequest(requestType, url, token string, payload []byte ) ([]byte, error){client := &http.Client{}var request *http.Requestif payload != nil {requestBody := bytes.NewReader(payload)request, _ = http.NewRequest(requestType, url, requestBody)} else {request, _ = http.NewRequest(requestType, url, nil)}request.Header.Set("Accept", "application/vnd.github+json")if token != "" {request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))}response, err := client.Do(request)if err != nil {return nil, fmt.Errorf("request failed: %w", err)}body, _ := io.ReadAll(response.Body)return body, nil
}func MakeGetRequest(url string, token string) ([]byte, error) {return makeRequest("GET", url, token, nil)
}func MakePostRequest(url, token string, payload []byte) ([]byte, error){return makeRequest("POST", url, token, payload)
}

该makeRequest()函数在内部用于向指定的 URL 发出请求。除了指定 URL 之外,请求类型、令牌和负载也会传递给该函数。使用这些,可以准备请求并与函数返回的 API 响应一起发送。

和函数分别包裹该函数以MakeGetRequest()发送GET 和 POST 请求。MakePostRequest()makeRequest()

将辅助函数绑定到应用程序
有了 API 功能,您可以声明一些将绑定到前端的辅助函数。这是通过为结构添加接收器函数来完成的App。

您可以在app.go末尾看到一个示例,其中Greet()声明了一个名为 的接收器函数。

func (a *App) Greet(name string) string {return fmt.Sprintf("Hello %s, It's show time!", name)
}

现在,将以下代码添加到app.go。

type APIResponse []interface{}
type Gist struct {Description string      `json:"description"`Public      bool        `json:"public"`Files       interface{} `json:"files"`
}const BaseUrl = "https://api.github.com"var githubResponse APIResponsefunc (a *App) GetPublicRepositories() (APIResponse, error) {url := fmt.Sprintf("%s/repositories", BaseUrl)response, err := MakeGetRequest(url, "")if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}func (a *App) GetPublicGists() (APIResponse, error) {url := fmt.Sprintf("%s/gists/public", BaseUrl)response, err := MakeGetRequest(url, "")if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}func (a *App) GetRepositoriesForAuthenticatedUser(token string) (APIResponse, error) {url := fmt.Sprintf("%s/user/repos?type=private", BaseUrl)response, err := MakeGetRequest(url, token)if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}func (a *App) GetGistsForAuthenticatedUser(token string) (APIResponse, error) {url := fmt.Sprintf("%s/gists", BaseUrl)response, err := MakeGetRequest(url, token)if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}func (a *App) GetMoreInformationFromURL(url, token string) (APIResponse, error) {response, err := MakeGetRequest(url, token)if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}func (a *App) GetGistContent(url, token string) (string, error) {githubResponse, err := MakeGetRequest(url, token)if err != nil {return "", err}return string(githubResponse), nil
}func (a *App) CreateNewGist(gist Gist, token string) (interface{}, error) {var githubResponse interface{}requestBody, _ := json.Marshal(gist)url := fmt.Sprintf("%s/gists", BaseUrl)response, err := MakePostRequest(url, token, requestBody)if err != nil {return nil, err}json.Unmarshal(response, &githubResponse)return githubResponse, nil
}

然后,如果您的文本编辑器或 IDE 没有自动为您执行此操作,请将“encoding/json”添加到文件顶部的导入列表中。

除了现有代码之外,它还声明了两种新类型:APIResponse和Gist。这些将分别用于对来自 API 的响应和 Gist 的结构进行建模。接下来,它声明该App结构的接收器函数:

该GetPublicRepositories()函数通过 GET 请求从 GitHub API 检索公共存储库列表。由于此路由不需要身份验证,因此将传递一个空字符串作为令牌。
该GetPublicGists()函数通过 GET 请求从 GitHub API 检索公共要点列表。也不需要身份验证,因此将空字符串作为令牌传递。
该GetRepositoriesForAuthenticatedUser()函数用于获取经过身份验证的用户的私有存储库的列表。该函数将令牌作为参数。
该GetGistsForAuthenticatedUser()函数用于检索经过身份验证的用户的要点。该函数还采用令牌作为参数。
该GetMoreInformationFromURL()函数用于获取有关存储库的更多信息。此信息可以是提交历史记录、贡献者列表或已为存储库添加星标的用户列表。它需要两个参数,即要调用的 url 和身份验证令牌。对于公共存储库,令牌将为空字符串。
该GetGistContent()函数用于获取 Gist 的内容。该函数采用 Gist 原始内容的 URL 和身份验证令牌(公共 Gists 为空字符串)。它返回与 Gist 内容相对应的字符串。
该CreateNewGist()函数用于为经过身份验证的用户创建新的要点。该函数采用两个参数,即要创建的要点以及用户的身份验证令牌。

构建前端
前端的所有代码都存储在frontend文件夹中。但在编写任何代码之前,请使用以下命令添加 JavaScript 依赖项。

cd frontend
npm install antd @ant-design/icons react-router-dom prismjs

依赖关系如下:

Ant Design - 这可以帮助设计师/开发人员轻松构建美观且灵活的产品
Ant-design 图标- 这使您可以访问 AntD 的 SVG 图标集
React-router - 这将用于实现客户端路由
Prismjs - 这将用于实现 Gists 的语法突出显示

接下来,在frontend/src文件夹中创建一个名为Components的文件夹。

添加身份验证
为了进行身份验证,用户需要提供 GitHub个人访问令牌。该令牌包含在对需要身份验证的端点的请求标头中。如果您没有,请创建一个 - 但是,您必须为您的令牌设置以下权限才能用于此项目。

在这里插入图片描述
对于此项目,React Context API将用于存储令牌一小时,之后用户必须再次提供令牌来重新进行身份验证。

在frontend/src/components文件夹中,创建一个名为context的新文件夹。在该文件夹中,创建一个名为AuthModal.jsx的新文件并向其中添加以下代码。

import {Form, Input, Modal} from "antd";
import {EyeInvisibleOutlined, EyeTwoTone} from "@ant-design/icons";const AuthModal = ({shouldShowModal, onSubmit, onCancel}) => {const [form] = Form.useForm();const onFormSubmit = () => {form.validateFields().then((values) => {onSubmit(values.token);});};return (<Modaltitle="Provide Github Authentication Token"centeredokText="Save"cancelText="Cancel"open={shouldShowModal}onOk={onFormSubmit}onCancel={onCancel}><Formform={form}name="auth_form"initialValues={{token: "",}}><Form.Itemname="token"label="Token"rules={[{required: true, message: "Please provide your Github Token!",},]}><Input.Passwordplaceholder="Github Token"iconRender={(visible) => visible ? <EyeTwoTone/> : <EyeInvisibleOutlined/>}/></Form.Item></Form></Modal>);
};export default AuthModal;

该组件呈现身份验证表单。该表单有一个字段供用户粘贴和保存令牌。propshouldShowModal用于有条件地渲染表单,而onSubmit和onCancelprop 用于响应用户的操作。

接下来,再次在context文件夹中创建一个名为AuthContext.jsx的新文件,并向其中添加以下代码。

import {Button, Result} from "antd";
import React, {createContext, useContext, useEffect, useState} from "react";
import AuthModal from "./AuthModal";
import {useNavigate} from "react-router-dom";const AuthContext = createContext({});const AuthContextProvider = ({children}) => {const [token, setToken] = useState(null);const [shouldShowModal, setShouldShowModal] = useState(true);const navigate = useNavigate();useEffect(() => {const timer = setTimeout(() => {if (token !== null) {setToken(null);setShouldShowModal(true);}}, 3600000);return () => clearTimeout(timer);}, [token]);const onSubmit = (token) => {setToken(token);setShouldShowModal(false);};const onCancel = () => {setShouldShowModal(false);};if (!shouldShowModal && !token) {return (<Resultstatus="error"title="Authentication Failed"subTitle="A Github token is required to view this page"extra={[<Buttontype="link"key="home"onClick={() => {navigate("/");}}>Public Section</Button>,<Buttonkey="retry"type="primary"onClick={() => {setShouldShowModal(true);}}>Try Again</Button>,]}/>);}return (<>{shouldShowModal && (<AuthModalshouldShowModal={shouldShowModal}onSubmit={onSubmit}onCancel={onCancel}/>)}<AuthContext.Provider value={{token}}>{children}</AuthContext.Provider></>);
};export const useAuthContext = () => {const context = useContext(AuthContext);if (context === undefined) {throw new Error("useAuthContext must be used within a AuthContextProvider");}return context;
};export default AuthContextProvider;

exports这个文件里有两个。第一个是useAuthContext钩子。该钩子将用于检索保存在 中的令牌Context。第二个是AuthContextProvider组件。该组件负责呈现身份验证表单(在页面加载时或令牌在 1 小时后“过期”时)。

如果用户单击身份验证表单上的“取消”,它还会呈现错误页面。该组件采用 JSX 元素(名为children)作为 prop,并用上下文提供程序将其包装起来 — 从而使子元素能够访问令牌的值。

添加主从布局
为了显示存储库和要点,将使用主从布局。将呈现项目列表,单击其中一项将在列表旁边显示有关所选项目的更多信息。

在Components文件夹中,创建一个名为ListItem.jsx的新文件,并向其中添加以下代码。

import { useEffect, useState } from "react";
import { Avatar, Card, Skeleton } from "antd";const ListItem = ({ item, onSelect, selectedItem, title }) => {const [loading, setLoading] = useState(true);const [gridStyle, setGridStyle] = useState({margin: "3%",width: "94%",});useEffect(() => {const isSelected = selectedItem?.id === item.id;setGridStyle({margin: "3%",width: "94%",...(isSelected && { backgroundColor: "lightblue" }),});}, [selectedItem]);const onClickHandler = () => {onSelect(item);};useEffect(() => {setTimeout(() => {setLoading(false);}, 3000);}, []);return (<Card.Grid hoverable={true} style={gridStyle} onClick={onClickHandler}><Skeleton loading={loading} avatar active><Card.Metaavatar={<Avatar src={item.owner.avatar_url} />}title={title}description={`Authored by ${item.owner.login}`}/></Skeleton></Card.Grid>);
};export default ListItem;

该组件使用 AntD Card组件呈现列表中的单个项目。卡片的标题作为组件道具提供。除了标题之外,该组件还接收其他三个属性:

该onSelect道具用于通知父项该卡已被点击
item对应于将在卡上呈现的要点或存储库
selectedItem组件使用它来确定用户是否单击了呈现的项目;在这种情况下,浅蓝色背景将添加到卡片样式中。
接下来,在组件文件夹中创建一个名为MasterDetail.jsx的新文件,并向其中添加以下代码。

import {useState} from "react";
import {Affix, Card, Col, Row, Typography} from "antd";
import ListItem from "./ListItem";const MasterDetail = ({title, items, getItemDescription, detailLayout}) => {const [selectedItem, setSelectedItem] = useState(null);return (<><Row justify="center"><Col><Typography.Title level={3}>{title}</Typography.Title></Col></Row><Row><Col span={6}><Affix offsetTop={20}><divid="scrollableDiv"style={{height: "80vh", overflow: "auto", padding: "0 5px",}}><Card bordered={false} style={{boxShadow: "none"}}>{items.map((item, index) => (<ListItemkey={index}item={item}onSelect={setSelectedItem}selectedItem={selectedItem}title={getItemDescription(item)}/>))}</Card></div></Affix></Col><Col span={18}>{selectedItem && detailLayout(selectedItem)}</Col></Row></>);
};export default MasterDetail;

该组件负责呈现一列中的项目列表以及另一列中所选项目的详细信息。要渲染的项目作为组件的道具提供。

除此之外,getItemDescription()prop 是一个获取用户头像下显示内容的函数;这是存储库名称或要点描述。

propdetailLayout()是父组件提供的函数,它根据提供的项目返回详细信息部分的 JSX 内容。这允许 Gists 和存储库在使用相同的子组件进行渲染时具有完全不同的布局。

添加存储库相关组件
接下来,在组件文件夹中,创建一个名为Repository的新文件夹来保存与存储库相关的组件。然后,创建一个名为RepositoryDe​​tails.jsx的新文件并向其中添加以下代码。

import {useEffect, useState} from "react";
import {Avatar, Card, Divider, List, Spin, Timeline, Typography} from "antd";
import {GetMoreInformationFromURL} from "../../../wailsjs/go/main/App";const UserGrid = ({users}) => (<Listgrid={{gutter: 16, column: 4}}dataSource={users}renderItem={(item, index) => (<List.Item key={index} style={{marginTop: "5px"}}><Card.Metaavatar={<Avatar src={item.avatar_url}/>}title={item.login}/></List.Item>)}
/>);const RepositoryDetails = ({repository, token = ""}) => {const [commits, setCommits] = useState([]);const [contributors, setContributors] = useState([]);const [stargazers, setStargazers] = useState([]);const [isLoading, setIsLoading] = useState(true);useEffect(() => {const getRepositoryDetails = async () => {setIsLoading(true);const stargazers = await GetMoreInformationFromURL(repository.stargazers_url, token);const commits = await GetMoreInformationFromURL(repository.commits_url.replace(/{\/[a-z]*}/, ""), token);const contributors = await GetMoreInformationFromURL(repository.contributors_url, token);setCommits(commits);setContributors(contributors);setStargazers(stargazers);setIsLoading(false);};getRepositoryDetails();}, [repository]);return (<Cardtitle={repository.name}bordered={false}style={{margin: "1%",}}>{repository.description}<Divider/><Spin tip="Loading" spinning={isLoading}><Typography.Title level={5} style={{margin: 10}}>Contributors</Typography.Title><UserGrid users={contributors}/><Divider/><Typography.Title level={5} style={{marginBottom: 15}}>Stargazers</Typography.Title><UserGrid users={stargazers}/><Divider/><Typography.Title level={5} style={{marginBottom: 15}}>Commits</Typography.Title><Timeline mode="alternate">{commits.map((commit, index) => (<Timeline.Item key={index}>{commit.commit?.message}</Timeline.Item>))}</Timeline></Spin></Card>);
};export default RepositoryDetails;

接下来,创建用于渲染公共存储库的组件。在Components/Repository文件夹中,创建一个名为PublicRepositories.jsx的新文件,并向其中添加以下代码。

import {useEffect, useState} from "react";
import {GetPublicRepositories} from "../../../wailsjs/go/main/App";
import RepositoryDetails from "./RepositoryDetails";
import MasterDetail from "../MasterDetail";
import {message} from "antd";const PublicRepositories = () => {const [repositories, setRepositories] = useState([]);const [messageApi, contextHolder] = message.useMessage();useEffect(() => {const getRepositories = async () => {GetPublicRepositories().then((repositories) => {setRepositories(repositories);}).catch((error) => {messageApi.open({type: "error", content: error,});});};getRepositories();}, []);const title = "Public Repositories";const getItemDescription = (repository) => repository.name;const detailLayout = (repository) => (<RepositoryDetails repository={repository}/>);return (<>{contextHolder}<MasterDetailtitle={title}items={repositories}getItemDescription={getItemDescription}detailLayout={detailLayout}/></>);
};export default PublicRepositories;

该组件进行调用以从 GitHub API 检索公共存储库。它使用app.goGetPublicRepositories()中声明的函数来执行此操作,该函数由 Wails 自动绑定到前端。

以这种方式导出的函数是异步的并返回Promise。使用MasterDetail和RepositoryDetails组件,将相应地呈现返回的响应。

接下来,在Repository文件夹中创建另一个名为PrivateRepositories.jsx的文件,并向其中添加以下代码。

import { useEffect, useState } from "react";
import { useAuthContext } from "../context/AuthContext";
import { GetRepositoriesForAuthenticatedUser } from "../../../wailsjs/go/main/App";
import RepositoryDetails from "./RepositoryDetails";
import MasterDetail from "../MasterDetail";
import { message } from "antd";const PrivateRepositories = () => {const { token } = useAuthContext();const [repositories, setRepositories] = useState([]);const [messageApi, contextHolder] = message.useMessage();useEffect(() => {const getRepositories = async () => {if (token) {GetRepositoriesForAuthenticatedUser(token).then((repositories) => {setRepositories(repositories);}).catch((error) => {messageApi.open({type: "error",content: error,});});}};getRepositories();}, [token]);const title = "Private Repositories";const getItemDescription = (repository) => repository.name;const detailLayout = (repository) => (<RepositoryDetails repository={repository} token={token}/>);return (<>{contextHolder}<MasterDetailtitle={title}items={repositories}getItemDescription={getItemDescription}detailLayout={detailLayout}/></>);
};export default PrivateRepositories;

该组件与 组件非常相似PublicRepositories,但有两个关键点。首先,该组件将用 包装AuthContextProvider,这使得可以通过useAuthContext钩子检索保存的令牌。其次,它使用另一个绑定函数GetRepositoriesForAuthenticatedUser()来获取提供令牌的用户的存储库。

添加Gist相关组件
接下来,在组件文件夹中,创建一个名为Gist的新文件夹来保存与 Gist 相关的组件。然后,在该新文件夹中创建一个名为GistDetails.jsx的新文件并向其中添加以下代码。

import { Carousel, Col, Row, Spin, Typography } from "antd";
import React, { useEffect, useState } from "react";
import "prismjs/themes/prism-okaidia.min.css";
import Prism from "prismjs";
import { GetGistContent } from "../../../wailsjs/go/main/App";const GistDetails = ({ gist }) => {const [snippets, setSnippets] = useState([]);const [isLoading, setIsLoading] = useState(true);useEffect(() => {Prism.highlightAll();}, [snippets]);useEffect(() => {const getSnippets = async () => {setIsLoading(true);const snippets = await Promise.all(Object.values(gist.files).map(async (file) => {const fileContent = await GetGistContent(file.raw_url, "");return {language: file.language?.toLowerCase() || "text",content: fileContent,};}));setSnippets(snippets);setIsLoading(false);};getSnippets();}, [gist]);return (<Spin tip="Loading" spinning={isLoading}><Row justify="center"><Col>{gist.description && (<Typography.Text strong>{gist.description}</Typography.Text>)}</Col></Row><div><Carouselautoplaystyle={{ backgroundColor: "#272822", height: "100%" }}>{snippets.map((snippet, index) => (<pre key={index}><code className={`language-${snippet.language}"`}>{snippet.content}</code></pre>))}</Carousel></div></Spin>);
};export default GistDetails;

该组件呈现文件中给定要点的代码。每个 Gist 响应都带有一个files密钥。这是一个包含 Gist 所有文件的对象。每个文件对象都包含文件原始内容的 URL 以及与文件关联的语言。该组件使用该函数检索所有文件GetGistContent()并将它们呈现在轮播中。Prism用于呈现 IDE 中的代码。

接下来,在 Gist 文件夹中,创建一个名为PublicGists.jsx的文件并向其中添加以下代码。

import { useEffect, useState } from "react";
import GistDetails from "./GistDetails";
import { GetPublicGists } from "../../../wailsjs/go/main/App";
import MasterDetail from "../MasterDetail";
import { message } from "antd";const PublicGists = () => {const [gists, setGists] = useState([]);const [messageApi, contextHolder] = message.useMessage();useEffect(() => {const getGists = async () => {GetPublicGists().then((gists) => {setGists(gists);}).catch((error) => {messageApi.open({type: "error",content: error,});});};getGists();}, []);const title = "Public Gists";const getItemDescription = (gist) =>gist.description || "No description provided";const detailLayout = (gist) => <GistDetails gist={gist} />;return (<>{contextHolder}<MasterDetailtitle={title}items={gists}getItemDescription={getItemDescription}detailLayout={detailLayout}/></>);
};export default PublicGists;

正如公共存储库的渲染一样,app.goGetPublicGists()中声明的函数用于从 Github API 检索公共 Gist 并将其传递给组件,以及获取 Gist 描述和显示有关该 Gist 的更多信息的函数。选择时要点。MasterDetail

接下来,在Gist文件夹中创建一个名为PrivateGists.jsx的新文件,并向其中添加以下代码。

import { useEffect, useState } from "react";
import { useAuthContext } from "../context/AuthContext";
import { GetGistsForAuthenticatedUser } from "../../../wailsjs/go/main/App";
import MasterDetail from "../MasterDetail";
import GistDetails from "./GistDetails";
import { message } from "antd";const PrivateGists = () => {const [gists, setGists] = useState([]);const { token } = useAuthContext();const [messageApi, contextHolder] = message.useMessage();useEffect(() => {const getGists = async () => {if (token) {GetGistsForAuthenticatedUser(token).then((gists) => {setGists(gists);}).catch((error) => {messageApi.open({type: "error",content: error,});});}};getGists();}, [token]);const title = "Private Gists";const getItemDescription = (gist) =>gist.description || "No description provided";const detailLayout = (gist) => <GistDetails gist={gist} />;return (<>{contextHolder}<MasterDetailtitle={title}items={gists}getItemDescription={getItemDescription}detailLayout={detailLayout}/></>);
};
export default PrivateGists;

该组件将用一个AuthContextProvider组件包装,从而使其能够访问所提供的令牌。使用令牌,通过函数对 GitHub API 进行异步调用GetGistsForAuthenticatedUser()。然后将结果MasterDetail与其他所需的 props 一起传递给组件以进行适当的渲染。

最后要构建的 Gist 相关组件是创建新 Gist 的表单。为此,请在Gist文件夹中创建一个名为CreateGist.jsx的新文件,并向其中添加以下代码。

import { useAuthContext } from "../context/AuthContext";
import { Button, Card, Divider, Form, Input, message, Switch } from "antd";
import { DeleteTwoTone, PlusOutlined } from "@ant-design/icons";
import { CreateNewGist } from "../../../wailsjs/go/main/App";
import { useNavigate } from "react-router-dom";const CreateGist = () => {const { token } = useAuthContext();const [messageApi, contextHolder] = message.useMessage();const navigate = useNavigate();const onFinish = async (values) => {const { description, files, isPublic } = values;const gist = {description,public: !!isPublic,files: files.reduce((accumulator, { filename, content }) =>Object.assign(accumulator, {[filename]: { content },}),{}),};CreateNewGist(gist, token).then((gist) => {messageApi.open({type: "success",content: `Gist ${gist.id} created successfully`,});navigate("/gists/private");}).catch((error) => {messageApi.open({type: "error",content: error,});});};const onFinishFailed = (errorInfo) => {console.log("Failed:", errorInfo);};return (<>{contextHolder}<Card title="Create a new Gist"><Formname="gist"onFinish={onFinish}onFinishFailed={onFinishFailed}autoComplete="off"><Form.Item name="description"><Input placeholder="Gist description..." /></Form.Item><Form.Itemlabel="Make gist public"valuePropName="checked"name="isPublic"><Switch /></Form.Item><Form.Listname="files"rules={[{validator: async (_, files) => {if (!files || files.length < 1) {return Promise.reject(new Error("At least 1 file is required to create a Gist"));}},},]}>{(fields, { add, remove }, { errors }) => (<>{fields.map((field) => (<div key={field.key}><Form.ItemshouldUpdate={(prevValues, curValues) =>prevValues.area !== curValues.area ||prevValues.sights !== curValues.sights}>{() => (<div><Divider /><Form.Item{...field}name={[field.name, "filename"]}rules={[{required: true,message: "Missing filename",},]}noStyle><Inputplaceholder="Filename including extension..."style={{ width: "90%", marginRight: "5px" }}/></Form.Item><DeleteTwoTonestyle={{fontSize: "30px",verticalAlign: "middle",}}twoToneColor="#eb2f96"onClick={() => remove(field.name)}/></div>)}</Form.Item><Form.Item{...field}name={[field.name, "content"]}rules={[{required: true,message: "Missing content",},]}><Input.TextArea rows={20} placeholder="Gist content" /></Form.Item></div>))}<Form.ItemwrapperCol={{offset: 10,}}><Buttontype="dashed"onClick={() => add()}icon={<PlusOutlined />}>Add file</Button><Form.ErrorList errors={errors} /></Form.Item></>)}</Form.List><Form.ItemwrapperCol={{offset: 10,}}><Button type="primary" htmlType="submit">Submit</Button></Form.Item></Form></Card></>);
};export default CreateGist;

创建新 Gist 的请求包含三个字段:

description:如果提供的话,这将描述要点中的代码旨在实现的目标。该字段是可选的,并在表单中由输入字段表示
public:这是必填字段,决定 Gist 是否具有公共访问权限。在您创建的表单中,这由默认设置为关闭的开关表示。这意味着除非用户另有指定,否则创建的要点将是秘密的,并且仅对拥有其链接的用户可用。
files:这是另一个必填字段。它是一个对象,对于对象中的每个条目,键是文件的名称(包括扩展名),值是文件的内容。
这以您创建的动态列表的形式表示,其中每个列表项都包含文件名的文本字段和文件内容的文本区域。通过单击“添加文件”按钮,您可以添加多个文件。您还可以删除文件。请注意,您将需要至少有一个文件,如果没有,将显示一条错误消息。
当表单正确填写并提交后,该onFinish()函数用于创建一个符合app.goGist中声明的结构的对象,并调用接收器函数。CreateNewGist()

因为该组件是用 包装的AuthContextProvider,所以可以根据函数的需要检索保存的令牌并与 Gist 一起传递。收到成功响应后,应用程序将重定向到经过身份验证的用户的要点列表。

将各个部分放在一起
添加导航
所有单独的组件就位后,接下来要添加的是导航 - 用户可以在应用程序中移动的一种方式。要添加此内容,请在组件文件夹中创建一个名为NavBar.jsx的新文件,并向其中添加以下代码。

import { LockOutlined, UnlockOutlined } from "@ant-design/icons";
import { Layout, Menu } from "antd";
import { Link } from "react-router-dom";
import logo from "../assets/images/logo-universal.png";function getItem(label, key, icon, children, type) {return {key,icon,children,label,type,};
}
const items = [getItem("Public Actions", "sub1", <UnlockOutlined />, [getItem("Repositories","g1",null,[getItem(<Link to={"repositories/public"}>View all repositories</Link>,"1"),],"group"),getItem("Gists","g2",null,[getItem(<Link to={"gists/public"}>View all gists</Link>, "3")],"group"),]),getItem("Private Actions", "sub2", <LockOutlined />, [getItem("Repositories","g3",null,[getItem(<Link to={"repositories/private"}>View my repositories</Link>,"5"),],"group"),getItem("Gists","g4",null,[getItem(<Link to={"gists/private"}>View my gists</Link>, "6"),getItem(<Link to={"gist/new"}>Create new gist</Link>, "7"),],"group"),]),
];const NavBar = () => {return (<Layout.Header theme="light" style={{ background: "white" }}><divclassName="logo"style={{float: "left",marginRight: "200px",padding: "1%",}}><Link to="/"><img src={logo} style={{ width: "50px" }} /></Link></div><MenudefaultSelectedKeys={["1"]}mode="horizontal"items={items}style={{position: "relative",}}/></Layout.Header>);
};export default NavBar;

该组件在窗口顶部呈现一个导航栏,其中包含两个主要项目 - Public Actions和Private Actions。然后,每个项目都有子项目,这些子项目是最终将呈现与子项目关联的组件的链接。完成此操作后,您可以将路由添加到您的应用程序中。

添加路由
在frontend/src文件夹中,创建一个名为routes.jsx的新文件,并向其中添加以下代码。

import App from "./App";import CreateGist from "./components/Gist/CreateGist";
import PrivateGists from "./components/Gist/PrivateGists";
import PublicGists from "./components/Gist/PublicGists";import PrivateRepositories from "./components/Repository/PrivateRepositories";
import PublicRepositories from "./components/Repository/PublicRepositories";
import AuthContextProvider from "./components/context/AuthContext";const routes = [{path: "/",element: <App />,children: [{ index: true, element: <PublicRepositories /> },{path: "repositories/public",element: <PublicRepositories />,},{path: "gists/public",element: <PublicGists />,},{path: "gist/new",element: (<AuthContextProvider><CreateGist /></AuthContextProvider>),},{path: "repositories/private",element: (<AuthContextProvider><PrivateRepositories /></AuthContextProvider>),},{path: "gists/private",element: (<AuthContextProvider><PrivateGists /></AuthContextProvider>),},],},
];export default routes;

在这里,您指定了应用程序中的路由以及要为每个路径呈现的组件。除此之外,您还包装了需要用户为组件提供令牌的组件AuthContextProvider。

接下来,打开App.jsx并更新文件的代码以匹配以下内容。

import NavBar from "./components/NavBar";
import { FloatButton, Layout } from "antd";
import { Outlet } from "react-router-dom";const { Content } = Layout;const App = () => {return (<Layoutstyle={{minHeight: "100vh",}}><NavBar /><Layout className="site-layout"><Contentstyle={{background: "white",padding: "0 50px",}}><divstyle={{padding: 24,}}><Outlet /><FloatButton.BackTop /></div></Content></Layout></Layout>);
};export default App;

在这里,您已经包含了NavBar之前声明的组件。您还声明了一个Outlet由 提供的组件react-router-dom来渲染子路由元素。

最后更新main.jsx中的代码以匹配以下内容。

import React from 'react'
import {createRoot} from 'react-dom/client'
import { createHashRouter, RouterProvider } from 'react-router-dom'
import routes from './routes'const container = document.getElementById('root')const root = createRoot(container)const router = createHashRouter(routes, {basename:'/'})root.render(<React.StrictMode><RouterProvider router={router}/></React.StrictMode>
)

HashRouter是官方推荐的路由方法。这是通过createHashRouter()函数创建的。使用routes您之前声明的对象,所有路由器对象都会传递到此组件以呈现您的应用程序并启用其余 API。完成此操作后,您的应用程序将在加载后呈现索引页面。

测试应用程序是否有效
您已经使用Wails成功构建了您的第一个应用程序。再次运行应用程序,并通过从项目的顶级文件夹运行以下命令来试用它。

wails dev

默认情况下,当应用程序加载时,您将看到一个公共存储库列表。使用导航菜单,您可以通过单击相应的菜单项来查看公共(和私有)存储库和要点。

当您选择私有存储库或私有 Gist 的菜单项时,将显示一个弹出窗口,询问您的 GitHub 令牌,如下所示。

在这里插入图片描述
粘贴您的个人访问令牌 (PAT) 并单击“保存”。然后将呈现您的存储库(或 Gists,视情况而定)。您将能够在应用程序的私人部分中导航,而无需在几分钟内重新输入令牌。

这就是如何使用 Go 和 Wails 构建跨平台桌面应用程序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/158688.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

解决Win10电脑无线网卡的移动热点无法开启问题

一、目的 利用无线网卡连接网络&#xff0c;然后又用无线网卡通过移动热点分享该网络。 移动热点&#xff0c;简单地说&#xff0c;就是将台式机或笔记本的 Internet 连接转化成 WIFI 信号以供移动设备无线上网的功能&#xff0c;硬件前提是电脑须安装有无线网卡。 二、问题 …

C/C++陷阱——变量名和函数名的冲突问题

C语言/C陷阱——变量名和函数名的冲突问题 先来看这两串代码&#xff1a; 代码一&#xff1a; #include <stdio.h> #include <stdlib.h>int rand 1;int main() {printf("%d\n", rand);return 0; }代码二&#xff1a; #include <stdio.h> #inc…

介绍一款小巧的Excel比对工具-DiffExcel

【缘起&#xff1a;此前找了一通&#xff0c;没有找到免费又好用的Excel比对工具&#xff0c;而ExcelBDD需要把Excel文件存放到Git&#xff0c;因此迫切需要Excel比对工具。 最新升级到V1.3.3&#xff0c;因为git diff有变化&#xff0c;原来是git diff会修改文件名&#xff0…

Linux Kernel 4.13 RC6发布:正式版9月3日发布

美国当地时间上周末&#xff0c;大神Linus Torvalds发布了Linux Kernel 4.13内核的又一候选版本。上周发布的RC5版本更新幅度也要比上上周的RC4要小&#xff0c;Linus Torvalds表示本周发布的RC6版本属于常规更新&#xff0c;在过去一周的开发过程中并没有出现任何意外。RC6版本…

使用antd-pro脚手架搭建react ts项目

Pro 中使用 TypeScript 来作为默认的开发语言&#xff0c;TypeScript 的好处已经无须赘述&#xff0c;无论是开发成本还是维护成本都能大大减少&#xff0c;是中后台开发的必选。 初始化 提供了 pro-cli 来快速的初始化脚手架。 # 使用 npm npm i ant-design/pro-cli -g pro…

Cadence 设计实践笔记-小哥allegro 2层板笔记

本章节主要跟着B站PCB入门首选视频-小哥Cadence Allegro 2层板视频,结合自己的实践一步步完成一个完整的PCB板的设计。 视频链接地址: PCB入门首选视频-小哥Cadence Allegro 2层板视频_哔哩哔哩_bilibili 规范建立文件夹 建立八个文件夹 DATASHEET 主要存放设计项目…

【数据结构】线性表与顺序表

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;浅谈Java &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; 线性表与顺序表 1. 线性表2. 顺序表2.1 …

9.Linear Maps

线性映射 线性映射是将向量作为输入并产生一些新向量作为输出的转换。 从坐标定义开始(数组)&#xff0c;再到2&#xff0c;3&#xff0c;并展示它们是如何关联的 线性映射的坐标表示最终是矩阵&#xff0c; 1.坐标定义&#xff08;数组&#xff09; 列向量是向量的坐标表示…

mysql误删误操作恢复数据,比传统方式和binlog2sql更快速用的恢复方式-reverse_sql恢复数据(单表多表)

场景&#xff1a; 误操作删除了某个表的数据&#xff0c;本文只讲工具的使用&#xff0c;首先自己通过mysqlbinlog或者记录找到误操作的时间范围&#xff1a;开始时间和结束时间&#xff0c;已经确定好是哪个binlog了下面以误删为例。 查看binlog是否开启 show variables like …

Python实现某音短视频JS XB逆向解析

哈喽兄弟们&#xff0c;今天来实现一下某音短视频的JS逆向解析。 知识点 动态数据抓包在这里插入代码片 requests发送请求 X-Bogus 参数逆向环境模块 python 3.8 运行代码 pycharm 2022.3 辅助敲代码 requests pip install request…

R语言的计量经济学实践技术应用

计量经济学通常使用较小样本&#xff0c;但这种区别日渐模糊&#xff0c;机器学习在经济学领域、特别是经济学与其它学科的交叉领域表现日益突出&#xff0c;R语言是用于统计建模的主流计算机语言&#xff0c;在本次培训中&#xff0c;我们将从实际应用出发&#xff0c;重点从数…

Java设计模式之六大设计原则

为什么要学习设计模式&#xff1f; 要知道设计模式就是软件工程的方法经验的总结&#xff0c;也是可以认为是过去一段时间软件工程的一个最佳实践&#xff0c;要理解&#xff0c;不要死记硬背。掌握这些方法后&#xff0c;可以让你的程序获得以下好处&#xff1a; 代码重用性…

无法启动此程序,因为计算机中丢失MSVCR71.dll的详细解决修复方法

大家好&#xff01;今天我来给大家分享一下msvcp71.dll丢失的修复方法。 首先&#xff0c;让我们来了解一下msvcp71.dll文件。msvcp71.dll是一个动态链接库文件&#xff0c;它是Microsoft Visual C 2010 Redistributable Package所包含的一个文件。这个文件被许多软件和游戏需…

【力扣每日一题】2023.10.13 避免洪水泛滥

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 给我们一个一维数组&#xff0c;元素为0表示对应日期不下雨&#xff0c;非0则表示对应日期对应号的湖泊下雨&#xff0c;下雨之后会导致该…

【MySQL】事务四大特性ACID、并发事务问题、事务隔离级别

&#x1f40c;个人主页&#xff1a; &#x1f40c; 叶落闲庭 &#x1f4a8;我的专栏&#xff1a;&#x1f4a8; c语言 数据结构 javaEE 操作系统 Redis 石可破也&#xff0c;而不可夺坚&#xff1b;丹可磨也&#xff0c;而不可夺赤。 MySQL 一、事务四大特性ACID1.1 原子性1.2 …

Zabbix监控系统详解2:基于Proxy分布式实现Web应用监控及Zabbix 高可用集群的搭建

文章目录 1. zabbix-proxy的分布式监控的概述1.1 分布式监控的主要作用1.2 监控数据流向1.3 构成组件1.3.1 zabbix-server1.3.2 Database1.3.3 zabbix-proxy1.3.4 zabbix-agent1.3.5 web 界面 2. 部署zabbix代理服务器2.1 前置准备2.2 配置 zabbix 的下载源&#xff0c;安装 za…

《Node.js+Express+MongoDB+Vue.js全栈开发实战》简介

今天介绍的这本书是《Node.jsExpressMongoDBVue.js全栈开发实战》。该书由清华大学出版社于2023年1月出版 外观 从书名故名思议&#xff0c;就是基于Node.jsExpressMongoDBVue.js来实现企业级应用全栈开发。 封面风格比较简约&#xff0c;插图是一张类似于罗马时代战车形象&…

微软10月补丁 | 修复103个漏洞,包括2个零日漏洞,13个严重漏洞

近日&#xff0c;微软发布了2023年10月的补丁更新&#xff0c;解决了其软件中的103个漏洞。 在这103个漏洞中&#xff0c;有13个的评级为严重漏洞&#xff0c;90个被评为重要漏洞。自9月12日以来&#xff0c;谷歌已经解决了基于chrome的Edge浏览器的18个安全漏洞。 这两个零日…

Puppeteer监听网络请求、爬取网页图片(二)

Puppeteer监听网络请求、爬取网页图片&#xff08;二&#xff09; Puppeteer监听网络请求、爬取网页图片&#xff08;二&#xff09;一、爬取需求二、实现讲解三、效果查看 一、爬取需求 首先打开浏览器&#xff0c;打开指定网站监听网站发出的所有请求&#xff0c;记录请求&a…

【AI视野·今日Robot 机器人论文速览 第五十一期】Tue, 10 Oct 2023

AI视野今日CS.Robotics 机器人学论文速览 Tue, 10 Oct 2023 Totally 54 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers On Multi-Fidelity Impedance Tuning for Human-Robot Cooperative Manipulation Authors Ethan Lau, Vaibhav Srivastava, Sh…