在学习新项目BluePrint时,又遇到知识盲点:templ,补一补。
templ即用golang来构建html,想必更加灵活和“熟悉”,功能也更强大。
以下是直接机翻的templ特点:
- 服务器端渲染:部署为无服务器函数、Docker 容器或标准 Go 程序。
- 静态渲染:创建静态 HTML 文件以根据您的选择进行部署。
- 编译代码:组件被编译成高性能的 Go 代码。
- 使用 Go:调用任何 Go 代码,并使用标准的 if、switch 和 for 语句。
- 无 JavaScript:不需要任何客户端或服务器端 JavaScript。
- 出色的开发人员体验:附带 IDE 自动完成功能。
示例代码:
package main
templ Hello(name string) {
<div>Hello, { name }</div>
}
templ Greeting(person Person) {
<div class="greeting">
@Hello(person.Name)
</div>
}
看起来是golang中允许html代码使用
安装工具:go install github.com/a-h/templ/cmd/templ@latest 似乎应该是将golang代码转换为html的。
创建简单组件
- 创建项目
mkdir hello
go mod init hello
go get github.com/a-h/templ # 支持库
- 创建hello.templ文件
package main
templ hello(name string) {
<div>Hello, { name }</div>
}
-
生成go代码
templ generate
将产生一个hello_templ.go文件。代码使用了一些无意义的变量,看着有点晕。 -
创建主程序main.go
package main
import (
"context"
"os"
)
func main() {
component := hello("John")
component.Render(context.Background(), os.Stdout)
}
运行主程序,将在命令行输出:Hello, John
此时修改hello.templ也没什么影响了,因为golang去调用hello_templ.go去了。
以上只是作简单演示过程,实际上我们是需要建立一个Web服务器。将main.go作一些修改:
package main
import (
"fmt"
"net/http"
"github.com/a-h/templ"
)
func main() {
component := hello("John")
http.Handle("/", templ.Handler(component))
fmt.Println("Listening on :3000")
http.ListenAndServe(":3000", nil)
}
这样就可以访问web,在浏览器中得到输出。
如hello示例中看到的 hello 定义为了一个组件,关键词 templ 。在组件中可以使用golang语句。
package main
templ headerTemplate(name string) {
<header data-testid="headerTemplate">
<h1>{ name }</h1>
</header>
}
在hello示例中添加一个button组件
package main
templ hello(name string) {
<div>Hello, { name }</div>
}
templ button(text string) {
<button class="button">{ text }</button>
}
templ generate 后修改main.go (没有templ转go的过程,在IDE工具中,main.go中就不会有自动提示。当然也可以盲写后再生成,再运行)
package main
import (
"fmt"
"net/http"
"github.com/a-h/templ"
)
func main() {
component := hello("John")
http.Handle("/", templ.Handler(component))
http.Handle("/button", templ.Handler(button("ClickMe")))
fmt.Println("Listening on :3000")
http.ListenAndServe(":3000", nil)
}
文档中着重强调了要有关闭标记, 或 这种都是允许的。
组件的调用:
templ component(testID string) {
<p data-testid={ testID }>Text</p>
}
templ page() {
@component("testid-123")
}
组件也可以使用golang定义的函数:
func testID(isTrue bool) string {
if isTrue {
return "testid-123"
}
return "testid-456"
}
templ component() {
<p data-testid={ testID(true) }>Text</p>
}
条件属性:
templ component() {
<hr style="padding: 10px"
if true {
class="itIsTrue"
}
/>
}
输出为:
为标签增加附加值
类似于 1111
这个位置的解释有点不好理解,后面再说。看示例直接点。
templ component(shouldBeUsed bool, attrs templ.Attributes) {
<p { attrs... }>Text</p>
<hr
if shouldBeUsed {
{ attrs... }
}
/>
}
templ usage() {
@component(false, templ.Attributes{"data-testid": "paragraph"})
}
输出为:
<p data-testid="paragraph">Text</p>
<hr>
templ.Attributes是map[string]any,是一个键/值对。
URL属性
对URL属性要求特殊一些,它希望自己来处理/过滤链接,而不是直接给字符串。即使用templ.URL函数:
templ component(p Person) {
<a href={ templ.URL(p.URL) }>{ strings.ToUpper(p.Name) }</a>
}
JavaScript脚本
script withParameters(a string, b string, c int) {
console.log(a, b, c);
}
script withoutParameters() {
alert("hello");
}
templ Button(text string) {
<button onClick={ withParameters("test", text, 123) } onMouseover={ withoutParameters() } type="button">{ text }</button>
}
JSON
要将属性的值设置为 JSON 字符串,请使用函数将值序列化为字符串。
func countriesJSON() string {
countries := []string{"Czech Republic", "Slovakia", "United Kingdom", "Germany", "Austria", "Slovenia"}
bytes, _ := json.Marshal(countries)
return string(bytes)
}
templ SearchBox() {
<search-webcomponent suggestions={ countriesJSON() } />
}
表达式
注意:以下代码可能将templ中的内容和调用它的部份混合到了一起,为了简单。认真读应该看得出来。
直接字符串
package main
templ component() {
<div>{ "print this" }</div>
<div>{ `and this` }</div>
}
变量
package main
templ greet(prefix string, p Person) {
<div>{ prefix } { p.Name }{ exclamation }</div>
}
// ---------------
type Person struct {
Name string
}
func main() {
p := Person{ Name: "John" }
component := greet("Hello", p)
component.Render(context.Background(), os.Stdout)
}
function 即函数,它可以返回string或string,error
package main
import "strings"
import "strconv"
func getString() (string, error) {
return "DEF", nil
}
templ component() {
<div>{ strings.ToUpper("abc") }</div>
<div>{ getString() }</div>
}
转义
package main
templ component() {
<div>{ `</div><script>alert('hello!')</script><div>` }</div>
}
将输出
<div></div><script>alert('hello!')</script><div></div>
流程控制
if/else
switch
for loops
templ login(isLoggedIn bool) {
if isLoggedIn {
<div>Welcome back!</div>
} else {
<input name="login" type="button" value="Log in"/>
}
}
templ userTypeDisplay(userType string) {
switch userType {
case "test":
<span>{ "Test user" }</span>
case "admin":
<span>{ "Admin user" }</span>
default:
<span>{ "Unknown user" }</span>
}
}
templ nameList(items []Item) {
<ul>
for _, item := range items {
<li>{ item.Name }</li>
}
</ul>
}
模板组成
templ showAll() {
@left()
@middle()
@right()
}
templ left() {
<div>Left</div>
}
templ middle() {
<div>Middle</div>
}
templ right() {
<div>Right</div>
}
子组件
templ showAll() {
@wrapChildren() {
<div>Inserted from the top</div>
}
}
templ wrapChildren() {
<div id="wrapper">
{ children... }
</div>
}
输出为:
<div id="wrapper">
<div>
Inserted from the top
</div>
</div>
有点像点内容直接传递到组件中,代替 { children… } 的位置
组件作为参数
package main
templ heading() {
<h1>Heading</h1>
}
templ layout(contents templ.Component) {
<div id="heading">
@heading()
</div>
<div id="contents">
@contents
</div>
}
templ paragraph(contents string) {
<p>{ contents }</p>
}
func main() {
c := paragraph("Dynamic contents")
layout(c).Render(context.Background(), os.Stdout)
}
输出:
<div id="heading">
<h1>Heading</h1>
</div>
<div id="contents">
<p>Dynamic contents</p>
</div>
这里有点不好理解。
PS:运行它时,被火绒报病毒….
看起来 layout 是框架定义,必然会首先运行。在@contents时才会引用paragraph组件到当前位置。
导入导出组件
package components
templ Hello() {
<div>Hello</div>
}
引用,同golang类似:
package main
import ".../components"
templ Home() {
@components.Hello()
}
CSS
package main
css red() {
background-color: #ff0000;
}
templ button(text string, isPrimary bool) {
<button class={ "button", templ.KV("is-primary", isPrimary), templ.KV(red(), isPrimary) }>{ text }</button>
}
button("Click me", true).Render(context.Background(), os.Stdout)
输出:
<style type="text/css">.red_1cbd{background-color:#ff0000;}</style><button class="button is-primary red_1cbd">ClickMe</button>
即templ.KV为一个键/值对,当后者为true时,则输出前者。
当调用button(“Click me”, false).Render(context.Background(), os.Stdout)时,输出为: Click me 。即它自动的识别并取消无用的css内容。
templ.KV(“hover:red”, true) 应该是类似css的 .btn:hover { color: white; border: 0; }
代码里也支持直接定义css
templ page() {
<style type="text/css">
p {
font-family: sans-serif;
}
.button {
background-color: black;
foreground-color: white;
}
</style>
<p>
Paragraph contents.
</p>
}
CSS组件
除了之前见到的html组件,还支持CSS组件
package main
var red = "#ff0000"
var blue = "#0000ff"
css primaryClassName() {
background-color: #ffffff;
color: { red };
}
css className() {
background-color: #ffffff;
color: { blue };
}
templ button(text string, isPrimary bool) {
<button class={ "button", className(), templ.KV(primaryClassName(), isPrimary) }>{ text }</button>
}
CSS组件参数
package main
css loading(percent int) {
width: { fmt.Sprintf("%d%%", percent) };
}
templ index() {
<div class={ loading(50) }></div>
<div class={ loading(100) }></div>
}
这样就更灵活了,减少了一些不必要的定义。
CSS清理
为了防止CSS注入攻击,它会自动清理动态CSS。使用SafeCSSProperty来标记安全。
css windVaneRotation(degrees float64) {
transform: { templ.SafeCSSProperty(fmt.Sprintf("rotate(%ddeg)", int(math.Round(degrees)))) };
}
templ Rotate(degrees float64) {
<div class={ windVaneRotation(degrees) }>Rotate</div>
}
CSS中间件
要提供全局样式表,可以使用CSS中间件,并在应用程序启动注册templ类。
中间件将 HTTP 路由添加到 Web 服务器(默认为 /styles/templ.css)。
在 HTML 中添加 a 以包含生成的 CSS 类名称
要阻止将 className CSS 类添加到输出中,可以使用 HTTP 中间件。
c1 := className()
handler := NewCSSMiddleware(httpRoutes, c1)
http.ListenAndServe(":8000", handler)
JavaScript
templ body() {
<script type="text/javascript">
function handleClick(event) {
alert(event + ' clicked');
}
</script>
<button onclick="handleClick(this)">Click me</button>
}
要确保 templ 组件中的 标记在每个 HTTP 响应中仅呈现一次,使用 templ.OnceHandle
package main
import "net/http"
var helloHandle = templ.NewOnceHandle()
templ hello(label, name string) {
// This script is only rendered once per HTTP request.
@helloHandle.Once() {
<script type="text/javascript">
function hello(name) {
alert('Hello, ' + name + '!');
}
</script>
}
<div>
<input type="button" value={ label } data-name={ name }/>
<script type="text/javascript">
// To prevent the variables from leaking into the global scope,
// this script is wrapped in an IIFE (Immediately Invoked Function Expression).
(() => {
let scriptElement = document.currentScript;
let parent = scriptElement.closest('div');
let nearestButtonWithName = parent.querySelector('input[data-name]');
nearestButtonWithName.addEventListener('click', function() {
let name = nearestButtonWithName.getAttribute('data-name');
hello(name);
})
})()
</script>
</div>
}
templ page() {
@hello("Hello User", "user")
@hello("Hello World", "world")
}
func main() {
http.Handle("/", templ.Handler(page()))
http.ListenAndServe("127.0.0.1:8080", nil)
}
另一个示例,这两个都有点不太明白,留待以后
var helloHandle = templ.NewOnceHandle()
var surrealHandle = templ.NewOnceHandle()
templ hello(label, name string) {
@helloHandle.Once() {
<script type="text/javascript">
function hello(name) {
alert('Hello, ' + name + '!');
}
</script>
}
@surrealHandle.Once() {
<script src="https://cdn.jsdelivr.net/gh/gnat/surreal@3b4572dd0938ce975225ee598a1e7381cb64ffd8/surreal.js"></script>
}
<div>
<input type="button" value={ label } data-name={ name }/>
<script type="text/javascript">
// me("-") returns the previous sibling element.
me("-").addEventListener('click', function() {
let name = this.getAttribute('data-name');
hello(name);
})
</script>
</div>
}
将服务端数据传递给脚本
templ body(data any) {
<button id="alerter" alert-data={ templ.JSONString(data) }>Show alert</button>
}
可以调用
component := body(123)
component.Render(context.Background(), os.Stdout)
输出
<button id="alerter" alert-data="123">Show alert</button>
若调用
component := body("123")
component.Render(context.Background(), os.Stdout)
则输出,即会转义字符
<button id="alerter" alert-data=""123"">Show alert</button>
这样可以在客户端JavaScript访问该属性中的数据
const button = document.getElementById('alerter');
const data = JSON.parse(button.getAttribute('alert-data'));
这其实是常规JavaScript访问,只是在模板中定义了附加数据
另一个示例:
templ body(data any) {
@templ.JSONScript("id", data)
}
调用body(123)输出例如:123
在客户端JavaScript访问数据:
const data = JSON.parse(document.getElementById('id').textContent);
脚本模板
这里示例也讲了如何把服务端的数据传给前端
package main
script graph(data []TimeValue) {
const chart = LightweightCharts.createChart(document.body, { width: 400, height: 300 });
const lineSeries = chart.addLineSeries();
lineSeries.setData(data);
}
templ page(data []TimeValue) {
<html>
<head>
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
</head>
<body onload={ graph(data) }></body>
</html>
}
主程序建立服务器
package main
import (
"fmt"
"log"
"net/http"
)
type TimeValue struct {
Time string `json:"time"`
Value float64 `json:"value"`
}
func main() {
mux := http.NewServeMux()
// Handle template.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := []TimeValue{
{Time: "2019-04-11", Value: 80.01},
{Time: "2019-04-12", Value: 96.63},
{Time: "2019-04-13", Value: 76.64},
{Time: "2019-04-14", Value: 81.89},
{Time: "2019-04-15", Value: 74.43},
{Time: "2019-04-16", Value: 80.01},
{Time: "2019-04-17", Value: 96.63},
{Time: "2019-04-18", Value: 76.64},
{Time: "2019-04-19", Value: 81.89},
{Time: "2019-04-20", Value: 74.43},
}
page(data).Render(r.Context(), w)
})
// Start the server.
fmt.Println("listening on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Printf("error listening: %v", err)
}
}
这里生成了一个折线图
我也试过随机生成数据
var data []TimeValue
now := time.Now()
for i := 0; i < 120; i++ {
date := now.AddDate(0, 0, +i).Format("2006-01-02")
value := math.Round(50 + rand.Float64()*(100-50)) // 随机生成50到100之间的值
data = append(data, TimeValue{Time: date, Value: value})
}
在组件中引用script组件
package main
import "fmt"
script printToConsole(content string) {
console.log(content)
}
templ page(content string) {
<html>
<body>
@printToConsole(content)
@printToConsole(fmt.Sprintf("Again: %s", content))
</body>
</html>
}
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Format the current time and pass it into our template
page(time.Now().String()).Render(r.Context(), w)
})
fmt.Println("listening on :8080")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Printf("error listening: %v", err)
}
JSExpression 类型用于将任意 JavaScript 表达式传递给 templ 脚本模板。
一个常见的用例是将事件或 this 对象传递给事件处理程序。
package main
script showButtonWasClicked(event templ.JSExpression) {
const originalButtonText = event.target.innerText
event.target.innerText = "I was Clicked!"
setTimeout(() => event.target.innerText = originalButtonText, 2000)
}
templ page() {
<html>
<body>
<button type="button" onclick={ showButtonWasClicked(templ.JSExpression("event")) }>Click Me</button>
</body>
</html>
}
实现了一个Button,当点击后显示新内容在Button上,2秒后恢复原标题
注释也是用
<!-- Single line -->
<!--
Single or multiline.
-->
使用上下文
在 templ 组件中,使用隐式 ctx 变量来访问上下文。
templ themeName() {
<div>{ ctx.Value(themeContextKey).(string) }</div>
}
type contextKey string
var themeContextKey contextKey = "theme"
ctx := context.WithValue(context.Background(), themeContextKey, "test")
themeName().Render(ctx, w)
作一些安全处理,例如判断是否存在。这也便于整洁化模板。
func GetTheme(ctx context.Context) string {
if theme, ok := ctx.Value(themeContextKey).(string); ok {
return theme
}
return ""
}
将上下文与中间件结合
type contextKey string
var contextClass = contextKey("class")
func Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request ) {
ctx := context.WithValue(r.Context(), contextClass, "red")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
templ Page() {
@Show()
}
templ Show() {
<div class={ ctx.Value(contextClass) }>Display</div>
}
func main() {
h := templ.Handler(Page())
withMiddleware := Middleware(h)
http.Handle("/", withMiddleware)
http.ListenAndServe(":8080", h)
}
与html/template一起使用
Templ 组件可以与 Go 标准库 html/template 包一起使用。
package testgotemplates
import "html/template"
var goTemplate = template.Must(template.New("example").Parse("<div>{{ . }}</div>"))
templ Example() {
<!DOCTYPE html>
<html>
<body>
@templ.FromGoHTML(goTemplate, "Hello, World!")
</body>
</html>
}
package testgotemplates
import "html/template"
var example = template.Must(template.New("example").Parse(`<!DOCTYPE html>
<html>
<body>
{{ . }}
</body>
</html>
`))
templ greeting() {
<div>Hello, World!</div>
}
直接使用HTML
@templ.Raw可以直接包含认为安全的HTML
templ Example() {
<!DOCTYPE html>
<html>
<body>
@templ.Raw("<div>Hello, World!</div>")
</body>
</html>
}
*OnceHandler.Once()
*OnceHandler.Once() 方法确保内容在传递给组件的 Render 方法的每个不同上下文中仅呈现一次,即使组件被多次呈现
package deps
var jqueryHandle = templ.NewOnceHandle()
templ JQuery() {
@jqueryHandle.Once() {
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
}
}
这里定义了deps包和JQuery组件,即使JQuery组件调用多次,也只会调用内容一次,即加载jquery。
package main
import "deps"
templ page() {
<html>
<head>
@deps.JQuery()
</head>
<body>
<h1>Hello, World!</h1>
@button()
</body>
</html>
}
templ button() {
@deps.JQuery()
<button>Click me</button>
}
纯代码组件
package main
import (
"context"
"io"
"os"
"github.com/a-h/templ"
)
func button(text string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<button>"+text+"</button>")
return err
})
}
func main() {
button("Click me").Render(context.Background(), os.Stdout)
}
方法组件
package main
import "os"
type Data struct {
message string
}
templ (d Data) Method() {
<div>{ d.message }</div>
}
func main() {
d := Data{
message: "You can implement methods on a type.",
}
d.Method().Render(context.Background(), os.Stdout)
}
package main
import "os"
type Data struct {
message string
}
templ (d Data) Method() {
<div>{ d.message }</div>
}
templ Message() {
<div>
@Data{
message: "You can implement methods on a type.",
}.Method()
</div>
}
func main() {
Message().Render(context.Background(), os.Stdout)
}
使用 templ 创建 HTTP 服务器
也就是直接路由到模板函数
package main
templ hello() {
<div>Hello</div>
}
package main
import (
"net/http"
"github.com/a-h/templ"
)
func main() {
http.Handle("/", templ.Handler(hello()))
http.ListenAndServe(":8080", nil)
}
用户会话状态
这是与另一个代码相关联的,这里主要看session的使用。
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/alexedwards/scs/v2"
)
type GlobalState struct {
Count int
}
var global GlobalState
var sessionManager *scs.SessionManager
func getHandler(w http.ResponseWriter, r *http.Request) {
userCount := sessionManager.GetInt(r.Context(), "count")
component := page(global.Count, userCount)
component.Render(r.Context(), w)
}
func postHandler(w http.ResponseWriter, r *http.Request) {
// Update state.
r.ParseForm()
// Check to see if the global button was pressed.
if r.Form.Has("global") {
global.Count++
}
if r.Form.Has("user") {
currentCount := sessionManager.GetInt(r.Context(), "count")
sessionManager.Put(r.Context(), "count", currentCount+1)
}
// Display the form.
getHandler(w, r)
}
func main() {
// Initialize the session.
sessionManager = scs.New()
sessionManager.Lifetime = 24 * time.Hour
mux := http.NewServeMux()
// Handle POST and GET requests.
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
postHandler(w, r)
return
}
getHandler(w, r)
})
// Add the middleware.
muxWithSessionMiddleware := sessionManager.LoadAndSave(mux)
// Start the server.
fmt.Println("listening on http://localhost:8000")
if err := http.ListenAndServe("localhost:8000", muxWithSessionMiddleware); err != nil {
log.Printf("error listening: %v", err)
}
}
HTMX
可用于有选择地替换网页中的内容。AJAX?
引用:
要在不进行完整回发的情况下更新页面上的计数,必须将 hx-post=“/” 和 hx-select=“#countsForm” 属性添加到 元素中,并添加 id 属性以唯一标识该元素。
以下只是部份代码,具体使用还得看HTMX相关
templ counts(global, session int) {
<form id="countsForm" action="/" method="POST" hx-post="/" hx-select="#countsForm" hx-swap="outerHTML">
<div class="columns">
<div class={ "column", "has-text-centered", "is-primary", border }>
<h1 class="title is-size-1 has-text-centered">{ strconv.Itoa(global) }</h1>
<p class="subtitle has-text-centered">Global</p>
<div><button class="button is-primary" type="submit" name="global" value="global">+1</button></div>
</div>
<div class={ "column", "has-text-centered", border }>
<h1 class="title is-size-1 has-text-centered">{ strconv.Itoa(session) }</h1>
<p class="subtitle has-text-centered">Session</p>
<div><button class="button is-secondary" type="submit" name="session" value="session">+1</button></div>
</div>
</div>
</form>
}
基本看完一遍,了解了七七八八。还是需要在实例中学习。自带的示例还是比较丰富。
比如在实例中就看到了templ工具这样的用法:templ generate –watch –proxy=“http://localhost:8080” –cmd=“go run .”
于是又回去看了看templ命令行
-path
-f <file> 可以选择为单个文件生成代码
-sourceMapVisualisations 设置为true以生成html文件
-include-version 设置为false,跳过生成的代码中包含的templ版本(没发现有作用)
-include-timestamp 设置为true,在生成的代码中包含当前时间。
-watch 设置为true,监视更改的路径并重新生成代码。
-cmd 生成代码后运行的命令
-proxy 生成代码并执行命令后,将 URL 设置为代理。
-proxyport 代理将监听的端口
-proxybind 代理将监听的地址
-w 生成代码时要使用的工人数量。
-lazy 只有源 .templ 文件更新时,才能生成 .go 文件。
-pprof 运行 pprof 服务器的端口。
-keep-orphaned-files 保留已生成的模板文件
-v 将日志冗余级别设置为 “调试”。
-log-level 设置日志冗余级别
-help
虽然机翻,但基本是明白了。为什么用个proxy,不得而知,反正就是启动了一个服务,可以通过另一端口访问main.go中的服务端口。