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

时间:2013-08-16 14:51:57

标签: templates go go-templates

在Go模板中,有时将正确数据传递给正确模板的方式对我来说感觉很尴尬。使用管道参数调用模板看起来就像调用只有一个参数的函数。

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

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

输出:

*The great GopherBook*    (logged in as Dewey)

    [Most popular]  
        >> Huey
        >> Dewey
        >> Louie

    [Most active]   
        >> Huey
        >> Louie

    [Most recent]   
        >> Louie

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

  • 显然我可以将子模板代码复制粘贴到主模板中(我不想这样做,因为它降低了对子模板的兴趣)。
  • 或者我可以使用访问器来处理某些全局变量(我也不想这样做)。
  • 或者我可以为每个模板参数列表创建一个新的特定结构类型(不太好)。

8 个答案:

答案 0 :(得分:47)

您可以在模板中注册“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")

答案 1 :(得分: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}}
操场上的

This updated version在当前用户周围输出非常!!

*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() {
    // Default value returns `false` (only need the correct type)
    t = template.New("home").Funcs(defaultfuncs)
    t, _ = t.Parse(subtmpl)
    t, _ = t.Parse(hometmpl)
}

func main() {
    // When actually serving, we update the closure:
    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)
}

虽然我不确定当几个goroutines试图访问同一个模板时它会如何播放......

The working example

答案 2 :(得分:2)

基于@ tux21b

我已经对函数进行了改进,因此即使不指定索引也可以使用它(只是为了让变量附加到模板)

所以现在你可以这样做:

{{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")

答案 3 :(得分:1)

最直接的方法(尽管不是最优雅的方法)(特别是对于相对较新的人)是“即时”使用anon结构。早在2012年安德鲁·格朗(Andrew Gerrand)出色的演讲“您可能不了解的10件事”中就对此进行了记录/建议。

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

下面的示例:

// 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("new_tmpl").Parse(someTemplate)
    if err != nil {
        panic(err)
    }

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

    err = t.Execute(buf, data)

请注意,这在技术上不会按原样运行,因为模板需要进行一些较小的清理(即摆脱范围循环最后一行的逗号),但这相当琐碎。将模板的参数包装在匿名结构中可能看起来很繁琐和冗长,但是它的另一个好处是可以明确说明模板执行后将使用的内容。绝对不必为您编写的每个新模板都定义一个命名结构而感到乏味。

答案 4 :(得分:0)

到目前为止我发现的最好(我并不喜欢它)是使用“通用”对容器对参数进行复用和解复用:

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

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

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

我使用这个技巧来构建我的网站很多,我想知道是否存在一些更惯用的方法来实现同样的目标。

答案 5 :(得分:0)

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

从某种意义上说,每个函数都需要一个参数 - 一个多值的调用记录。使用模板是相同的,“调用”记录可以是原始值,也可以是多值{map,struct,array,slice}。模板可以在任何地方选择“单个”管道参数中使用的{key,field,index}。

在这种情况下,一个就足够了。

答案 6 :(得分:0)

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

type Gopher struct {
    Name string
    IsCurrent bool
    IsAdmin bool
}

以下是您的Playground代码的更新:http://play.golang.org/p/NAyZMn9Pep

显然,对于具有额外深度的示例结构进行手工编码会有点繁琐,但由于实际上它们是机器生成的,因此将IsCurrentIsAdmin标记为是直截了当的需要的。

答案 7 :(得分: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 // make a copy, so we don't actually alter the original
    c.I = data
    return &c
}

这允许我保持状态(HomeData),但将任意值传递给子模板。

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