用 DUEL 双端模板引擎构建 TodoMVC:从 Maven 构建、客户端 DOM 渲染到 Tomcat 调试运行
2026/9/19 12:52:04 网站建设 项目流程

用 DUEL 双端模板引擎构建 TodoMVC:从 Maven 构建、客户端 DOM 渲染到 Tomcat 调试运行

【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc

本指南以 TodoMVC 仓库中的 DUEL 示例(examples/duel)为核心,系统讲解 DUEL(Duel-Sided Templating Engine,双端模板引擎)的核心思想、工程结构、构建流程与运行方式。读完本文,你将掌握:DUEL 如何用一份 HTML 模板同时生成客户端 JavaScript 与服务端 Java 视图、如何在纯 JavaScript 中把编译后的视图渲染为 DOM 对象,以及如何使用 Maven 完成静态应用构建并用 Tomcat 7 进行可调试运行。

DUEL 是什么:一个"双端"模板引擎

根据示例文档(examples/duel/readme.md)的官方定义:

DUEL 是一个双端模板引擎,使用 HTML 作为布局语言、100% 纯 JavaScript 作为绑定语言。同一份视图既可以直接在浏览器中执行(客户端模板),也可以在服务端执行(服务端模板)。

这一设计带来两个关键特性:

  1. 单份模板、双端复用:视图以 HTML 标记(markup)书写,随后被预编译(precompiled)为 JavaScript(客户端模板)和 Java(服务端模板)两套产物,避免为浏览器与服务端各写一套视图代码;
  2. 渲染形态灵活:客户端模板被编译成纯 JavaScript 函数,执行结果既可以渲染为文本标记(text markup),也可以渲染为 DOM 对象(DOM objects)。本示例采用后者——toDOM()直接产出 DOM 节点并插入页面。

以 TodoMVC 项目而言,这意味着"新增任务、切换过滤、编辑任务"等所有交互都发生在浏览器端,服务端视图仅作为调试辅助手段存在(详见下文"构建"一节)。

工程结构一览

DUEL 示例的目录组织如下(均相对于仓库根目录):

examples/duel/ ├── pom.xml # Maven 构建配置(DUEL 编译、合并、静态应用插件) ├── staticapp.json # 静态应用生成配置 ├── package.json # npm 脚本:拉取 TodoMVC 公共 CSS/JS 到 webapp ├── readme.md # 示例说明文档 └── src/main/webapp/ ├── WEB-INF/web.xml # 部署描述符:RoutingServlet 路由配置 ├── css/ │ ├── base.css # TodoMVC 基础样式 │ ├── index.css │ └── styles.merge # CSS 合并清单 └── js/ ├── lib/ │ ├── duel.js # DUEL 客户端运行时 │ └── todos.js # TodoMVC 公共基础脚本 ├── todos/ │ ├── model.js # 数据模型(localStorage 存储) │ └── controller.js # 控制器(事件处理与视图刷新) └── scripts.merge # JS 合并清单

其中www/目录(examples/duel/www)存放的是 Maven 构建后生成的静态应用产物,index.html与带哈希指纹的 CDN 资源都在其中;目录下还保留了src/main/webapp/js/todos/下的模型与控制器源码,作为理解渲染链路的第一手材料。

模板如何被编译:duel-maven-plugin

DUEL 视图的编译发生在 Maven 构建阶段,由 examples/duel/pom.xml 中的duel-maven-plugin负责:

<plugin> <groupId>org.duelengine</groupId> <artifactId>duel-maven-plugin</artifactId> <version>${duel.version}</version> <!-- 0.9.7 --> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <clientPrefix>${duel.clientPrefix}</clientPrefix> <serverPrefix>${duel.serverPrefix}</serverPrefix> <inputDir>${duel.sourceDir}</inputDir> <outputClientPath>${duel.clientPath}</outputClientPath> </configuration> </execution> </executions> </plugin>

对应的属性定义在pom.xml<properties>中,是理解编译产物的关键:

属性默认值(本示例)含义
duel.clientPrefixtodos.views客户端编译产物的命名空间前缀
duel.serverPrefixcom.todomvc.duel.views服务端 Java 产物的包名前缀
duel.sourceDir${project.basedir}/src/main/resources/views/DUEL 视图源码(.duel文件)所在目录
duel.clientPath/js/客户端 JS 产物输出路径

该插件的generate目标会读取src/main/resources/views/下的 DUEL 视图源码,生成两类产物:

  • 客户端 JS 模板:输出到outputClientPath(即/js/),生成后的视图命名空间为todos.views
  • 服务端 Java 模板:生成到target/generated-sources/duel/目录——这正是示例文档中"服务端模板仅用于调试"所指的产物位置。

此外,pom.xml<resources>配置中通过<exclude>**/*.duel</exclude>将 DUEL 视图源码从 WAR 的目标输出中排除,说明视图源码只是构建期输入,不会被打包进最终应用。

从 src/main/webapp/js/scripts.merge 这份合并清单可以直观看到编译产物的形状:

# compiled views /js/todos/views/Stats.js /js/todos/views/Task.js /js/todos/views/Tasks.js /js/todos/views/TodoApp.js

也就是说,本示例的四个视图(统计栏Stats、单条任务Task、任务列表Tasks、整体应用TodoApp)在构建期被编译为todos.views命名空间下的四个 JS 模块,随后由 merge 插件打包进最终静态资源。

客户端渲染:在纯 JavaScript 中调用编译视图

DUEL 的客户端模板编译产物是一个个 JavaScript 函数,调用它们即可得到渲染结果。本示例的渲染入口在 src/main/webapp/js/todos/controller.js 的refreshView()中:

function refreshView() { // get the data var data = todos.model.viewData(curFilter()); // build the view var view = todos.views.TodoApp(data).toDOM(); var old = find('.todoapp'); if (old) { // replace old task list old.parentNode.replaceChild(view, old); } else { // insert at top document.body.insertBefore(view, document.body.firstChild); } find('.new-todo').focus(); }

这条调用链清晰体现了"双端模板"的客户端形态:

  1. 取数todos.model.viewData(curFilter())根据当前 URL hash 过滤(all/active/completed)得到视图数据;
  2. 渲染todos.views.TodoApp(data).toDOM()调用编译好的客户端模板函数,并以toDOM()方式产出 DOM 对象——对应示例文档中"本示例为视图生成 DOM 对象"的说明;
  3. 挂载:通过replaceChildinsertBefore将新 DOM 替换/插入页面,属于典型的全量重渲染式交互。

视图刷新由"穷人版路由"驱动:addEventListener('hashchange', refreshView, false)监听 URL hash 变化,切换过滤条件即触发重渲染。

数据层位于 src/main/webapp/js/todos/model.js,以localStorage(key 为'todos-duel')作为持久化存储,提供addedittoggletoggleAllremoveexpunge等操作,并通过viewData(filter)返回视图所需的{ tasks, stats }数据结构。事件处理器则集中导出为todos.actions,覆盖回车新增(ENTER_KEY)、双击编辑、Esc 取消、勾选完成、清除已完成等 TodoMVC 标准交互。

构建静态应用:merge 与 staticapps 插件

除 DUEL 编译外,mvn clean package还会依次执行另外两个 DUEL 生态插件,把应用收敛为纯静态站点。

merge-maven-plugin:资源合并与 CDN 指纹

该插件读取 src/main/webapp/js/scripts.merge 与 src/main/webapp/css/styles.merge 两份清单,将列表中的 JS/CSS 依次合并,并依据内容生成带哈希的文件名(CDN 指纹)。pom.xml中相关配置:

<merge.cdnMapFile>/cdn.properties</merge.cdnMapFile> <merge.cdnRoot>/cdn/</merge.cdnRoot> <merge.cdnFiles>.ico .png .jpg .gif .cur .eot .woff .ttf .svg .svgz</merge.cdnFiles>

即:合并产物输出到/cdn/根下,cdn.properties记录"原路径 → 哈希文件名"的映射,字体图标等二进制资源同样纳入 CDN 处理。构建后的 examples/duel/www 目录中cdn/3aba0c24...jscdn/b12c1274...css即为带内容哈希的合并产物。

duel-staticapps-maven-plugin:生成静态应用

duel-staticapps-maven-plugin依据 examples/duel/staticapp.json 生成最终静态站点:

{ "targetDir": "www/", "sourceDir": "target/todomvc/", "serverPrefix" : "com.todomvc.duel.views", "cdnMap": "cdn", "cdnLinksMap": "cdnLinks", "cdnHost": "./", "isDevMode": false, "views": { "index.html": { "view": "HomePage", "data": {}, "extras": {} } }, "files": ["robots.txt", "favicon.ico"] }

关键配置项说明:

  • targetDir/sourceDir:输出目录与输入 WAR 目录;
  • views.index.html.view:指定首页由名为HomePage的服务端视图渲染(此时data为空对象,即服务端不注入业务数据,渲染结果只做页面骨架,这正是"服务端模板仅用于调试"的体现);
  • cdnHost: "./":CDN 资源使用相对路径引用,便于静态托管;
  • isDevMode: false:关闭开发模式,产出最终优化产物。

生成后的 examples/duel/www/index.html 即为最终静态页面:引用带哈希的 CSS/JS 文件,页面主体由浏览器端编译视图渲染,印证了"客户端模板负责全部交互渲染"的架构。

运行与调试:Tomcat 7

前置条件

示例文档明确要求Apache Maven 3作为构建工具(本示例使用 Maven 管理构建生命周期与插件);示例本身同时是一个 Maven Web 项目(pom.xml<packaging>war</packaging>),依赖duel-runtimeduel-staticappsslf4j-jdk14三个运行时库。

标准构建

在包含pom.xml的目录(即examples/duel/)执行:

mvn clean package

Maven 将自动下载依赖、清理先前构建产物,并依次执行 DUEL 编译、资源合并、静态应用生成三个插件目标,最终在www/目录产出静态应用。

可调试运行

若希望在开发期获得可调试的版本,使用 Tomcat 7 作为 Web 服务器:

mvn tomcat7:run

随后在浏览器中访问 http://localhost:8080/ 即可。pom.xmltomcat7-maven-plugin(版本 2.2)的配置为:应用路径/、端口8080、UTF-8 URI 编码,并将target/todomvc/作为 WAR 源码目录加载。该模式跑的是完整 WAR(包含 RoutingServlet 路由与静态资源),因此也覆盖了服务端模板的调试场景。

服务端路由配置

src/main/webapp/WEB-INF/web.xml 将根路径/映射到org.duelengine.duel.staticapps.RoutingServlet,并传入config-path=staticapp.jsondev-mode-override=true两个初始化参数——前者指明静态应用配置来源,后者允许以开发模式覆盖staticapp.json中的isDevMode设置,方便调试时即时反映改动。

小结:DUEL 的"一份视图,双端执行"

回顾整个示例,DUEL 的工程形态可概括为一条编译流水线:

  1. 编写:以 HTML + 纯 JavaScript 绑定语言书写.duel视图;
  2. 编译duel-maven-plugin):同时产出客户端 JS 模板(todos.views.*)与服务端 Java 模板(com.todomvc.duel.views,落入target/generated-sources/duel/,仅用于调试);
  3. 合并merge-maven-plugin):按scripts.merge/styles.merge清单生成带哈希指纹的 CDN 资源;
  4. 生成duel-staticapps-maven-plugin):依据staticapp.json渲染首页并输出纯静态www/
  5. 运行:浏览器中客户端模板以toDOM()直接渲染 DOM,配合hashchange路由与 localStorage 模型完成全部 TodoMVC 交互;开发期可借助mvn tomcat7:runlocalhost:8080上做可调试运行。

对于想深入研究的读者,可从以下仓库路径继续探索:构建配置见 examples/duel/pom.xml,静态应用配置见 examples/duel/staticapp.json,渲染入口与数据模型见 controller.js 与 model.js,最终产物见 examples/duel/www/index.html。该示例由 Stephen McKamey 基于 DUEL 引擎移植,与仓库中其他框架示例(React、Angular、Vue 等)共同构成 TodoMVC 的框架对比矩阵。

【免费下载链接】todomvcHelping you select a JavaScript framework - Todo apps for React.js, Angular, Vue and many more项目地址: https://gitcode.com/gh_mirrors/to/todomvc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询