Vue + Go: делаем фронтовые острова на беке
- воскресенье, 8 ноября 2026 г. в 00:00:10
Предлагаю вашему вниманию собственный велосипед (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 от бэка, изучает их, при необходимости подгружает компонент и монтирует его внутрь контейнера.
Бек - это по своей сути 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 }}
Имя компонента + затем пары "ключ - значение" ...
Самый главный кусоек:
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.