Введение

Добро пожаловать в очередную статью из цикла «How to»! На этот раз, мы разберём создание нативного десктоп приложения для macOS, напишем немного кода на Golang и React.js, который будет обладать защитой от копирования.

В этот раз, постараемся обойтись без магии!

Цели статьи

  1. Показать один из самых простых способов создания нативного десктоп приложения для macOS на Golang;
  2. Показать вариант защиты кода вашего приложения от изменения третьими лицами (например, при коммерческом распространении);

Инструменты

Рабочее окружение

  • Go 1.12.5;
  • Node.js 12.3.1;

Операционная система

  • Apple macOS 10.14.5 Mojave (darwin/amd64);

Менеджер пакетов и зависимостей

  • dep 0.5.3 (Golang);
  • npm 6.9.0 (Node.js);

Используемые пакеты Golang

  • net/http — стандартный пакет для создания веб-сервера (godoc);
  • gobuffalo/packr — пакет для упаковки всех необходимых исходников в один исполняемый бинарный файл (GitHub);
  • zserge/webview — кросс-платформенный пакет для создания нативного окна операционной системы со встроенным браузером (GitHub);

Используемые библиотеки Node.js

  • facebook/create-react-app — фронтенд для macOS-приложения (GitHub);
  • axios/axios — для более простого написания AJAX-запросов (GitHub);

Теоретическая база

Чтобы лучше понять происходящее, предлагаю вам изучить работу некоторых пакетов, на которые мы будем опираться и использовать.

net/http

Пакет, который обеспечивает реализацию HTTP клиента и сервера. Входит в состав стандартной поставки Go и не требует отдельной установки и настройки.

Он интересен нам, так как очень прост в понимании, имеет хорошую документацию и обладает функцией http.FileServer().

Более подробно, читайте в официальной документации.

http.FileServer()

Эта функция является ключевой и даёт полный доступ веб-серверу к указанной папке и всем её файлам. То есть, функция http.FileServer() позволяет смонтировать папку на любой указанный адрес (route) веб-сервера.

Например, смонтируем корневую папку ./static/images/photos так, чтобы она была доступна по адресу http://localhost:8000/photos:

http.Handle("/photos", http.FileServer("./static/images/photos"))

gobuffalo/packr

Пакет с говорящим названием. Именно он позволит нам упаковать все необходимые файлы в один бинарный файл.

Обратите внимание, что в статье описана работа с веткой v1.

Допустим, у нас есть следующая структура каталога проекта:

tree .

├── main.go
└── templates
    ├── admin
    │   └── index.html
    └── index.html

Файл ./main.go содержит:

package main

import (
	"fmt"

	"github.com/gobuffalo/packr"
)

func main() {
	// Папка, с шаблонами, которые необходимо
	// добавить в бинарный файл (упаковать)
	box := packr.NewBox("./templates")

	// Производим поиск файла внутри папки
	s, err := box.FindString("admin/index.html")

	// Если файл не найден, то ошибка
	if err != nil {
		log.Fatal(err)
	}

	// Выводим содержимое файла
	fmt.Println(s)
}

Теперь скомпилируем проект в исполняемый бинарный файл. При этом, пакет packr упакует в него и всё содержимое папки ./templates:

packr build ./main.go

Если вы хотите создать бинарный файл для ОС или архитектуры, отличной от той, с которой вы работаете сейчас, то вызывайте packr вот так:

# Пример для GNU/Linux, x64 бит
GOOS=linux GOARCH=amd64 packr build ./main.go

zserge/webview

Крошечный кросс-платформенный пакет для веб-просмотра, используемый для создания современных графических интерфейсов.

Обратите внимание, что в статье описана работа с версией 0.1.0.

Файл ./main.go содержит:

package main

import "github.com/zserge/webview"

func main() {
	// Открыть главную страницу Google в окне, с размером 1024х768 px,
	// без изменения размеров окна
	webview.Open("Google", "https://google.com", 1024, 768, false)
}

Чтобы создать macOS-приложение, перейдём в консоль и выполним:

# Создание структуры директорий macOS-приложения
mkdir -p example.app/Contents/MacOS

# Компилируем ./main.go в папку приложения
go build -o example.app/Contents/MacOS/example

# Запуск приложения
open example.app

Структура macOS-приложения

tree .

├── vendor
├── ui
│   ├── build
│   ├── node_modules
│   ├── public
│   ├── src
│   ├── package-lock.json
│   └── package.json
├── helloworld.app
├── Gopkg.lock
├── Gopkg.toml
└── main.go

Описание основных файлов и папок

  • vendor — тут будут храниться все пакеты, установленные с помощью dep;
  • ui — папка с React.js приложением (фронтенд);
    • build — папка с production-версией приложения после сборки;
    • src — папка с исходным кодом React-приложения;
    • package.json — файл зависимостей npm;
  • helloworld.app — приложение macOS (специально подготовленная папка);
  • Gopkg.toml — файл зависимостей dep;
  • main.go — исходный код Golang-приложения (бэкенд);

Пишем код

Хватит теории. Как говорил, без преувеличения, один из великих программистов нашего времени Линус Торвальдс:

Talk is cheap. Show me the code.

— Linus Torvalds

Давайте последуем этому совету и напишем немного кода.

Я не буду разбирать каждую строку кода в отдельности, так как считаю это избыточным и контр-продуктивным. Все листинги кода — снабжены подробными комментариями.

Памятка для начинающих/copy-paste разработчиков

Отлично, когда есть полный листинг кода в конце статьи, правда? Можно сразу же, не вчитываясь в текст, скопировать весь код программы и посмотреть её выполнение…

На этом моменте, я хотел бы обратиться ко всем читателям, которые не хотят тратить время на теорию:

Не копируйте бездумно код из Интернета! Это не поможет ни вам (в понимании кода и предмета статьи), ни автору (при объяснении/помощи в комментариях).

Фронтенд для приложения

React.js — это мощная, но в то же время, простая в изучении JavaScript-библиотека для создания пользовательских интерфейсов, которая отлично подойдёт нам для реализации фронтенд части приложения.

В рамках данной статьи, мы не будем использовать ничего, кроме стандартной «It works!» страницы React.js.

Как и всё в современном фронтенде, мы начинаем с установки React.js и всех необходимых вспомогательных библиотек.

# Создадим папку для приложения и перейдём в неё
mkdir ~/helloworld_project && cd ~/helloworld_project

# Согласно структуре готового приложения,
# установим React.js в директорию ./ui
npx create-react-app ui

# Перейдём в папку и проверим, что всё работает
cd ui && npm start && open http://localhost:3000

# Далее, остановим dev-сервер (нажмите Crtl+C)
# и установим библиотеку axios
npm i --save axios

Листинг файла ./ui/src/App.js

// Импортируем React и методы хуков
import React, { useState, useEffect } from "react";

// Импортируем библиотеку axios
import axios from "axios";

// Импортируем логотип и CSS
import logo from "./logo.svg";
import "./App.css";

function App() {
  // Определяем хранилище для полученных данных
  const [state, setState] = useState([]);

  // Получение данных из AJAX запроса.
  // Помните, что функция, переданная в useEffect, будет запущена
  // после того, как рендер будет зафиксирован на экране.
  // См. https://reactjs.org/docs/hooks-reference.html#useeffect
  useEffect(() => {
    axios
      .get("/hello") // GET-запрос на URL /hello
      .then(resp => setState(resp.data)) // сохраняем ответ в state
      .catch(err => console.log(err)); // ловим ошибку, если возникла
  });

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello, {state.text}!</p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

Вот и всё, фронтенд для приложения готов! 👌

Бэкенд для приложения

Установим необходимые пакеты:

dep ensure -add github.com/gobuffalo/packr
dep ensure -add github.com/zserge/webview

Также, нам потребуется утилита packr, которая должна быть доступна для вызова из консоли в $GOPATH/bin/packr:

go get -u github.com/gobuffalo/packr/packr

Листинг файла ./main.go

package main

import (
	"encoding/json"
	"net/http"

	"github.com/gobuffalo/packr"
	"github.com/zserge/webview"
)

// Message : структура для сообщения
type Message struct {
	Text string `json:"text"`
}

func main() {
	// Определяем папку, которая будет упакована.
	// Так как мы работаем с React.js, то после сборки,
	// production-версия будет находиться в папке ./build,
	// в корне React.js приложения
	folder := packr.NewBox("./ui/build")

	// Монтируем папку ./ui/build в корневой роут
	http.Handle("/", http.FileServer(folder))

	// Создаём роут для функции showMessage
	http.HandleFunc("/hello", showMessage)

	// Старт сервера на 8000 порту.
	// Определяем, как горутину, чтобы не
	// блокировать выполнение остальной
	// части программы
	go http.ListenAndServe(":8000", nil)

	// Откроем webview с параметрами:
	//  - имя: Golang App
	//  - адрес: http://localhost:8000
	//  - размеры окна: 800x600 px
	//  - изменяемый размер окна: true
	webview.Open("Golang App", "http://localhost:8000", 800, 600, true)
}

func showMessage(w http.ResponseWriter, r *http.Request) {
	// Определение JSON данных
	message := Message{"World"}

	// Вернуть JSON для отображения
	output, err := json.Marshal(message)

	// Отлавливаем ошибки
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Задаём заголовок Content-Type
	w.Header().Set("Content-Type", "application/json")

	// Отображение страницы
	w.Write(output)
}

Сборка приложения

# Создание структуры директорий macOS-приложения
mkdir -p helloworld.app/Contents/MacOS

# Компилируем ./main.go в папку приложения
go build -o example.app/Contents/MacOS/helloworld

# Запуск приложения
open helloworld.app

gif result

Кросс-компиляция для Windows и GNU/Linux

Теоретический блок и приведённый в статье код — являются актуальными для разработки подобного приложения для других ОС. При этом, код остаётся неизменным.

Пиши код один раз — запускай везде!

Это становится возможным, благодаря кросс-системной природе Go. Вы можете с лёгкостью скомпилировать исполняемый файл вашего приложения для запуска на любой поддерживаемой операционной системе:

  • GNU/Linux — исполняемый бинарный файл;
  • Microsoft Windows — исполняемый файл .exe;
  • Apple macOS — бинарный файл, расположенный внутри структуры .app;

Мы рассмотрим это в следующих статьях.

Следите за обновлениями, комментируйте и пишите только хороший код!

Закрепление материала

Вы находитесь в конце статьи. Теперь вы знаете намного больше, чем 15 минут назад. Принимайте мои поздравления! 🎉

Отдохните 10-15 минут и восстановите в памяти прочитанный текст и изученный код из статьи. Далее, попробуйте ответить на вопросы и сделать упражнения, чтобы лучше закрепить материал.

Да, подглядывать можно, но только если не смогли вспомнить.

Вопросы

  1. Какая функция стандартного Go-пакета net/http используется для монтирования папки на указанный адрес (route)?
  2. Что делает функция Marshal из стандартного Go-пакета encoding/json?
  3. Какие параметры нужно изменить в исходном коде бэкенд приложения, чтобы открылось окно с размерами Full HD?
  4. Какое поведение будет у приложения (после запуска), если сделать старт веб-сервера без горутины?
  5. Для чего используется команда packr build ./main.go?

Упражнения

  • Перепишите код AJAX-запроса (во фронтенд приложении) без использования библиотеки axios. Подсказка: воспользуйтесь возможностями Fetch API.
  • Добавьте в функцию showMessage больше JSON данных для вывода на фронтенд. Пример: добавьте новый атрибут Emoji в структуру Message и выведите его (с любимым смайлом) после атрибута Text.
  • Попробуйте усовершенствовать внешний вид своего приложения, например, с помощью библиотеки визуальных компонентов Material UI (GitHub).