Code split the client, update dependencies
This commit is contained in:
parent
84c3d5cc88
commit
d930365eeb
37 changed files with 2036 additions and 1181 deletions
|
@ -1,12 +1,14 @@
|
|||
<%! data *indexData, cssPath, jsPath string %>
|
||||
<%! data *indexData, cssPath string, inlineScript string, scripts []string %>
|
||||
|
||||
<%% import "github.com/mailru/easyjson" %%>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#222">
|
||||
|
||||
<title>Dispatch</title>
|
||||
|
||||
|
@ -16,12 +18,20 @@
|
|||
<link rel="preload" href="/font/Montserrat-Bold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
<link rel="preload" href="/font/RobotoMono-Bold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
|
||||
|
||||
<% if cssPath != "" { %>
|
||||
<link href="/<%== cssPath %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<link rel="icon" href="data:;base64,=">
|
||||
|
||||
<script><%== inlineScript %></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script id="env" type="application/json"><% easyjson.MarshalToWriter(data, w) %></script>
|
||||
<script src="/<%== jsPath %>"></script>
|
||||
<% for _, script := range scripts { %>
|
||||
<script src="/<%== script %>"></script>
|
||||
<% } %>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -7,13 +7,23 @@ import (
|
|||
"github.com/mailru/easyjson"
|
||||
)
|
||||
|
||||
func IndexTemplate(w io.Writer, data *indexData, cssPath, jsPath string) error {
|
||||
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Dispatch</title><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link href=\"/")
|
||||
func IndexTemplate(w io.Writer, data *indexData, cssPath string, inlineScript string, scripts []string) error {
|
||||
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"theme-color\" content=\"#222\"><title>Dispatch</title><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\">")
|
||||
if cssPath != "" {
|
||||
io.WriteString(w, "<link href=\"/")
|
||||
io.WriteString(w, cssPath )
|
||||
io.WriteString(w, "\" rel=\"stylesheet\"><link rel=\"icon\" href=\"data:;base64,=\"></head><body><div id=\"root\"></div><script id=\"env\" type=\"application/json\">")
|
||||
io.WriteString(w, "\" rel=\"stylesheet\">")
|
||||
}
|
||||
io.WriteString(w, "<link rel=\"icon\" href=\"data:;base64,=\"><script>")
|
||||
io.WriteString(w, inlineScript )
|
||||
io.WriteString(w, "</script></head><body><div id=\"root\"></div><script id=\"env\" type=\"application/json\">")
|
||||
easyjson.MarshalToWriter(data, w)
|
||||
io.WriteString(w, "</script><script src=\"/")
|
||||
io.WriteString(w, jsPath )
|
||||
io.WriteString(w, "\"></script></body></html>")
|
||||
io.WriteString(w, "</script>")
|
||||
for _, script := range scripts {
|
||||
io.WriteString(w, "<script src=\"/")
|
||||
io.WriteString(w, script )
|
||||
io.WriteString(w, "\"></script>")
|
||||
}
|
||||
io.WriteString(w, "</body></html>")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ package server
|
|||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -16,9 +17,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/dsnet/compress/brotli"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/khlieng/dispatch/assets"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const longCacheControl = "public, max-age=31536000, immutable"
|
||||
|
@ -34,25 +34,32 @@ type File struct {
|
|||
Compressed bool
|
||||
}
|
||||
|
||||
var (
|
||||
files = []*File{
|
||||
&File{
|
||||
Path: "bundle.js",
|
||||
Asset: "bundle.js.br",
|
||||
ContentType: "text/javascript",
|
||||
CacheControl: longCacheControl,
|
||||
Compressed: true,
|
||||
},
|
||||
&File{
|
||||
Path: "bundle.css",
|
||||
Asset: "bundle.css.br",
|
||||
ContentType: "text/css",
|
||||
CacheControl: longCacheControl,
|
||||
Compressed: true,
|
||||
},
|
||||
type h2PushAsset struct {
|
||||
path string
|
||||
hash string
|
||||
}
|
||||
|
||||
func newH2PushAsset(name string) h2PushAsset {
|
||||
return h2PushAsset{
|
||||
path: "/" + name,
|
||||
hash: strings.Split(name, ".")[1],
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
files []*File
|
||||
|
||||
indexStylesheet string
|
||||
indexScripts []string
|
||||
inlineScript string
|
||||
inlineScriptSha256 string
|
||||
|
||||
h2PushAssets []h2PushAsset
|
||||
h2PushCookieValue string
|
||||
|
||||
contentTypes = map[string]string{
|
||||
".js": "text/javascript",
|
||||
".css": "text/css",
|
||||
".woff2": "font/woff2",
|
||||
".woff": "application/font-woff",
|
||||
".ttf": "application/x-font-ttf",
|
||||
|
@ -63,47 +70,58 @@ var (
|
|||
)
|
||||
|
||||
func (d *Dispatch) initFileServer() {
|
||||
if !viper.GetBool("dev") {
|
||||
data, err := assets.Asset(files[0].Asset)
|
||||
if viper.GetBool("dev") {
|
||||
indexScripts = []string{"bundle.js"}
|
||||
} else {
|
||||
data, err := assets.Asset("asset-manifest.json")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
hash := md5.Sum(data)
|
||||
files[0].Hash = base64.RawURLEncoding.EncodeToString(hash[:])[:8]
|
||||
files[0].Path = "bundle." + files[0].Hash + ".js"
|
||||
|
||||
br, err := brotli.NewReader(bytes.NewReader(data), nil)
|
||||
manifest := map[string]string{}
|
||||
err = json.Unmarshal(data, &manifest)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
|
||||
runtime, err := assets.Asset(manifest["runtime~main.js"] + ".br")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
runtime = decompressAsset(runtime)
|
||||
inlineScript = string(runtime)
|
||||
|
||||
io.Copy(gzw, br)
|
||||
gzw.Close()
|
||||
files[0].GzipAsset = buf.Bytes()
|
||||
hash := sha256.New()
|
||||
hash.Write(runtime)
|
||||
inlineScriptSha256 = base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||
|
||||
data, err = assets.Asset(files[1].Asset)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
indexStylesheet = manifest["main.css"]
|
||||
indexScripts = []string{
|
||||
manifest["vendors~main.js"],
|
||||
manifest["main.js"],
|
||||
}
|
||||
|
||||
hash = md5.Sum(data)
|
||||
files[1].Hash = base64.RawURLEncoding.EncodeToString(hash[:])[:8]
|
||||
files[1].Path = "bundle." + files[1].Hash + ".css"
|
||||
h2PushAssets = []h2PushAsset{
|
||||
newH2PushAsset(indexStylesheet),
|
||||
newH2PushAsset(indexScripts[0]),
|
||||
newH2PushAsset(indexScripts[1]),
|
||||
}
|
||||
|
||||
br.Reset(bytes.NewReader(data))
|
||||
buf = &bytes.Buffer{}
|
||||
gzw.Reset(buf)
|
||||
for _, asset := range h2PushAssets {
|
||||
h2PushCookieValue += asset.hash
|
||||
}
|
||||
|
||||
io.Copy(gzw, br)
|
||||
gzw.Close()
|
||||
files[1].GzipAsset = buf.Bytes()
|
||||
for _, assetPath := range manifest {
|
||||
file := &File{
|
||||
Path: assetPath,
|
||||
Asset: assetPath + ".br",
|
||||
ContentType: contentTypes[filepath.Ext(assetPath)],
|
||||
CacheControl: longCacheControl,
|
||||
Compressed: true,
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
fonts, err := assets.AssetDir("font")
|
||||
if err != nil {
|
||||
|
@ -121,22 +139,18 @@ func (d *Dispatch) initFileServer() {
|
|||
Compressed: strings.HasSuffix(font, ".br"),
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.Compressed {
|
||||
data, err = assets.Asset(file.Asset)
|
||||
data, err := assets.Asset(file.Asset)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
br.Reset(bytes.NewReader(data))
|
||||
buf = &bytes.Buffer{}
|
||||
gzw.Reset(buf)
|
||||
|
||||
io.Copy(gzw, br)
|
||||
gzw.Close()
|
||||
file.GzipAsset = buf.Bytes()
|
||||
file.GzipAsset = gzipAsset(data)
|
||||
}
|
||||
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
if viper.GetBool("https.hsts.enabled") && viper.GetBool("https.enabled") {
|
||||
|
@ -154,6 +168,34 @@ func (d *Dispatch) initFileServer() {
|
|||
}
|
||||
}
|
||||
|
||||
func decompressAsset(data []byte) []byte {
|
||||
br, err := brotli.NewReader(bytes.NewReader(data), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
io.Copy(buf, br)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func gzipAsset(data []byte) []byte {
|
||||
br, err := brotli.NewReader(bytes.NewReader(data), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
io.Copy(gzw, br)
|
||||
gzw.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (d *Dispatch) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
d.serveIndex(w, r)
|
||||
|
@ -181,7 +223,7 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
|
|||
connectSrc = "ws://" + r.Host
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src data:; connect-src "+connectSrc)
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self' 'sha256-"+inlineScriptSha256+"'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src data:; connect-src "+connectSrc)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
@ -200,22 +242,22 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
|
|||
"Accept-Encoding": r.Header["Accept-Encoding"],
|
||||
},
|
||||
}
|
||||
|
||||
cookie, err := r.Cookie("push")
|
||||
if err != nil {
|
||||
pusher.Push("/"+files[1].Path, options)
|
||||
pusher.Push("/"+files[0].Path, options)
|
||||
for _, asset := range h2PushAssets {
|
||||
pusher.Push(asset.path, options)
|
||||
}
|
||||
|
||||
setPushCookie(w, r)
|
||||
} else {
|
||||
pushed := false
|
||||
|
||||
if files[1].Hash != cookie.Value[8:] {
|
||||
pusher.Push("/"+files[1].Path, options)
|
||||
pushed = true
|
||||
}
|
||||
if files[0].Hash != cookie.Value[:8] {
|
||||
pusher.Push("/"+files[0].Path, options)
|
||||
pushed = true
|
||||
for i, asset := range h2PushAssets {
|
||||
if len(cookie.Value) >= (i+1)*8 &&
|
||||
asset.hash != cookie.Value[i*8:(i+1)*8] {
|
||||
pusher.Push(asset.path, options)
|
||||
pushed = true
|
||||
}
|
||||
}
|
||||
|
||||
if pushed {
|
||||
|
@ -228,17 +270,17 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Content-Encoding", "gzip")
|
||||
|
||||
gzw := gzip.NewWriter(w)
|
||||
IndexTemplate(gzw, getIndexData(r, state), files[1].Path, files[0].Path)
|
||||
IndexTemplate(gzw, getIndexData(r, state), indexStylesheet, inlineScript, indexScripts)
|
||||
gzw.Close()
|
||||
} else {
|
||||
IndexTemplate(w, getIndexData(r, state), files[1].Path, files[0].Path)
|
||||
IndexTemplate(w, getIndexData(r, state), indexStylesheet, inlineScript, indexScripts)
|
||||
}
|
||||
}
|
||||
|
||||
func setPushCookie(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "push",
|
||||
Value: files[0].Hash + files[1].Hash,
|
||||
Value: h2PushCookieValue,
|
||||
Path: "/",
|
||||
Expires: time.Now().AddDate(1, 0, 0),
|
||||
HttpOnly: true,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue