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

news2024/11/26 12:15:33

由于多种原因,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.Request
        if 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 APIResponse

func (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 (<Modal
            title="Provide Github Authentication Token"
            centered
            okText="Save"
            cancelText="Cancel"
            open={shouldShowModal}
            onOk={onFormSubmit}
            onCancel={onCancel}
        >
            <Form
                form={form}
                name="auth_form"
                initialValues={{
                    token: "",
                }}
            >
                <Form.Item
                    name="token"
                    label="Token"
                    rules={[{
                        required: true, message: "Please provide your Github Token!",
                    },]}
                >
                    <Input.Password
                        placeholder="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 (
            <Result
                status="error"
                title="Authentication Failed"
                subTitle="A Github token is required to view this page"
                extra={[
                    <Button
                        type="link"
                        key="home"
                        onClick={() => {
                            navigate("/");
                        }}
                    >
                        Public Section
                    </Button>,
                    <Button
                        key="retry"
                        type="primary"
                        onClick={() => {
                            setShouldShowModal(true);
                        }}
                    >
                        Try Again
                    </Button>,
                ]}
            />
        );
    }

    return (
        <>
            {shouldShowModal && (
                <AuthModal
                    shouldShowModal={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.Meta
          avatar={<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}>
                        <div
                            id="scrollableDiv"
                            style={{
                                height: "80vh", overflow: "auto", padding: "0 5px",
                            }}
                        >
                            <Card bordered={false} style={{boxShadow: "none"}}>
                                {items.map((item, index) => (<ListItem
                                        key={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}) => (<List
    grid={{gutter: 16, column: 4}}
    dataSource={users}
    renderItem={(item, index) => (<List.Item key={index} style={{marginTop: "5px"}}>
        <Card.Meta
            avatar={<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 (<Card
        title={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}
            <MasterDetail
                title={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}
      <MasterDetail
        title={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>
        <Carousel
          autoplay
          style={{ 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}
      <MasterDetail
        title={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}
      <MasterDetail
        title={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">
        <Form
          name="gist"
          onFinish={onFinish}
          onFinishFailed={onFinishFailed}
          autoComplete="off"
        >
          <Form.Item name="description">
            <Input placeholder="Gist description..." />
          </Form.Item>
          <Form.Item
            label="Make gist public"
            valuePropName="checked"
            name="isPublic"
          >
            <Switch />
          </Form.Item>
          <Form.List
            name="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.Item
                      shouldUpdate={(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
                          >
                            <Input
                              placeholder="Filename including extension..."
                              style={{ width: "90%", marginRight: "5px" }}
                            />
                          </Form.Item>

                          <DeleteTwoTone
                            style={{
                              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.Item
                  wrapperCol={{
                    offset: 10,
                  }}
                >
                  <Button
                    type="dashed"
                    onClick={() => add()}
                    icon={<PlusOutlined />}
                  >
                    Add file
                  </Button>
                  <Form.ErrorList errors={errors} />
                </Form.Item>
              </>
            )}
          </Form.List>
          <Form.Item
            wrapperCol={{
              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" }}>
      <div
        className="logo"
        style={{
          float: "left",
          marginRight: "200px",
          padding: "1%",
        }}
      >
        <Link to="/">
          <img src={logo} style={{ width: "50px" }} />
        </Link>
      </div>
      <Menu
        defaultSelectedKeys={["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 (
    <Layout
      style={{
        minHeight: "100vh",
      }}
    >
      <NavBar />
      <Layout className="site-layout">
        <Content
          style={{
            background: "white",
            padding: "0 50px",
          }}
        >
          <div
            style={{
              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.coloradmin.cn/o/1092824.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LENOVO联想笔记本小新 Pro-14 2021AMD处理器ACH版(82MS)原厂Win10系统

下载链接&#xff1a;https://pan.baidu.com/s/1-KZ8Y9NmkS7nDXcMbhZLHw?pwdyrkx 系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、lenovo联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&#xff1…

VScode运行C/C++

VScode运行C/C VScode的安装这里不讲 一、mingw64的下载 二、VS code打开文件夹与创建C文件 ----------------这一步给萌新看&#xff0c;有C和VScode的基础可跳过---------------- 1.创建一个文件夹 2.vscode打开刚刚创建的文件夹 3.新建文件&#xff0c;在输入文件名1.c后…

C语言中的文件操作指南

阅读导航 前言一、文件类型1. 程序文件2. 数据文件PS.文件名 二、文件的打开和关闭1. 文件指针2. 文件的打开和关闭 三、文件的顺序读写四、文件的随机读写1. fseek() 函数2. ftell() 函数3. rewind() 函数 总结 前言 在C语言中&#xff0c;文件操作是一项重要的任务。通过文件…

20和遍历以及迭代器有关的一些东西

知识点有点散&#xff0c;只能这样记录了 1、这边是和遍历有关的&#xff1a; class Person:def __init__(self):self.result 1def __getitem__(self, item):self.result 1if self.result > 6:raise StopIteration(停止遍历)return self.resultpassp Person() for i in…

Unity中Shader光照模型Phong的实现

文章目录 前言一、对主平行光实现高光效果1、在属性面板定义高光颜色2、在属性面板定义高光系数3、在属性面板定义高光范围系数4、获取 V (模型顶点的世界坐标 指到 相机世界坐标 的单位向量)5、由上一篇推理出的公式得到  R 向量6、由公式计算得出高光效果 二、使用已有的数…

常用的软件项目管理工具一览

软件项目管理工具是帮助团队成功管理和完成软件开发项目的软件程序和应用程序。根据项目及其规模和复杂性&#xff0c;可以使用各种各样的这些工具来协助完成任务&#xff0c;从任务跟踪和调度&#xff0c;到项目报告&#xff0c;到版本控制和协作。 项目经理对软件项目的整体成…

Git 回退代码的两种方法对比

Git 回退代码版本 在项目的开发中&#xff0c;有时候还是会出现&#xff0c;一些误提交了一些代码&#xff0c;这时候就会想撤回提交的代码&#xff0c;在Git中有两种方法可以使用&#xff0c;现在通过对比方法比较这两种方法的区别&#xff0c;分别适用于哪些情况&#xff1f…

Google zxing 生成带logo的二维码图片

环境准备 开发环境 JDK 1.8SpringBoot2.2.1Maven 3.2 开发工具 IntelliJ IDEAsmartGitNavicat15 添加maven配置 <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.4.0</version> </…

Stm32_标准库_12_串口_发送数据

波特率&#xff1a;约定的传输速率&#xff0c;1000bps,1s发1000位 引脚 结构 数据帧的传输特点 代码&#xff1a; #include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h"GPIO_InitTypeDef GPIO_InitStruct; USART…

计算机毕业设计选什么题目好?springboot 学习笔记系统

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

【互联网】实习一个月感受

说明&#xff1a;岗位&#xff1a;golang开发实习生&#xff0c;实习已经一个月多点了&#xff0c;这篇文章谈谈实习应该注意些什么点&#xff0c;以及该做些什么事情 说实话这不是我第一次实习了&#xff0c;但是感受很深 注意点&#xff1a; 1、没事找话说 比如中午和同事吃…

12.JVM

一.JVM类加载机制:把类从硬盘文件加载到内存中 1.java文件,编写时是一个.java文件,编译后现成一个.class的字节码文件,运行的时候,JVM就会读取.class文件,放到内存中,并且构造类对象. 2.类加载流程: a.加载:找到.class文件,打开文件,读取内容,尝试解析文件内容. b.验证:检查…

【Python学习笔记】字符串

1. 字符串定义 可以用双引号 、 单三引号、双三引号&#xff0c;下面的定义都是正确的 "你好" 你好 """你好"""其中三引号可以 直接写内容有多行 的字符串。如下 letter 刘总&#xff1a;您好&#xff01;您发的货我们已经收到&am…

百度开放平台第三方代小程序开发,授权事件、消息与事件通知总结

大家好&#xff0c;我是小悟 关于百度开放平台第三方代小程序开发的两个事件接收推送通知&#xff0c;是开放平台代小程序实现业务的重要功能。 授权事件推送和消息与事件推送类型都以event的值判断。 授权事件推送通知 授权事件推送包括&#xff1a;推送票据、授权成功、取…

【C++进阶】:特殊类的设计

特殊类的设计 一.设计一个类不能被拷贝二.设计一个类只能在堆上创建对象三.设计一个类只能在栈上创建4.设计一个类不能被继承五.设计一个类只能有一个对象(单例模式) 一.设计一个类不能被拷贝 拷贝只会放生在两个场景中&#xff1a;拷贝构造函数以及赋值运算符重载&#xff0c…

蓝桥杯(跳跃 C++)

思路&#xff1a; 1、根据题目很容易知道可以用深度搜索、广度搜索、动态规划的思想解题。 2、这里利用深度搜素&#xff0c;由题目可知&#xff0c;可以往九个方向走。 3、这里的判断边界就是走到终点。 #include<iostream> using namespace std; int max1 0; int …

Vue3中ref创建的引用为什么一定要用.value来获取值

Vue3中ref创建的引用为什么一定要用.value来获取值 回顾jsjs中修改复制变量,改变原始变量vue3中使用ref 回顾js js中一个变量赋值给另一个变量,改变另一个变量,原始变量不变 let a 0; let b a; b 2 console.log("a: "a);js中修改复制变量,改变原始变量 let obj…

【Java 进阶篇】JavaScript 中的全局对象和变量

JavaScript 是一门非常强大的编程语言&#xff0c;它提供了许多全局对象和变量&#xff0c;以便于在整个应用程序中共享数据和功能。本文将详细介绍 JavaScript 中的全局对象和变量&#xff0c;包括全局对象、全局变量、全局函数以及它们的用途和示例。 全局对象 JavaScript …

15.项目讲解之前端页面的实现

项目讲解之前端页面的实现 本项目前端使用HBuilerX软件编写HBuilderX下载安装配置一键直达&#xff0c; uniapp框架uniapp官网&#xff0c; 使用Element-ui组件Element-ui组件网址进行前端页面的完成。 前端项目下载地址 前端项目 前端项目展示 首页 首页展示 echarts实现…

9.Linear Maps

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