Предлагаю вашему вниманию собственный велосипед (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/