调用具有多个管道参数的模板

huangapple go评论76阅读模式
英文:

Calling a template with several pipeline parameters

问题

在Go模板中,有时候将正确的数据传递给正确的模板感觉很别扭。使用管道参数调用模板看起来就像只有一个参数的函数调用。

假设我有一个关于Gophers的网站。它有一个主页主模板和一个用于打印Gophers列表的实用模板。

http://play.golang.org/p/Jivy_WPh16

输出:

*The great GopherBook*    (以Dewey的身份登录)
	
	[最受欢迎]	
		>> Huey
		>> Dewey
		>> Louie
		
	[最活跃]	
		>> Huey
		>> Louie
		
	[最新]	
		>> Louie

现在我想在子模板中添加一些上下文:在列表中以不同的方式格式化名称"Dewey",因为它是当前登录用户的名称。但是我不能直接传递名称,因为只有一个可能的"dot"参数管道!我该怎么办?

  • 显然,我可以将子模板代码复制粘贴到主模板中(但我不想这样做,因为这样就失去了使用子模板的好处)。
  • 或者我可以使用一些全局变量和访问器(但我也不想这样做)。
  • 或者我可以为每个模板参数列表创建一个新的特定结构类型(不太好)。
英文:

In a Go template, sometimes the way to pass the right data to the right template feels awkward to me. Calling a template with a pipeline parameter looks like calling a function with only one parameter.

Let's say I have a site for Gophers about Gophers. It has a home page main template, and a utility template to print a list of Gophers.

http://play.golang.org/p/Jivy_WPh16

Output :

*The great GopherBook*    (logged in as Dewey)
	
	[Most popular]	
		>> Huey
		>> Dewey
		>> Louie
		
	[Most active]	
		>> Huey
		>> Louie
		
	[Most recent]	
		>> Louie

Now I want to add a bit of context in the subtemplate : format the name "Dewey" differently inside the list because it's the name of the currently logged user. But I can't pass the name directly because there is only one possible "dot" argument pipeline! What can I do?

  • Obviously I can copy-paste the subtemplate code into the main template (I don't want to because it drops all the interest of having a subtemplate).
  • Or I can juggle with some kind of global variables with accessors (I don't want to either).
  • Or I can create a new specific struct type for each template parameter list (not great).

答案1

得分: 74

你可以在模板中注册一个名为"dict"的函数,用于在模板调用中传递多个值。调用本身将如下所示:

{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}

下面是包含"dict"辅助函数的代码,包括将其注册为模板函数:

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "dict": func(values ...interface{}) (map[string]interface{}, error) {
        if len(values)%2 != 0 {
            return nil, errors.New("invalid dict call")
        }
        dict := make(map[string]interface{}, len(values)/2)
        for i := 0; i < len(values); i+=2 {
            key, ok := values[i].(string)
            if !ok {
                return nil, errors.New("dict keys must be strings")
            }
            dict[key] = values[i+1]
        }
        return dict, nil
    },
}).ParseGlob("templates/*.html")
英文:

You could register a "dict" function in your templates that you can use to pass multiple values to a template call. The call itself would then look like that:

{{template &quot;userlist&quot; dict &quot;Users&quot; .MostPopular &quot;Current&quot; .CurrentUser}}

The code for the little "dict" helper, including registering it as a template func is here:

var tmpl = template.Must(template.New(&quot;&quot;).Funcs(template.FuncMap{
	&quot;dict&quot;: func(values ...interface{}) (map[string]interface{}, error) {
		if len(values)%2 != 0 {
			return nil, errors.New(&quot;invalid dict call&quot;)
		}
		dict := make(map[string]interface{}, len(values)/2)
		for i := 0; i &lt; len(values); i+=2 {
			key, ok := values[i].(string)
			if !ok {
				return nil, errors.New(&quot;dict keys must be strings&quot;)
			}
			dict[key] = values[i+1]
		}
		return dict, nil
	},
}).ParseGlob(&quot;templates/*.html&quot;)

答案2

得分: 5

你可以在模板中定义函数,并将这些函数作为闭包定义在数据上,像这样:

template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User); }}

然后,在模板中可以简单地调用这个函数:

{{define "sub"}}
    {{range $y := .}}>> {{if isUser $y}}!!{{$y}}!!{{else}}{{$y}}{{end}}
    {{end}}
{{end}}

在 playground 上的这个更新版本会在当前用户周围输出漂亮的 !!

*The great GopherBook*    (logged in as Dewey)

[Most popular]    

>> Huey
>> !!Dewey!!
>> Louie



[Most active]    

>> Huey
>> Louie



[Most recent]    

>> Louie

编辑

由于在调用 Funcs 时可以覆盖函数,你实际上可以在编译模板时预先填充模板函数,并使用实际的闭包更新它们,像这样:

var defaultfuncs = map[string]interface{}{
    "isUser": func(g Gopher) bool { return false; },
}

func init() {
    // 默认值返回 `false`(只需要正确的类型)
    t = template.New("home").Funcs(defaultfuncs)
    t, _ = t.Parse(subtmpl)
    t, _ = t.Parse(hometmpl)
}

func main() {
    // 在实际服务时,我们更新闭包:
    data := &HomeData{
        User:    "Dewey",
        Popular: []Gopher{"Huey", "Dewey", "Louie"},
        Active:  []Gopher{"Huey", "Louie"},
        Recent:  []Gopher{"Louie"},
    }
    t.Funcs(template.FuncMap{"isUser": func(g Gopher) bool { return string(g) == string(data.User); }})
    t.ExecuteTemplate(os.Stdout, "home", data)
}

虽然我不确定当多个 goroutine 尝试访问同一个模板时会发生什么...

这个工作示例

英文:

You can define functions in your template, and have these functions being closures defined on your data like this:

template.FuncMap{&quot;isUser&quot;: func(g Gopher) bool { return string(g) == string(data.User);},}

Then, you can simply call this function in your template:

{{define &quot;sub&quot;}}

    {{range $y := .}}&gt;&gt; {{if isUser $y}}!!{{$y}}!!{{else}}{{$y}}{{end}}
    {{end}}
{{end}}

This updated version on the playground outputs pretty !! around the current user:

*The great GopherBook*    (logged in as Dewey)

[Most popular]	

&gt;&gt; Huey
&gt;&gt; !!Dewey!!
&gt;&gt; Louie



[Most active]	

&gt;&gt; Huey
&gt;&gt; Louie



[Most recent]	

&gt;&gt; Louie

EDIT

Since you can override functions when calling Funcs, you can actually pre-populate the template functions when compiling your template, and update them with your actual closure like this:

var defaultfuncs = map[string]interface{} {
    &quot;isUser&quot;: func(g Gopher) bool { return false;},
}

func init() {
    // Default value returns `false` (only need the correct type)
    t = template.New(&quot;home&quot;).Funcs(defaultfuncs)
    t, _ = t.Parse(subtmpl)
    t, _ = t.Parse(hometmpl)
}

func main() {
    // When actually serving, we update the closure:
    data := &amp;HomeData{
        User:    &quot;Dewey&quot;,
        Popular: []Gopher{&quot;Huey&quot;, &quot;Dewey&quot;, &quot;Louie&quot;},
        Active:  []Gopher{&quot;Huey&quot;, &quot;Louie&quot;},
        Recent:  []Gopher{&quot;Louie&quot;},
    }
    t.Funcs(template.FuncMap{&quot;isUser&quot;: func(g Gopher) bool { return string(g) == string(data.User); },})
    t.ExecuteTemplate(os.Stdout, &quot;home&quot;, data)
}

Although I am not sure how that plays when several goroutines try to access the same template...

The working example

答案3

得分: 3

最直接的方法(尽管不是最优雅的方法)-特别适合相对新手的人-是在代码中使用匿名结构体。这个方法早在2012年Andrew Gerrand的演讲《关于Go你可能不知道的10件事》中就有记录和建议。

https://talks.golang.org/2012/10things.slide#1

下面是一个简单的示例:

// 定义模板
const someTemplate = insert into {{.Schema}}.{{.Table}} (field1, field2) values {{ range .Rows }} ({{.Field1}}, {{.Field2}}), {{end}};

// 封装你的值并执行模板
data := struct {
Schema string
Table string
Rows []MyCustomType
}{
schema,
table,
someListOfMyCustomType,
}

t, err := template.New("new_tmpl").Parse(someTemplate)
if err != nil {
panic(err)
}

// 工作缓冲区
buf := &bytes.Buffer{}

err = t.Execute(buf, data)

请注意,这段代码不能直接运行,因为模板需要进行一些小的清理(即去掉循环的最后一行的逗号),但这是相当简单的。在匿名结构体中封装模板参数可能看起来有些繁琐和冗长,但它的好处是在模板执行时明确地声明了将要使用的内容。绝对比为每个新模板定义一个命名结构体来得简单。

英文:

The most straightforward method (albeit not the most elegant) - especially for someone relatively new to go - is to use anon structs "on the fly". This was documented/suggested as far back as Andrew Gerrand's excellent 2012 presentation "10 things you probably don't know about go"

https://talks.golang.org/2012/10things.slide#1

Trivial example below :

// define the template

const someTemplate = `insert into {{.Schema}}.{{.Table}} (field1, field2)
values
   {{ range .Rows }}
       ({{.Field1}}, {{.Field2}}),
   {{end}};`

// wrap your values and execute the template

	data := struct {
		Schema string
		Table string
		Rows   []MyCustomType
	}{
		schema,
		table,
		someListOfMyCustomType,
	}

	t, err := template.New(&quot;new_tmpl&quot;).Parse(someTemplate)
	if err != nil {
		panic(err)
	}

	// working buffer
	buf := &amp;bytes.Buffer{}

	err = t.Execute(buf, data)

Note that this won't technically run as-is, since the template needs some minor cleaning-up (namely getting rid of the comma on the last line of the range loop), but that's fairly trivial. Wrapping the params for your template in an anonymous struct may seem tedious and verbose, but it has the added benefit of explicitly stating exactly what will be used once the template executes. Definitely less tedious than having to define a named struct for every new template you write.

答案4

得分: 2

根据你的目标,https://github.com/josharian/tstruct(博客文章)可能会有所帮助。你可以定义一个名为UserList的Go结构体,使用tstruct自动生成FuncMap助手,然后编写类似以下的代码:

{{ template "userlist" UserList (Users .MostPopular) (Current .CurrentUser) }}
英文:

Depending on your goals, https://github.com/josharian/tstruct (blog post) might be helpful. You would define a Go struct called UserList, use tstruct to autogenerate FuncMap helpers for it, and then write something like:

{{ template &quot;userlist&quot; UserList (Users .MostPopular) (Current .CurrentUser) }}

答案5

得分: 1

我为这个问题实现了一个支持类似管道的参数传递和检查的库。

示例

{{define "foo"}}
    {{if $args := . | require "arg1" | require "arg2" "int" | args }}
        {{with .Origin }} // 原始点
            {{.Bar}}
            {{$args.arg1}}
        {{ end }}
    {{ end }}
{{ end }}

{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" 42 }}
{{ template "foo" . | arg "arg1" "Arg1" | arg "arg2" "42" }} // 将引发错误

Github 仓库

英文:

I implemented a library for this issue which supports pipe-like arguments passing&check.

Demo

{{define &quot;foo&quot;}}
    {{if $args := . | require &quot;arg1&quot; | require &quot;arg2&quot; &quot;int&quot; | args }}
        {{with .Origin }} // Original dot
            {{.Bar}}
            {{$args.arg1}}
        {{ end }}
    {{ end }}
{{ end }}

{{ template &quot;foo&quot; . | arg &quot;arg1&quot; &quot;Arg1&quot; | arg &quot;arg2&quot; 42 }}
{{ template &quot;foo&quot; . | arg &quot;arg1&quot; &quot;Arg1&quot; | arg &quot;arg2&quot; &quot;42&quot; }} // will raise an error

Github repo

答案6

得分: 1

基于@tux21b的代码,我改进了这个函数,使其可以在不指定索引的情况下使用(只是为了保持go将变量附加到模板的方式)。

现在你可以这样做:

{{template "userlist" dict "Users" .MostPopular "Current" .CurrentUser}}

或者

{{template "userlist" dict .MostPopular .CurrentUser}}

或者

{{template "userlist" dict .MostPopular "Current" .CurrentUser}}

但是,如果参数(.CurrentUser.name)不是一个数组,你肯定需要放一个索引,以便将这个值传递给模板:

{{template "userlist" dict .MostPopular "Name" .CurrentUser.name}}

请看我的代码:

var tmpl = template.Must(template.New("").Funcs(template.FuncMap{
    "dict": func(values ...interface{}) (map[string]interface{}, error) {
        if len(values) == 0 {
            return nil, errors.New("invalid dict call")
        }

        dict := make(map[string]interface{})

        for i := 0; i < len(values); i++ {
            key, isset := values[i].(string)
            if !isset {
                if reflect.TypeOf(values[i]).Kind() == reflect.Map {
                    m := values[i].(map[string]interface{})
                    for i, v := range m {
                        dict[i] = v
                    }
                } else {
                    return nil, errors.New("dict values must be maps")
                }
            } else {
                i++
                if i == len(values) {
                    return nil, errors.New("specify the key for non array values")
                }
                dict[key] = values[i]
            }

        }
        return dict, nil
    },
}).ParseGlob("templates/*.html"))

希望对你有帮助!

英文:

based on @tux21b

I have improved the function so it can be used even without specifying the indexes ( just to keep the way go attaches variables to the template)

So now you can do it like this:

{{template &quot;userlist&quot; dict &quot;Users&quot; .MostPopular &quot;Current&quot; .CurrentUser}}

or

{{template &quot;userlist&quot; dict .MostPopular .CurrentUser}}

or

{{template &quot;userlist&quot; dict .MostPopular &quot;Current&quot; .CurrentUser}}

but if the parameter (.CurrentUser.name) is not an array you definitely need to put an index in order to pass this value to the template

{{template &quot;userlist&quot; dict .MostPopular &quot;Name&quot; .CurrentUser.name}}

see my code:

var tmpl = template.Must(template.New(&quot;&quot;).Funcs(template.FuncMap{
    &quot;dict&quot;: func(values ...interface{}) (map[string]interface{}, error) {
        if len(values) == 0 {
            return nil, errors.New(&quot;invalid dict call&quot;)
	    }

	    dict := make(map[string]interface{})

	    for i := 0; i &lt; len(values); i ++ {
		    key, isset := values[i].(string)
		    if !isset {
			    if reflect.TypeOf(values[i]).Kind() == reflect.Map {
				    m := values[i].(map[string]interface{})
				    for i, v := range m {
					    dict[i] = v
				    }
			    }else{
				    return nil, errors.New(&quot;dict values must be maps&quot;)
			   }
		    }else{
			    i++
			    if i == len(values) {
				    return nil, errors.New(&quot;specify the key for non array values&quot;)
			    }
			    dict[key] = values[i]
		    }

	    }
	    return dict, nil
    },
}).ParseGlob(&quot;templates/*.html&quot;)

答案7

得分: 0

到目前为止,我找到的最好的方法(虽然我不太喜欢它)是使用一个“通用”的配对容器来进行复用和解复用参数:

http://play.golang.org/p/ri3wMAubPX

type PipelineDecorator struct {
    // 实际的流水线
    Data interface{}
    // 作为“第二个流水线”传递的一些辅助数据
    Deco interface{}
}

func decorate(data interface{}, deco interface{}) *PipelineDecorator {
    return &PipelineDecorator{
        Data: data,
        Deco: deco,
    }
}

我经常使用这个技巧来构建我的网站,我想知道是否存在一种更符合惯用方式的方法来实现相同的功能。

英文:

The best i've found so far (and I don't really like it) is muxing and demuxing parameters with a "generic" pair container :

http://play.golang.org/p/ri3wMAubPX

type PipelineDecorator struct {
	// The actual pipeline
	Data interface{}
	// Some helper data passed as &quot;second pipeline&quot;
	Deco interface{}
}

func decorate(data interface{}, deco interface{}) *PipelineDecorator {
	return &amp;PipelineDecorator{
		Data: data,
		Deco: deco,
	}
}

I use this trick a lot for building my website, and I wonder if there exist some more idiomatic way to achieve the same.

答案8

得分: 0

“...看起来像是只有一个参数的函数调用。”:

从某种意义上说,每个函数都接受一个参数 - 一个多值调用记录。对于模板来说也是一样的,这个“调用”记录可以是一个原始值,或者是一个多值的{map,struct,array,slice}。模板可以从“单一”的管道参数中选择要使用的{key,field,index},放在任何位置。

换句话说,在这种情况下,一个参数就足够了。

英文:

Ad "... looks like calling a function with only one parameter.":

In a sense, every function takes one paramater - a multivalued invocation record. With templates it's the same, that "invocation" record can be a primitive value, or a multivalued {map,struct,array,slice}. The template can select which {key,field,index} it'll use from the "single" pipeline parameter in whatever place.

IOW, one is enough in this case.

答案9

得分: 0

有时候,地图是解决这种情况的快速简便方法,正如其他答案中提到的那样。由于你经常使用Gophers(根据你的其他问题,你关心当前的Gopher是否是管理员),我认为它应该有自己的结构体:

type Gopher struct {
    Name      string
    IsCurrent bool
    IsAdmin   bool
}

这是你Playground代码的更新版本:http://play.golang.org/p/NAyZMn9Pep

显然,手动编写带有额外层级的示例结构体会有些繁琐,但实际上它们将由机器生成,因此很容易根据需要标记IsCurrentIsAdmin

英文:

Sometimes maps are a quick and easy solution to situations like this, as mentioned in a couple of the other answers. Since you're using Gophers a lot (and since, based on your other question, you care if the current Gopher is an admin), I think it deserves its own struct:

type Gopher struct {
    Name string
    IsCurrent bool
    IsAdmin bool
}

Here's an update to your Playground code: http://play.golang.org/p/NAyZMn9Pep

Obviously it gets a little cumbersome hand-coding the example structs with an extra level of depth, but since in practice they'll be machine-generated, it's straightforward to mark IsCurrent and IsAdmin as needed.

答案10

得分: 0

我处理这个问题的方式是对通用的流水线进行装饰:

type HomeData struct {
    User    Gopher
    Popular []Gopher
    Active  []Gopher
    Recent  []Gopher
}

通过创建一个上下文特定的流水线:

type HomeDataContext struct {
    *HomeData
    I interface{}
}

分配上下文特定的流水线非常廉价。通过复制指向它的指针,您可以访问可能很大的HomeData

t.ExecuteTemplate(os.Stdout, "home", &HomeDataContext{
    HomeData: data,
})

由于HomeData嵌入在HomeDataContext中,您的模板可以直接访问它(例如,您仍然可以使用.Popular而不是.HomeData.Popular)。此外,您现在可以访问一个自由格式的字段(.I)。

最后,我创建了一个Using函数用于HomeDataContext,如下所示:

func (ctx *HomeDataContext) Using(data interface{}) *HomeDataContext {
    c := *ctx // 创建一个副本,以便我们不会实际修改原始数据
    c.I = data
    return &c
}

这使我能够保持一个状态(HomeData),但将任意值传递给子模板。

请参阅 http://play.golang.org/p/8tJz2qYHbZ

英文:

The way I approach this is to decorate the general pipeline:

type HomeData struct {
    User    Gopher
    Popular []Gopher
    Active  []Gopher
    Recent  []Gopher
}

by creating a context-specific pipeline:

type HomeDataContext struct {
    *HomeData
    I interface{}
}

Allocating the context-specific pipeline is very cheap. You get access to the potentially large HomeData by copying the pointer to it:

t.ExecuteTemplate(os.Stdout, &quot;home&quot;, &amp;HomeDataContext{
    HomeData: data,
})

Since HomeData is embedded in HomeDataContext, your template will access it directly (e.g. you can still do .Popular and not .HomeData.Popular). Plus you now have access to a free-form field (.I).

Finally, I make a Using function for HomeDataContext like so.

func (ctx *HomeDataContext) Using(data interface{}) *HomeDataContext {
    c := *ctx // make a copy, so we don&#39;t actually alter the original
    c.I = data
    return &amp;c
}

This allows me to keep a state (HomeData) but pass an arbitrary value to the sub-template.

See http://play.golang.org/p/8tJz2qYHbZ.

huangapple
  • 本文由 发表于 2013年8月16日 22:51:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/18276173.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定