本站在允许 JavaScript 运行的环境下浏览效果更佳

解决 Vite 破坏 Thymeleaf 模板内联 JS & CSS 的方法

前言

Thymeleaf 有一个叫自然模板(natural templates)的概念。如官方文档中提到的内联 Javascript 自然模板CSS 自然模板

官方文档中给出的示例如下:

<script th:inline="javascript">
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>

<style th:inline="css">
    .main\ elems {
      text-align: /*[[${align}]]*/ left;
    }
</style>

可以发现,自然模板都是通过注释实现的。当你在 vite 将模板 HTML 作为入口文件(在 vite.config.ts 中通过 build.rollupOptions.input 指定),以方便进行 Tailwindcss 类名混淆、Tree Shaking 优化、代码压缩、CSS 拆分等处理时,会遇到自然模板被 Vite 破坏的问题。针对此类情况,我为读者提供以下解决方案。

解决方案

我们将要跳过处理的内联块分为三类:

  1. 内联 script 元素,无 type="module"
  2. 内联 script 元素,有 type="module"
  3. 内联 style 元素。

内联 script 元素,无 type="module"

根据 Vite 文档的说明,<script src> 不会被处理,因此这种情况无需额外操作:

<script th:inline="javascript">
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>

内联 script 元素,有 type="module"

根据 Vite 文档说明,<script type="module" src> 是在处理范围内的。同时从 Vite 文档得知,仅需在元素上添加 vite-ignore 即可跳过处理。

即以下示例:

<script vite-ignore type="module" th:inline="javascript">
    var username = /*[[${session.user.name}]]*/ "Gertrud Kiwifruit";
</script>

内联 style 元素

经研究,Vite 并未提供跳过处理内联 style 的方法。我尝试开发一个运行尽可能早的 Vite 插件获取内联 style 元素,内容也是被压缩处理后的。
根据 Thymeleaf 文档可知有一个叫 prototype-only comment blocks(仅用于原型的注释块) 的标记方法:

<span>hello!</span>
<!--/*/
  <div th:text="${...}">
    ...
  </div>
/*/-->
<span>goodbye!</span>

其解释为:Thymeleaf 允许定义一种特殊的注释块,当模板以静态方式(即作为原型)打开时,它们被视为注释;但在 Thymeleaf 执行模板时,它们会被当作正常标记处理。

这意味着 Thymeleaf 的解析系统会直接移除 <!--/*//*/--> 标记符,但会保留其中的内容,使其不再处于注释状态。因此当模板执行时,Thymeleaf 实际会看到如下内容:

<span>hello!</span>
 
  <div th:text="${...}">
    ...
  </div>
 
<span>goodbye!</span>

在你未加载 HTML minify 插件时,Vite 不会动 HTML 内的注释,因此我们可以将内联 style 元素套在 <!--/*//*/--> 标记符中以跳过 Vite 的处理,即以下示例:

<!--/*/
<style th:inline="css">
    .main\ elems {
      text-align: /*[[${align}]]*/ left;
    }
</style>
/*/-->

如果你使用了 HTML minify 相关处理的插件,可以考虑开发一个 Vite 插件来避免其处理这些特殊注释。此处给出一个插件示例,不保证可用性。

用于应对 HTML minify 的 Vite 插件

将插件内容放入你项目的 ./plugins/vite-plugin-html-ignore-block.ts 文件中。
同时在 vite.config.tsplugins 数组中注册插件。

import vitePluginHtmlIgnoreBlock from './plugins/vite-plugin-html-ignore-block';

export default defineConfig({
  // ...其他配置
  plugins: [
    vitePluginHtmlIgnoreBlock(),
    // ...其他插件
  ],
  // ...其他配置
});

以下是插件内容,如果你要自行开发,请注意 enforce 字段和 transformIndexHtmlorder 字段,确保能够充分处理。

import type { Plugin } from 'vite';

export default function vitePluginHtmlIgnoreBlock(): Plugin[] {
  const rawMap = new Map<string, string>();
  let idx = 0;

  // 匹配 <!--/*/ ... /*/-->,支持跨行
  const blockReg = /<!--\/\*\/([\s\S]*?)\/\*\/-->/g;

  // pre 阶段:替换为 <ignore-数字></ignore-数字>
  const pre: Plugin = {
    name: 'vite-plugin-html-ignore-block-pre',
    enforce: 'pre',
    transformIndexHtml: {
      order: 'pre',
      handler(html) {
        return html.replace(blockReg, (match) => {
          const tag = `ignore-${idx}`;
          rawMap.set(tag, match);
          idx++;
          return `<${tag}></${tag}>`;
        });
      }
    }
  };

  // post 阶段:还原 <ignore-数字></ignore-数字> 为原始内容
  const post: Plugin = {
    name: 'vite-plugin-html-ignore-block-post',
    enforce: 'post',
    transformIndexHtml: {
      order: 'post',
      handler(html) {
        let result = html;
        for (const [tag, raw] of rawMap.entries()) {
          const reg = new RegExp(`<${tag}></${tag}>`, 'g');
          result = result.replace(reg, raw);
        }
        return result;
      }
    }
  };

  return [pre, post];
}

其他

欢迎在评论区留言交流,祝你生活愉快。


0
上一篇 Fixing Vite Breaking Inline JS & CSS in Thymeleaf Templates
下一篇 我的博客,为什么是月更?