DevGang
Авторизоваться

Использование функций внутри шаблонов Go

В этом уроке мы рассмотрим, как использовать функции шаблонов, такие как and, eq и index, чтобы добавить некоторую базовую логику в наши шаблоны. Как только у нас будет довольно хорошее понимание того, как использовать эти функции, мы рассмотрим, как добавить некоторые пользовательские функции в наши шаблоны и использовать их.

Функция and

По умолчанию действие if в шаблонах оценивает, является ли аргумент пустым, но что происходит, когда вы хотите оценить несколько аргументов? Вы можете написать вложенные блоки if/else, но это выглядело бы ужасно.

Вместо этого пакет html/template предоставляет функцию and. Его использование аналогично тому, как вы могли использовать функцию и в Lisp (другом языке программирования). Это легче показать, чем объяснить, поэтому давайте просто перейдем к примеру кода. Откройте main.go и добавьте следующее:

package main

import (
  "html/template"
  "net/http"
)

var testTemplate *template.Template

type User struct {
  Admin bool
}

type ViewData struct {
  *User
}

func main() {
  var err error
  testTemplate, err = template.ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  vd := ViewData{&User{true}}
  err := testTemplate.Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

Затем откройте hello.gohtml и добавьте следующее в свой шаблон.

{{if and .User .User.Admin}}
  You are an admin user!
{{else}}
  Access denied!
{{end}}

Если вы запустите этот код, вы должны увидеть результат: You are an admin user! Если вы обновите main.go, не включив в него объект *User, либо установите для Admin значение false, или даже если вы предоставите nil для метода testTemplate.Execute(), вы увидите вместо этого Access denied!.

Функция and принимает два аргумента, позволяет вызывать их, а затем выполняет логику, примерно эквивалентную, if a then b else a. Самое странное, что это действительно функция, а не то, что вы помещаете между двумя переменными. Просто помните, что это функция, а не логическая операция, и все должно быть в порядке.

Аналогично, пакет шаблона также предоставляет функцию or, которая работает во многом так же как and, за исключением того, что она будет иметь короткое замыкание, когда ее значение true. То есть логика для or a b примерно эквивалентна, if a then a else b, поэтому b никогда не будет оцениваться, если a не пусто.

Функции сравнения (равно, меньше и т.д.)

До сих пор мы имели дело с относительно простой логикой, вращающейся вокруг того, является ли что-то пустым или нет, но что происходит, когда нам нужно сделать другие сравнения? Например, что если мы захотим настроить класс на объекте в зависимости от того, приближается ли пользователь к превышению своего предела использования?

Пакет html/template предоставляет нам несколько классов для сравнения.

  • eq - Аналогичен  arg1 == arg2
  • ne - Аналогичен  arg1 != arg2
  • lt - Аналогичен  arg1 < arg2
  • le - Аналогичен  arg1 <= arg2
  • gt - Аналогичен  arg1 > arg2
  • ge - Аналогичен  arg1 >= arg2

Они используются аналогично тому, как используются and или or, когда вы сначала вводите функцию, а затем вводите аргументы. Например, вы можете использовать следующий код в своем шаблоне, чтобы определить, какой текст рендерить в отношении их использования API.

{{if (ge .Usage .Limit)}}
  <p class="danger">
    You have reached your API usage limit. Please upgrade or contact support for more help.
  </p>
{{else if (gt .Usage .Warning)}}
  <p class="warning">
    You have used {{.Usage}} of {{.Limit}} API calls and are nearing your limit. Have you considered upgrading?
  </p>
{{else if (eq .Usage 0)}}
  <p>
    You haven't used the API yet! What are you waiting for?
  </p>
{{else}}
  <p>
    You have used {{.Usage}} of {{.Limit}} API calls.
  </p>
{{end}}

if...else if...else
Этот код также демонстрирует, как создать блок if ... else if ... else, который мы еще не рассмотрели. Они работают так же, как блок if ... else, но они позволяют вам иметь несколько различных условных предложений.

Использование функциональных переменных

До сих пор мы в основном имели дело со структурами данных внутри наших шаблонов, но что произойдет, если мы захотим вызывать наши собственные функции из шаблона? Например, давайте представим, что у нас есть тип User, и нам нужно выяснить, есть ли у текущего пользователя разрешение на доступ к нашей корпоративной функции при создании пользовательского интерфейса. Мы могли бы создать структуру клиента для представления и добавить поле для разрешения.

type ViewData struct {
  Permissions map[string]bool
}

// or

type ViewData struct {
  Permissions struct {
    FeatureA bool
    FeatureB bool
  }
}

Проблема с этим подходом состоит в том, что нам всегда нужно знать все функции, которые используются в текущем представлении, или, если бы вместо этого мы использовали map[string]bool, нам нужно было бы заполнить ее значением для каждой возможной функции. Было бы намного проще, если бы мы могли просто вызывать функцию, когда хотели узнать, есть ли у пользователя доступ к этой функции. Есть несколько способов сделать это в Go, поэтому я расскажу о нескольких возможных способах сделать это.

1.Создайте метод у типа User

Первый и пожалуй самый простой способ. Допустим, у нас есть тип User, который мы уже предоставили представлению, мы можем просто добавить метод HasPermission() к объекту и затем использовать его. Чтобы увидеть это в действии, добавьте следующее в hello.gohtml.

{{if .User.HasPermission "feature-a"}}
  <div class="feature">
    <h3>Feature A</h3>
    <p>Some other stuff here...</p>
  </div>
{{else}}
  <div class="feature disabled">
    <h3>Feature A</h3>
    <p>To enable Feature A please upgrade your plan</p>
  </div>
{{end}}

{{if .User.HasPermission "feature-b"}}
  <div class="feature">
    <h3>Feature B</h3>
    <p>Some other stuff here...</p>
  </div>
{{else}}
  <div class="feature disabled">
    <h3>Feature B</h3>
    <p>To enable Feature B please upgrade your plan</p>
  </div>
{{end}}

<style>
  .feature {
    border: 1px solid #eee;
    padding: 10px;
    margin: 5px;
    width: 45%;
    display: inline-block;
  }
  .disabled {
    color: #ccc;
  }
</style>

А затем добавьте следующее в main.go

package main

import (
  "html/template"
  "net/http"
)

var testTemplate *template.Template

type ViewData struct {
  User User
}

type User struct {
  ID    int
  Email string
}

func (u User) HasPermission(feature string) bool {
  if feature == "feature-a" {
    return true
  } else {
    return false
  }
}

func main() {
  var err error
  testTemplate, err = template.ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  vd := ViewData{
    User: User{1, "jon@calhoun.io"},
  }
  err := testTemplate.Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

Мы успешно включаем и отключаем функции на внешнем интерфейсе в зависимости от того, есть ли у пользователя доступ к ним! Когда мы объявляем функции для типов, мы можем вызывать их так же, как мы обращаемся к данным внутри структуры, так что все это должно показаться вам довольно знакомым.

Теперь, когда мы увидели, как вызывать методы, давайте проверим более динамичный способ вызова функций внутри шаблона с использованием функции call.

2. Вызов функциональных переменных и полей

Давайте представим, что по какой либо причине вы не можете использовать описанный выше подход, потому что ваш метод определения логики должен время от времени меняться. В этом случае имеет смысл создать атрибут типа HasPermission func(string)bool для типа User, а затем назначить его с помощью функции. Откройте main.go и измените свой код, чтобы добавить следующее.

package main

import (
  "html/template"
  "net/http"
)

var testTemplate *template.Template

type ViewData struct {
  User User
}

type User struct {
  ID            int
  Email         string
  HasPermission func(string) bool
}

func main() {
  var err error
  testTemplate, err = template.ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  vd := ViewData{
    User: User{
      ID:    1,
      Email: "jon@calhoun.io",
      HasPermission: func(feature string) bool {
        if feature == "feature-b" {
          return true
        }
        return false
      },
    },
  }
  err := testTemplate.Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

Все выглядит хорошо, но если вы запустите localhost: 3000 в своем браузере после запуска сервера, вы заметите, что мы получаем ошибку вроде

template: hello.gohtml:1:10: executing "hello.gohtml" at <.User.HasPermission>: HasPermission has arguments but cannot be invoked as function

Когда мы назначаем функции переменным, нам нужно сообщить пакету html/template, что мы хотим вызвать функцию. Откройте файл hello.gohtml и добавьте слово call сразу после ваших операторов if, вот так.

{{if (call .User.HasPermission "feature-a")}}
...

{{if (call .User.HasPermission "feature-b")}}
...

Скобки можно использовать в шаблонах
Хотя круглые скобки обычно не требуются в шаблонах Go, они могут быть невероятно полезны для разъяснения того, какие аргументы необходимо передать в какие функции, и для определения четкого порядка операций.

Перезагрузите сервер и снова проверьте localhost. Вы должны увидеть ту же страницу, что и раньше, но на этот раз функция B включена вместо функции A.

call - это функция, уже предоставленная пакетом html/template, которая вызывает первый переданный ей аргумент (в нашем случае - функцию .User.HasPermission), используя остальные аргументы в качестве аргументов вызова функции.

3. Создание пользовательских функций с помощью template.FuncMap

Последний способ вызова наших собственных функций, о которых я расскажу, - это создание пользовательских функций с шаблоном template.FuncMap. На мой взгляд, это наиболее полезный и мощный способ определения функций, поскольку он позволяет нам создавать глобальные вспомогательные методы, которые можно использовать в нашем приложении.

Чтобы начать, сначала перейдите к документации для template.FuncMap. Первое, на что нужно обратить внимание, это то, что этот тип выглядит как map[string]interface{}, но ниже есть примечание, что каждый интерфейс должен быть функцией с одним возвращаемым значением или функцией с двумя возвращаемыми значениями, где первое, это данные, к которым вам нужно получить доступ в шаблоне, а второе, ошибка, которая прекратит выполнение шаблона, если он не равен нулю.

Поначалу это может сбить с толку, поэтому давайте просто рассмотрим пример. Снова откройте main.go и обновите его, чтобы он соответствовал приведенному ниже коду.

package main

import (
  "html/template"
  "net/http"
)

var testTemplate *template.Template

type ViewData struct {
  User User
}

type User struct {
  ID    int
  Email string
}

func main() {
  var err error
  testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "hasPermission": func(user User, feature string) bool {
      if user.ID == 1 && feature == "feature-a" {
        return true
      }
      return false
    },
  }).ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  user := User{
    ID:    1,
    Email: "jon@calhoun.io",
  }
  vd := ViewData{user}
  err := testTemplate.Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

И еще раз откройте hello.gohtml и обновите каждый оператор if, чтобы использовать новую функцию следующим образом.

{{if hasPermission .User "feature-a"}}
...

{{if hasPermission .User "feature-b"}}
...

Функция hasPermission теперь должна включать вашу логику, которая определяет, включена функция или нет. В main.go мы определили template.FuncMap, который сопоставил имя метода («hasPermission») с функцией, которая принимает два аргумента (пользователя и строку), а затем возвращает true или false. Затем мы вызвали функцию template.New(), чтобы создать новый шаблон, и вызвали метод Funcs() для этого нового шаблона, чтобы определить наши пользовательские функции, а затем, наконец, мы проанализировали наш файл hello.gohtml как источник для нашего шаблона.

Определите функции перед разбором шаблонов

В предыдущих примерах мы создавали наш шаблон, вызывая функцию template.ParseFiles, предоставляемую пакетом html/template. Это функция уровня пакета, которая возвращает шаблон после анализа файлов. Теперь мы вызываем метод ParseFiles для типа template.Template, который имеет те же возвращаемые значения, но применяет изменения к существующему шаблону (а не к новому) и затем возвращает результат.

В этой ситуации нам нужно использовать метод, потому что нам нужно сначала определить любые пользовательские функции, которые мы планируем использовать в наших шаблонах, и как только мы сделаем это с пакетом шаблонов, он вернет *template.Template. После определения этих пользовательских функций мы можем приступить к анализу шаблонов, которые используют функции. Если бы мы сначала проанализировали шаблоны, вы бы увидели ошибку, связанную с неопределенной функцией, вызываемой в вашем шаблоне.

Далее мы рассмотрим, как заставить эту функцию работать без необходимости передавать объект User каждый раз, когда мы его вызываем.

Делая наши функции глобальными во всем приложении

Функция hasPermission, которую мы определили в последнем разделе, великолепна, но одна проблема с ней заключается в том, что мы можем использовать ее только тогда, когда у нас есть доступ к объекту User. На первый взгляд, это не так уж плохо, но по мере роста приложения у него будет много шаблонов, и довольно легко забыть передать объект User в шаблон или пропустить его во вложенном шаблоне.

Наша функция была бы намного проще, если бы мы могли ее упростить, и нам нужно было только передать имя функции, поэтому давайте продолжим и обновим наш код, чтобы это произошло.

Первое, что нам нужно сделать, это создать функцию, котора не принимает объект User. Мы установим это в template.FuncMap перед синтаксическим анализом нашего шаблона, чтобы у нас не было ошибок синтаксического анализа, и чтобы убедиться, что у нас есть некоторая логика в случае, если пользователь недоступен.

Откройте main.go и обновите функцию main() в соответствии с приведенным ниже кодом.

func main() {
  var err error
  testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "hasPermission": func(feature string) bool {
      return false
    },
  }).ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

Далее нам нужно определить нашу функцию, которая использует замыкание. Это в основном причудливый способ сказать, что мы собираемся определить динамическую функцию, которая имеет доступ к переменным, которые не обязательно передаются в нее, но доступны, когда мы определяем функцию. В нашем случае эта переменная будет объектом User. Обновите функцию handler() внутри main.go с помощью следующего кода.

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  user := User{
    ID:    1,
    Email: "jon@calhoun.io",
  }
  vd := ViewData{user}
  err := testTemplate.Funcs(template.FuncMap{
    "hasPermission": func(feature string) bool {
      if user.ID == 1 && feature == "feature-a" {
        return true
      }
      return false
    },
  }).Execute(w, vd)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

Несмотря на то, что мы определили функцию hasPermission в нашей функции main(), мы перезаписываем ее внутри нашего обработчика, когда имеем доступ к объекту User, но перед тем, как выполнить шаблон. Это действительно мощно, потому что теперь мы можем использовать функцию hasPermission в любом шаблоне, не беспокоясь о том, был ли передан объект User в шаблон или нет.

HTML безопасные строки и комментарии HTML

К сожалению, пакет html/template по умолчанию удалит эти комментарии, поэтому нам нужно найти способ сделать комментарии безопасными для HTML. В частности, нам нужно создать функцию, которая предоставляет нам объект template.HTML с содержимым <!--[if IE]> и другую для содержимого <![endif]-->.

Откройте main.go и замените его содержимое следующим.

package main

import (
  "html/template"
  "net/http"
)

var testTemplate *template.Template

func main() {
  var err error
  testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "ifIE": func() template.HTML {
      return template.HTML("<!--[if IE]>")
    },
    "endif": func() template.HTML {
      return template.HTML("<![endif]-->")
    },
  }).ParseFiles("hello.gohtml")
  if err != nil {
    panic(err)
  }

  http.HandleFunc("/", handler)
  http.ListenAndServe(":3000", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
  w.Header().Set("Content-Type", "text/html")

  err := testTemplate.Execute(w, nil)
  if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
  }
}

В основной функции мы реализуем функции, которые я описал ранее и называем их ifIE и endif. Это позволяет нам обновлять наш шаблон (hello.gohtml) следующим образом.

{{ifIE}}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{{endif}}

А затем, если вы перезапустите сервер, перезагрузите страницу и затем просмотрите исходный код страницы, вы должны увидеть в нем следующее:

<!--[if IE]>
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
<![endif]-->

Это прекрасно работает, но создание функции для каждого отдельного комментария, который мы можем захотеть использовать в нашем приложении, очень быстро утомляет. Для действительно распространенных комментариев (таких как endif выше) создание собственной функции имеет смысл, но нам нужен способ передать любой комментарий HTML и убедиться, что он не закодирован. Для этого нам нужно определить функцию, которая принимает строку и преобразует ее в шаблон. HTML. Снова откройте main.go и обновите ваш template.FuncMap, чтобы он соответствовал приведенному ниже.

func main() {
  // ...
  testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{
    "ifIE": func() template.HTML {
      return template.HTML("<!--[if IE]>")
    },
    "endif": func() template.HTML {
      return template.HTML("<![endif]-->")
    },
    "htmlSafe": func(html string) template.HTML {
      return template.HTML(html)
    },
  }).ParseFiles("hello.gohtml")
  //...
}

С нашей новой функцией htmlSafe мы можем добавлять пользовательские комментарии по мере необходимости, например, оператор if для IE6.

{{htmlSafe "<!--[if IE 6]>"}}
<meta http-equiv="Content-Type" content="text/html; charset=Unicode">
{{htmlSafe "<![endif]-->"}}

Последняя строка в этом примере также может быть {{endif}}, так как у нас все еще определена эта функция, но я решил использовать htmlSafe для согласованности.

Наша функция htmlSafe может даже использоваться в сочетании с другими методами (например, {{htmlSafe .User.Widget}}), если мы хотим, но, вообще говоря, если вы хотите, чтобы эти методы возвращали безопасные строки HTML, вам, вероятно, следует обновить их тип возврата template.HTML, чтобы ваши намерения были разъяснены для будущих разработчиков.

#Golang
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу