HTML 模板技术与服务端渲染
HTML 模板技术与服务端渲染
引言
在现代前端开发生态中,HTML模板技术与服务端渲染(SSR)构成了连接前后端的重要桥梁。当单页应用(SPA)因其客户端渲染特性而面临首屏加载速度慢、白屏时间长和SEO不友好等问题时,服务端渲染技术提供了一种优雅的解决方案。
传统SPA虽然在交互体验上有优势,但在首次加载时需要下载大量JavaScript,由浏览器执行后才能生成可见内容,这不仅增加了用户等待时间,也使搜索引擎爬虫难以获取页面内容。服务端渲染通过在服务器生成完整HTML并发送到客户端,有效解决了这些问题。
本文将深入探讨HTML模板引擎的工作原理、实现机制以及在不同场景下的应用策略,帮助我们在面对复杂项目时能够设计出兼顾性能、SEO与开发效率的渲染方案。
模板引擎的基本原理
模板引擎如何工作
模板引擎本质上是一种将数据与模板结合生成HTML的工具。我们在开发中经常需要将相同的HTML结构应用于不同的数据集,而不是手动复制粘贴HTML并替换内容。模板引擎正是为解决这个问题而生。
其核心工作流程可概括为三个主要步骤:
- 模板解析:将包含特殊语法的模板字符串解析为结构化的中间表示
- 数据合并:将数据模型注入到模板结构中
- 输出生成:输出最终的HTML字符串
以下是一个简化的模板引擎实现示例,展示了其基本原理:
// 简化的模板引擎工作原理
function render(template, data) {// 1. 解析模板,识别特殊语法标记const tokens = parse(template);// 2. 用数据替换标记,生成最终HTMLreturn tokens.map(token => {if (token.type === 'text') return token.value;if (token.type === 'variable') return data[token.value] || '';// 处理其他类型的标记(条件、循环等)}).join('');
}// 模板解析函数
function parse(template) {const tokens = [];let current = 0;let text = '';// 一个非常简化的词法分析过程while (current < template.length) {// 检测开始标记 {{if (template[current] === '{' && template[current + 1] === '{') {if (text) tokens.push({ type: 'text', value: text });text = '';current += 2;let variable = '';// 收集变量名直到结束标记 }}while (template[current] !== '}' || template[current + 1] !== '}') {variable += template[current];current++;}tokens.push({ type: 'variable', value: variable.trim() });current += 2;} else {text += template[current];current++;}}if (text) tokens.push({ type: 'text', value: text });return tokens;
}
这个过程在专业的模板引擎中通常包含更复杂的词法分析、语法分析和代码生成三个阶段:
-
词法分析(Lexical Analysis):将模板字符串分割成一系列标记(tokens),如文本块、变量引用、控制语句等。这一阶段识别模板中的特殊标记和普通文本。
-
语法分析(Syntax Analysis):将标记流转换为抽象语法树(AST),表示模板的结构和层次关系。例如,循环和条件语句会创建树的分支节点。
-
代码生成(Code Generation):遍历AST,结合数据生成最终的HTML。现代模板引擎通常会将模板预编译为高效的JavaScript函数,避免运行时重复解析。
模板引擎的强大之处在于它支持各种控制结构,如条件渲染、循环、包含子模板等,这使得前端开发人员可以用声明式的方式描述界面,而不必手写命令式的DOM操作代码。
主流模板引擎对比
市场上存在多种模板引擎,每种都有其独特的语法和特性。理解它们的差异对于选择适合项目的工具至关重要:
特性 | EJS | Pug | Handlebars | Nunjucks |
---|---|---|---|---|
语法接近HTML | ✓ | ✗ | ✓ | ✓ |
支持条件渲染 | ✓ | ✓ | ✓ | ✓ |
支持循环 | ✓ | ✓ | ✓ | ✓ |
布局/继承 | 有限 | ✓ | 有限 | ✓ |
性能 | 高 | 中 | 中 | 高 |
学习曲线 | 低 | 中 | 低 | 低 |
选择模板引擎时需要考虑的因素包括:
-
团队熟悉度:如果团队已经熟悉某种模板语法,使用相同或相似语法的引擎可以减少学习成本。
-
语法偏好:有些开发者偏好接近HTML的语法(如EJS),而另一些则偏好简洁的缩进式语法(如Pug)。语法偏好会直接影响开发体验和效率。
-
功能需求:不同项目对模板引擎功能的需求不同。如果项目需要复杂的布局继承和组件复用,那么Pug或Nunjucks可能是更好的选择。
-
性能要求:在高流量应用中,模板渲染性能至关重要。EJS和经过预编译的Nunjucks通常提供更好的性能。
-
生态系统集成:某些框架可能对特定模板引擎有更好的支持。例如,Express框架默认支持多种模板引擎,而有些CMS系统可能专门设计为与特定模板引擎配合使用。
模板引擎的选择应该基于项目的具体需求和团队的技术栈,而不仅仅是跟随流行趋势。对于大型项目,进行小规模的概念验证测试也很有价值,可以验证模板引擎在实际场景中的表现。
EJS与Pug的深入剖析
EJS:熟悉中的强大
EJS(Embedded JavaScript)是一种流行的模板引擎,它保留了HTML的原始结构,同时允许开发者嵌入JavaScript代码来生成动态内容。EJS之所以受欢迎,很大程度上是因为它的语法对于熟悉HTML和JavaScript的开发者来说几乎没有学习曲线。
EJS模板看起来就像普通的HTML,但增加了特殊的标记来插入动态内容:
<!-- EJS语法示例 -->
<h1><%= title %></h1>
<ul><% users.forEach(function(user){ %><li><%= user.name %></li><% }); %>
</ul>
EJS的主要标记及其含义:
-
<%= ... %>
:输出转义后的变量值,防止XSS攻击。这是最常用的标记,适用于大多数场景。例如,用户提供的内容应始终使用此标记输出。 -
<%- ... %>
:输出原始未转义的内容。这在输出已知安全的HTML(如从数据库中检索的格式化内容)时非常有用,但对不可信内容使用此标记会带来安全风险。 -
<% ... %>
:执行JavaScript代码而不输出任何内容。这用于条件语句、循环和其他控制流结构。
EJS的优势在于它允许开发者使用完整的JavaScript功能,而不是学习模板引擎特定的受限语法。这意味着你可以在模板中使用任何JavaScript函数、条件逻辑或循环结构。
EJS在服务端渲染中的典型使用方式如下:
const ejs = require('ejs');
const express = require('express');
const app = express();// 设置EJS为视图引擎
app.set('view engine', 'ejs');
app.set('views', './views');app.get('/users', async (req, res) => {// 从数据库获取用户数据const users = await db.getUsers();// 渲染模板并发送响应res.render('users', {title: '用户列表',users: users,isAdmin: req.user && req.user.role === 'admin'});
});
虽然EJS简单易用,但它也有一些局限性。例如,它不直接支持布局继承(类似于其他引擎的模板扩展功能),虽然可以通过include部分模板来实现类似功能:
<%- include('header', { title: '用户列表' }) %><main><!-- 页面特定内容 -->
</main><%- include('footer') %>
这种方式虽然可行,但不如某些其他模板引擎的布局系统那么强大和灵活。
Pug:简约而不简单
Pug(原名Jade)采用了与HTML完全不同的缩进式语法,摒弃了传统HTML的尖括号和闭合标签,这使得模板更加简洁,但也增加了学习成本:
//- Pug语法示例
h1= title
uleach user in usersli= user.name
Pug的核心特性包括:
-
基于缩进的语法:使用缩进表示层次结构,无需闭合标签,使代码更简洁。
-
强大的布局系统:通过extends和block提供了完整的模板继承功能,便于维护一致的页面结构:
//- layout.pug
doctype html
htmlheadtitle #{title} - 我的网站block stylesbodyheaderh1 我的网站mainblock contentfooterp © 2023 我的公司//- page.pug
extends layoutblock styleslink(rel="stylesheet" href="/css/page.css")block contenth2= pageTitlep 这是页面内容
- 混合(Mixins):类似于函数,可以创建可重用的模板片段:
//- 定义一个产品卡片混合
mixin productCard(product).product-cardimg(src=product.image alt=product.name)h3= product.namep.price ¥#{product.price.toFixed(2)}button.add-to-cart 加入购物车//- 使用混合
.productseach product in products+productCard(product)
- 条件与循环:Pug提供了简洁的条件和循环语法:
//- 条件渲染
if user.isAdmina.admin-link(href="/admin") 管理面板
else if user.isEditora.editor-link(href="/editor") 编辑面板
elsep 您没有管理权限//- 循环
ul.product-listeach product, index in productsli(class=index % 2 === 0 ? 'even' : 'odd')= product.name
Pug通过预编译模板获得优秀性能,这在大规模应用中尤为重要。预编译将模板转换为高效的JavaScript函数,避免了运行时解析模板的开销:
// Node.js中使用Pug
const pug = require('pug');// 预编译模板为函数
const renderFunction = pug.compileFile('template.pug');// 多次使用同一编译函数
const html1 = renderFunction({ name: '张三' });
const html2 = renderFunction({ name: '李四' });
Pug特别适合需要大量模板复用的复杂项目,其布局继承和混合系统使得维护大型网站的一致性变得更加容易。然而,其缩进语法对新手不够友好,团队成员需要适应这种与HTML完全不同的写法。
在选择EJS还是Pug时,需要权衡各种因素。如果项目团队熟悉HTML和JavaScript,并且希望最小化学习曲线,EJS是更好的选择。如果项目复杂度高,需要强大的模板继承和组件复用功能,同时团队愿意适应新语法,那么Pug可能更合适。
服务端渲染(SSR)实现机制
SSR工作流程详解
服务端渲染是一个多步骤流程,从接收请求到返回完整HTML页面,每个环节都至关重要:
-
客户端发起HTTP请求:用户访问URL或点击链接,浏览器向服务器发送HTTP请求。
-
服务器路由处理:服务器根据URL路径将请求路由到相应的处理器。这一步通常由Web框架(如Express、Django或Rails)处理。
-
数据获取:处理器从各种数据源(数据库、API、文件系统等)获取渲染页面所需的数据。这可能涉及多个异步操作,如数据库查询或API调用。
-
模板选择与渲染:基于请求和数据,选择适当的模板,并将数据注入其中进行渲染。模板引擎将模板和数据转换为最终的HTML字符串。
-
HTML响应返回:服务器将渲染好的HTML作为HTTP响应发送给客户端,同时可能设置一些HTTP头(如缓存控制、内容类型等)。
-
客户端接收与处理:浏览器接收HTML并开始解析,显示页面内容。浏览器还会请求HTML中引用的其他资源(CSS、JavaScript、图片等)。
-
可选的激活(Hydration):如果使用现代前端框架,服务器可能同时发送JavaScript代码,在客户端接管页面交互,使静态HTML"活"起来。这个过程称为激活或水合(Hydration)。
这个流程的主要优势在于,浏览器接收到的是已经渲染好的HTML,可以立即显示内容,无需等待JavaScript加载和执行。这显著提升了首屏加载速度和用户体验,尤其是在网络条件不佳或设备性能有限的情况下。
实现简易SSR服务器
下面是一个使用Express和EJS实现的基本SSR服务器示例,它展示了服务端渲染的核心机制:
// 使用Express和EJS实现基本SSR服务器
const express = require('express');
const app = express();
const path = require('path');// 设置EJS为模板引擎
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));// 静态文件服务
app.use(express.static(path.join(__dirname, 'public')));// 路由处理 - 产品列表页
app.get('/products', async (req, res) => {try {// 从API或数据库获取数据const products = await fetchProducts(req.query.category);const categories = await fetchCategories();// 记录渲染时间,用于调试和性能监控const startTime = Date.now();// 使用EJS渲染页面res.render('products', {title: '产品目录',products,categories,user: req.user || null,query: req.query});console.log(`页面渲染耗时: ${Date.now() - startTime}ms`);} catch (error) {console.error('渲染产品页面失败:', error);res.status(500).render('error', { message: '无法加载产品数据' });}
});// 路由处理 - 产品详情页
app.get('/products/:id', async (req, res) => {try {const productId = req.params.id;const product = await fetchProductById(productId);if (!product) {return res.status(404).render('404', { message: '产品不存在' });}// 并行获取相关数据const [relatedProducts, reviews] = await Promise.all([fetchRelatedProducts(product.category, productId),fetchProductReviews(productId)]);res.render('product-detail', {title: product.name,product,relatedProducts,reviews,user: req.user || null});} catch (error) {console.error('渲染产品详情失败:', error);res.status(500).render('error', { message: '加载产品详情时出错' });}
});app.listen(3000, () => {console.log('SSR服务器运行在端口3000');
});// 模拟数据获取函数
async function fetchProducts(category) {// 实际项目中会从数据库或API获取const allProducts = [{ id: 1, name: '商品A', price: 99, category: 'electronics' },{ id: 2, name: '商品B', price: 199, category: 'electronics' },{ id: 3, name: '商品C', price: 299, category: 'clothing' }];if (category) {return allProducts.filter(p => p.category === category);}return allProducts;
}async function fetchCategories() {return ['electronics', 'clothing', 'home'];
}async function fetchProductById(id) {const products = await fetchProducts();return products.find(p => p.id === parseInt(id, 10));
}async function fetchRelatedProducts(category, excludeId) {const products = await fetchProducts(category);return products.filter(p => p.id !== parseInt(excludeId, 10));
}async function fetchProductReviews(productId) {return [{ id: 101, rating: 5, comment: '很好用!', user: '用户A' },{ id: 102, rating: 4, comment: '还不错', user: '用户B' }];
}
这个示例展示了SSR的几个关键实践:
-
错误处理:每个路由处理器都包含错误捕获机制,确保在数据获取或渲染失败时能够优雅地响应。
-
并行数据获取:使用Promise.all并行获取多个数据源,减少总等待时间。
-
条件渲染:基于请求参数(如类别过滤)调整渲染内容。
-
性能监控:记录渲染时间,便于后续性能优化。
-
状态码设置:根据情况返回适当的HTTP状态码(如404表示资源不存在)。
在实际生产环境中,还需要考虑更多因素,如:
- 缓存策略:对不常变化的页面实施缓存,减轻服务器负担
- 安全措施:防范XSS攻击、CSRF等安全威胁
- 响应压缩:使用gzip或brotli压缩响应内容,减少传输时间
- 负载均衡:在多服务器环境中分散请求处理
- 健康监控:监控服务器状态,及时发现并解决问题
服务端渲染虽然增加了服务器负载,但为用户提供了更好的初始加载体验,也便于搜索引擎爬取内容,在许多场景下这种权衡是值得的。
动态内容注入与性能优化
高效数据注入策略
在服务端渲染中,数据注入是关键环节。不当的数据获取和注入策略会导致渲染缓慢,影响用户体验和服务器负载。以下是一些优化策略:
// 低效数据注入示例
app.get('/products', async (req, res) => {// 问题1: 串行数据获取,每个请求必须等待前一个完成const products = await db.getAll(); // 可能返回大量记录const categories = await db.getAllCategories();const settings = await db.getSettings();// 问题2: 没有分页,可能传输过多不必要数据res.render('products', { products, categories, settings });
});// 优化后的数据注入
app.get('/products', async (req, res) => {// 解决方案1: 并行请求数据,减少总等待时间const [products, categories, settings] = await Promise.all([db.getProducts({ page: parseInt(req.query.page || '1', 10), limit: 20, // 实现分页category: req.query.category, // 支持过滤sort: req.query.sort || 'newest' // 支持排序}),categoryCache.get() || db.getCategoriesWithCache(), // 使用缓存settingsCache.get() // 从内存缓存获取]);// 解决方案2: 只注入当前页面所需数据// 解决方案3: 添加元数据,支持分页UI渲染res.render('products', { products: products.items,pagination: {currentPage: products.page,totalPages: products.totalPages,totalItems: products.total},categories,settings: filterClientSettings(settings) // 过滤敏感设置});
});// 缓存常用数据
const categoryCache = {data: null,lastUpdated: 0,ttl: 3600000, // 1小时缓存async get() {const now = Date.now();if (this.data && now - this.lastUpdated < this.ttl) {return this.data;}try {this.data = await db.getAllCategories();this.lastUpdated = now;return this.data;} catch (error) {console.error('刷新类别缓存失败:', error);return this.data; // 出错时返回旧数据,避免完全失败}}
};
这个优化示例展示了几种关键策略:
-
并行数据获取:使用Promise.all同时发起多个数据请求,显著减少等待时间。当多个数据源互相独立时,没有理由串行获取它们。
-
分页与过滤:实现适当的分页和过滤机制,只获取并传输当前页面真正需要的数据。这减少了数据库负担、网络传输和模板渲染时间。
-
数据缓存:对不频繁变化的数据(如网站设置、产品类别)实施缓存,避免重复查询数据库。缓存可以在多个级别实现,如内存缓存、Redis或CDN缓存。
-
数据精简:仅传输模板渲染所需的字段,避免将整个数据对象传递给模板,特别是当对象包含大量不需要显示的属性时。
-
错误弹性:添加适当的错误处理和降级策略,确保即使某些数据获取失败,页面仍然能够部分渲染,而不是完全崩溃。
这些优化策略的重要性会随着应用规模的增长而增加。对于高流量网站,毫秒级的优化可能意味着显著的服务器成本节约和用户体验改善。
模板片段与局部刷新
在现代Web应用中,用户期望流畅的交互体验,而不必为每个操作刷新整个页面。模板片段(Partials)和局部刷新技术可以兼顾SSR的SEO优势和SPA的交互体验:
<!-- main.ejs - 主页面模板 -->
<%- include('partials/header', { title }) %><main class="container" data-page="products"><div class="filter-bar"><%- include('partials/product-filters', { categories }) %></div><div class="product-container" id="product-list"><%- include('partials/product-list', { products, pagination }) %></div>
</main><%- include('partials/footer') %><!-- partials/product-list.ejs - 可独立渲染的产品列表片段 -->
<div class="products-grid"><% if (products.length > 0) { %><% products.forEach(product => { %><div class="product-card"><img src="<%= product.image %>" alt="<%= product.name %>"><h3><%= product.name %></h3><p class="price">¥<%= product.price.toFixed(2) %></p><button class="add-to-cart" data-id="<%= product.id %>">加入购物车</button></div><% }); %><% } else { %><p class="no-results">没有找到匹配的产品</p><% } %>
</div><div class="pagination"><% if (pagination.totalPages > 1) { %><% for (let i = 1; i <= pagination.totalPages; i++) { %><a href="?page=<%= i %>" class="page-link <%= pagination.currentPage === i ? 'active' : '' %>"data-page="<%= i %>"><%= i %></a><% } %><% } %>
</div>
// 支持局部刷新的API端点
app.get('/api/products', async (req, res) => {try {const products = await db.getProducts({page: parseInt(req.query.page || '1', 10),limit: 20,category: req.query.category,sort: req.query.sort || 'newest'});// 检查是否为AJAX请求if (req.xhr || req.headers.accept.includes('application/json')) {// AJAX请求,只返回产品列表HTML片段或JSON数据if (req.query.format === 'html') {// 返回HTML片段res.render('partials/product-list', { products: products.items,pagination: {currentPage: products.page,totalPages: products.totalPages}}, (err, html) => {if (err) return res.status(500).json({ error: '渲染失败' });res.json({ html });});} else {// 返回JSON数据,由客户端处理渲染res.json({products: products.items,pagination: {currentPage: products.page,totalPages: products.totalPages,totalItems: products.total}});}} else {// 常规请求,返回完整页面const categories = await categoryCache.get();res.render('main', { title: '产品目录',products: products.items, pagination: {currentPage: products.page,totalPages: products.totalPages},categories});}} catch (error) {console.error('获取产品数据失败:', error);if (req.xhr || req.headers.accept.includes('application/json')) {res.status(500).json({ error: '获取产品失败' });} else {res.status(500).render('error', { message: '加载产品数据时出错' });}}
});
客户端JavaScript配合实现无刷新交互:
// 客户端JavaScript - 实现分页和筛选的无刷新交互
document.addEventListener('DOMContentLoaded', function() {const productContainer = document.getElementById('product-list');// 如果不在产品页面,直接返回if (!productContainer) return;// 处理分页点击document.addEventListener('click', function(e) {// 检查是否点击了分页链接if (e.target.classList.contains('page-link')) {e.preventDefault();const page = e.target.dataset.page;loadProducts({ page });}});// 处理筛选变化const filterForm = document.querySelector('.filter-form');if (filterForm) {filterForm.addEventListener('submit', function(e) {e.preventDefault();const formData = new FormData(filterForm);const params = {category: formData.get('category'),sort: formData.get('sort'),page: 1 // 筛选时重置到第一页};loadProducts(params);});}// 加载产品的函数function loadProducts(params) {// 显示加载状态productContainer.classList.add('loading');// 构建查询参数const queryParams = new URLSearchParams(params);queryParams.append('format', 'html');// 发起AJAX请求fetch(`/api/products?${queryParams.toString()}`).then(response => {if (!response.ok) throw new Error('请求失败');return response.json();}).then(data => {// 更新产品列表HTMLproductContainer.innerHTML = data.html;// 更新浏览器历史和URLconst url = new URL(window.location);Object.entries(params).forEach(([key, value]) => {if (value) url.searchParams.set(key, value);else url.searchParams.delete(key);});history.pushState({}, '', url);// 移除加载状态productContainer.classList.remove('loading');// 滚动到顶部window.scrollTo({top: 0, behavior: 'smooth'});}).catch(error => {console.error('加载产品失败:', error);productContainer.innerHTML = '<p class="error">加载产品时出错,请刷新页面重试</p>';productContainer.classList.remove('loading');});}
});
这种混合渲染策略结合了服务端渲染和客户端交互的优势:
-
首次加载利用SSR:用户首次访问页面时,获得完整渲染的HTML,实现快速首屏加载和良好SEO。
-
后续交互使用AJAX:用户进行分页、筛选等操作时,只替换页面中需要更新的部分,避免完整页面刷新。
-
渐进增强:即使用户禁用了JavaScript,页面仍然可以通过常规链接点击正常工作,只是失去了无刷新交互体验。
-
灵活的响应格式:同一端点支持返回完整HTML、HTML片段或纯JSON数据,根据请求类型和格式参数动态调整。
-
维护导航历史:使用History API更新URL和浏览器历史,确保用户可以使用浏览器的前进/后退按钮导航。
这种方法在许多大型内容网站(如新闻网站、电商平台)中广泛应用,它在保持良好SEO的同时提供了更流畅的用户体验。
安全性挑战与解决方案
XSS漏洞防范详解
跨站脚本攻击(XSS)是Web应用中最常见的安全威胁之一,在服务端渲染和模板处理中尤其需要注意。当不可信的用户输入被直接插入到HTML中时,攻击者可能注入恶意JavaScript代码,从而窃取cookie、会话令牌或重定向用户到钓鱼网站。
模板引擎通常提供两种输出方式:转义输出和原始(非转义)输出。安全使用这些功能对防范XSS至关重要:
<!-- 不安全的模板 - EJS -->
<div class="user-comment"><%- userComment %></div> <!-- 直接输出未转义内容 --><!-- 安全的模板 - EJS -->
<div class="user-comment"><%= userComment %></div> <!-- 自动HTML转义 -->
在Pug中,类似的安全和不安全输出方式如下:
//- Pug中的安全输出
div.user-comment= userComment //- 自动转义
div.user-comment!= userComment //- 不转义,危险
不同场景下的正确转义选择:
-
用户生成内容:评论、个人资料描述、产品评价等用户输入的内容应始终使用转义输出(
<%= %>
或=
)。这是最重要的防护层,可以防止大多数XSS攻击。 -
受信任的HTML:当需要输出确认安全的HTML(如CMS编辑器生成的内容)时,可以使用非转义输出(
<%- %>
或!=
),但应该先对内容进行额外的安全过滤。 -
HTML属性:在属性中嵌入动态值时也需要注意转义:
<!-- 不安全的属性输出 -->
<input type="text" value="<%- userInput %>"><!-- 安全的属性输出 -->
<input type="text" value="<%= userInput %>">
除了使用模板引擎的内置转义功能外,还应考虑以下额外安全措施:
- 内容安全策略(CSP):通过HTTP头部或meta标签设置CSP可以限制页面可以加载的资源来源,防止XSS攻击的影响范围:
// 在Express应用中设置CSP头
app.use((req, res, next) => {res.setHeader('Content-Security-Policy',"default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' https://trusted-cdn.com; img-src 'self' data: https://trusted-cdn.com");next();
});
- 输入验证与净化:在服务器端对输入进行严格验证和净化,只接受预期的格式和内容:
const sanitizeHtml = require('sanitize-html');app.post('/comments', (req, res) => {// 净化HTML,只允许安全的标签和属性const sanitizedComment = sanitizeHtml(req.body.comment, {allowedTags: ['b', 'i', 'em', 'strong', 'a'],allowedAttributes: {'a': ['href']},allowedIframeHostnames: []});// 存储和使用净化后的内容db.saveComment({userId: req.user.id,content: sanitizedComment,createdAt: new Date()});res.redirect('/post/' + req.body.postId);
});
- X-XSS-Protection头:虽然现代浏览器已逐渐弃用此功能,但在支持的浏览器中仍可提供额外保护:
app.use((req, res, next) => {res.setHeader('X-XSS-Protection', '1; mode=block');next();
});
防止模板注入攻击
模板注入是另一种常见的安全威胁,它允许攻击者控制模板本身而不仅仅是模板中的数据。现代模板引擎通常实现了上下文隔离,但仍需采取措施防范:
// 危险:不要这样做
const template = req.query.template; // 用户可控制的模板
const html = ejs.render(template, data);// 安全:只允许使用预定义模板
const templateName = allowedTemplates.includes(req.query.template) ? req.query.template : 'default';
const html = ejs.renderFile(`./views/${templateName}.ejs`, data);
避免模板注入的最佳实践:
-
永不接受用户提供的模板:模板应该是应用程序的一部分,而不是由用户提供。如果需要用户自定义视图,应提供安全的配置选项而非直接使用用户提供的模板代码。
-
白名单模板名称:如果允许用户选择模板(如主题切换功能),使用白名单严格限制可用模板,并防止目录遍历攻击:
const path = require('path');app.get('/page/:template', (req, res) => {const allowedTemplates = ['home', 'about', 'contact', 'products'];const templateName = allowedTemplates.includes(req.params.template) ? req.params.template : 'home';// 防止目录遍历,确保只访问views目录中的文件const templatePath = path.join(__dirname, 'views', `${templateName}.ejs`);// 验证规范化路径仍在views目录内const viewsDir = path.join(__dirname, 'views');if (!templatePath.startsWith(viewsDir)) {return res.status(403).send('禁止访问');}res.render(templateName);
});
- 最小权限原则:模板应只有渲染所需的最小权限,避免在模板中执行系统命令或访问敏感API:
// EJS配置限制
app.engine('ejs', ejs.renderFile);
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.set('view options', {// 不允许模板包含的功能outputFunctionName: false,client: false,escape: function(markup) {// 自定义转义函数,增强安全性return typeof markup === 'string' ? markup.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''): markup;}
});
- 沙箱化模板执行:如果必须允许用户自定义模板,考虑使用沙箱环境执行模板,限制可访问的对象和函数:
const vm = require('vm');function renderSandboxedTemplate(template, data) {// 创建安全的上下文对象const sandbox = {// 只提供安全的函数和对象data: { ...data },helpers: {formatDate: (date) => new Date(date).toLocaleDateString(),escape: (str) => String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')},result: ''};// 安全的模板执行函数const script = new vm.Script(`result = \`${template}\`;`);// 在沙箱中执行const context = vm.createContext(sandbox);try {script.runInContext(context, { timeout: 100 }); // 设置执行超时return sandbox.result;} catch (err) {console.error('模板执行错误:', err);return '模板执行错误';}
}
综合实施这些安全措施可以显著降低XSS和模板注入攻击的风险。安全不是一次性的工作,而是一个持续的过程,需要随着新威胁的出现不断更新防护策略。
SEO优化与SSR
SSR对SEO的影响详解
搜索引擎优化(SEO)是选择服务端渲染的主要动机之一。尽管现代搜索引擎爬虫已有能力执行JavaScript,但它们仍然更倾向于直接分析HTML内容,因此SSR为SEO提供了明显优势。
SSR如何增强SEO:
-
完整内容立即可用:爬虫第一次访问就能获取完整HTML内容,无需执行JavaScript。这确保了所有内容都能被爬虫索引,即使是使用AJAX加载的内容。
-
更快的爬取速度:由于不需要执行JavaScript和等待异步数据加载,爬虫可以更快地抓取和索引页面。
-
更好的内容关联性:页面标题、描述、headings等SEO关键元素在首次加载时就包含在HTML中,确保它们与页面内容准确对应。
在SSR应用中实施SEO最佳实践:
// 为SEO优化的服务器响应头
app.use((req, res, next) => {// 设置适当的缓存控制,允许搜索引擎缓存内容res.setHeader('Cache-Control', 'public, max-age=300');// 支持条件请求,减少带宽使用res.setHeader('ETag', generateETag(req.url));// 添加规范链接,防止内容重复const protocol = req.headers['x-forwarded-proto'] || req.protocol;const host = req.headers['x-forwarded-host'] || req.get('host');const fullUrl = `${protocol}://${host}${req.originalUrl}`;res.locals.canonicalUrl = fullUrl;// 预先准备结构化数据res.locals.jsonLd = {"@context": "https://schema.org","@type": "WebPage","name": "我的网站","url": fullUrl};next();
});
模板中添加必要的SEO元素:
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title><%= title %> | 我的网站</title><meta name="description" content="<%= description %>"><!-- 规范链接,防止重复内容 --><link rel="canonical" href="<%= canonicalUrl %>"><!-- Open Graph标签,优化社交媒体分享 --><meta property="og:title" content="<%= title %>"><meta property="og:description" content="<%= description %>"><meta property="og:image" content="<%= socialImage %>"><meta property="og:url" content="<%= canonicalUrl %>"><meta property="og:type" content="website"><!-- Twitter卡片标签 --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="<%= title %>"><meta name="twitter:description" content="<%= description %>"><meta name="twitter:image" content="<%= socialImage %>"><!-- 结构化数据,增强搜索结果显示 --><script type="application/ld+json"><%- JSON.stringify(jsonLd) %></script>
</head>
<body><!-- 页面内容 -->
</body>
</html>
在路由处理中为每个页面设置个性化SEO信息:
app.get('/products/:id', async (req, res) => {try {const product = await db.getProductById(req.params.id);if (!product) {return res.status(404).render('404', { title: '产品未找到',description: '您访问的产品不存在或已被移除。' });}// 设置丰富的SEO元数据const pageData = {title: product.name,description: product.description.substring(0, 160), // 限制描述长度socialImage: product.images[0] || '/images/default-product.jpg',product};// 产品特定的结构化数据res.locals.jsonLd = {"@context": "https://schema.org","@type": "Product","name": product.name,"description": product.description,"image": product.images,"sku": product.sku,"mpn": product.mpn,"brand": {"@type": "Brand","name": product.brand},"offers": {"@type": "Offer","price": product.price,"priceCurrency": "CNY","availability": product.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"}};res.render('product-detail', pageData);} catch (error) {console.error('渲染产品详情失败:', error);res.status(500).render('error', { title: '服务器错误',description: '加载产品时发生错误,请稍后再试。' });}
});
SSR和静态生成的SEO比较
SSR和静态站点生成(SSG)都能提供良好的SEO效果,但各有优劣:
方面 | SSR | 静态生成(SSG) |
---|---|---|
内容新鲜度 | 实时生成,始终最新 | 构建时生成,可能过时 |
服务器负载 | 较高,每次请求都渲染 | 很低,只提供静态文件 |
构建时间 | 无构建时间 | 可能较长,尤其是大型站点 |
部署复杂度 | 需要运行Node.js服务器 | 简单,任何静态文件服务器即可 |
适用场景 | 动态内容/个性化内容 | 内容较为稳定的网站 |
SEO效果 | 优秀 | 极佳(潜在更好的页面速度) |
CDN兼容性 | 需要额外配置 | 天然兼容,易于缓存 |
对于SEO优化,两种方法的细微差别:
-
页面加载速度:由于SSG无需服务器动态生成内容,通常加载速度更快,这对SEO有积极影响,因为页面速度是搜索引擎排名因素之一。
-
内容更新频率:SSR可以确保搜索引擎始终抓取最新内容,特别适合内容频繁更新的站点。而SSG需要在内容变更后重新构建和部署。
-
个性化内容:SSR可以根据用户参数(如地理位置)提供个性化内容,而SSG在构建时就确定了所有内容。
选择SSR还是SSG应基于项目具体需求:
-
选择SSR的场景:
- 内容频繁更新(如新闻网站、实时数据展示)
- 需要用户个性化内容(如基于用户历史的推荐)
- 依赖于实时API数据
-
选择SSG的场景:
- 内容相对稳定(如公司网站、文档、博客)
- 性能优先级高于内容实时性
- 安全要求高,希望减少服务器暴露面
在实践中,许多现代框架支持混合方法,如Next.js的静态生成与增量静态再生成(ISR),允许在同一应用中使用不同渲染策略。
SSR与静态生成对比
SSR、SSG与CSR性能对比
三种主要渲染方式的性能特性各不相同:
客户端渲染(CSR):
- 初始加载:发送最小HTML → 加载JS → 执行JS → 获取数据 → 渲染内容
- 首屏时间较长,存在明显白屏期
- 后续导航非常快,不需要重新加载页面
- 服务器负载低,主要提供API数据
- 带宽使用高效,只传输必要数据
服务端渲染(SSR):
- 初始加载:服务器获取数据 → 渲染HTML → 发送完整HTML → 加载JS → 激活(Hydration)
- 首屏时间较短,用户立即看到内容
- 完全交互时间(TTI)可能较长,需等待JavaScript加载和激活
- 服务器负载高,需处理每个请求
- 可能重复传输数据(HTML中和JSON数据)
静态生成(SSG):
- 构建时:获取数据 → 预渲染所有页面 → 生成静态HTML
- 访问时:加载预渲染HTML → 加载JS → 激活(可选)
- 最快的首屏时间,页面已预渲染
- 可能最快的完全交互时间
- 几乎无服务器负载,只提供静态文件
- 部署简单,兼容所有静态托管服务
CSR Timeline:
初始HTML请求 ------> 接收小型HTML ------> 加载JS ------> 执行JS ------> API请求 ------> 渲染内容|V首次内容绘制(FCP)|V可交互时间(TTI)
SSR Timeline:
初始HTML请求 ------> 服务器处理(获取数据+渲染) ------> 接收完整HTML ------> 首次内容绘制(FCP) ------> 加载JS ------> 激活 ------> 可交互时间(TTI)
SSG Timeline:
初始HTML请求 ------> 接收预渲染HTML ------> 首次内容绘制(FCP) ------> 加载JS ------> 激活 ------> 可交互时间(TTI)
在各种网络条件和设备性能下的实际测量结果通常显示:
- 慢速网络:SSG > SSR > CSR
- 快速网络:SSG ≈ SSR > CSR
- 低性能设备:SSG > SSR > CSR
- 高性能设备:差异减小,但SSG和SSR仍优于CSR
何时选择SSR而非SSG
选择服务端渲染(SSR)而非静态生成(SSG)的决策涉及多个因素:
-
内容更新频率:当内容需要实时反映最新状态时,SSR是更合适的选择。例如:
- 电商网站的产品库存和价格
- 新闻网站的最新报道
- 社交媒体平台的实时内容流
-
个性化需求:当页面内容需要根据用户身份或状态定制时,SSR是必要的:
- 用户专属仪表板
- 基于用户历史的推荐内容
- 基于地理位置的本地化内容
-
数据来源:当页面依赖不同API的实时数据时,SSR可以保证数据最新:
- 显示实时市场数据的金融应用
- 整合多个外部API的聚合服务
- 实时分析或统计展示
-
路由动态性:当可能的URL路径不能预先确定时,SSR是更灵活的选择:
- 用户生成内容,如配置文件页面
- 复杂的搜索或筛选结果页面
- 参数极多的动态路由
-
构建时间考量:当页面数量极大时,SSG的构建时间可能变得不切实际:
- 大型电商平台的数百万产品页面
- 包含数年内容的大型媒体档案
在Next.js等现代框架中,可以实现混合渲染策略,根据不同页面的需求选择适当的渲染方式:
// Next.js中的混合渲染策略
// pages/static.js - 静态生成的页面
export async function getStaticProps() {const data = await fetchData();return {props: { data },// 增量静态再生成(ISR):1小时后重新生成revalidate: 3600};
}// pages/products/[id].js - 静态生成带有动态路径的页面
export async function getStaticPaths() {// 获取热门产品预渲染const popularProducts = await fetchPopularProducts();return {// 预渲染这些热门产品页面paths: popularProducts.map(p => ({ params: { id: p.id.toString() } })),// fallback: true 意味着其他产品页面将按需生成fallback: true};
}export async function getStaticProps({ params }) {const product = await fetchProductById(params.id);return {props: { product },revalidate: 60 // 1分钟更新频率};
}// pages/dashboard.js - 服务端渲染的个性化页面
export async function getServerSideProps(context) {// 验证用户会话const session = await getSession(context.req);if (!session) {return {redirect: {destination: '/login',permanent: false,},};}// 获取用户特定数据const userData = await fetchUserData(session.user.id);return {props: { user: session.user,userData}};
}
这种混合策略结合了各种渲染方式的优点:
- 静态页面享受最佳性能和缓存
- 增量静态再生成(ISR)保持内容相对新鲜,同时保留静态页面的性能优势
- 服务端渲染用于真正需要实时数据或个性化的页面
为获得最佳结果,应根据每个页面的具体需求选择最适合的渲染策略,而不是为整个应用使用单一方法。
实际案例:内容管理系统
案例需求与挑战
构建一个现代博客内容管理系统需要平衡多个目标:
- 高SEO效果:内容需要对搜索引擎完全可见
- 合理的服务器负载:系统应该能够处理流量高峰而不需要过度的服务器资源
- 良好的用户体验:内容应该快速加载并支持流畅的交互
- 支持动态功能:评论、点赞等交互功能需要实时响应
这些需求点之间存在潜在冲突:最佳SEO通常需要服务端渲染,但这会增加服务器负载;流畅的交互通常需要客户端渲染,但这可能影响SEO和首屏加载速度。
混合渲染方案详解
博客系统可以采用混合渲染策略,结合静态生成、服务端渲染和客户端交互的优势:
// 博客系统的Express实现示例
const express = require('express');
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');const app = express();
app.set('view engine', 'ejs');// 静态资源
app.use(express.static('public'));// 缓存控制中间件
function cacheControl(maxAge) {return (req, res, next) => {if (req.method === 'GET') {res.set('Cache-Control', `public, max-age=${maxAge}`);} else {res.set('Cache-Control', 'no-store');}next();};
}// 博客首页 - 动态渲染,包含最新内容
app.get('/', cacheControl(60), async (req, res) => {try {const latestArticles = await fetchLatestArticles();const featured = await fetchFeaturedArticles();res.render('home', {title: '博客首页',description: '最新文章和精选内容',latestArticles,featured,user: req.user});} catch (error) {console.error('渲染首页失败:', error);res.status(500).render('error');}
});// 博客文章页面 - 使用静态生成 + 动态评论
app.get('/blog/:slug', async (req, res) => {const slug = req.params.slug;try {// 尝试读取预生成的HTML(静态部分)const cacheDir = path.join(__dirname, 'cache', 'blog');const staticHtmlPath = path.join(cacheDir, `${slug}.html`);// 生成和验证ETagconst articleETag = `"article-${slug}-${fs.existsSync(staticHtmlPath) ? fs.statSync(staticHtmlPath).mtime.getTime() : Date.now()}"`;// 如果浏览器已有最新版本,返回304状态if (req.header('If-None-Match') === articleETag) {return res.status(304).end();}// 设置ETag响应头res.setHeader('ETag', articleETag);if (fs.existsSync(staticHtmlPath)) {// 获取动态内容(评论)const comments = await fetchComments(slug);// 是否为AJAX请求,只获取评论数据if (req.xhr || req.headers.accept.includes('application/json')) {return res.json({ comments });}// 读取缓存的静态HTMLlet html = fs.readFileSync(staticHtmlPath, 'utf8');// 注入动态评论组件所需数据html = html.replace('<!--COMMENTS_DATA-->',`<script>window.INITIAL_COMMENTS = ${JSON.stringify(comments)}</script>`);// 注入用户数据(如果已登录)if (req.user) {html = html.replace('<!--USER_DATA-->',`<script>window.USER = ${JSON.stringify({id: req.user.id,name: req.user.name,avatar: req.user.avatar})}</script>`);}return res.send(html);}// 缓存未命中,执行完整SSRconst article = await fetchArticle(slug);if (!article) return res.status(404).render('404');const comments = await fetchComments(slug);// 渲染完整页面res.render('blog/article', { title: article.title,description: article.excerpt,article, comments,user: req.user});// 异步缓存静态部分(不阻塞响应)ejs.renderFile(path.join(__dirname, 'views', 'blog', 'article.ejs'),{ title: article.title,description: article.excerpt,article, comments: [], user: null},(err, html) => {if (!err) {fs.mkdirSync(path.dirname(staticHtmlPath), { recursive: true });fs.writeFileSync(staticHtmlPath, html);}});} catch (error) {console.error('渲染错误:', error);res.status(500).render('error');}
});
客户端JavaScript部分示例:
// 博客文章页面的客户端JavaScript
document.addEventListener('DOMContentLoaded', function() {// 评论功能const commentForm = document.getElementById('comment-form');const commentsContainer = document.getElementById('comments-container');if (commentForm) {commentForm.addEventListener('submit', async function(e) {e.preventDefault();const contentInput = commentForm.querySelector('textarea');const content = contentInput.value.trim();const articleId = commentForm.dataset.articleId;if (content.length < 3) {showError('评论内容太短');return;}try {const response = await fetch('/api/comments', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ articleId, content }),credentials: 'same-origin'});if (!response.ok) {const data = await response.json();throw new Error(data.error || '提交评论失败');}const comment = await response.json();// 渲染新评论并添加到列表const commentElement = createCommentElement(comment);commentsContainer.insertBefore(commentElement, commentsContainer.firstChild);// 清空输入contentInput.value = '';// 显示成功消息showMessage('评论发布成功!');} catch (error) {showError(error.message || '提交评论时出错');}});}// 辅助函数:创建评论元素function createCommentElement(comment) {const div = document.createElement('div');div.className = 'comment';div.innerHTML = `<div class="comment-header"><img src="${comment.user.avatar || '/images/default-avatar.png'}" alt="${comment.user.name}" class="avatar"><div class="comment-meta"><div class="comment-author">${comment.user.name}</div><div class="comment-date">${formatDate(comment.createdAt)}</div></div></div><div class="comment-content">${escapeHTML(comment.content)}</div>`;return div;}
});
这套混合渲染方案提供了多层性能优化:
-
静态缓存层:文章内容预渲染为静态HTML,最大限度减少服务器负载
- 缓存文件保存在文件系统,避免重复渲染
- ETag支持有条件请求,减少带宽使用
- 缓存自动失效机制确保内容更新后及时反映
-
动态内容分离:将静态内容与动态内容(如评论)分离
- 静态内容可以长时间缓存
- 动态内容通过JavaScript异步加载
- 用户数据仅在客户端处理,保持页面可缓存
-
渐进式增强:即使没有JavaScript,基本功能也能工作
- 所有页面都能通过服务器渲染获得初始内容
- JavaScript增强交互性,而不是必需条件
- 支持无JS环境的评论查看(虽然评论提交需要JS)
-
按需渲染:首次访问时生成缓存,后续访问使用缓存
- 不常访问的文章不会消耗服务器资源
- 热门内容自动获得缓存支持
这种方案在各维度上达到了较好的平衡:SEO优化、服务器负载、用户体验和开发效率。
模板引擎性能优化技巧
模板预编译详解
模板引擎的一个常见性能瓶颈是模板解析和编译。每次渲染模板时重复执行这些步骤会浪费CPU资源。模板预编译可以显著提升性能,特别是在大规模应用中:
// EJS预编译示例
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');// 模板文件目录
const templateDir = path.join(__dirname, 'views');// 缓存编译后的模板函数
const templateCache = {};// 预编译并缓存所有模板
function precompileTemplates() {// 递归获取所有EJS文件function scanDirectory(dir) {const files = fs.readdirSync(dir);files.forEach(file => {const filePath = path.join(dir, file);const stat = fs.statSync(filePath);if (stat.isDirectory()) {scanDirectory(filePath);} else if (path.extname(file) === '.ejs') {// 读取模板文件const template = fs.readFileSync(filePath, 'utf8');const relativePath = path.relative(templateDir, filePath);// 编译并缓存模板函数templateCache[relativePath] = ejs.compile(template, {filename: filePath, // 用于包含其他模板cache: true,compileDebug: process.env.NODE_ENV !== 'production'});}});}scanDirectory(templateDir);console.log(`预编译完成,共${Object.keys(templateCache).length}个模板`);
}
预编译模板带来的性能提升可通过基准测试量化:
操作 | 未预编译 | 预编译 | 性能提升 |
---|---|---|---|
首次渲染 | 10ms | 8ms | 20% |
后续渲染 | 8ms | 0.5ms | 1500% |
1000次渲染 | 8000ms | 500ms | 1500% |
在生产环境中,预编译通常在以下场景中实施:
- 构建时预编译:在应用部署前,将模板编译为JavaScript函数并打包
- 服务启动时预编译:服务器启动时预编译所有模板并保存在内存中
- 按需编译并缓存:首次使用时编译,然后永久缓存编译结果
缓存策略详解
除了模板预编译外,适当的缓存策略也能显著提高渲染性能:
const NodeCache = require('node-cache');
const Redis = require('ioredis');// 内存缓存 - 用于热门页面
const pageCache = new NodeCache({ stdTTL: 600, // 10分钟过期checkperiod: 60, // 每分钟检查过期项maxKeys: 1000 // 最多缓存1000个页面
});// Redis缓存 - 用于分布式部署和持久化
const redisClient = new Redis({host: process.env.REDIS_HOST || 'localhost',port: process.env.REDIS_PORT || 6379
});// 中间件:分层页面缓存
function cachePageMiddleware(options = {}) {const {ttl = 600, // 默认10分钟keyPrefix = 'page:',useRedis = false,useMemory = true,varyByQuery = false} = options;return async (req, res, next) => {// 跳过非GET请求if (req.method !== 'GET') return next();// 如果需要个性化且用户已登录,跳过缓存if (req.user) return next();// 生成缓存键let cacheKey = keyPrefix + req.originalUrl;// 检查内存缓存if (useMemory) {const cachedPage = pageCache.get(cacheKey);if (cachedPage) {res.set('X-Cache', 'HIT-MEMORY');return res.send(cachedPage);}}// 检查Redis缓存if (useRedis) {try {const cachedPage = await redisClient.get(cacheKey);if (cachedPage) {res.set('X-Cache', 'HIT-REDIS');// 刷新内存缓存if (useMemory) {pageCache.set(cacheKey, cachedPage);}return res.send(cachedPage);}} catch (err) {console.error('Redis缓存读取错误:', err);}}// 缓存未命中,拦截响应发送const originalSend = res.send;res.send = function(body) {// 只缓存HTML响应const isHTML = typeof body === 'string' && (res.get('Content-Type')?.includes('text/html'));if (isHTML) {// 保存到内存缓存if (useMemory) {pageCache.set(cacheKey, body, ttl);}// 保存到Redis缓存if (useRedis) {redisClient.set(cacheKey, body, 'EX', ttl).catch(err => console.error('Redis缓存保存错误:', err));}res.set('X-Cache', 'MISS');}// 调用原始send方法originalSend.call(this, body);};next();};
}
通过引入多级缓存,可以显著减轻服务器负载并提高响应速度:
- 内存缓存:速度最快,适用于热门页面和小型应用
- Redis缓存:平衡速度和持久性,适用于分布式部署
- CDN缓存:适用于静态资源和可公开缓存的页面
- 浏览器缓存:通过合理HTTP头控制客户端缓存
缓存失效是缓存系统的关键环节,常见策略包括:
- 定时失效:设置合理的TTL自动过期
- 主动失效:内容变更时主动清除相关缓存
- 模式失效:通过模式匹配清除相关缓存(如清除特定分类的所有页面)
前后端协同开发策略
共享模板组件
在前后端共享组件可减少代码重复并提高一致性:
// components/ProductCard.js
module.exports = function(product) {return `<div class="product-card" data-id="${product.id}"><img src="${product.image}" alt="${product.name}"><h3>${product.name}</h3><p class="price">¥${product.price.toFixed(2)}</p><button class="add-to-cart">加入购物车</button></div>`;
};// 服务端使用
app.get('/products', async (req, res) => {const products = await fetchProducts();const ProductCard = require('./components/ProductCard');const productCardsHtml = products.map(p => ProductCard(p)).join('');res.render('products', { productCardsHtml });
});// 客户端使用(通过Webpack加载)
import ProductCard from './components/ProductCard';async function loadMoreProducts() {const response = await fetch('/api/products?page=2');const products = await response.json();const container = document.querySelector('.products-container');products.forEach(product => {const html = ProductCard(product);container.insertAdjacentHTML('beforeend', html);});
}
更复杂的组件可以采用通用JavaScript模板库(如Handlebars)实现更好的共享:
// components/ProductCard.js
const Handlebars = require('handlebars');// 注册自定义辅助函数
Handlebars.registerHelper('formatPrice', function(price) {return typeof price === 'number' ? price.toFixed(2) : '0.00';
});// 编译模板
const template = Handlebars.compile(`<div class="product-card" data-id="{{id}}"><img src="{{image}}" alt="{{name}}"><h3>{{name}}</h3><p class="price">¥{{formatPrice price}}</p>{{#if inStock}}<button class="add-to-cart">加入购物车</button>{{else}}<button class="notify-me" disabled>暂时缺货</button>{{/if}}</div>
`);// 导出渲染函数
module.exports = function(product) {return template(product);
};
这种方法的优势在于:
- 一致性保证:同一组件在服务器和客户端渲染结果完全一致
- 维护简化:修改组件只需在一处进行,自动反映在所有使用位置
- 性能优化:可以在服务器预渲染,在客户端重用相同模板进行局部更新
- 渐进增强:服务器渲染提供基本功能,客户端JavaScript添加交互
API与模板协作模式
当需要后续客户端交互时,SSR页面需要与API无缝协作。这通常采用"同构渲染"模式:
// 服务端:准备初始状态
app.get('/dashboard', async (req, res) => {// 验证用户是否登录if (!req.user) {return res.redirect('/login?next=/dashboard');}try {// 获取初始数据const initialData = await fetchDashboardData(req.user.id);// 处理数据格式,确保安全(移除敏感字段)const safeData = {user: {id: req.user.id,name: req.user.name,role: req.user.role},stats: initialData.stats,recentActivities: initialData.recentActivities};// 注入初始状态到页面res.render('dashboard', {title: '用户仪表板',description: '查看您的账户活动和统计数据',initialData: JSON.stringify(safeData).replace(/</g, '\\u003c')});} catch (error) {console.error('加载仪表板数据失败:', error);res.status(500).render('error', { message: '加载仪表板时出错' });}
});
模板文件(dashboard.ejs):
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title><%= title %></title><link rel="stylesheet" href="/css/dashboard.css">
</head>
<body><header><%- include('partials/header') %></header><main><!-- 放置初始渲染的仪表板 --><div id="dashboard" data-initial='<%= initialData %>'><!-- 静态渲染的初始内容,用于无JS环境 --><% const data = JSON.parse(initialData); %><div class="stats-container"><div class="stat-card"><h3>总访问量</h3><p class="stat-value"><%= data.stats.totalVisits %></p></div><!-- 其他统计卡片 --></div></div></main><footer><%- include('partials/footer') %></footer><!-- 客户端脚本 --><script src="/js/dashboard.js"></script>
</body>
</html>
客户端JavaScript(dashboard.js):
// 客户端接管渲染
document.addEventListener('DOMContentLoaded', function() {const dashboard = document.getElementById('dashboard');const initialData = JSON.parse(dashboard.dataset.initial);// 初始化客户端应用initDashboardApp(dashboard, initialData);// 设置轮询更新setInterval(async () => {try {const response = await fetch('/api/dashboard/updates');if (!response.ok) throw new Error('获取更新失败');const updates = await response.json();updateDashboard(updates);} catch (error) {console.error('更新仪表板失败:', error);showNotification('更新数据时出错,将在稍后重试', 'error');}}, 30000); // 每30秒更新一次
});
这种协作模式的优势:
- 最佳首屏体验:用户立即看到完整内容,无需等待JavaScript加载和执行
- 良好SEO:搜索引擎获取完整HTML内容
- 渐进增强:即使JavaScript失败,用户仍能看到基本内容
- 高效数据处理:避免二次请求,服务器已注入初始数据
- 无缝过渡:从服务器渲染到客户端交互无可见闪烁
未来趋势与最佳实践
增量静态再生成(ISR)
Next.js的ISR技术结合了静态生成和按需更新的优势:
// Next.js中的ISR实现
export async function getStaticProps() {const products = await fetchProducts();return {props: {products,generatedAt: new Date().toISOString()},// 关键配置:每600秒后重新生成revalidate: 600};
}export async function getStaticPaths() {// 预先生成热门产品页面const popularProducts = await fetchPopularProducts();return {paths: popularProducts.map(p => ({ params: { id: p.id.toString() } })),// 其他产品页首次访问时生成fallback: true};
}
ISR的工作原理:
- 构建时静态生成:在构建时为指定路径预渲染HTML
- 按需静态生成:对于未预渲染的路径,首次访问时生成并缓存
- 后台重新验证:在设定的时间间隔后,触发后台重新生成
- 平滑过渡:用户始终看到缓存版本,更新在后台进行
这种方法特别适合:
- 电商产品页面(数据偶尔变化)
- 内容管理系统(内容定期更新)
- 大型文档网站(内容相对稳定但偶有更新)
流式SSR与Progressive Hydration
最新的服务端渲染技术支持HTML流式传输和渐进式激活:
// React 18 的流式SSR示例
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';app.get('/', (req, res) => {const { pipe } = renderToPipeableStream(<App />,{bootstrapScripts: ['/client.js'],onShellReady() {// 发送页面框架,不等待所有数据加载res.setHeader('content-type', 'text/html');pipe(res);}});
});
与流式SSR密切相关的是渐进式激活(Progressive Hydration),这项技术允许页面按区块逐步激活,而不是等待所有JavaScript加载后一次性激活整个页面:
// React 组件示例 - 使用懒加载和Suspense实现渐进式激活
import React, { lazy, Suspense } from 'react';// 懒加载组件
const HeavyChart = lazy(() => import('./HeavyChart'));
const CommentSection = lazy(() => import('./CommentSection'));function ProductPage({ product }) {return (<div className="product-page">{/* 关键产品信息 - 立即渲染 */}<header><h1>{product.name}</h1><p className="price">${product.price}</p></header>{/* 次要内容 - 延迟加载和激活 */}<Suspense fallback={<div className="chart-placeholder">加载图表...</div>}><HeavyChart productId={product.id} /></Suspense><Suspense fallback={<div className="comments-placeholder">加载评论...</div>}><CommentSection productId={product.id} /></Suspense></div>);
}
这些技术的核心优势:
- 减少首次内容绘制时间:快速发送页面的骨架和首屏内容
- 增量处理大型页面:分批传输长列表或数据密集型组件的内容
- 优先处理重要内容:优先渲染关键UI部分,延迟渲染次要内容
- 降低服务器内存使用:服务器可以逐步处理和释放资源
通过将流式SSR和渐进式激活结合,可以实现最佳性能指标:
- FCP (First Contentful Paint) 更快:关键内容更早显示
- TTI (Time to Interactive) 更早:核心功能更快可用
- CLS (Cumulative Layout Shift) 更小:内容结构预先确定
- TBT (Total Blocking Time) 更短:主线程不被单个大型JavaScript bundle阻塞
总结与实践建议
关键点回顾
-
模板引擎基础:模板引擎如EJS和Pug通过不同语法风格提供数据与视图分离的能力,选择应基于项目需求和团队熟悉度。
-
SSR工作机制:服务端渲染通过在服务器生成完整HTML并发送到客户端,解决了首屏加载速度和SEO挑战,但增加了服务器负载。
-
安全考量:在处理模板时,数据转义和输入验证至关重要,可防止XSS和模板注入攻击等安全问题。
-
性能优化策略:模板预编译、多层缓存、流式传输等技术可显著提升渲染性能和用户体验。
-
渲染模式对比:SSR、SSG、CSR和混合渲染各有优劣,选择应基于具体场景需求。
-
前后端协作:通过共享组件和同构渲染可实现前后端无缝协作,提高开发效率和用户体验。
实践建议
在实际项目中应用这些技术时,以下建议可能有所帮助:
-
从需求出发选择技术:不要盲目追随趋势,应根据项目的具体需求选择适当的渲染策略和模板技术。
-
采用混合渲染策略:为不同类型的页面选择不同的渲染方式,如内容页面使用SSG/ISR,动态页面使用SSR,交互部分使用客户端渲染。
-
注重性能监测:实施渲染性能监控,收集核心Web指标数据,持续优化用户体验。
-
安全优先:始终关注安全最佳实践,特别是数据转义和输入验证,防止常见的注入攻击。
-
渐进增强:确保基本功能在JavaScript禁用或失败的环境中仍然可用,提高可访问性和可靠性。
-
缓存策略:设计多层次缓存策略,平衡内容新鲜度和服务器负载。
-
代码共享:尽可能在服务器和客户端共享代码和组件,减少维护成本和不一致问题。
展望未来
HTML模板技术与服务端渲染正在不断演进,未来的发展趋势包括:
- 更细粒度的渲染控制:组件级别的渲染策略决策,而非页面级别
- Edge Computing的应用:将渲染计算移至网络边缘,进一步降低延迟
- AI辅助优化:使用机器学习预测用户行为,优先渲染可能需要的内容
- 服务器组件:如React Server Components,从根本上重新思考组件渲染位置
作为前端工程师,熟练掌握HTML模板技术与服务端渲染策略,对于构建高性能、SEO友好且用户体验出色的Web应用至关重要。无论技术如何变化,平衡用户体验、开发效率和业务需求的能力将始终是成功的关键。
学习资源
- EJS官方文档
- Pug模板引擎
- Next.js文档:数据获取策略
- Web.dev:渲染性能优化
- MDN:内容安全策略指南
- React文档:服务器组件
- Google Web.dev:Core Web Vitals
- Smashing Magazine:高级缓存策略
通过不断学习和实践,我们才能够在这个快速发展的领域保持前沿,设计出兼顾性能、安全与开发效率的现代Web应用。
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻
相关文章:
HTML 模板技术与服务端渲染
HTML 模板技术与服务端渲染 引言 在现代前端开发生态中,HTML模板技术与服务端渲染(SSR)构成了连接前后端的重要桥梁。当单页应用(SPA)因其客户端渲染特性而面临首屏加载速度慢、白屏时间长和SEO不友好等问题时,服务端渲染技术提供了一种优雅的解决方案…...
MySQL的MVCC【学习笔记】
MVCC 事务的隔离级别分为四种,其中Read Committed和Repeatable Read隔离级别,部分实现就是通过MVCC(Multi-Version Concurrency Control,多版本并发控制) 版本链 版本链是通过undo日志实现的, 事务每次修改…...
linux安装单节点Elasticsearch(es),安装可视化工具kibana
真的,我安装个es和kibana,找了好多帖子,问了好几遍ai才安装成功,在这里记录一下,我相信,跟着我的步骤走,99%会成功; 为了让大家直观的看到安装过程,我把我服务器的es和ki…...
(Go Gin)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
1. 路由 gin 框架中采用的路优酷是基于httprouter做的 HttpRouter 是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。 主要特点 显式匹配:与其他路由…...
纯HTMLCSS静态网站——元神
《原神》主题网页介绍 以对该网页的详细介绍 网页整体结构 头部(header):包含网站的 logo 和导航栏。logo 部分展示了 “原神” 字样,点击可返回首页。导航栏提供了多个页面链接,包括首页、音乐、视频、壁纸、世界、…...
嵌入式开发:基础知识介绍
一、嵌入式系统 1、介绍 以提高对象体系智能性、控制力和人机交互能力为目的,通过相互作用和内在指标评价的,嵌入到对象体系中的专用计算机系统。 2、分类 按其形态的差异,一般可将嵌入式系统分为:芯片级(MCU、SoC&am…...
华为VRP系统简介配置TELNET远程登录!
1.华为 VRP 系统概述 1.1 什么是 VRP VRP(Versatile Routing Platform 华为数通设备操作系统)是华为公司数据通信产品的通用操作系统平台,从低端到核心的全系列路由器、以太网交换机、业务网关等产品的软件核心引擎。 1.2 VRP 的功能 统一…...
【高频考点精讲】CSS accent-color属性:如何快速自定义表单控件的颜色?
用CSS accent-color属性3分钟搞定表单控件换肤,原来这么简单! 前几天有个学员问我,checkbox和radio这些表单控件默认样式太丑了,有没有什么办法能快速改颜色?" 我一看这问题就乐了——这不正是CSS accent-color属性的拿手好戏吗?今天咱们就来好好聊聊这个被低估的C…...
【Git】连接github时的疑难杂症(DNS解析失败)
大家好,我是jstart千语。最近在将项目推送到github的时候,突然github就拒绝访问了,即使挂了VPN,网页也进不去,通过git也不能把代码推送上去。 即使后面看别人的一些解决方案,比如取消代理啊、更换ssh的方式…...
成熟的前端vue vite websocket,Django后端实现方案包含主动断开websocket连接的实现
可参考实现方式点击进入查看 具体实现方案如下所示: import { useWebsocketMsessageStore } from /stores/websocketMsessageStore.js import {ElMessage} from "element-plus"; import {useUserStore} from "/stores/userStore.js"; // impo…...
广州 3D 展厅开启企业展示新时代
为了突破传统展厅的局限,满足企业日益增长的展示需求,3D 展厅应运而生。3D 展厅是利用虚拟现实(VR)、增强现实(AR)和三维建模等先进技术,构建出的一个高度逼真的数字化展示空间 。它打破了传统展…...
【Django】新增字段后兼容旧接口 This field is required
背景 我在Django模型里新增了两个字段后,旧的接口由于没有同时新增这两个字段的处理,因此旧的接口就报: This field is required 解决 把序列化时的 required 的字段设置为False即可 class ServiceSerializer(DynamicFieldsModelSerializ…...
基于AIGC的3D场景生成实战:从文本描述到虚拟世界构建
一、3D AIGC技术解析 1.1 技术挑战与突破 挑战维度 传统方案局限 AIGC创新方案 建模效率 人工建模耗时数天 文本到3D秒级生成 细节丰富度 重复使用素材库 无限风格化生成 物理合理性 手动调整物理参数 自动符合物理规律 多平台适配 需手动优化模型 自适应LOD生成 1.2 主流技术路…...
手写Java线程池与定时器:彻底掌握多线程任务调度
目录 一、线程池 1.1、什么是线程池 1.2、Java标准库中的线程池 1.3、ThreadPoolExecutor的七大参数 1.4、模拟实现线程池 1.4.1、submit () 1.4.2、构造方法 1.4.3、运行结果 二、定时器 2.1、标准库中的定时器 2.2、模拟实现定时器 2.2.1、MyTimerTask类 2…...
【科研绘图系列】R语言绘制区间点图(dot plot)
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍加载R包数据下载导入数据数据预处理画图1画图2输出图片系统信息介绍 【科研绘图系列】R语言绘制区间点图(dot plot) 加载R包 library(tidyverse) library(ggtext) library(r…...
【Agent实战】从0到1开发一个Python 解释器 MCP SSE Server
写在前面 想象一个场景:LLM Agent(如 AutoGPT、MetaGPT 或我们自己构建的 Agent)在规划任务后,决定需要运行一段 Python 代码来处理数据或调用某个 API。它不能直接在自己的环境中执行(通常不具备这个能力,也不安全),而是需要将这段代码发送给一个专门的外部服务来执行…...
C语言大写转小写2.0
一、阐述关系 上一次的题目是大写转小写,这一次代码不一样,运行的结果也不一样,这次的代码在此基础之上改动了一下,虽然看起来相似,但实际上运行结果不一样 二、题目展示 下面代码中,运行的结果是多少? 三、分析过程 首先,进入main函数,先声明了一个字符型数组是"012…...
pnpm常见报错解决办法
PS D:\code\gitlab\manus-web> pnpm add -D types/framer-motion --save-exact --config.strict-peer-depsfalse Debugger attached. WARN deprecated eslint8.57.1: This version is no longer supported. Please see https://eslint.org/version-support for other op…...
【Linux网络】:套接字之UDP
一、UDP和TCP协议 TCP (Transmission Control Protocol 传输控制协议)的特点: 传输层协议有连接(在正式通信前要先建立连接)可靠传输(在内部帮我们做可靠传输工作)面向字节流 UDP (U…...
量子威胁下的安全革命:后量子密码学技术路线与迁移挑战全解析
引言 量子计算技术的快速发展正在重塑现代密码学的安全版图。随着Shor算法对传统公钥密码体系的根本性威胁[1],全球范围内后量子密码学(Post-Quantum Cryptography, PQC)的研究与标准化进程已进入关键阶段。 本文基于权威文献分析,…...
多模态大语言模型(MLLM)- kimi-vl technical report论文阅读
前言 kimi-vl是月之暗面团队于2025年4月10日发布的多模态大模型。 代码链接:https://github.com/MoonshotAI/Kimi-VL 背景 随着人工智能技术的快速发展,人们对AI助手的需求已从单一文本交互转向多模态理解。新一代多模态模型如GPT-4o和Gemini虽展现…...
ai聊天流式响应,阻塞式和流式响应 nginx遇到的坑
问题 现在做ai的流式请求,在开发环境使用代理访问接口,显示是正常的。上到正式环境,代理通过nginx配置可以访问到流式接口。在本地测试postman请求流式接口,返回的东西是流式返回, 在正式环境里面使用postman请求流式…...
Linux安全模块:SELinux与AppArmor深度解析
引言 在Linux安全领域,SELinux和AppArmor就像两位忠诚的"系统保镖"💂,为你的服务器提供强制访问控制(MAC)保护!本文将深入解析这两大安全模块的工作原理、配置方法和实战技巧。无论你是要加固Web服务器,还是…...
FlinkJobmanager深度解析
1. JobManager 概述 Flink 是一个分布式流处理框架,其核心组件包括 JobManager、TaskManager 和客户端(如 CLI 或 Web UI)。JobManager 是 Flink 集群的“大脑”,负责协调作业的整个生命周期,包括作业调度、资源管理、…...
FlinkSql入门与实践
一、为什么需要 Flink SQL? 传统 SQL 是面向静态数据的查询语言,而现代实时业务要求对动态数据流进行即时分析。Flink SQL 应运而生,它让开发者无需编写复杂的状态管理代码,就能实现实时ETL、复杂事件处理(CEP&#x…...
【物联网】基于LORA组网的远程环境监测系统设计(ThingsCloud云平台版)
演示视频: 基于LORA组网的远程环境监测系统设计(ThingsCloud云平台版) 前言:本设计是基于ThingsCloud云平台版,还有另外一个版本是基于机智云平台版本,两个设计只是云平台和手机APP的区别,其他功能都一样。如下链接: 【物联网】基于LORA组网的远程环境监测系统设计(机…...
C++中指针Ptr(一级指针、二级指针)的基本使用详解(1)
C 中的指针是非常强大的工具,理解一级指针、二级指针以及它们与数组的关系,对于写出高效且安全的程序非常重要。下面我将从基础讲起,详细解释 一级指针、二级指针 的使用,注意事项,以及它们和数组之间的联系与区别&…...
科技赋能建筑新未来:中建海龙模块化建筑产品入选中国建筑首批产业化推广产品
在建筑工业化浪潮中,中国建筑国际集团旗下中建海龙科技有限公司(以下简称“中建海龙”)致力以科技创新赋能传统建造转型升级,大力发展新质生产力,促进科技成果在建筑产业体系化、规模化应用,面向“产品化、…...
示例:Spring JDBC 声明式事务(xml配置形式)
声明式事务是指在不修改源代码的情况下通过配置applicationContext.xml自动实现事务控制,其本质是AOP环绕通知。它的触发时机为:1、当目标方法执行成功时自动提交事务,2、当目标方法抛出运行时异常时,自动事务回滚 核心步骤示例&a…...
java多线程(7.0)
目录 编辑 定时器 定时器的使用 三.定时器的实现 MyTimer 3.1 分析思路 1. 创建执行任务的类。 2. 管理任务 3. 执行任务 3.2 线程安全问题 定时器 定时器是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定好的…...
sgpt在kali应用
Kali Linux 下 sgpt 渗透测试相关案例 1. 扫描目标主机存活 sgpt -s "使用 nmap 扫描 192.168.1.100 是否存活"示例命令: nmap -sn 192.168.1.1002. 扫描目标主机开放端口和服务 sgpt -s "使用 nmap 扫描 192.168.1.100 常见端口和服务"示例…...
小白电路设计-设计11-恒功率充电电路设计
介绍 作为电子信息工程的我,电路学习是一定要学习的,可惜目前作为EMC测试工程师,无法兼顾太多,索性不如直接将所学的知识进行运用,并且也可以作为契机,进行我本人的个人提升。祝大家与我一起进行提升。1.本…...
express的模板handlebars用app.engine()创建配置和用exphbs.create()的区别
在使用 express-handlebars 时,app.engine 和 exphbs.create 都可以用来配置 Handlebars 模板引擎,但它们的使用方式和功能有一些区别。以下是详细的对比和说明 app.engine 方法 app.engine 是 Express 提供的方法,用于注册一个新的模板引擎…...
【Python数据库与后端开发】从ORM到RESTful API
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比 二、实战演示环境配置要求核心代码实现案例1:SQLAlchemy模型定义案例2:FastAPI异步接口案例3:连接池配…...
数据结构(java)二叉树的基本操作
1.二叉树的性质: 1.若规定根结点的层数为1,则一棵非空二叉树的第i层上最多有2*-1(i>0)个结点 2.若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2都k次方-1 3.对于任何一个二叉树,如果其叶结点个数为 n…...
windows编程字符串处理
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、windows常用字符出处理函数?二、测试代码总结 前言 提示:这里可以添加本文要记录的大概内容: Windows编程中主要使用两…...
CentOS系统防火墙服务介绍
CentOS 系统使用的是 firewalld 防火墙服务(从 CentOS 7 开始),它基于 zone(区域) 和 service(服务) 的机制来配置网络访问控制,替代了传统的 iptables。 iptables 是 Linux 系统中…...
59、微服务保姆教程(二)Nacos--- 微服务 注册中心 + 配置中心
Nacos— 微服务 注册中心 + 配置中心 一.什么是Nacos? Nacos是阿里的一个开源产品,是针对微服务架构中的服务发现、配置管理、服务治理的综合型解决方案。 Nacos核心定位是“一个更易于帮助构建云原生应用的动态服务发现、配置和服务管理平台”,也就是我们的注册中心和配…...
Git命令行中vim的操作
Git命令行用vim打开文件,或者用其他git命令打开了文件,需要编辑和保存文件等,有些命令表情奇怪,往往容易忘记这些命令。记录下。 下面这篇比较实用和简练: gitvim编辑文件命令 • Worktile社区https://worktile.com/…...
【分布式系统中的“瑞士军刀”_ Zookeeper】一、Zookeeper 快速入门和核心概念
在分布式系统的复杂世界里,协调与同步是确保系统稳定运行的关键所在。Zookeeper 作为分布式协调服务的 “瑞士军刀”,为众多分布式项目提供了高效、可靠的协调解决方案。无论是在分布式锁的实现、配置管理,还是在服务注册与发现等场景中&…...
【昇腾】【训练】800TA2-910B使用LLaMA-Factory训练Qwen
文章目录 1. 使用docker安装1.1 配置docker1. 2 拉取 LLaMA-Factory1.3 修改配置 2. 下载模型3. 准备训练数据3.1 下载数据集3.2 自定义数据集配置 4. 训练4.1 训练配置4.2 启动训练4.3 训练效果测试 5. 合并权重 更好的阅读体验:传送门 服务器:800TA2 芯…...
Python自动化解决滑块验证码的最佳实践
1. 引言:滑块验证码的挑战与自动化需求 滑块验证码(Slider CAPTCHA)是当前互联网广泛使用的反爬机制之一,它要求用户手动拖动滑块到指定位置以完成验证。这种验证方式可以有效阻止简单的自动化脚本,但对爬虫开发者来说…...
知识蒸馏和迁移学习的区别
知识蒸馏和迁移学习虽然都涉及知识的传递,但并不是同一个概念,它们在目的、方法和应用场景上有显著区别: 1. 定义与核心思想 迁移学习(Transfer Learning) 是一种广义的机器学习范式,核心是将从一个任务或领…...
二项分布详解:从基础到应用
二项分布详解:从基础到应用 目录 引言二项分布的定义概率质量函数及其证明期望与方差推导二项分布的重要性质常见应用场景与其他分布的关系知识梳理练习与思考 引言 概率论中,二项分布是最基础也是最常用的离散概率分布之一。它描述了在固定次数的独…...
迁移学习(基础)
迁移学习理论 目标 迁移学习中的有关概念掌握迁移学习的两种方式 概念 预训练模型微调微调脚本 预训练模型(Pretrained model) 一般情况下预训练模型都是大型模型, 具备复杂的网络结构, 众多的参数量, 以及足够大的数据集进行训练而产生的模型, 在NLP领域, 预训练模型往往…...
云服务器和独立服务器的区别在哪
在当今数字化的时代,服务器成为了支撑各种业务和应用的重要基石。而在服务器的领域中,云服务器和独立服务器是两个备受关注的选项。那么,它们到底有何区别呢? 首先,让我们来聊聊成本。云服务器通常采用按需付费的模式…...
大模型时代的深度学习框架
作者:算力魔方创始人/英特尔创新大使刘力 在CNN时代,AI模型的参数规模都在百万级别,仅需在单张消费类显卡上即可完成训练。例如,以业界知名的CNN模型:ResNet50为例,模型参数量是约为 25.63M,在…...
BIOS主板(非UEFI)安装fedora42的方法
BIOS主板(非UEFI)安装fedora42的方法 现实困难:将Fedora-Workstation-Live-42-1.1.x86_64.iso写入U盘制作成可启动U盘启动fedora42,按照向导将fedora42安装到真机的sda7分区中得到报错如下内容: /boot/efi 必需的 /boot/efi必须位于格式化为e…...
C# 综合示例 库存管理系统7 主界面(FormMain)
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的 图99A-22 主界面窗口设计 主界面是多文档界面容器,需要将窗体属性IsMdiContainer设置为True。关于多文档界面编程请参看教程第7.12节《多文档界面》。 主界面并不提…...
1、RabbitMQ的概述笔记
一、什么是RabbitMQ Rabbit是一个公司名.MQ(nessage queue) 消息队列的意思,RabbitMQ 是 Rabbit企业下的一个消息队列产品。 RabbitMQ 是⼀个实现了 AMQP 的 消息队列 服务,是当前主流的消息中间件之⼀. AMQP:即Advanced MessageQueuingProtocol(高级…...