Shanraq.org Shanraq.org
Go-дағы статика: CSS, қаріп және сурет
IT

Go тілі: нөлден өз блогыңызға дейін 24-сабақ (барлығы 50)

Go-дағы статика: CSS, қаріп және сурет

Go курсының жиырма төртінші сабағы. Сайт жұмыс істеп қана қоймай, көріне бастайды. http.FileServerFS, StripPrefix дискіге неге керек, кірістірілгенге неге керек емес, Content-Type қайдан шығады, static қалтасы неге түгел көрінеді және кірістірілген файлды браузер неге қайта жүктейді.

Не үшін керек

Блог жұмыс істейді. Сыртқы түрі сонда да 1994 жылы ұмытып кеткен құжаттай: ақ бетте қара Times, экранның бүкіл ені бойынша.

Жетіспейтіннің бәрі — файлдар: мәнерлер кестесі, қаріп, сурет. Оларды үлгіден бет жинағандай жинаудың қажеті жоқ. Оларды дәл сол күйінде беру керек, бүкіл айырма да осында.

Бірден тұтас

Жаңа қалта, go mod init sabaq22. Ішінде екі қалта — templates және static.

static/style.css:

:root { --ink: #1a1a1a; --paper: #fffdf8; --accent: #d32f2f; }

body {
  margin: 0 auto;
  max-width: 38rem;
  padding: 2rem 1rem;
  background: var(--paper);
  color: var(--ink);
  font: 1.05rem/1.6 -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif;
}

h1 { font-size: 1.6rem; letter-spacing: -0.01em; }
.brand { display: flex; align-items: center; gap: 0.75rem; }
.brand h1 { margin: 0; }
a { color: var(--accent); }
ul { padding-left: 1.1rem; }
li { margin: 0.4rem 0; }
img { max-width: 100%; height: auto; }

@media (max-width: 30rem) {
  body { padding: 1.2rem 0.9rem; }
  h1 { font-size: 1.35rem; }
}

Осы файл туралы бірден ескерту. Курс — Go туралы, әрі CSS оны үйретпейді. Мұндағы мәнерлер бет лайықты көрінуі үшін дайын күйінде алынған, әрі олардың ережелерін біз бүгін де, әрі қарай да талдамаймыз: бұл — осындай курс көлеміндегі бөлек кәсіп. Сол күйінде көшіріңіз, түстерін өз талғамыңызша өзгертіңіз — Go-ға бұл ешқалай әсер етпейді.

Мұны шындап үйренгіңіз келсе, қайталап айтқандарға емес, бастапқы дереккөздерге барыңыз: MDN — CSS анықтамалығы және MDN — CSS негіздері. Бұл беттердің қазақша нұсқасы жоқ, сондықтан сілтемелер ағылшын тіліне. Файлдың соңында тар экрандарға арналған минимум бар — шаблондағы <meta name="viewport"> екеуі бірге беттің телефонда оқылуына жетеді. Біздің сабаққа біреуін түсінсеңіз жеткілікті: мәнерлер файлы — кәдімгі файл, ал сервер оны сол күйінде береді.

templates/list.html:

<!doctype html>
<html lang="kk">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Менің блогым</title>
    <link rel="stylesheet" href="/static/style.css">
    <link rel="icon" href="/static/logo.svg" type="image/svg+xml">
  </head>
  <body>
    <header class="brand">
      <img src="/static/logo.svg" width="40" height="40" alt="">
      <h1>Менің блогым</h1>
    </header>
    <ul>
      {{ range .Articles }}<li><a href="/read/{{ .Slug }}">{{ .Title }}</a></li>{{ end }}
    </ul>
  </body>
</html>

static/logo.svg — сайттың белгісі. Ішінде мәтін жоқ, сондықтан бір файл барлық тілге жарайды. Ол әрі қойындының белгісі — үлгідегі <link rel="icon"> жолы, бөлек favicon.ico жасаудың қажеті жоқ:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="40" height="40">
  <g fill="none" stroke-linecap="round">
    <g stroke="#ffadad" stroke-opacity="0.5" stroke-width="45">
      <line x1="76" y1="76" x2="324" y2="324"/>
      <line x1="137" y1="37" x2="363" y2="263"/>
      <line x1="37" y1="137" x2="263" y2="363"/>
      <line x1="76" y1="324" x2="324" y2="76"/>
      <line x1="137" y1="363" x2="363" y2="137"/>
      <line x1="37" y1="263" x2="263" y2="37"/>
    </g>
    <g stroke="#e53935" stroke-width="30">
      <line x1="76" y1="76" x2="324" y2="324"/>
      <line x1="137" y1="37" x2="363" y2="263"/>
      <line x1="37" y1="137" x2="263" y2="363"/>
      <line x1="76" y1="324" x2="324" y2="76"/>
      <line x1="137" y1="363" x2="363" y2="137"/>
      <line x1="37" y1="263" x2="263" y2="37"/>
    </g>
  </g>
</svg>

Және main.go:

package main

import (
	"bytes"
	"embed"
	"html/template"
	"log"
	"net/http"
)

//go:embed templates/*.html static
var files embed.FS

var tpl = template.Must(template.ParseFS(files, "templates/*.html"))

type Article struct{ Slug, Title string }

var blog = []Article{
	{Slug: "dala", Title: "Дала туралы"},
	{Slug: "shanyraq", Title: "Шаңырақ деген не"},
}

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		var buf bytes.Buffer
		if err := tpl.ExecuteTemplate(&buf, "list.html", map[string]any{"Articles": blog}); err != nil {
			log.Println("үлгі:", err)
			http.Error(w, "серверде қате", http.StatusInternalServerError)
			return
		}
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		buf.WriteTo(w)
	})

	mux.Handle("GET /static/", http.FileServerFS(files))

	log.Println("http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}

go run . — сонда бет ақыры бетке ұқсай бастайды.

Талдау

Статиканы жинамайды

Бет өңдеушісі әр жолы жұмыс істейді: деректі алады, үлгіге қояды, жапсырады. Файл сервері ештеңе істемейді — файлды тауып, байттарды береді.

Сондықтан сайттың бүкіл статикасына бір жол жетеді:

mux.Handle("GET /static/", http.FileServerFS(files))

HandleFunc емес, Handle: FileServerFS функция емес, дайын http.Handler қайтарады. Бұл — өңдеушілер сабағындағы сол интерфейс, әрі мінеки сізге бөтен өңдеуші, оны бағытқа қоя саласыз.

Соңындағы қиғаш сызыққа назар аударыңыз: GET /static/ — нақты мекенжай емес, префикс. /read/{slug} кез келген мақаланы ұстағандай, /static/ одан кейінгінің бәрін ұстайды.

Мұнда StripPrefix неге жоқ — және ол қашан керек

Кітаптар мен мақалаларда бұл жол әрдайым дерлік былай жазылады:

mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))

Орама қайдан шыққанын талдайық. Файл сервері файлды сұраныстағы жол бойынша іздейді. /static/style.css келді — ол өзіне берілген қалтаның ішінен style.css-ті іздейді, яғни static/static/style.css-ті. Ондай файл жоқ.

Өз компьютеріңізде тексеріңіз. Қасына орамасыз екінші бағыт қойыңыз:

mux.Handle("GET /raw/", http.FileServer(http.Dir("static")))

және екеуін де сұраңыз:

curl -o /dev/null -w "%{http_code}\n" http://localhost:8080/raw/style.css
curl -o /dev/null -w "%{http_code}\n" http://localhost:8080/static/style.css
404
200

StripPrefix мекенжайдан /static/ бөлігін кесіп тастайды, содан кейін ғана файл табылады.

Ал біздің бағдарламада орама жоқ — бәрі де істеп тұр. Себебі embed-те: біз static қалтасының өзін кірістірдік, сондықтан files ішінде файл static/style.css жолымен жатыр. Сұраныс мекенжайы файлдық жүйедегі жолмен әріппе-әріп сәйкес келеді, кесетін ештеңе жоқ.

Елестетіп көріңіз. «Шұлық» деп жазылған жәшік «шұлық» шкафының ішінде. «Шұлық шкафы, шұлық жәшігі» деп сұрайсыз — шкаф шынымен солай жасалса ғана бәрі сәйкеседі. Әйтпесе өтінішті қысқарту керек.

Content-Type кеңейтімнен алынады

Тақырыптарды сұрайық:

curl -sD- -o /dev/null http://localhost:8080/static/style.css
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 616
Content-Type: text/css; charset=utf-8

text/css дегенді ешкім жазған жоқ — Go оны .css кеңейтімінен алды. logo.svg үшін дәл сол әдіспен image/svg+xml шығады.

Бұл ұсақ-түйек емес. Браузер мәнерлер кестесін тек ол text/css болып келгенде ғана қолданады. Сол файлды text/plain етіп беріңіз — бет безендірусіз қалады, ал консольде тип сәйкес келмегені туралы дәл бір жол тұрады. Тақырып әу бастан көрініп тұрған жерде бір сағат іздеу.

Тұзақ: қалта түгел көрінеді

http://localhost:8080/static/ ашыңыз — файл атынсыз.

<pre>
<a href="logo.svg">logo.svg</a>
<a href="style.css">style.css</a>
</pre>

FileServer каталог тізімін көрсетеді. Қате емес, әдепкі мінез, әрі ол туралы алдын ала білген жөн: static ішіне не салсаңыз, соның бәрін бөтен адам тізім болып көреді. Жобаны, дерекқор көшірмесін, ұмытылған мұрағатты.

Қарапайым ереже: static ішінде тек көрсетуге келісетін нәрсе жатады. Қалғаны — басқа қалтаға және өз өңдеушіңіз арқылы, онда кімге болатынын өзіңіз шешесіз.

Кірістірілген файлды браузер әр жолы қайта жүктейді

Мінеки embed-тің көзге бірден түспейтін салдары, соның кесірінен сайт болуы мүмкіннен баяу.

Дискідегі файлдың өзгерту уақыты болады. Оны бергенде Go Last-Modified қояды, браузер келесі жолы «өзгерген жоқ па» деп сұрайды да, файлдың бәрінің орнына бос 304 Not Modified жауабын алады.

Кірістірілген файлдарда өзгерту уақыты жоқ — ол нөл, сондықтан тақырып қойылмайды. Өзіңіз салыстырыңыз.

Дискіден:

Last-Modified: Sat, 05 Sep 2026 10:36:02 GMT

ал If-Modified-Since қосылған қайталама сұраныс 304 береді. embed-тен тақырып мүлде жоқ, әрі сол қайталама сұраныс 200 мен файлдың толық денесін береді.

Бұл embed-тен бас тартуға себеп емес — файл құйрығы жоқ бір бағдарлама қымбат тұрады. Бұл кэшті өзіңіз басқаратыныңызды білуге себеп: Cache-Control тақырыбымен және мазмұнмен бірге өзгеретін style.7f3a1c.css тәрізді өзгермейтін файл атымен. Сайт нағыз серверге көшкенде бұған қайта ораламыз.

Қаріп: ең жылдам қаріп — жүктелмейтіні

style.css ішінде қаріп былай берілген:

font: 1.05rem/1.6 -apple-system, "Segoe UI", Roboto, "Noto Sans", sans-serif;

Мұнда ештеңе жүктелмейді. Браузер жүйелік қаріпті алады: Mac-та өзінікі, Windows-та өзінікі, Android-та өзінікі. Мәтін лезде шығады, әрі бұл нөл байт тұрады.

Өз қаріпіңіз — ақысын оқырман төлейтін шешім. woff2-дегі бір нұсқа — 20–40 килобайт, ал нұсқаны әдетте төртеу алады. Баяу желіде бұл байқалады. Сонда да алсаңыз — тек woff2, тек керек нұсқалар және міндетті түрде font-display: swap, қаріп келе жатқанда мәтін көрініп тұруы үшін.

Сурет: ұмытуға болмайтын екі атрибут

<img src="/static/logo.svg" width="40" height="40" alt="">

width пен height өлшем үшін емес — өлшемді CSS басқарады. Олар браузер жүктелуге дейін ара қатынасты біліп, орын қалдыруы үшін керек. Онсыз бет секіреді: мәтін көрініп қалды, сурет келеді де бәрін оқырманның саусағының астында төмен ысырады.

alt — суретті көрмейтіндерге арналған мәтін: экраннан оқитын бағдарлама, баяу желі, іздеу роботы.

Мұнда ол бос, әрі бұл ұмытқандық емес. Белгінің қасында блогтың аты кәдімгі мәтінмен тұр, ал белгінің өзі оған ештеңе қоспайды. Бос alt «оқитын ештеңе жоқ» дегенді білдіреді — экраннан оқитын бағдарлама суретті аттап өтеді де, атауды екі рет айтпайды. Ал ұмытылған alt-ты ол файл аты ретінде дауыстап оқиды: «логотип нүкте эс-ви-джи».

Бірінші экраннан төмендегі суреттерге loading="lazy" қосады — оқырман жетпейінше браузер оларды жүктемейді.

Сабақ картасы

Сабақ картасы: бет жиналады, ал файл сол күйінде беріледі

Өз сөзіңізбен айтыңыз

Қарамай, дауыстап немесе қағазға жауап беріңіз. Жауаптар — сабақтың соңында.

  1. Content-Type: text/plain болып берілген мәнерлер кестесі бетті неге безендірмейді?
  2. Дискідегі файлға StripPrefix неге керек, ал кірістірілгенге — керек емес?
  3. Оқырман сайтты екінші рет ашты. Браузер style.css-ті неге қайтадан толық жүктеді?

Тапсырма

Міндетті. /read/{slug} мекенжайында өз безендіруі бар мақала беті жасаңыз: article.html үлгісі, ортақ style.css және width, height, alt қойылған мұқаба суреті. Суреттің Content-Type-ы дұрыс екенін curl -sD- -o /dev/null арқылы тексеріңіз.

Болашаққа ескерту. Файл үшеу болғанда олар static ішінде қатар жатады, әрі бұл қалыпты жағдай. Оларға бөтен нәрсе — оқырмандар жүктеген суреттер — қосылған сәтте қалталар міндетті болады: логотипке brand, сосын css, js, өз суреттеріңізге images және бөтендерге uploads. 25-қадамнан бастап солай жасалған, әрі себебі қарапайым: қырық файлды кейін жіктеу бес қалтаны уақытында ашқаннан қымбатырақ.

Қалауыңызша.

  • static ішіне zhazbalar.txt файлын салыңыз да, /static/ ашыңыз. Оны тізімнен табыңыз.
  • Дәл сол файлдарды екінші бағытпен дискіден, http.Dir арқылы беріңіз де, жауап тақырыптарын кірістірілген нұсқамен салыстырыңыз.
  • /static/ бағытын өткен сабақтағы аралық қабатыңызбен орап, Cache-Control: max-age=3600 қойыңыз. Бетті жаңартқанда не өзгеретінін қараңыз.

Блогта бұл қайда тұрады

Блог жалаңаш мәтін болудан қалды. Әрі қарай — қате мен журнал: ақ экранның орнына не бұзылғанын көру.

Бүгін алынған қарыздар. Кэшті қолмен басқаруға тура келеді, әрі онымен бірге файл атындағы нұсқа келеді. Жауапты сығу — gzip не br — CSS-ті үш есе кішірейтеді, ал мұнда ол жоқ. Және static әзірге тізіммен ашық; нағыз сайтта мұны жабады.

Жауаптар

Жауаптарды көрсету
  1. Өйткені браузер файлды мекенжайдағы кеңейтім бойынша емес, мәлімделген тип бойынша қолданады. text/plain келді — демек бұл мәтін, онымен бетті безендірмейді. Тип сервердегі файл кеңейтімінен анықталады, сондықтан беттеудегі файл атын емес, жауап тақырыбын тексеру керек.
  2. Өйткені файл сервері файлды сұраныстағы жол бойынша өзіне берілген қалтаның ішінен іздейді. http.Dir("static") үшін /static/style.css сұранысы static/static/style.css-ті іздеуге айналады — 404 да, префиксті кесу қажеттігі де содан. embed-те біз static қалтасының өзін кірістірдік, сондықтан сұраныстағы жол мен іштегі жол сәйкес келеді.
  3. Өйткені кірістірілген файлдардың өзгерту уақыты нөл, әрі Go Last-Modified қоймайды. Браузердің «өзгерген жоқ па» деп қайта сұрайтын ештеңесі жоқ, сондықтан ол файлды қайтадан алады. Cache-Control тақырыбымен және мазмұнмен бірге өзгеретін файл атымен емделеді.

Дереккөздер

Мәтінде қате не теру қатесі кездессе, бізге айтыңыз

Тапсырманы тексеру

Алдымен VS Code-та шешіп, іске қосыңыз — редактор қатені сол жерде көрсетеді. Дайын шешімді осында қойыңыз. Тексеретін — модель: ол қатені атап көрсетеді, бірақ дайын жауапты бермейді.

Тексеру үшін кіру керек. Кіру

Пікірлер (0)

Әзірге пікір жоқ. Бірінші болыңыз.