Vue + Go: делаем фронтовые острова на беке

—

от автора

Предлагаю вашему вниманию собственный велосипед (SSR) на go и vue. Я пишу эту статью, потому что, когда я искал подобную информацию, то ничего не нашел. Наверное, и хорошо: лучше так не писать, но мы же должны познать плохие практики, чтобы понимать, где добро, а где зло !)

Именно таким образом и родился учебный проект https://gitlab.com/pnym/sync-go-vue: на Go рендерится HTML, а Vue оживляет только опредленные кусочки.

Договор

Паттерн у меня опирается на соглашение. Сервер у себя рендерит HTML в таком формате:

<div data-island="Counter" data-props='{"initial":0,"step":1}'></div>

Все просто, два атрибута:

  • data-island — название vue компонента

  • data-props — tего props, сериализованные в JSON

Это весь контракт между бэком и фронтом. На фронте тем временем скрипт находит div от бэка, изучает их, при необходимости подгружает компонент и монтирует его внутрь контейнера.

Сторона Go

Бек — это по своей сути chi, html/template и совсем немного строк рендера. В шаблонах вызывается функция island, с помощью которой и собирает нужный контейнер:

tmpl, err := template.New("").Funcs(template.FuncMap{    "island": island,}).ParseFS(templatesFS, "*.tmpl")

и вот сам island:

func island(name string, props ...any) template.HTML {    data := map[string]any{}    for i := 0; i < len(props); i += 2 {        data[fmt.Sprint(props[i])] = props[i+1]    }    b, err := json.Marshal(data)    if err != nil {        panic(fmt.Sprintf("island %q: marshal props: %v", name, err))    }    return template.HTML(fmt.Sprintf(        `<div data-island="%s" data-props="%s"></div>`,        template.HTMLEscapeString(name),        template.HTMLEscapeString(string(b)),    ))}

(Важный момент: если вы все же надумаете писать или копировать какое-то решение, то не забывайте про лазейку для XSS)

Пример в шаблоне:

{{ island "Counter" "initial" 0 "step" 1 }}

Имя компонента + затем пары «ключ — значение» …

Сторона Vue

Самый главный кусоек:

import { createApp } from 'vue'const components = import.meta.glob('./islands/*.vue')document.querySelectorAll<HTMLElement>('[data-island]').forEach(async (el) => {  const name = el.dataset.island!  const loader = components[`./islands/${name}.vue`]  if (!loader) {    console.warn(`[sync-go-vue] component not found: ${name}`)    return  }  let props: Record<string, unknown> = {}  try {    props = JSON.parse(el.dataset.props || '{}')  } catch {    console.error(`[sync-go-vue] invalid props for ${name}`)    return  }  const mod = await loader() as { default: any }  createApp(mod.default, props).mount(el)})

import.meta.glob— важная строка. Vite смотрит на нее и создаёт схему (карту) «путь + ленивый импорт», каждый компонент такой собирается в отдельный чанк. Отдельно вести документацию не нужно: закинул файл «Example.vue» в папку и забыл. Еще один важный момент, имя в data-island должно совпадать с именем файла.

Скрытый текст

Отдельно про монтирование (кому интересно)

Я реализовал простым путем SSR, монтирую в пустой div, после чего Vue строит DOM заново. Если нужно будет добавить логику, чтобы контент компонентов был виден до загрузки JS, то тогда уже нужно делать createSSRApp и сериализация разметки.

Самая приятная часть

Иногда при разработки «бек + Vite» возникает боль с двумя портами, cookies и cors. Я решил эту проблему простым способом: Vite сам работает реверс-прокси.

В vite.config.ts описано правило:

server: {  proxy: {    '^(?!/@|/src/|/node_modules/|/__vite)': {      target: 'http://localhost:8080',      changeOrigin: true,    },  },},

Всё, что не относится к Vite, уходит на 8080. Это значит, что браузер видит один порт. Страницу отдаёт Go, а Vue-код идёт через HMR и обновляет .vue, и компонент обновляется без перезагрузки. Также в самих шаблонах Go я тоже добавил похожую логику, поэтому в конфиге Vite живёт маленький плагин, который следит за *.tmpl и присылает full-reload.

Перезапуск Go при обновление кода делает air. Таким образом оба сервера обновляются без перезагрузки. Запускается все одной командой:

make dev

Итог

Этот проект я создал побаловаться, спасибо, что дочитали. Хоть он и получился маленьким: один go файл с маршрутами, рендер на 40 строк, три компонента. Зато это работает и показывает настоящую механику паттерна:

  • страница статична

  • vue зоны размечены data-island / data-props

  • клиент лениво монтирует компоненты

  • данные приходят с сервера

Вы сами для себя решаете, нужно ли вам это делать, поддерживать и брать на себя все риски, которые будут обязательно. Ну или кому вы оставите после себя это наследие.

Исходники лежат в репозитории make install && make dev.

ссылка на оригинал статьи https://habr.com/ru/articles/1104910/