Skip to main content

nargo_ssr/
lib.rs

1#![warn(missing_docs)]
2
3mod prefetch;
4mod router;
5
6use nargo_bundler::targets::js::{JsBackend, JsWriter};
7use nargo_ir::{IRModule, TemplateNodeIR};
8use nargo_types::{CompileMode, Result};
9use std::{
10    collections::{HashMap, HashSet},
11    hash::{Hash, Hasher},
12};
13
14pub use prefetch::PrefetchManager;
15pub use router::{RouteHandler, RouteMatch, Router};
16
17/// SSR 渲染缓存键
18type CacheKey = String;
19
20/// SSR 渲染缓存值
21type CacheValue = String;
22
23pub struct SsrBackend {
24    pub runtime_path: String,
25    pub resumable: bool,
26    pub mode: CompileMode,
27    pub cache: HashMap<CacheKey, CacheValue>,
28    pub cache_enabled: bool,
29}
30
31impl SsrBackend {
32    pub fn new(mode: CompileMode) -> Self {
33        let runtime_path = match mode {
34            CompileMode::Vue2 => "nargo".to_string(),
35            CompileMode::Vue => "vue".to_string(),
36        };
37        Self { runtime_path, resumable: false, mode, cache: HashMap::new(), cache_enabled: true }
38    }
39
40    pub fn with_resumable(mut self, resumable: bool) -> Self {
41        self.resumable = resumable;
42        self
43    }
44
45    pub fn with_cache(mut self, enabled: bool) -> Self {
46        self.cache_enabled = enabled;
47        self
48    }
49
50    pub fn clear_cache(&mut self) {
51        self.cache.clear();
52    }
53}
54
55impl Default for SsrBackend {
56    fn default() -> Self {
57        Self::new(CompileMode::Vue2)
58    }
59}
60
61impl SsrBackend {
62    pub fn generate(&mut self, ir: &IRModule) -> Result<String> {
63        // 生成缓存键
64        let cache_key = self.generate_cache_key(ir);
65
66        // 检查缓存
67        if self.cache_enabled {
68            if let Some(cached) = self.cache.get(&cache_key) {
69                return Ok(cached.clone());
70            }
71        }
72
73        let mut writer = JsWriter::new();
74        let mut used_core = HashSet::new();
75
76        // 1. Generate SSR Function Body
77        let mut body_writer = JsWriter::new();
78        self.generate_ssr_body(ir, &mut body_writer, &mut used_core)?;
79
80        // 2. Generate Imports
81        if !used_core.is_empty() {
82            let mut imports: Vec<_> = used_core.into_iter().collect();
83            imports.sort();
84            let import_source = if self.mode == CompileMode::Vue { "vue".to_string() } else { format!("{}/core", self.runtime_path) };
85            writer.write_line(&format!("import {{ {} }} from '{}';", imports.join(", "), import_source));
86            writer.newline();
87        }
88
89        // 3. Append Body
90        writer.append(body_writer);
91
92        let result = writer.finish().0;
93
94        // 缓存结果
95        if self.cache_enabled {
96            self.cache.insert(cache_key, result.clone());
97        }
98
99        Ok(result)
100    }
101
102    fn generate_cache_key(&self, ir: &IRModule) -> CacheKey {
103        // 基于模块名称和模板内容生成缓存键
104        let mut key = format!("{}-{:?}", ir.name, self.mode);
105        if let Some(template) = &ir.template {
106            key.push_str(&format!("-{}", template.nodes.len()));
107        }
108        key
109    }
110
111    fn generate_ssr_body(&self, ir: &IRModule, writer: &mut JsWriter, used_core: &mut HashSet<String>) -> Result<()> {
112        let _ = writer.write_block("export function render(ctx)", |writer| {
113            // Create a JsBackend instance for generating statements
114            let js_backend = JsBackend::new(false, false, None, self.mode);
115            let mut used_dom = HashSet::new();
116
117            // Include server and normal scripts
118            if let Some(script) = &ir.script_server {
119                for stmt in &script.body {
120                    JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
121                }
122            }
123            if let Some(script) = &ir.script {
124                for stmt in &script.body {
125                    JsWriter::generate_stmt(stmt, writer, ir, used_core, &mut used_dom, false);
126                }
127            }
128
129            // 优化:使用更高效的字符串构建方式
130            writer.write("let html = [];");
131            writer.newline();
132
133            if self.resumable {
134                // Serialize state for resumability
135                writer.write_line("html.push(`<script type=\"nargo/state\">${JSON.stringify(ctx)}</script>`);");
136            }
137
138            if let Some(template) = &ir.template {
139                let mut node_index = 0;
140                for node in &template.nodes {
141                    self.generate_node_ssr_optimized(node, writer, &mut node_index);
142                }
143            }
144
145            // 优化:使用 join 方法拼接字符串,减少内存分配
146            writer.write_line("return html.join('');");
147            Ok(())
148        });
149        Ok(())
150    }
151
152    fn generate_node_ssr_optimized(&self, node: &TemplateNodeIR, writer: &mut JsWriter, node_index: &mut usize) {
153        match node {
154            TemplateNodeIR::Element(el) => {
155                let current_index = *node_index;
156                *node_index += 1;
157
158                // 预分配字符串容量,减少内存分配
159                let mut start_tag = String::with_capacity(64);
160                start_tag.push_str("<");
161                start_tag.push_str(&el.tag);
162
163                // Add data-nargo-id for non-static elements or elements with dynamic content
164                if !el.is_static {
165                    start_tag.push_str(&format!(" data-nargo-id=\"{}\"", current_index));
166                }
167
168                for attr in &el.attributes {
169                    if !attr.is_directive {
170                        if attr.is_dynamic {
171                            writer.write_line(&format!("html.push('{}');", start_tag));
172                            writer.write_line(&format!("html.push('{}=\"' + ({}) + '\"');", attr.name, attr.value.as_deref().unwrap_or("")));
173                            start_tag.clear();
174                        }
175                        else {
176                            match &attr.value {
177                                Some(v) => start_tag.push_str(&format!(" {}=\"{}\"", attr.name, v)),
178                                None => start_tag.push_str(&format!(" {}", attr.name)),
179                            }
180                        }
181                    }
182                    else if self.resumable {
183                        // Handle event directives for resumability
184                        if attr.is_directive && (attr.name == "on" || attr.name.starts_with("@")) {
185                            let event_name = if attr.name == "on" {
186                                attr.argument.as_deref().unwrap_or("")
187                            }
188                            else {
189                                // Handle @click syntax
190                                attr.name.trim_start_matches('@')
191                            };
192
193                            if !event_name.is_empty() {
194                                // For the prototype, we assume the handler is in a chunk named after the component
195                                // and the handler name is the value of the attribute.
196                                let handler = attr.value.as_deref().unwrap_or("");
197                                if !handler.is_empty() {
198                                    start_tag.push_str(&format!(" on:{}=\"/assets/{}.js#{}\"", event_name, "test", handler));
199                                }
200                            }
201                        }
202                    }
203                }
204
205                if !start_tag.is_empty() {
206                    start_tag.push_str(">\n");
207                    writer.write_line(&format!("html.push('{}');", start_tag));
208                }
209
210                for child in &el.children {
211                    self.generate_node_ssr_optimized(child, writer, node_index);
212                }
213
214                writer.write_line(&format!("html.push('</{}>');", el.tag));
215            }
216            TemplateNodeIR::Text(text, _, _) => {
217                *node_index += 1;
218                // 预计算字符串长度,减少内存分配
219                let escaped_text = text.replace("'", "\\'");
220                writer.write_line(&format!("html.push('{}');", escaped_text));
221            }
222            TemplateNodeIR::Interpolation(expr) => {
223                let current_index = *node_index;
224                *node_index += 1;
225                // Wrap interpolation in a span with ID for hydration
226                writer.write_line(&format!("html.push('<span data-nargo-id=\"{}\">');", current_index));
227                writer.write_line(&format!("html.push({});", expr.code));
228                writer.write_line(&format!("html.push('</span>');"));
229            }
230            TemplateNodeIR::Comment(comment, _, _) => {
231                *node_index += 1;
232                let escaped_comment = comment.replace("'", "\\'");
233                writer.write_line(&format!("html.push('<!-- {} -->');", escaped_comment));
234            }
235            TemplateNodeIR::Hoisted(id) => {
236                *node_index += 1;
237                writer.write_line(&format!("html.push({});", id));
238            }
239            TemplateNodeIR::If(if_node) => {
240                let current_index = *node_index;
241                *node_index += 1;
242
243                // For SSR, we generate a JS if-else structure
244                writer.write_line(&format!("if ({}) {{", if_node.condition.code));
245                writer.indent();
246                for child in &if_node.consequent {
247                    self.generate_node_ssr_optimized(child, writer, node_index);
248                }
249                writer.dedent();
250
251                for (condition, nodes) in &if_node.else_ifs {
252                    writer.write_line(&format!("}} else if ({}) {{", condition.code));
253                    writer.indent();
254                    for child in nodes {
255                        self.generate_node_ssr_optimized(child, writer, node_index);
256                    }
257                    writer.dedent();
258                }
259
260                if let Some(alt) = &if_node.alternate {
261                    writer.write_line("} else {");
262                    writer.indent();
263                    for child in alt {
264                        self.generate_node_ssr_optimized(child, writer, node_index);
265                    }
266                    writer.dedent();
267                }
268                writer.write_line("}");
269            }
270            TemplateNodeIR::For(for_node) => {
271                let current_index = *node_index;
272                *node_index += 1;
273
274                // For SSR, we generate a JS loop
275                writer.write_line(&format!("({}).forEach(({}, {}) => {{", for_node.iterator.collection.code, for_node.iterator.item, for_node.iterator.index.as_deref().unwrap_or("_i")));
276                writer.indent();
277                for child in &for_node.body {
278                    self.generate_node_ssr_optimized(child, writer, node_index);
279                }
280                writer.dedent();
281                writer.write_line("});");
282            }
283        }
284    }
285
286    fn generate_node_ssr(&self, node: &TemplateNodeIR, writer: &mut JsWriter, node_index: &mut usize) {
287        match node {
288            TemplateNodeIR::Element(el) => {
289                let current_index = *node_index;
290                *node_index += 1;
291
292                let mut start_tag = format!("html += '<{}", el.tag);
293
294                // Add data-nargo-id for non-static elements or elements with dynamic content
295                if !el.is_static {
296                    start_tag.push_str(&format!(" data-nargo-id=\"{}\"", current_index));
297                }
298
299                for attr in &el.attributes {
300                    if !attr.is_directive {
301                        if attr.is_dynamic {
302                            start_tag.push_str(&format!(" {}=\"' + ({}) + '\"", attr.name, attr.value.as_deref().unwrap_or("")));
303                        }
304                        else {
305                            match &attr.value {
306                                Some(v) => start_tag.push_str(&format!(" {}=\"{}\"", attr.name, v)),
307                                None => start_tag.push_str(&format!(" {}", attr.name)),
308                            }
309                        }
310                    }
311                    else if self.resumable {
312                        // Handle event directives for resumability
313                        if attr.is_directive && (attr.name == "on" || attr.name.starts_with("@")) {
314                            let event_name = if attr.name == "on" {
315                                attr.argument.as_deref().unwrap_or("")
316                            }
317                            else {
318                                // Handle @click syntax
319                                attr.name.trim_start_matches('@')
320                            };
321
322                            if !event_name.is_empty() {
323                                // For the prototype, we assume the handler is in a chunk named after the component
324                                // and the handler name is the value of the attribute.
325                                let handler = attr.value.as_deref().unwrap_or("");
326                                if !handler.is_empty() {
327                                    start_tag.push_str(&format!(" on:{}=\"/assets/{}.js#{}\"", event_name, "test", handler));
328                                }
329                            }
330                        }
331                    }
332                }
333                start_tag.push_str(">';");
334                writer.write_line(&start_tag);
335
336                for child in &el.children {
337                    self.generate_node_ssr(child, writer, node_index);
338                }
339
340                writer.write_line(&format!("html += '</{}>';", el.tag));
341            }
342            TemplateNodeIR::Text(text, _, _) => {
343                *node_index += 1;
344                writer.write_line(&format!("html += '{}';", text.replace("'", "\\'")));
345            }
346            TemplateNodeIR::Interpolation(expr) => {
347                let current_index = *node_index;
348                *node_index += 1;
349                // Wrap interpolation in a span with ID for hydration
350                writer.write_line(&format!("html += '<span data-nargo-id=\"{}\">' + ({}) + '</span>';", current_index, expr.code));
351            }
352            TemplateNodeIR::Comment(comment, _, _) => {
353                *node_index += 1;
354                writer.write_line(&format!("html += '<!-- {} -->';", comment.replace("'", "\\'")));
355            }
356            TemplateNodeIR::Hoisted(id) => {
357                *node_index += 1;
358                writer.write_line(&format!("html += {};", id));
359            }
360            TemplateNodeIR::If(if_node) => {
361                let current_index = *node_index;
362                *node_index += 1;
363
364                // For SSR, we generate a JS if-else structure
365                writer.write_line(&format!("if ({}) {{", if_node.condition.code));
366                writer.indent();
367                for child in &if_node.consequent {
368                    self.generate_node_ssr(child, writer, node_index);
369                }
370                writer.dedent();
371
372                for (condition, nodes) in &if_node.else_ifs {
373                    writer.write_line(&format!("}} else if ({}) {{", condition.code));
374                    writer.indent();
375                    for child in nodes {
376                        self.generate_node_ssr(child, writer, node_index);
377                    }
378                    writer.dedent();
379                }
380
381                if let Some(alt) = &if_node.alternate {
382                    writer.write_line("} else {");
383                    writer.indent();
384                    for child in alt {
385                        self.generate_node_ssr(child, writer, node_index);
386                    }
387                    writer.dedent();
388                }
389                writer.write_line("}");
390            }
391            TemplateNodeIR::For(for_node) => {
392                let current_index = *node_index;
393                *node_index += 1;
394
395                // For SSR, we generate a JS loop
396                writer.write_line(&format!("({}).forEach(({}, {}) => {{", for_node.iterator.collection.code, for_node.iterator.item, for_node.iterator.index.as_deref().unwrap_or("_i")));
397                writer.indent();
398                for child in &for_node.body {
399                    self.generate_node_ssr(child, writer, node_index);
400                }
401                writer.dedent();
402                writer.write_line("});");
403            }
404        }
405    }
406}