InquirerProsecution: Answer Sara Duterte’s alleged ₱818-M unexplained wealthThe Jerusalem Post‘They don't have to fight alone’: Why victims of jihadism are turning to the JewsPunchLawyer who defended Tupac’s killer dies at 55ZDF heuteAktuelle Pressemitteilungen des ZDFRTP Desporto"Sporting de Braga é favorito porque é melhor do que o Rio Ave"SRF NewsTeure Spitzenmedizin – Krebserkrankungen durch das Medikament Zolgensma?BillboardElvis Presley Rights Owners Sue Suno Over AI Voice Cloning: ‘Elvis’ Identity Was Stolen’TagesschauPolen: Mindestens fünf Tote nach Schüssen in KrakauInteriaStrzelanina w Krakowie. Szef MSWiA jedzie na miejsce zdarzeniaSportstarLeeds United claim smash-and-grab 1-0 win over Tottenham Hotspur; Jayden Bogle escapes red cardOnetNowe ustalenia po strzelaninie w Krakowie. Sprawca miał działać samGuardian SportArsenal v Hull City, Fulham v Newcastle United, FA Cup and more: football – live
The Daily Newsstand · Free, Always
Saturday, November 7, 2026

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

Translate

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

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

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

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.