用 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 作为绑定语言。同一份视图既可以直接在浏览器中执行(客户端模板),也可以在服务端执行(服务端模板)。
这一设计带来两个关键特性:
- 单份模板、双端复用:视图以 HTML 标记(markup)书写,随后被预编译(precompiled)为 JavaScript(客户端模板)和 Java(服务端模板)两套产物,避免为浏览器与服务端各写一套视图代码;
- 渲染形态灵活:客户端模板被编译成纯 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.clientPrefix | todos.views | 客户端编译产物的命名空间前缀 |
duel.serverPrefix | com.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(); }这条调用链清晰体现了"双端模板"的客户端形态:
- 取数:
todos.model.viewData(curFilter())根据当前 URL hash 过滤(all/active/completed)得到视图数据; - 渲染:
todos.views.TodoApp(data).toDOM()调用编译好的客户端模板函数,并以toDOM()方式产出 DOM 对象——对应示例文档中"本示例为视图生成 DOM 对象"的说明; - 挂载:通过
replaceChild或insertBefore将新 DOM 替换/插入页面,属于典型的全量重渲染式交互。
视图刷新由"穷人版路由"驱动:addEventListener('hashchange', refreshView, false)监听 URL hash 变化,切换过滤条件即触发重渲染。
数据层位于 src/main/webapp/js/todos/model.js,以localStorage(key 为'todos-duel')作为持久化存储,提供add、edit、toggle、toggleAll、remove、expunge等操作,并通过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...js、cdn/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-runtime、duel-staticapps与slf4j-jdk14三个运行时库。
标准构建
在包含pom.xml的目录(即examples/duel/)执行:
mvn clean packageMaven 将自动下载依赖、清理先前构建产物,并依次执行 DUEL 编译、资源合并、静态应用生成三个插件目标,最终在www/目录产出静态应用。
可调试运行
若希望在开发期获得可调试的版本,使用 Tomcat 7 作为 Web 服务器:
mvn tomcat7:run随后在浏览器中访问 http://localhost:8080/ 即可。pom.xml中tomcat7-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.json与dev-mode-override=true两个初始化参数——前者指明静态应用配置来源,后者允许以开发模式覆盖staticapp.json中的isDevMode设置,方便调试时即时反映改动。
小结:DUEL 的"一份视图,双端执行"
回顾整个示例,DUEL 的工程形态可概括为一条编译流水线:
- 编写:以 HTML + 纯 JavaScript 绑定语言书写
.duel视图; - 编译(
duel-maven-plugin):同时产出客户端 JS 模板(todos.views.*)与服务端 Java 模板(com.todomvc.duel.views,落入target/generated-sources/duel/,仅用于调试); - 合并(
merge-maven-plugin):按scripts.merge/styles.merge清单生成带哈希指纹的 CDN 资源; - 生成(
duel-staticapps-maven-plugin):依据staticapp.json渲染首页并输出纯静态www/; - 运行:浏览器中客户端模板以
toDOM()直接渲染 DOM,配合hashchange路由与 localStorage 模型完成全部 TodoMVC 交互;开发期可借助mvn tomcat7:run在localhost: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),仅供参考