Afortunadamente eres conciente del modelo MVC (Modelo, Vista, Controlador), donde los modelos procesan datos, las vistas muestran los restulados y los controladores manejan las peticiones. Para las vistas, muchos lenguajes dinámicos generan dataos escribiendo código en archivos HTML estáticos. Por ejemplo JSP, los implementa insertando <%=...=%>
, PHP insertando <?php ... ?>
, etc.
La siguiente imagen muestra el mecanismo de las plantillas:
Figura 7.1 Mecanismo de Plantillas
La mayoría del contenido que las aplicaciones web responden a los clientes es estático, y el contenido dinámico usualmente es muy pequeño. Por ejemplo, si necesitas mostrar una lista de usuarios que han visitado la página, solo el nombre de usuario necesita ser dinámico. El estilo de la lista es el mismo. Como puedes ver, las plantillas son útiles para reusar contenido.
En Go, tenemos el paquete templata
que nos ayuda a manejar plantillas. Podemos usar funciones como Pare
, ParseFile
y Execute
para cargar las plantillas desde texto plano o archivos, luego evaluar las partes dinámicas, como se muestra en la figura 7.1
Ejemplo:
func handler(w http.ResponseWriter, r *http.Request) {
t := template.New("some template") // Create a template.
t, _ = t.ParseFiles("tmpl/welcome.html", nil) // Parse template file.
user := GetUser() // Get current user infomration.
t.Execute(w, user) // merge.
}
Como puedes ver, son muy fáciles de usar, cargar la información en plantillas de Go, como en cualquier otro lenguaje de programación.
Para conveniencia, vamos a usar las siguientes reglas en nuestros ejemplos:
- Usaremos
Parse
para reemplazarParseFiles
, porqueParse
puede probar texto directamente desde cadenas, entonces no necesitaremos archivos extra. - Usaremos
main
en cada ejemplo, y no usaremoshandler
. - Usaremos
os.Stdout
para reemplazarhttp.ResponseWriter
, desde queos.Stdout
también implementa la interfazio.Writer
.
Hemos mostrado como puedes analizar y renderizar plantillas. Vamos a dar un paso mas adelante para renderizar información en nuestras plantillas. Cada plantilla es un objeto en Go, entonces ¿cómo insertamos campos en nuestras plantillas?
En Go, cada campo que intentas renderizar en una plantilla debería ser colocado dentro de {{}}
. {{.}}
es un atajo para el objeto actual, que es similar a su contraparte en Java o C++. Si necesitas accesar a los campos del objeto actual, deperías usar {{.NombreDelCampo}}
. Nota que solament elos campos exportados pueden ser accesados en las plantillas. Aquí está un ejemplo:
package main
import (
"html/template"
"os"
)
type Person struct {
UserName string
}
func main() {
t := template.New("fieldname example")
t, _ = t.Parse("hello {{.UserName}}!")
p := Person{UserName: "Astaxie"}
t.Execute(os.Stdout, p)
}
El ejemplo superior muestra hello Astaxie
correctamente, pero si modificamos la estructura un poco, el siguiente error aparecerá:
type Person struct {
UserName string
email string // Field is not exported.
}
t, _ = t.Parse("hello {{.UserName}}! {{.email}}")
Esta parte del código no será compilado porque intenta acceder a un campo que no ha sido exportado. Sin embargo, si tratamos de usar un campo que no existe, Go simplemente mostrará una cadena vacía en vez de un error.
Si imprimes {{.}}
en una plantilla, Go mostrará una cadena formateada de este objeto, llamando fmt
por debajo.
Sabemos como mostrar un campo. ¿Qué pasa si el campo es un objeto y también tiene sus propios campos? ¿Cómo los imprimimos todos en un ciclo? Podemos usar {{with ...}}... {{end}}
y {{range ...}}{{end}}
para este propósito.
- {% raw %}
{{range}}
{% endraw %} funciona comorange
en Go. - {% raw %}
{{with}}
{% endraw %} Permite escribir el mismo objeto una vez mas usando.
como abreviación (Similar alwith
de CB).
Mas ejemplos:
package main
import (
"html/template"
"os"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an email {{.}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"[email protected]", "[email protected]"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
Si necesitas verificar por condicionales en las plantillas, puedes usar la sintaxis if-else
como lo haces en programas regulares en Go. Si el argumento estéa vacío, el valor por defecto del if
es false
. El siguiente ejemplo muestra como se usa if-else
en las plantillas:
package main
import (
"os"
"text/template"
)
func main() {
tEmpty := template.New("template test")
tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} will not be outputted. {{end}}\n"))
tEmpty.Execute(os.Stdout, nil)
tWithValue := template.New("template test")
tWithValue = template.Must(tWithValue.Parse("Not empty pipeline if demo: {{if `anything`}} will be outputted. {{end}}\n"))
tWithValue.Execute(os.Stdout, nil)
tIfElse := template.New("template test")
tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if part {{else}} else part.{{end}}\n"))
tIfElse.Execute(os.Stdout, nil)
}
Como puedes ver, es fácil usar if-else
en las plantillas.
** Atención ** No puedes usar expresiones condicionales dentro del if, por ejemplo: .Mail=="[email protected]"
. Solo variables booleanas son aceptadas.
Los usuarios de Unix deben estar familiarizados con el operador pipe
, como ls | gre "beego"
. Este comando filtra archivos y solamente muestra los que contienen la palabra "beego". Una cosa que me gusta sobre las plantillas de Go es que soportan filtros. Cualquier cosa en {{}}
puede ser información de los filtros. El e-mail que usamos para renderizar en nuestra aplicación puede ser vulnerable a un ataque XSS. ¿Cómo podemos prevenir esta problemática usando filtros?
{{. | html}}
Podemos usar este comando para escapar el cuerpo del correo a HTML. Es muy similar a trabajar con el comando de Unix, es muy conveniente en las funciones de plantillas.
A veces necesitamos usar variables locales en las plantillas. Podemos usarlas con las palabras reservadas with
, range
, e if
, y estará en el ámbito hasta que se use {{end}}
. Aquí está un ejemplo de ocmo declarar una variable global:
$variable := pipeline
Mas ejemplos:
{{with $x := "output" | printf "%q"}}{{$x}}{{end}}
{{with $x := "output"}}{{printf "%q" $x}}{{end}}
{{with $x := "output"}}{{$x | printf "%q"}}{{end}}
Go usa el paquete fmt
para darle formato a la salida de las plantillas. pero a veces necesitamos hacer algo mas. Por ejemplo considera el siguiente escenario: vamos a decir que queremos reemplazar @
por de
en nuestra dirección de email, como astaxie de beego.me
. En este punto tenemos que escribir nuestra función personalizada.
Acada función de plantilla tiene un único nombre y está asociada con nuestro programa en Go como sigue:
type FuncMap map[string]interface{}
Supón que tienes una función de plantilla llamada emailDeal
asociada con una función EmailDealWith
en nuestro progrmaa en Go. Usamos el siguiente código para registrar esta función:
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
definición de EmailDealWith
:
func EmailDealWith(args …interface{}) string
Ejemplo:
package main
import (
"fmt"
"html/template"
"os"
"strings"
)
type Friend struct {
Fname string
}
type Person struct {
UserName string
Emails []string
Friends []*Friend
}
func EmailDealWith(args ...interface{}) string {
ok := false
var s string
if len(args) == 1 {
s, ok = args[0].(string)
}
if !ok {
s = fmt.Sprint(args...)
}
// encontrar el símbolo @
substrs := strings.Split(s, "@")
if len(substrs) != 2 {
return s
}
// reemplazar el símbolo @ por " de "
return (substrs[0] + " at " + substrs[1])
}
func main() {
f1 := Friend{Fname: "minux.ma"}
f2 := Friend{Fname: "xushiwei"}
t := template.New("fieldname example")
t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith})
t, _ = t.Parse(`hello {{.UserName}}!
{{range .Emails}}
an emails {{.|emailDeal}}
{{end}}
{{with .Friends}}
{{range .}}
my friend name is {{.Fname}}
{{end}}
{{end}}
`)
p := Person{UserName: "Astaxie",
Emails: []string{"[email protected]", "[email protected]"},
Friends: []*Friend{&f1, &f2}}
t.Execute(os.Stdout, p)
}
Aquí está una lista de las funciones de plantillas por defecto:
var builtins = FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
}
El paquete de plantillas tiene una función llamada Must
que es para validar plantillas, como verificar las llaves, comentarios y variables. Miremos un ejemplo de Must
:
package main
import (
"fmt"
"text/template"
)
func main() {
tOk := template.New("first")
template.Must(tOk.Parse(" algún texto estático /* y un comentario */"))
fmt.Println("Analizis de la primera plantilla: OK.")
template.Must(template.New("second").Parse("algún texto estático {{ .Name }}"))
fmt.Println("Análisis de la segunda plantilla OK.")
fmt.Println("La siguiente plantilla va a fallar.")
tErr := template.New("Verificar el error de análisi con Must")
template.Must(tErr.Parse(" some static text {{ .Name }"))
}
Salida:
Analizis de la primera plantilla: OK.
Análisis de la segunda plantilla OK.
La siguiente plantilla va a fallar.
panic: template: Verificar el error de análisi con Must:1: unexpected "}" in command
Como en la mayoría de aplicaciones web, ciertas partes de las plantillas pueden ser reusada en otras plantillas, como encabezados, pies de páginas de un blog. Podemos declarar header
, content
y footer
como sub plantillas y luego declararlas en go usando la siguiente sintaxis:
{{define "sub-template"}}content{{end}}
La subplantilla es llamada usando la siguiente sintaxis:
{{template "sub-template"}}
Aquí está un ejemplo completo, suponiendo que tenemos los siguientes tres archivos: header.tmpl
, content.tmpl
y footer.tmpl
in la carpeta templates
, también leeremos la carpeta y almacenaremos los nombres en un arreglo de cadenas, que luego usaremos para analizar los archivos.
Plantilla principal:
{% raw %}
//header.tmpl
{{define "header"}}
<html>
<head>
<title>Something here</title>
</head>
<body>
{{end}}
//content.tmpl
{{define "content"}}
{{template "header"}}
<h1>Nested here</h1>
<ul>
<li>Nested usag</li>
<li>Call template</li>
</ul>
{{template "footer"}}
{{end}}
//footer.tmpl
{{define "footer"}}
</body>
</html>
{{end}}
// Cuando usamos subplantillas, asegúrate de haber analizado cada archivo de subplantillas, de otra manera el compilador no entenderá que sustituir cuando lea {{template "header"}}
{% endraw %}
Código:
package main
import (
"fmt"
"os"
"io/ioutil"
"text/template"
)
var templates *template.Template
func main() {
var allFiles []string
files, err := ioutil.ReadDir("./templates")
if err != nil {
fmt.Println(err)
}
for _, file := range files {
filename := file.Name()
if strings.HasSuffix(filename, ".tmpl") {
allFiles = append(allFiles, "./templates/"+filename)
}
}
templates, err = template.ParseFiles(allFiles...) #parses all .tmpl files in the 'templates' folder
s1 := templates.Lookup("header.tmpl")
s1.ExecuteTemplate(os.Stdout, "header", nil)
fmt.Println()
s2 := templates.Lookup("content.tmpl")
s2.ExecuteTemplate(os.Stdout, "content", nil)
fmt.Println()
s3 := templates.Lookup("footer.tmpl")
s3.ExecuteTemplate(os.Stdout, "footer", nil)
fmt.Println()
s3.Execute(os.Stdout, nil)
}
Como podemos ver aquí template.ParseFiles
analiza todos las plantillas analizadas en un caché y cada plantilla definida por {{define}}
es independiente de la otra. Ellas persisten en algo como un mapa, donde el nombre de la plantilla es la llave y el valor es el cuerpo de la plantilla. Podemos entonces usar ExecuteTemplate
para ejecutar el subtemplate correspondiente, entonces el encavezado y el pié de página sin independientes y el contenido los tiene a ambos. Nota que si tratamos de ejecutar s1.Execute
nada saldrá porque no hay ningún subtemplate disponible.
Cuando quieras usar define
, tienes que crear un archivo de texto con el mismo nombre de la subplantilla, por ejemplo _head.tmpl
es una subplantila que se usa al rededor de tuproyecto, entonces crea este archivo en una carpeta para palanillas y úsala la sintaxis normal. El caché de verificación es creado básicamente para que no tengas que leer la plantilla cada vez que sirvas la petición, porque si lo haces, estás gastando un montón de recursos para leer un archivo que no cambia a menos que el código base sea reescrito, lo cual no tiene sentido para hacer en cada petición GET, entonces esta técnica es usada para analizar los archivos y luego usar un Lookup()
en el caché para ejecutar la plantilla cuando se necesite mostrar la información.
Las plantillas en un conjunto se pueden conocer unas a otras, pero debes analizar por cada conjunto en específico.
Algunas veces vas a querer contextualizar las plantillas, por ejemplo, si tienes un _head.html
, puedes tener un encabezado que que muestre información basada en loque estes cargando, por ejemplo una lista de tareas por hacer, que puede tener categorías como pendiente
, completada
, eliminada
Supón que tienes el siguiente texto:
<title>{{if eq .Navigation "pendiente"}} Tareas
{{ else if eq .Navigation "completada"}}Completada
{{ else if eq .Navigation "eliminada"}}Eliminada
{{ else if eq .Navigation "editar"}} Editar
{{end}}
</title>
Nota: Las platillas en Go siguen la notación polaca para realizar la comparación, donde tienes el operador primero y luego los valores de comparación. La parte del else if
es igual.
Típicamente, usamos un operador {{range}}
para recorrer las variales de contexto las cuales son pasadas a la plantilla de la siguiente manera:
//present in views package
context := db.GetTasks("pending") //true when you want non deleted notes
homeTemplate.Execute(w, context)
Tenemos un objeto de contexto de la base de datos como una estructura, la definición está abajo:
// Estructura de tareas usada para la identificación de ellas
type Task struct {
Id int
Title string
Content string
Created string
}
// Context es la estructura pasada a las plantillas
type Context struct {
Tasks []Task
Navigation string
Search string
Message string
}
// Presente en el paquete de bases de Datos
var task []types.Task
var context types.Context
context = types.Context{Tasks: task, Navigation: status}
// Esta línea está en el paquete de bases de datos, donde el contexto es retornado a la vista
Usamos el arreglo de Task
y Navigation
en nuestras plantillas , vimos como usar Navigation
en nuestras plantillas. Veremos como usar el arreglo e nuestra plantilla.
Aquí en el {{if .Tast}}
primero verificamos si el campo Tasks
que pasamos a nuestra plantilla en el contexto está vacío o no. Si no está vacío, entonces colocamos range
a través del arreglo para llenar el título y el contenido de cada Task
. El ejemplo de abajo es muy importante en lo que se refiere a recorrer un arreglo en una plantilla, iniciamos usando un operador Range
, luego le damos un miembro de la estructura como {{.Name}}
, la estructura de Task
tiene un Title
y un Content
(Nota que Title y Content tienen la primera letra en mayúscula, por lo tanto está exportadas).
{{ range .Tasks }}
{{ .Title }}
{{ .Content }}
{{ end }}
Este bloque de código imprimirá el título y el contenido del arreglo de Task
Debajo hay un ejemplo completo de github.com/thewhitetulip/Tasks
. Plantilla home.html:
<div class="timeline">
{{ if .Tasks}} {{range .Tasks}}
<div class="note">
<p class="noteHeading">{{.Title}}</p>
<hr>
<p class="noteContent">{{.Content}}</p>
</ul>
</span>
</div>
{{end}} {{else}}
<div class="note">
<p class="noteHeading">No Tasks here</p>
<p class="notefooter">
Create new task<button class="floating-action-icon-add" > here </button> </p>
</div>
{{end}}
En esta sección aprendiste como combinar datos dinámicos con plantillas usando técnicas de impresión de información en ciclos, funciones de plantillas y plantillas anidadas. Al aprender sobre las plantillas, podemos concluir la discusión de la V (Vista) de la arquitectura MVC. En los siguientes capítulos cubriremos los aspectos M (Modelo) y C (Controlador) del MVC.
- Índice
- Sección Anterior: Expresiones regulares
- Siguiente sección: Archivos