关于web开发中动态加载模板的问题

问题简化与描述: 模板内需要这样一个container,首先container内包含多少个div,每个div的宽度是由配置文件读取的,这个我还能做,但是接下来,每个div的内容模板由配置文件指定,这个我就蒙了,难道我要像下面一样做么……

{{range .Config}}

<div style="width:{{.Width}}%">

{{template .TemplateName .}}

</div>

{{end}}

但是试过后明显不可能啊,提示Golang在识别template后对之后的参数不会当做pipeline啊! 后来想过用两次模板生成,第一次根据模板和配置文件生成新的模板用os包保存,然后再载入新的模板与数据动态生成网页= =但是实际写的时候超级麻烦超级不爽,各种转义各种很恶心(自我感觉)的格式…… 所以求问各位,如何优雅的(用一个朋友的口头禅)实现模板的动态加载呢?

共 19 个回复


seefan

显示就是你现在的用法,前面加一个变量定义

{{with $x:=.}}就是把数据上下文给$x

# 0

Clounea

咦,可以么,我试试

# 1

Clounea

好像不行啊…… 如果说的是因为range而导致上下文不对,我不带range而是

{{template .NameA .}}
{{template .NameB .}}

也不行…… 如果是说template后用定义的$x……就会有错误提示unexpected "$x" in template invocation

# 2

Clounea

刚刚看了一个其它问题的答案,难道是因为pipeline都是interface{}类型不是string然后template就不行了?

# 3

Clounea

找到类似的旧帖子了……看来只能自己写include方法了==

# 4

seefan

    {{with $x:=.}}

    {{range .Config}}

<div style="width:{{.Width}}%">

{{template .TemplateName $x}}

</div>

{{end}}
    {{end}}
# 6

seefan

带$的变量在循环内可以访问

# 7

Clounea

不是不能访问变量的问题…………看我题目啦,是

{{template .TemplateName}}

不能用的问题,没办法这么调用模板,这才是我要问的。不是什么range的问题(崩溃脸)

# 8

seefan

试了一下,确认不行,估计是模板解析的问题。 可以用template.FuncMap,写个自定义函数解决

# 9

Clounea

查了一下template的处理的源代码,结果简单粗暴(没人性QAQ)到吐血,先贴上来

    func (t *Tree) templateControl() Node {
    var name string
    token := t.nextNonSpace()
    switch token.typ {
    case itemString, itemRawString:
        s, err := strconv.Unquote(token.val)
        if err != nil {
            t.error(err)
        }
        name = s
    default:
        t.unexpected(token, "template invocation")
    }
    var pipe *PipeNode
    if t.nextNonSpace().typ != itemRightDelim {
        t.backup()
        // Do not pop variables; they persist until "end".
        pipe = t.pipeline("template")
    }
    return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
}

可以看出来了,读取到template后对于后面的读到空格前的token判断是不是所需要的字符串……对于不是要求的字符串的一致报错处理,是不会管对方是不是代表字符串的标识符还是函数名还是括号的……(感觉和整个template都脱节了)= = 求帮助吖,直接改源代码不怎么敢下手,用模板函数好像没什么用,代码里已经表示不会识别了= =究竟该怎么做呢? 求思路求帮助!

# 10

seefan

参考这个

//生成一个文件

func makeFile(p *model.BuildContext, t *template.Template, src, output string) error {
    //已经存在的文件,不在重复处理
    if !goutils.ExistsFile(src) {
        return fmt.Errorf("输入的文件%s不存在", src)
    }
    //解析文件链接为真实路径
    input, err := filepath.EvalSymlinks(src)
    if err != nil {
        return goerr.NewError(err, "解析来源文件名%s出错", src)
    }

    //log.Debug("make file ", input, output)
    //首先判断输出目录是否存在该目录,不存在就创建
    i := strings.LastIndex(output, "/")
    if i != -1 { //有目录
        path := utils.SubString(output, 0, i)
        //log.Debug("path is ", path)
        if !goutils.ExistsFile(path) {
            os.MkdirAll(path, 0766) //所有人读写
        }
    }
    dstFile, err := os.Create(output)
    if err != nil {
        return goerr.NewError(err, "创建输出模板文件%s 时出错", dstFile)
    }
    buf, err := ioutil.ReadFile(input)
    if err != nil {
        return goerr.NewError(err, "读取输入模板文件%s 时出错", input)
    }
    tm := t.New(input)
    // add our funcmaps
    tm.Funcs(templates.UtilFuncs)
    // Bomb out if parse fails. We don't want any silent server starts.
    if _, err := tm.Parse(string(buf)); err != nil {
        return goerr.NewError(err, "解析模板%s 出错")
    }
    if err := t.ExecuteTemplate(dstFile, input, p); err != nil {
        return goerr.NewError(err, "生成模板%s 出错", input)
    }
    return nil
}
# 12

Clounea

hhhh楼上就是两层处理方式啊,第一次执行配置文件和模板,第二次再执行数据和生成模板及确定的子模板。我再看看有没有可能像之前发类似问题的仁兄一样通过写个include函数解决吧……实在找不到的话就这么做好了www

# 13

Clounea

pongo2...还是算了……模板这块还不怎么想用包,谢谢推荐^-^

# 14

Clounea

突然觉得我好傻哈哈哈,可以用bytes.buffer来接受执行动态的子模板的输出,然后返回string,一个模板函数就可以了^_^

# 15

Clounea

把最后探索的include方法贴出来好了^_^坑已填平

一个用来传参的struct

   type ChildTemplate struct {
    Name         string
    Filenames    []string
    ChildContext interface{}
}

模板函数

func TemplateIncluder(args ...interface{}) template.HTML {
    if len(args) != 3 {
        log.Fatal("Wrong nums of args in TemplateIncluder,supposed to be 3")
        return ""
    }
    //get template name
    tname, ok := args[0].(string)
    if !ok {
        log.Fatal(("Wrong type of args[0]:template name in TemplateIncluder,supposed to be string"))
        return ""
    }
    //get template filenames []string
    tfnames, ok := args[1].([]string)
    if !ok {
        log.Fatal(("Wrong type of args[1]:template file names in TemplateIncluder,supposed to be []string"))
        return ""
    }
    //create template
    t := template.New(tname)
    for _, tfname := range tfnames {
        t = template.Must(t.ParseFiles(tfname))
    }

    //create buffer to contain the execute result
    buff := new(bytes.Buffer)
    err := t.ExecuteTemplate(buff, tname, args[2])
    if err != nil {
        log.Fatal("Something wrong in execute template in TemplateIncluder")
        return ""
    }

    return template.HTML(buff.String())

}

用来测试的handler函数

  func GetTestHandler(w http.ResponseWriter, r *http.Request) {
    t := template.New("testpage")
    t = t.Funcs(template.FuncMap{"include": TemplateIncluder})
    t, err := t.ParseFiles("static/templates/test.tmpl")
    if err != nil {
        log.Fatal("Wrong in parse files in test handler")
        panic(err)
    }

    context := [2]ChildTemplate{ChildTemplate{"child1", []string{"static/templates/child1.tmpl"}, nil},
        ChildTemplate{"child2", []string{"static/templates/child2.tmpl"}, nil}}

    err = t.ExecuteTemplate(w, "test", context)
    if err != nil {
        panic(err)
    }
}

对应的test.tmpl

{{define "test"}}
{{range .}}
{{include .Name .Filenames .ChildContext}}
{{end}}
{{end}}

需要注意的几点: 1.t.Func一定要在Parse前!因为Parse的过程就是根据注册和内置的各种处理方式把字符串翻译成动作和文本节点。Parse后再t.Func是会panic的 2.模板处理函数的返回值为template.HTML而不是string可以避免转义

# 16

Clounea

结贴撒花!enter image description here

# 17