从零手写PHP模板引擎,顺便理解前端模板语法

阿乐
阿乐 管理员 年卡会员
发布于 2026-09-04 07:44 ·22 浏览 ·0 回复

开场:模板引擎,被包装得很神秘

在这个前后端分离、框架林立、`v-for` 和 `map()` 信手拈来的年代,很多人已经不再关心“页面上的数据是怎么渲染出来的”了——反正框架帮你搞定了。但如果你曾经被某个魔改的 PHP 项目折磨过,或者面试时被问到“模板引擎的原理是什么”,你就会发现,这东西本质上并没有那么玄乎。

今天我们不打算引入 Composer 包,也不用任何框架的原生模板,纯粹用 PHP 从零开始手写一个“能用但不简陋”的模板引擎。写完之后你会发现:什么 Vue 的插值语法,什么 Django 的 `{{ variable }}`,万变不离其宗,都是同一波操作。

第一步:先搞清楚模板引擎到底在干嘛

模板引擎的使命只有一句话:把模板字符串 + 数据源 = 输出 HTML

模板本质是一个 带占位符的字符串,数据是一堆变量,引擎负责把占位符替换成数据实际的内容。核心动作是编译渲染

- 编译:把模板文字翻译成 PHP 可执行的代码(或者 AST)。
- 渲染:执行这段代码,输入数据,得到最终的 HTML。

我们直接上手写一个最简单的版本——先用字符串替换解决,再增加控制结构支持。

第二步:手写一个超级简单的 PHP 模板引擎

我们先从一个极简的替换类开始:

class SimpleTemplate {
    protected $template;
    
    public function __construct($template) {
        $this->template = $template;
    }
    
    public function render(array $data): string {
        $result = $this->template;
        foreach ($data as $key => $value) {
            $result = str_replace("{{{$key}}}", $value, $result);
        }
        return $result;
    }
}

// 使用示例
$html = '<h1>Hello, {{name}}!</h1><p>你是{{age}}岁。</p>';
$engine = new SimpleTemplate($html);
echo $engine->render(['name' => '小明', 'age' => 18]);

看到没?就是 `str_replace` 的高级马甲。

但这显然不够——遇到 `foreach`、`if` 条件怎么办?如果数据是数组,不允许进行变量点语法?单纯替换解决不了循环结构的问题。所以需要进入第二步:编译成 PHP 代码,然后 `eval` 它。

第三步:进阶版:支持循环和判断

我们先定一下我们要支持的语法风格:

- 变量:`{$var}`(吸 PHP 原生语法)
- 循环:`{foreach $list as $item}` ... `{/foreach}`
- 判断:`{if $ok}` ... `{else}` ... `{/if}`

编译器的工作逻辑是:找到这些自定义标签,替换成 PHP 原生代码,其余部分原样输出。

class TemplateEngine {
    public function compile(string $template): string {
        // 转义 PHP 标签(防止模板内写原生 PHP,仅为了安全示例)
        $pattern = [
            '/\{\{\s*\$(\w+)\s*\}\}/'   => '<?php echo $1; ?>',
            '/\{foreach\s+(\$\w+)\s+as\s+(\$\w+)\}/' => '<?php foreach($1 as $2): ?>',
            '/\{\/foreach\}/'           => '<?php endforeach; ?>',
            '/\{if\s+(.+?)\}/'          => '<?php if($1): ?>',
            '/\{else\}/'                => '<?php else: ?>',
            '/\{\/if\}/'                => '<?php endif; ?>',
        ];
        $compiled = preg_replace(array_keys($pattern), array_values($pattern), $template);
        return '?>' . $compiled . '<?php';
    }

    public function render(string $template, array $data): string {
        $compiledFile = tempnam(sys_get_temp_dir(), 'tpl_');
        file_put_contents($compiledFile, $this->compile($template));
        extract($data, EXTR_SKIP);
        ob_start();
        include $compiledFile;
        $output = ob_get_clean();
        unlink($compiledFile);
        return $output;
    }
}

// 使用示例:
$tpl = '<ul>{foreach $users as $user}<li>{$user}</li>{/foreach}</ul> 
{if $isAdmin}<p>管理员你好</p>{else}<p>普通用户</p>{/if}';

$engine = new TemplateEngine();
echo $engine->render($tpl, ['users' => ['小明', '小红'], 'isAdmin' => true]);

这段核心代码很有意思,它其实做了一件所有现代流行模板引擎的“本质动作”:语法转换 + include 执行。像 Smarty、Blade,本质上都是这个路子,只是它们做得更规范,加入了缓存、函数调用、美化转义。

第四步:一句话讲透前端模板语法

你再看 Vue 的模板:

<li v-for="user in users">{{ user }}</li>

Vue 在编译阶段也是把这段 HTML 模板解析成 AST,然后再生成所谓的“渲染函数”(render function),这个函数内部执行时依赖一个 `_c` 或 `with(this){...}` 的机制来访问数据。本质上就是一个“模板字符串到JS函数的转译”。

React 的 JSX 也大同小异:JSX 语法糖最终被 Babel 转译成 `React.createElement` 嵌套调用,这个和 `{foreach}{/foreach}`被替换为 `<?php foreach ?>` 是同构的。

所以你看,所谓模板引擎,核心就是两个能力:

1. 识别模板中的变量/结构语法。
2. 用目标语言的能力执行生成出的代码(或直接生成一份对应语言的渲染函数)。

前端模板语法里那句“不要写复杂逻辑,只要简单表达式和循环判断”的社区共识,并不是因为模板引擎开发者不想支持,而是因为解析器能力边界和最佳实践的限制。模板就应该是“从数据到展示”的轻量映射层,不承载业务逻辑。

结尾:从手写到理解,再去拥抱框架

自己写完一个几十行的 PHP 模板引擎,你会有一种“原来框架里那段‘自研模板’也没多复杂”的释然感。当然,生产环境中你不会去手写它——你已经有了 Blade、Twig、Vue、Svelte 这些成熟选择。

但理解这块拼图之后,你会发现很多东西底层原先是相通的:**从前,我们都是模板字符串 / 模板语法;现在,无非是在编译的前后步骤里多了 AST 和 DOM diff 而已。**

手里的轮子自己造一遍,只是为了一眼看穿它背后那套简洁逻辑。

全部回复 0

还没有回复,来抢沙发~