Template View (Шаблонизатор)

Паттерн проектирования Template View

Паттерн проектирования Template View

Описание Template View

Заполняет HTML-шаблон информацией при помощи маркеров, указанных в шаблоне.

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

В случае со статическими HTML-страницами, которые не меняются от запроса к запросу, можно использоваться удобный WYSIWYG-редактор. Даже те, кто любят обычные текстовые редакторы согласятся, что набирать текст и тэги проще и удобнее, чем собирать их через конкатенации в языке программирования.

Конечно, возникает проблема в случае с динамическими страницами, которые, например, берут данные из БД и наполняют ими HTML. Страницы выглядят по-разному каждый раз, и использование обычного HTML-редактора не подходит.

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

Примеры реализации

// Template View Pattern in JavaScript
class TemplateView {
    constructor() {
        this.templates = new Map();
    }
    
    registerTemplate(name, template) {
        this.templates.set(name, template);
        console.log(`Template registered: ${name}`);
    }
    
    render(templateName, data) {
        console.log(`Rendering template: ${templateName}`);
        
        const template = this.templates.get(templateName);
        if (!template) {
            throw new Error(`Template not found: ${templateName}`);
        }
        
        let html = template;
        
        // Replace placeholders with data
        for (const [key, value] of Object.entries(data)) {
            const placeholder = `{{${key}}}`;
            html = html.replace(new RegExp(placeholder, 'g'), value);
        }
        
        console.log('Template rendered successfully');
        return html;
    }
    
    renderWithHelper(templateName, data, helper) {
        console.log(`Rendering template with helper: ${templateName}`);
        
        const template = this.templates.get(templateName);
        if (!template) {
            throw new Error(`Template not found: ${templateName}`);
        }
        
        let html = template;
        
        // Replace placeholders with data
        for (const [key, value] of Object.entries(data)) {
            const placeholder = `{{${key}}}`;
            html = html.replace(new RegExp(placeholder, 'g'), value);
        }
        
        // Replace helper functions
        html = html.replace(/\{\{helper\.(\w+)\(([^)]*)\)\}\}/g, (match, method, args) => {
            if (helper[method]) {
                const parsedArgs = args.split(',').map(arg => arg.trim().replace(/['"]/g, ''));
                return helper[method](...parsedArgs);
            }
            return match;
        });
        
        console.log('Template with helper rendered successfully');
        return html;
    }
}

// Helper functions
const helper = {
    formatDate: (date) => {
        return new Date(date).toLocaleDateString();
    },
    
    formatCurrency: (amount) => {
        return `$${parseFloat(amount).toFixed(2)}`;
    },
    
    capitalize: (text) => {
        return text.charAt(0).toUpperCase() + text.slice(1);
    }
};

// Usage
const templateView = new TemplateView();

// Register templates
templateView.registerTemplate('user-profile', `
    
    
    
        {{title}}
        
    
    
        

{{name}}

Member since: {{helper.formatDate(joinDate)}}

Email: {{email}}

Role: {{helper.capitalize(role)}}

Balance: {{helper.formatCurrency(balance)}}

Bio: {{bio}}

`); templateView.registerTemplate('product-list', ` {{title}}

{{title}}

{{products}}
`); // Sample data const userData = { title: 'User Profile', name: 'John Doe', email: 'john@example.com', role: 'admin', balance: '1250.75', bio: 'Software developer with 5 years of experience.', joinDate: '2020-01-15' }; const productData = { title: 'Product Catalog', products: `

Laptop

$999.99

High-performance laptop for professionals

Mouse

$29.99

Wireless optical mouse

` }; // Render templates const userHtml = templateView.renderWithHelper('user-profile', userData, helper); console.log('User profile HTML generated'); const productHtml = templateView.render('product-list', productData); console.log('Product list HTML generated');
<?php
// Template View Pattern in PHP
class TemplateView {
    private $templates = [];
    
    public function registerTemplate($name, $template) {
        $this->templates[$name] = $template;
        echo "Template registered: $name\n";
    }
    
    public function render($templateName, $data) {
        echo "Rendering template: $templateName\n";
        
        if (!isset($this->templates[$templateName])) {
            throw new Exception("Template not found: $templateName");
        }
        
        $template = $this->templates[$templateName];
        $html = $template;
        
        // Replace placeholders with data
        foreach ($data as $key => $value) {
            $placeholder = "{{$key}}";
            $html = str_replace($placeholder, $value, $html);
        }
        
        echo "Template rendered successfully\n";
        return $html;
    }
    
    public function renderWithHelper($templateName, $data, $helper) {
        echo "Rendering template with helper: $templateName\n";
        
        if (!isset($this->templates[$templateName])) {
            throw new Exception("Template not found: $templateName");
        }
        
        $template = $this->templates[$templateName];
        $html = $template;
        
        // Replace placeholders with data
        foreach ($data as $key => $value) {
            $placeholder = "{{$key}}";
            $html = str_replace($placeholder, $value, $html);
        }
        
        // Replace helper functions
        $html = preg_replace_callback('/\{\{helper\.(\w+)\(([^)]*)\)\}\}/', function($matches) use ($helper) {
            $method = $matches[1];
            $args = array_map('trim', explode(',', $matches[2]));
            $args = array_map(function($arg) {
                return trim($arg, '\'"');
            }, $args);
            
            if (isset($helper[$method])) {
                return call_user_func_array($helper[$method], $args);
            }
            return $matches[0];
        }, $html);
        
        echo "Template with helper rendered successfully\n";
        return $html;
    }
}

// Helper functions
$helper = [
    'formatDate' => function($date) {
        return date('Y-m-d', strtotime($date));
    },
    
    'formatCurrency' => function($amount) {
        return '$' . number_format($amount, 2);
    },
    
    'capitalize' => function($text) {
        return ucfirst($text);
    }
];

// Usage
$templateView = new TemplateView();

// Register templates
$templateView->registerTemplate('user-profile', '
    
    
    
        {{title}}
        
    
    
        

{{name}}

Member since: {{helper.formatDate(joinDate)}}

Email: {{email}}

Role: {{helper.capitalize(role)}}

Balance: {{helper.formatCurrency(balance)}}

Bio: {{bio}}

'); $templateView->registerTemplate('product-list', ' {{title}}

{{title}}

{{products}}
'); // Sample data $userData = [ 'title' => 'User Profile', 'name' => 'John Doe', 'email' => 'john@example.com', 'role' => 'admin', 'balance' => '1250.75', 'bio' => 'Software developer with 5 years of experience.', 'joinDate' => '2020-01-15' ]; $productData = [ 'title' => 'Product Catalog', 'products' => '

Laptop

$999.99

High-performance laptop for professionals

Mouse

$29.99

Wireless optical mouse

' ]; // Render templates $userHtml = $templateView->renderWithHelper('user-profile', $userData, $helper); echo "User profile HTML generated\n"; $productHtml = $templateView->render('product-list', $productData); echo "Product list HTML generated\n"; ?>
// Template View Pattern in Go
package main

import (
    "fmt"
    "regexp"
    "strconv"
    "strings"
    "time"
)

type TemplateView struct {
    templates map[string]string
}

func NewTemplateView() *TemplateView {
    return &TemplateView{
        templates: make(map[string]string),
    }
}

func (tv *TemplateView) RegisterTemplate(name, template string) {
    tv.templates[name] = template
    fmt.Printf("Template registered: %s\n", name)
}

func (tv *TemplateView) Render(templateName string, data map[string]string) (string, error) {
    fmt.Printf("Rendering template: %s\n", templateName)
    
    template, exists := tv.templates[templateName]
    if !exists {
        return "", fmt.Errorf("template not found: %s", templateName)
    }
    
    html := template
    
    // Replace placeholders with data
    for key, value := range data {
        placeholder := fmt.Sprintf("{{%s}}", key)
        html = strings.ReplaceAll(html, placeholder, value)
    }
    
    fmt.Println("Template rendered successfully")
    return html, nil
}

func (tv *TemplateView) RenderWithHelper(templateName string, data map[string]string, helper map[string]func(...string) string) (string, error) {
    fmt.Printf("Rendering template with helper: %s\n", templateName)
    
    template, exists := tv.templates[templateName]
    if !exists {
        return "", fmt.Errorf("template not found: %s", templateName)
    }
    
    html := template
    
    // Replace placeholders with data
    for key, value := range data {
        placeholder := fmt.Sprintf("{{%s}}", key)
        html = strings.ReplaceAll(html, placeholder, value)
    }
    
    // Replace helper functions
    re := regexp.MustCompile(`\{\{helper\.(\w+)\(([^)]*)\)\}\}`)
    html = re.ReplaceAllStringFunc(html, func(match string) string {
        matches := re.FindStringSubmatch(match)
        if len(matches) >= 3 {
            method := matches[1]
            argsStr := matches[2]
            var args []string
            if argsStr != "" {
                args = strings.Split(argsStr, ",")
                for i, arg := range args {
                    args[i] = strings.TrimSpace(strings.Trim(arg, "'\""))
                }
            }
            
            if helperFunc, exists := helper[method]; exists {
                return helperFunc(args...)
            }
        }
        return match
    })
    
    fmt.Println("Template with helper rendered successfully")
    return html, nil
}

// Helper functions
func createHelper() map[string]func(...string) string {
    return map[string]func(...string) string{
        "formatDate": func(args ...string) string {
            if len(args) > 0 {
                if t, err := time.Parse("2006-01-02", args[0]); err == nil {
                    return t.Format("Jan 2, 2006")
                }
            }
            return args[0]
        },
        
        "formatCurrency": func(args ...string) string {
            if len(args) > 0 {
                if amount, err := strconv.ParseFloat(args[0], 64); err == nil {
                    return fmt.Sprintf("$%.2f", amount)
                }
            }
            return args[0]
        },
        
        "capitalize": func(args ...string) string {
            if len(args) > 0 {
                text := args[0]
                if len(text) > 0 {
                    return strings.ToUpper(text[:1]) + text[1:]
                }
            }
            return args[0]
        },
    }
}

func main() {
    templateView := NewTemplateView()
    
    // Register templates
    templateView.RegisterTemplate("user-profile", `
        
        
        
            {{title}}
            
        
        
            

{{name}}

Member since: {{helper.formatDate(joinDate)}}

Email: {{email}}

Role: {{helper.capitalize(role)}}

Balance: {{helper.formatCurrency(balance)}}

Bio: {{bio}}

`) templateView.RegisterTemplate("product-list", ` {{title}}

{{title}}

{{products}}
`) // Sample data userData := map[string]string{ "title": "User Profile", "name": "John Doe", "email": "john@example.com", "role": "admin", "balance": "1250.75", "bio": "Software developer with 5 years of experience.", "joinDate": "2020-01-15", } productData := map[string]string{ "title": "Product Catalog", "products": `

Laptop

$999.99

High-performance laptop for professionals

Mouse

$29.99

Wireless optical mouse

`, } // Render templates helper := createHelper() userHtml, err := templateView.RenderWithHelper("user-profile", userData, helper) if err != nil { fmt.Printf("Error rendering user profile: %v\n", err) return } fmt.Println("User profile HTML generated") productHtml, err := templateView.Render("product-list", productData) if err != nil { fmt.Printf("Error rendering product list: %v\n", err) return } fmt.Println("Product list HTML generated") // Print first 100 characters of each fmt.Printf("User HTML preview: %s...\n", userHtml[:100]) fmt.Printf("Product HTML preview: %s...\n", productHtml[:100]) }

Пример: при обработке шаблона, области, помеченные специальными маркерами (на иллюстрации - тегами <jsp:.../>) заменяются результатами вызовов методов helper'a.

Использована иллюстрация с сайта Мартина Фаулера.

Источник