使用 Go 和 Wails 构建跨平台桌面应用程序
由于多种原因,Electron 曾经(并且仍然)大受欢迎。首先,其跨平台功能使开发人员能够从单个代码库支持 Linux、Windows 和 macOS。最重要的是,它对于熟悉 Javascript 的开发人员来说有一个精简的学习曲线。
尽管它有其缺点(其中应用程序大小和内存消耗最为突出),但它为创建跨平台桌面应用程序提供了丰富的可能性。
然而,自其发布以来,许多替代品也加入了竞争。本文探讨了这样一种替代方案 - Wails,该项目使得使用 Go 和 Web 技术(例如 React 和 Vue)编写桌面应用程序成为可能。Wails 的一个主要卖点是它不嵌入浏览器,而是使用平台的本机渲染引擎。这使其成为Electron 的轻量级替代品。
为了熟悉 Wails,您将构建一个 GitHub 桌面客户端,它将与GitHub API交互,并提供以下功能:
- 查看公共存储库和要点
- 查看经过身份验证的用户的私有存储库和要点
- 为经过身份验证的用户创建一个新的要点。
后端将用 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的新文件夹来保存与存储库相关的组件。然后,创建一个名为RepositoryDetails.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 构建跨平台桌面应用程序
相关文章:

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

花2个月时间学习,面华为测开岗要30k,面试官竟说:你不是在搞笑。。。
背景介绍 计算机专业,代码能力一般,之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发,第二份实习由于大三暑假回国的时间比较短(小于两个月),于是找的实习是…...

【Python学习笔记】字符串
1. 字符串定义 可以用双引号 、 单三引号、双三引号,下面的定义都是正确的 "你好" 你好 """你好"""其中三引号可以 直接写内容有多行 的字符串。如下 letter 刘总:您好!您发的货我们已经收到&am…...

【AUTOSAR中断管理】TC3XX中断系统介绍
摘要 这段文本主要介绍了AURIX TC3XX的中断系统(Interrupt Router,简称IR)以及中断注册的过程以及举例说明中断机制。 AURIX TC3XX 中断系统(Interrupt Router)介绍 流程图描述中断路由器(IR)处理服务请求并与服务提供者交互。 中断系统的作用是将service request进行…...

Unity实现摄像机向屏幕中间发射射线射击物体
1.创建一个准星放在屏幕中间 外部找个PNG透明图,拖到Unity文件夹,右上角改成精灵sprite2d 2.添加到UI画布 3.写脚本 首先,我们需要引入一些 "工具",就像我们在玩游戏时要先下载游戏客户端一样。这里的 "工具&quo…...

测试时数据增广(TTA)与mmdetection3d中的实现
1. 测试时数据增广 测试时数据增广(TTA)在测试时使用数据增广技术获取同一数据的多个“变体”,使用同一网络在这些“变体”以及原始数据上进行推断,最后整合所有结果作为该原始数据最终的预测结果。 TTA类似于集成学习,…...

深入探索BP神经网络【简单原理、实际应用和Python示例】
人工神经网络(Artificial Neural Networks)是一种受到生物神经网络启发的机器学习模型,它的应用范围广泛,包括图像识别、语音识别、自然语言处理等领域。其中,BP神经网络(Backpropagation Neural Network&a…...

【LVGL】SquareLine Studio入门基础操作
1.SquareLine Studio基础 在这篇文章中将介绍SquareLine Studio的基础操作、解释如何加载一个项目、布局结构。 启动软件后,可以加载之前的项目、创建项目、加载一个示例。 这里以打开示例audio_mixer为例,可以双击该项目打开或者选中该项目点击右下角的【创建】按…...

【单片机】19-TFT彩屏
一、背景知识--显示器 1.什么是TFT (1)LCD显示器的构成:液晶面板驱动器【电压驱动】控制器【逻辑控制】 (2)液晶面板大致分为:TN,TFT,IPS等 (3)驱动器是跟随…...

高质量!推荐一些免费自学网站
大家好,我是 jonssonyan 说到自学网站,大家第一印象肯定是”菜鸟教程“、”w3school“、B 站大学。这些教程当然非常的好,而且适合入门学习,但是存在一些缺点,第一,知识点比较分散,没有一个整体…...

Linux之open/close/read/write/lseek记录
一、文件权限 这里不做过多描述,只是简单的记录,因为下面的命令会涉及到。linux下一切皆是文件包括文本、硬件设备、管道、数据库、socket等。通过ls -l 命令可以查看到以下信息 drwxrwxrwx 1 root root 0 Oct 10 17:06 open -rwxrwxrwx 1 root roo…...

3D调研-摄像头
参考资料: 来源1:https://leap2.ultraleap.com/leap-motion-controller-2 来源2: Gemini 2 _双目结构光相机_机器人感知-奥比中光官网 来源3: 国内外深度相机大盘点,仅用于学习科普!--机器视觉网 来源4&…...

光耦合器继电器与传统继电器:哪种最适合您的项目?
在电子和电气工程领域,继电器的选择可以显着影响项目的性能和安全性。两种常见类型的继电器是光耦合器继电器和传统机电继电器。每个都有其优点和缺点,因此选择过程对于项目的成功结果至关重要。 光耦合器继电器:基础知识 光耦合器继电器&…...

分享关于职场心态
1.解决问题而不是解释原因 2.秉承工匠思维而不是激情思维 什么是工匠思维? 工匠思维(The craftsman mindset)对待职业生涯的一种方式;是以产出为中心的职业观,关注自己给世界(工作)带来的价值…...

OK3568 UBUNTU 安装使用I2C-TOOLS
1. 安装 sudo apt-get update sudo apt-get install i2c-tools 使用I2Ctools 参考:https://blog.csdn.net/anyuliuxing/article/details/106382827 i2c-tools 是一组用于在Linux系统中进行I2C(Inter-Integrated Circuit)总线设备操作和调试…...

mysql面试题53:一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录
该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录 可以按照以下步骤进行: 确保…...

Docker服务更新与发现
一,docker-consul简介 这是一个基于分布式的服务发现和管理工具,它具有快速构建分布式框架,提供服务发现和服务治理等特点。同时consul还提供了可靠的保证,多数据中心和强大的API以满足高可用,分布式环境下的需求。 …...

【2023集创赛】安谋科技杯二等奖作品: 智能体感游戏机
本文为2023年第七届全国大学生集成电路创新创业大赛(“集创赛”)安谋科技杯二等奖作品分享,参加极术社区的【有奖征集】分享你的2023集创赛作品,秀出作品风采,分享2023集创赛作品扩大影响力,更有丰富电子礼…...

如何使用前端包管理器(如npm、Yarn)?
聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 欢迎来到前端入门之旅!感兴趣的可以订阅本专栏哦!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…...

Codeforces Round 903 (Div. 3) C(矩形旋转之后对应的坐标)
题目链接:Codeforces Round 903 (Div. 3) C 题目: 思想: 旋转之后对应的坐标: (i,j)(n1-j,i)(n1-i,n1-j)(j…...

月薪过万的Java面试
写了一个月,篇幅太长了,都写不下了,被逼无奈,只能拆分 面试题: HashMap底层实现原理,红黑树,B树,B树的结构原理,volatile关键字,CAS(比较与…...

html进阶语法
html进阶 列表、表格、表单 目标:掌握嵌套关系标签的写法,使用列表标签布局网页 01-列表 作用:布局内容排列整齐的区域。 列表分类:无序列表、有序列表、定义列表。 无序列表 作用:布局排列整齐的不需要规定顺序的…...

博客系统(java,MySQL,HTML)
项目展示: 1.输入 http://127.0.0.1:8080/blog_system/login.html 即可进入登录页面 2.输入正确的用户名和密码后进入博客列表页 要是用户名或密码输入错误,会弹出错误提示框 3.点击查看全文,可以进入博客详情页查看详细信息 4.点击写博客&a…...

Android Studio SDKGradleJDK等工具的正确使用
AS在安装使用过程中可能会占用C盘大量空间,对于C盘容量本来就小的人来说非常不友好,其实我们可以自定义安装路径 SDK默认安装位置 各种版本和NDK也会安装到这个路径 SDK版本选择性安装 通过选择图示的按钮,可以显示SDK的版本详情࿰…...

利用Python提取将Excel/PDF文件数据
使用Python来创建一个接口,用于接收Excel文件资源链接,下载文件并执行指定的操作,然后返回处理后的数据。以下是一个基本的示例,展示如何使用Flask来创建这样的接口。请注意,这是一个简化的示例,您可能需要…...

纯 CSS 实现瀑布流布局的方法
纯 CSS 实现瀑布流布局的方法 这种方式兼容性不是很好,全部支持需要些时间,但是目前是可以使用 css 写出来的 display: grid; grid-template-columns: repeat(4, 1fr); grid-gap: 10px; grid-template-rows: masonry;全部的 css .container {display:…...

输入法显示到语言栏_状态栏
设置–时间和语言–语言–最右侧"相关设置"中的"拼写、键入和键盘设置" 最下方的"高级键盘设置"–“使用桌面语言栏(如果可用)” 点击"语言栏选项" 接下来就是不同输入法的设置了 搜狗输入法:右键输入法选择"隐藏状态栏"–…...

[samba]同一个文件夹,分不同权限管理
#问题 有一个文件夹A能让用户1拥有写权限,而让用户2拥有只读权限,而用户3啥权限都没有,该如何设置samba呢? #解决办法 1.首先创建一个linux分组(group),命名为samba groupadd samba 2.创建一个samba默认…...

项目整合管理
项目整合管理概述 概述 项目的复杂性来源于组织的系统行为、人类行为以及组织或环境中的不确定性。在项目整合之前,项目经理需要考虑项目面临的内外部环境因素,检查项目的特征或属性。 作为项目的一种特征或属性,复杂性的含义: …...

基于RuoYi-Flowable-Plus的若依ruoyi-nbcio支持本地图片上传与回显的功能实现(一)
更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio 演示地址:RuoYi-Nbcio后台管理系统 原先这个基于RuoYi-Flowable-Plus的这个不支持本地图片上传,只支持oss图片上传,所以…...