|
| 1 | +# Under the Hood: The Philosophy and Mechanics of HTML Templates |
| 2 | + |
| 3 | +This guide explores the "why" and "how" behind the HTML template feature in Neo.mjs. It's a deep dive into an architecture |
| 4 | +designed to deliver on a core framework promise: a zero-builds, instant-feedback development mode that doesn't sacrifice |
| 5 | +production performance. |
| 6 | + |
| 7 | +This serves as a companion to the [Using HTML Templates](./HtmlTemplates.md) guide, which focuses on syntax |
| 8 | +and best practices. |
| 9 | + |
| 10 | +## The Core Philosophy: Why Not JSX? |
| 11 | + |
| 12 | +One of the most critical design goals for Neo.mjs is to provide a **zero-builds development environment**. We believe |
| 13 | +that developers should be able to write code and see their changes instantly in the browser, without a mandatory |
| 14 | +compilation step. This philosophy directly informed our approach to UI templating. |
| 15 | + |
| 16 | +Frameworks like React and Angular rely on non-standard syntax (JSX, Angular templates) that **must** be compiled into |
| 17 | +valid JavaScript. This requirement for a build step, even in development, introduces complexity and slows down |
| 18 | +the feedback loop. |
| 19 | + |
| 20 | +Neo.mjs chose a different path: **Tagged Template Literals**. This is a standard, native JavaScript feature. By using |
| 21 | +`html`... ``, we are not inventing a new language; we are leveraging the power of JavaScript itself. This allows for: |
| 22 | + |
| 23 | +1. **True Zero-Builds Development:** Your template code runs directly in the browser. What you write is what you get, |
| 24 | + with no hidden magic or required transformations. |
| 25 | +2. **No Special Directives:** Logic isn't handled by template-specific directives like `n-if` or `n-for`. You use |
| 26 | + standard JavaScript (`if/else`, `map()`) for all conditionals and loops, which is more powerful and familiar. |
| 27 | +3. **Architectural Purity:** The template is just a function call that returns a data structure (a VDOM object). |
| 28 | + This maintains a clean separation between your view definition and the framework's rendering engine. |
| 29 | + |
| 30 | +## Mechanism 1: The Zero-Builds Development Experience |
| 31 | + |
| 32 | +In development mode, templates must be parsed at runtime. This is where the trade-off for instant feedback becomes |
| 33 | +apparent. |
| 34 | + |
| 35 | +### Conditional Loading: A Smart Optimization |
| 36 | + |
| 37 | +To parse HTML strings, we need an HTML parser. Neo.mjs uses `parse5`, a robust and spec-compliant library. However, at |
| 38 | +~176KB, we don't want to load it unless absolutely necessary. |
| 39 | + |
| 40 | +This is why the parser is **only loaded if a component on the page actually uses an HTML template**. This check happens |
| 41 | +inside the `initAsync` method of `Neo.functional.component.Base`. |
| 42 | + |
| 43 | +```javascript |
| 44 | +// src/functional/component/Base.mjs |
| 45 | +async initAsync() { |
| 46 | + await super.initAsync(); |
| 47 | + |
| 48 | + if (this.enableHtmlTemplates && Neo.config.environment === 'development') { |
| 49 | + if (!Neo.ns('Neo.functional.util.HtmlTemplateProcessor')) { |
| 50 | + const module = await import('../util/HtmlTemplateProcessor.mjs'); |
| 51 | + this.htmlTemplateProcessor = module.default |
| 52 | + } |
| 53 | + } |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +If `enableHtmlTemplates` is true, the component dynamically imports the `HtmlTemplateProcessor`, which in turn pulls in |
| 58 | +`parse5`. This ensures that applications not using this feature pay no performance penalty. |
| 59 | + |
| 60 | +### The Runtime Parsing Process |
| 61 | + |
| 62 | +When a component's `createVdom()` method returns an `HtmlTemplate` object, it's handed off to the `HtmlTemplateProcessor`. |
| 63 | +You can inspect its source code here: |
| 64 | +[src/functional/util/HtmlTemplateProcessor.mjs](../../../../src/functional/util/HtmlTemplateProcessor.mjs). |
| 65 | + |
| 66 | +The processor executes a series of steps to convert the template literal into a VDOM object, which are detailed in the |
| 67 | +expandable section below. |
| 68 | + |
| 69 | +<details> |
| 70 | +<summary>Detailed Runtime Parsing Steps</summary> |
| 71 | + |
| 72 | +1. **Flattening:** It recursively flattens any nested templates into a single string and a corresponding array of |
| 73 | + dynamic values. |
| 74 | +2. **Placeholder Injection:** It replaces dynamic values (e.g., event handlers, component configs, other components) |
| 75 | + with special placeholders in the string (e.g., `__DYNAMIC_VALUE_0__`, `neotag1`). |
| 76 | +3. **Self-Closing Tag Conversion:** Since `parse5` does not handle self-closing custom element tags, a regular |
| 77 | + expression adds explicit closing tags (e.g., `<MyComponent />` becomes `<MyComponent></MyComponent>`). |
| 78 | +4. **Parsing:** The sanitized HTML string is parsed into a standard AST using `parse5.parseFragment()`. |
| 79 | +5. **VDOM Conversion:** The processor traverses the `parse5` AST and converts it back into a Neo.mjs VDOM object. |
| 80 | + During this process, it re-inserts the original dynamic values from the `values` array, preserving their rich data |
| 81 | + types (functions, objects, etc.). It also carefully reconstructs the original case-sensitive tag names for custom |
| 82 | + components. |
| 83 | + |
| 84 | +</details> |
| 85 | + |
| 86 | +Once the VDOM is constructed, it's passed back to the component's `continueUpdateWithVdom()` method, and the standard |
| 87 | +rendering lifecycle proceeds. |
| 88 | + |
| 89 | +## Mechanism 2: Maximum Performance for Production |
| 90 | + |
| 91 | +For production, the goal is to achieve the exact same VDOM output as the development mode, but with **zero runtime |
| 92 | +parsing overhead**. This is accomplished with a powerful build-time AST (Abstract Syntax Tree) transformation. |
| 93 | + |
| 94 | +This work is handled by two main scripts: |
| 95 | + |
| 96 | +- [buildScripts/util/templateBuildProcessor.mjs](../../../../buildScripts/util/templateBuildProcessor.mjs): |
| 97 | + Contains the core logic for parsing the template string and converting it to a serializable VDOM object. |
| 98 | +- [buildScripts/util/astTemplateProcessor.mjs](../../../../buildScripts/util/astTemplateProcessor.mjs): |
| 99 | + Orchestrates the overall process of reading a JS file, finding `html` templates, and replacing them with the final |
| 100 | + VDOM object via AST manipulation. |
| 101 | + |
| 102 | +### The AST Transformation Process |
| 103 | + |
| 104 | +The `astTemplateProcessor.mjs` script is a marvel of build-time engineering. Instead of just doing a simple text |
| 105 | +replacement, it performs a full AST transformation to ensure 100% accuracy. |
| 106 | + |
| 107 | +1. **Parse Code:** It uses `acorn` to parse the JavaScript file content into an AST. |
| 108 | +2. **Find Templates:** It traverses the AST to find all `html` tagged template expressions. |
| 109 | +3. **Process Template:** Each template is processed by `templateBuildProcessor.mjs`, which converts the HTML-like syntax |
| 110 | + into a serializable VDOM object. |
| 111 | +4. **Convert to AST:** The resulting VDOM object is converted back into a valid AST `ObjectExpression` node. |
| 112 | +5. **Replace Node:** The original `TaggedTemplateExpression` is replaced in the main AST with the new `ObjectExpression`. |
| 113 | +6. **Generate Code:** The modified AST is converted back into a string of JavaScript code using `astring`. |
| 114 | + |
| 115 | +As a developer convenience, if a template is the return value of a method named `render`, the build script automatically |
| 116 | +renames the method to `createVdom`. |
| 117 | + |
| 118 | +### Integration with Build Environments |
| 119 | + |
| 120 | +This logic is seamlessly integrated into all three of Neo.mjs's production build environments: |
| 121 | + |
| 122 | +- **`dist/esm`:** The [buildScripts/buildESModules.mjs](../../../../buildScripts/buildESModules.mjs) script directly |
| 123 | + invokes the `processFileContent` function from the `astTemplateProcessor` for each JavaScript file before minification. |
| 124 | +- **`dist/dev` & `dist/prod`:** These environments use Webpack. The transformation is handled by a custom loader: |
| 125 | + [buildScripts/webpack/loader/template-loader.mjs](../../../../buildScripts/webpack/loader/template-loader.mjs). |
| 126 | + This loader is strategically applied **only to the App worker's build configuration**, an optimization that saves |
| 127 | + build time by not processing code for other workers. |
| 128 | + |
| 129 | +### Key Differences from Runtime Parsing |
| 130 | + |
| 131 | +The build-time process is fundamentally different from the runtime parsing: |
| 132 | + |
| 133 | +- **No Lexical Scope:** The build script cannot access runtime variables like `this`. It captures the raw code (e.g., |
| 134 | + `this.name`) as a string. |
| 135 | +- **Placeholder Wrapping:** These code strings are wrapped in special placeholders (e.g., |
| 136 | + `##__NEO_EXPR__this.name##__NEO_EXPR__##`). |
| 137 | +- **Custom Resolution:** During the VDOM-to-AST conversion, the `jsonToAst` function uses `acorn.parseExpressionAt` |
| 138 | + to parse these placeholders back into proper AST nodes, perfectly preserving the original expressions for runtime |
| 139 | + evaluation. |
| 140 | +- **Component Tag Handling:** A tag like `<MyComponent>` is converted into a placeholder object |
| 141 | + (`{ __neo_component_name__: 'MyComponent' }`) which the `astTemplateProcessor` turns into a plain `Identifier` in |
| 142 | + the final AST. |
| 143 | + |
| 144 | +## Conclusion: The Neo.mjs Advantage |
| 145 | + |
| 146 | +The dual-mode approach to HTML templates is a perfect example of the Neo.mjs philosophy in action. It provides: |
| 147 | + |
| 148 | +- **An Unmatched Developer Experience:** The zero-builds development mode offers an instant feedback loop that is |
| 149 | + impossible in frameworks requiring mandatory compilation. You write standard JavaScript and it simply works. |
| 150 | +- **Maximum Production Performance:** The build-time AST transformation ensures that your production code is as fast |
| 151 | + as possible, with no client-side parsing overhead. The `parse5` library is completely eliminated from your final bundle. |
| 152 | +- **Architectural Consistency:** The system is designed to produce the exact same VDOM structure in both modes. |
| 153 | + This eliminates a whole class of bugs where development and production environments behave differently. |
| 154 | + |
| 155 | +This architecture isn't just a feature; it's a statement. It demonstrates a commitment to web standards, developer |
| 156 | +productivity, and end-user performance that sets Neo.mjs apart from the crowd. |
0 commit comments