当前位置: 首页 > news >正文

基于前端+Node.js 的 Markdown 笔记 PDF 导出系统完整实战

前言

在日常学习和工作中,经常需要将 Markdown 笔记导出为 PDF 进行分享或存档,但现有的在线编辑器功能受限,浏览器打印样式丢失,截图工具清晰度差。因此,开发了一个本地 Markdown 笔记应用 + PDF 后端服务,支持多种中英文字体、暗黑/明亮主题切换、实时预览和语法高亮,通过前端准备自包含 HTML + 后端 Puppeteer 渲染的方式,实现高质量、样式保真的 PDF 导出功能,满足个人笔记管理和分享的需求。

本文内容包括:常见方案比较、为什么选择「前端准备 HTML + 后端 Puppeteer 渲染」、具体实现(包含内联 CSS、字体、图片、分页、CORS、调试与生产注意项)。


一、把网页导成 PDF 容易遇到的问题

很多人以为:把页面截屏或直接用浏览器打印就行。但真正要把排版一致、清晰、可分页的 PDF 做好,会碰到很多问题:

  • 截图(html2canvas)清晰度差:这是把 DOM 渲成 bitmap,再把 bitmap 放进 PDF → 放大就模糊。
  • 样式丢失/不一致:许多样式(@font-face、外链 CSS、变量)在服务器端渲染或截图时无法正常加载。
  • 分页问题:页面 CSS 可能把内容限定为一屏(比如 height:100vh; display:flex; overflow:hidden),导致所有内容被强制压在一页。
  • 跨域 / 网络访问问题:后端渲染时可能没有外网权限去拉 Google Fonts / CDN 资源。
  • 字体不生效:客户端通过 CSS 变量或 JS 动态注入字体,但当你把 HTML 发到后端时,后端不识别这些运行时变量。
  • CORS / devServer 代理 / IPv6:前端请求后端时可能出现 404 或跨域错误,尤其在开发环境(不同端口)下。

结论:要做「高保真、可分页、服务器生成的 PDF」,推荐的稳健方案是 前端负责把要渲染的 HTML + 样式准备好(尽可能自包含)→ 发给后端(Puppeteer/Headless Chrome)渲染成 PDF


二、常见方案对比

方案优点缺点
html2canvas + jsPDF(纯前端截图)易用、无后端清晰度及样式一致性差,长页/分页差,资源限制
wkhtmltopdf稳定、可命令行基于旧版 WebKit,CSS 支持/现代特性差
Puppeteer(Headless Chrome)高保真、支持现代 CSS/JS、分页、header/footer需要后端环境,部署/资源成本高
前端生成静态 HTML → 后端 Puppeteer 渲染(推荐)灵活、最好还原度、能控制资源需处理资源内联、跨域、安全问题

三、核心原理(为什么要这么做)

  • 后端渲染的好处:Headless Chrome 会把 HTML 当成浏览器渲染页来处理,支持复杂 CSS(flex/grid/自定义变量)、打印样式、@font-face、JS(如果允许)等,生成 PDF 时可开启 printBackground 来保留背景色/图片。
  • 为什么要前端“准备”HTML:浏览器端能方便读取 computed style、将图片/字体转换为 data URI、提取相对路径的 CSS 内容。把这些工作做在前端,后端只负责渲染和生成 PDF,能最小化后端对外网/文件系统的依赖。
  • 为什么要内联样式/字体:避免后端渲染时找不到相对路径资源(特别是打包后的 dist),以及避免跨域导致资源加载失败(从而导致字体、图标、样式缺失)。

四、实现要点

下面给出一个可实战的流程,并贴上关键代码块(前端 + 后端)。每段代码后我都会解释为什么这样写、可能的坑与解决方案。

我们的项目采用前后端分离架构:

bendibiji/
├── src/                    # 前端代码
│   ├── index.html         # 主页面
│   ├── index.js          # 入口文件
│   ├── scripts/          # 模块化脚本
│   │   ├── app.js        # 主应用逻辑
│   │   ├── modules/      # 功能模块
│   │   │   ├── exporter.js    # PDF导出核心
│   │   │   ├── theme.js       # 主题和字体管理
│   │   │   ├── title.js       # 标题处理
│   │   │   └── ...
│   │   └── styles/
│   └── images/
└── pdf-server/           # 后端PDF服务├── server.js         # Express服务器└── package.json      # 依赖配置

4.1 前端:把要渲染的页面准备成「自包含」HTML

目标

  • 把 preview 内容包进一个容器;
  • 把页面用到的相对 CSS 文件内容 fetch() 出来并内联(并处理 CSS 里 url(...) 引用);
  • 保留外部 CDN 链接(可选)或把字体/资源也内联成 base64;
  • 将运行时的 CSS 变量(比如 --font-body)的 computed values 写入最终 HTML,避免后端无法读取变量值;
  • 覆盖掉影响分页的样式(比如 height:100vh, overflow:hidden, display:flex),以便 Puppeteer 正常分页。

关键实现(带注释)

// helpers
async function fetchTextSafe(url) {try {const res = await fetch(url);if (!res.ok) throw new Error(`HTTP ${res.status}`);return await res.text();} catch (e) {console.warn('[fetchTextSafe] failed', url, e);return null;}
}async function fetchAsDataURL(url) {// 把图片或字体资源转成 data:... base64 URItry {const res = await fetch(url);const blob = await res.blob();return await new Promise((resolve) => {const reader = new FileReader();reader.onloadend = () => resolve(reader.result);reader.readAsDataURL(blob);});} catch (e) {console.warn('[fetchAsDataURL] failed', url, e);return null;}
}// 用于把 CSS 里的相对 url(...) 替换成 data:URI(需要额外 fetch)
async function inlineCssResources(cssText, baseUrl) {// 匹配 url(...) 中非 data 非 http 的路径const urlRegex = /url\((['"]?)(?!data:|https?:|\/\/)([^'")]+)\1\)/g;const tasks = [];const replacements = new Map();let m;while ((m = urlRegex.exec(cssText)) !== null) {const relPath = m[2];if (replacements.has(relPath)) continue;const abs = new URL(relPath, baseUrl).toString();tasks.push((async () => {const data = await fetchAsDataURL(abs);if (data) replacements.set(relPath, data);})());}await Promise.all(tasks);// do replacementreturn cssText.replace(urlRegex, (match, q, p1) => {const data = replacements.get(p1);if (data) return `url("${data}")`;return match;});
}export async function getHTMLWithInlineCSS({ title = '导出 PDF' } = {}) {// 1) 复制内容const contentEl = document.querySelector('#previewContent'); // 你项目里实际元素const container = document.createElement('div');container.id = 'pdf-container';container.innerHTML = contentEl.innerHTML;// 2) 读取字体设置(computed)const docStyle = getComputedStyle(document.documentElement);const bodyFont = docStyle.getPropertyValue('--font-body').trim() || '"Inter", sans-serif';const codeFont = docStyle.getPropertyValue('--font-code').trim() || '"JetBrains Mono", monospace';// 3) 处理 <link rel="stylesheet">let inlineStyles = '';const links = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));for (const link of links) {const href = link.getAttribute('href');// 绝对 URL(CDN)保留(也可以选择 fetch 并内联)if (/^https?:\/\//.test(href) || href.startsWith('//')) {// choose to keep external links (or fetch and inline if you need offline)inlineStyles += `<link rel="stylesheet" href="${href}">`;continue;}// 相对路径:fetch CSS 内容并内联,同时把 CSS 内的 url(...) 也转成 data URIconst cssText = await fetchTextSafe(href);if (cssText) {const baseUrl = new URL(href, location.href).toString();const processed = await inlineCssResources(cssText, baseUrl);// 去掉 webpack loader 注释(例如 /*! ... */ )避免注释包含路径破坏HTMLconst cleaned = processed.replace(/\/\*![\s\S]*?\*\//g, '');inlineStyles += `<style>${cleaned}</style>`;}}// 4) 内联页面上的 <style> 标签(例如由js注入的样式)document.querySelectorAll('style').forEach(s => {inlineStyles += `<style>${s.innerHTML}</style>`;});// 5) 添加打印/分页重置样式(覆盖原页面会影响分页的地方)const resetStyles = `<style>/* 基本重置,确保 Puppeteer 打印时按内容分页 */:root {--font-body: ${currentBodyFont} !important;--font-code: ${currentCodeFont} !important;}html {margin: 0 !important;padding: 0 !important;height: 100% !important;background-color: ${isDarkMode ? '#1e1e2f' : '#ffffff'} !important;}body {margin: 0 !important;padding: 0 !important;min-height: 100vh !important;overflow: visible !important;line-height: 1.6 !important;font-family: ${currentBodyFont} !important;font-size: ${currentFontSize}px !important;color: ${isDarkMode ? '#e0e0e0' : '#333333'} !important;background-color: ${isDarkMode ? '#1e1e2f' : '#ffffff'} !important;writing-mode: horizontal-tb !important;}#pdf-container {width: 100% !important;page-break-inside: auto !important;background-color: transparent !important;min-height: 100vh !important;box-sizing: border-box !important;}/* 修复:允许代码块在页面中间分割 */pre { page-break-inside: auto !important; break-inside: auto !important;overflow-wrap: break-word !important;}/* 添加分页控制 */@media print {/* 在分页处添加视觉提示 */.page-break {break-before: page !important;page-break-before: always !important;}/* 确保代码块分割后仍然可读 */pre {break-inside: auto !important;page-break-inside: auto !important;}}</style>`;// 6) 标题(可选)const titleHTML = `<h1 style="text-align:center;margin-bottom:20px;">${title}</h1>`;// 7) 组装最终 HTMLconst html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title>${inlineStyles}${resetStyles}</head><body>${titleHTML}${container.outerHTML}</body></html>`;return html;
}

解释 / 注意点

  • 我们把相对 CSS fetch 到前端并内联,且会把 CSS 中引用的相对图片/字体也尝试转换为 data: URI —— 这一步能让后端无需访问静态文件系统或外网就能完整渲染样式与字体。
  • cleaned = processed.replace(/\/\*![\s\S]*?\*\//g, ''):Webpack loader 的注释例如 /*! *** css ./node_modules/... */ 会混入 CSS,直接内联到 HTML 可能造成混乱,所以删掉。
  • 我们从 document.documentElement 读取 computed --font-body/--font-code 并把字体字符串写进 resetStyles,因为 CSS 变量在后端环境中没有运行时上下文。

4.2 前端:发送 HTML 到后端并下载 PDF

export async function exportToPDF() {const html = await getHTMLWithInlineCSS({ title: els.pdfTitle.value || 'Markdown笔记导出' });// 获取当前主题的背景颜色const currentBgColor = getComputedStyle(document.documentElement).getPropertyValue('--bg').trim() || '#ffffff';const resp= await fetch('http://127.0.0.1:3000/api/render-pdf', 	   {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ html,backgroundColor: currentBgColor}),});if (!resp.ok) {const text = await resp.text();alert('PDF 生成失败: ' + text);return;}const blob = await resp.blob();const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = (els.pdfFilename?.value || 'notes') + '.pdf';a.click();URL.revokeObjectURL(url);
}

小贴士

  • 我用 127.0.0.1 而不是 localhost,避免 IPv6 [::1] 与 IPv4 监听不一致导致的 404 问题。
  • Content-Type: application/json,body 里直接放 HTML(注意可能很大,后端需要增加 bodyParser 大小)。

4.3 后端:Express + Puppeteer 渲染 PDF(

关键点:支持 CORS、允许大请求体、以可靠的方式启动 Puppeteer。

// server.js
import express from 'express';
import bodyParser from 'body-parser';
import puppeteer from 'puppeteer';
import cors from 'cors';const app = express();app.use(cors({ origin: '*' }));
app.use(bodyParser.json({ limit: '50mb' }));app.post('/api/render-pdf', async (req, res) => {const { html, backgroundColor = '#ffffff', margin = '10mm' } = req.body;if (!html) return res.status(400).send('No HTML provided');let browser;try {browser = await puppeteer.launch({ headless: true,args: ['--no-sandbox', '--disable-setuid-sandbox']});const page = await browser.newPage();// 创建完整的HTML文档,确保内容正确换行和分页const fullHtml = `<!DOCTYPE html><html><head><meta charset="UTF-8"><style>@page {margin: ${margin};size: A4;background-color: ${backgroundColor} !important;}html, body {margin: 0;padding: 0;width: 100%;min-height: 100vh;font-family: Arial, sans-serif;}.content {min-height: 100vh;/* 关键:确保内容不会溢出 */width: 100%;word-wrap: break-word;overflow-wrap: break-word;box-sizing: border-box;}/* 确保所有元素都能正确换行 */* {box-sizing: border-box;max-width: 100%;}/* 表格和长文本处理 */table {width: 100% !important;table-layout: fixed;}td, th {word-wrap: break-word;}/* 代码块和预格式化文本处理 */pre, code {white-space: pre-wrap;word-wrap: break-word;}/* 图片不会超出容器 */img {max-width: 100%;height: auto;}/* 链接和长URL换行 */a {word-wrap: break-word;}</style></head><body><div class="content">${html}</div></body></html>`;await page.setContent(fullHtml, { waitUntil: 'networkidle0' });// 等待内容渲染完成await page.evaluateHandle('document.fonts.ready');const pdfBuffer = await page.pdf({format: 'A4',printBackground: true,margin: {top: margin,right: margin,bottom: margin,left: margin},omitBackground: false,displayHeaderFooter: false});res.set({'Content-Type': 'application/pdf','Content-Disposition': 'attachment; filename="markdown-notes.pdf"','Content-Length': pdfBuffer.length,});res.send(pdfBuffer);} catch (err) {console.error(err);res.status(500).send('PDF生成失败: ' + err.message);} finally {if (browser) await browser.close();}
});app.listen(3000, '0.0.0.0', () => console.log('PDF服务已启动: http://localhost:3000'));

注意/建议

  • app.listen(3000, '0.0.0.0') 能避免 IPv6/localhost 地址问题。
  • page.setJavaScriptEnabled(false):如果你担心收到的 HTML 包含恶意脚本,最好禁用 JS 执行(但会影响某些依赖 client JS 的渲染)。更好的做法是 在前端把需要的脚本运行并把最终静态 HTML 发给后端(即后端不会执行页面内的脚本)。

4.4 效果如图:

在这里插入图片描述
在这里插入图片描述


五、常见问题与解决方案

下面是实际项目中最常见的问题与解决办法——把我遇到的坑全罗列,并给出可复制的解决动作。

问题 A:前端请求后端返回 404,但 curl 能成功

原因

  • 跨域/代理问题,或浏览器解析 localhost::1(IPv6),而 server 只监听 IPv4(127.0.0.1)。
    解决
  • 后端使用 app.listen(3000, '0.0.0.0')
  • 或者前端直接请求 http://127.0.0.1:3000
  • 在开发时可以使用 webpack devServer proxy,但生产最好用完整 URL + 后端 CORS。

问题 B:样式不对或全部挤成一页

原因

  • 页面有 height:100vh; overflow:hidden; display:flex,此类样式在打印/生成 PDF 时导致布局异常。
    解决
  • 在导出 HTML 时通过覆盖 CSS(resetStyles)移除这些属性(height:auto!important; overflow:visible!important; display:block!important;)。
  • 使用 page-break-inside / break-inside CSS 控制分页:pre, table { page-break-inside: avoid; }

问题 C:Webpack 打包生成的 CSS 带注释(/*! ... */)或 loader 信息混入

原因

  • 在 dev 模式或通过 css-loader/postcss-loader,生成的 CSS 里可能含 loaders 注释与路径。
    解决
  • 内联前先清理注释:css = css.replace(/\/\*![\s\S]*?\*\//g, '')
  • 或者直接读取打包输出的 dist/css/main.xxx.css 文件来获取纯 CSS,不要依赖页面内 <style> 那些带 loader 注释的输出。

问题 D:字体没有生效

原因

  • 前端通过 CSS 变量或运行时设置字体,但后端渲染时没有这些变量(或无法访问字体文件 CDN)。
    解决
  1. 在构造 HTML 时直接把 font-family 的 computed value 写到 html, body 中(如示例代码所示)。
  2. 如果后端无外网权限或要保证离线:把字体文件 base64 内联到 CSS(@font-face + data URI),或者把字体文件放在后端可访问的静态目录并把 <link> 指向绝对 URL。
  3. 若字体是 woff/woff2/ttf,内联到 CSS 示例:
@font-face {font-family: 'MyFont';src: url('data:font/woff2;base64,AAAB...') format('woff2');font-weight: 400;font-style: normal;
}
body { font-family: 'MyFont', sans-serif; }

提供一个 node 脚本把字体转换为 base64 的方法(可选):

import fs from 'fs';
const buf = fs.readFileSync('./dist/fonts/MyFont.woff2');
const base64 = buf.toString('base64');
console.log(`data:font/woff2;base64,${base64}`);

问题 E:图片不显示或报 CORS

原因

  • 图片来自第三方且没有 CORS 允许;Canvas 转 dataURI 会被阻止;Puppeteer 在 page.setContent 时如果图片是跨域,可能无法加载。
    解决
  • 在前端把图片转换为 base64 data URI(通过 fetch blob -> FileReader)并插到 <img src="data:...">
  • 或者后端获取图片并送回(后端渲染时自己去拉资源,而不是依赖浏览器)。

六、调试技巧

  1. 接口 curl vs 浏览器

    • 如果 curl 成功而浏览器 404/报错,先检查浏览器控制台 Network → 看请求是否被 Service Worker / devServer 拦截或是否触发 CORS。
  2. Remote Address 显示 [::1]

    • 使用 127.0.0.1 可以避免 IPv6 与 IPv4 的监听不一致问题,或者让 server 监听 0.0.0.0
  3. 字体问题

    • 用 developer tools 查看实际 CSS 是否包含 @font-face,或查看网络面板字体文件是否被请求并返回 200。
  4. 样式被压缩成一页

    • 检查页面是否有 height:100vhdisplay:flex; flex-direction:rowoverflow:hidden。在导出的 HTML 中覆盖这些样式。
  5. Webpack 打包 CSS 包含注释

    • 在内联前用正则删除 /*! ... */ 注释,或直接读取打包后的 CSS 文件而不是页面内联样式。

七、附前端代码高亮功能实现详解

整体流程概述

你的项目是一个Markdown编辑器,用户输入Markdown文本,系统将其转换为带代码高亮的HTML预览。

核心库作用

1. marked.js - Markdown解析器
  • 作用: 将Markdown文本转换为HTML

  • 转换后: 变成标准的HTML结构

2. highlight.js - 代码高亮库
  • 作用: 给代码块添加语法高亮颜色
  • 效果: 让代码中的关键字、字符串、注释等显示不同颜色
  • 示例: JavaScript代码中的functionconsole等会显示为不同颜色

完整实现步骤

步骤1: 引入库文件
<!-- Markdown解析器 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.2.12/marked.min.js"></script>
<!-- 代码高亮库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<!-- 高亮样式主题 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/github-dark.min.css" />
步骤2: 配置marked与highlight.js联动
marked.setOptions({highlight: function (code, lang) {// 如果指定了语言,就用指定语言高亮if (lang && hljs.getLanguage(lang)) {return hljs.highlight(code, { language: lang }).value;}// 否则自动检测语言return hljs.highlightAuto(code).value;},langPrefix: 'hljs language-', // 给代码块添加CSS类名breaks: true,  // 支持换行gfm: true      // 支持GitHub风格的Markdown
});
步骤3: 动态加载语言包(按需加载)
function loadHighlightLanguage(lang) {// 避免重复加载if (state.loadedLanguages.has(lang)) return Promise.resolve();// 动态创建script标签加载特定语言包const scriptSrc = `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/${lang}.min.js`;return new Promise((resolve, reject) => {const script = document.createElement('script');script.src = scriptSrc;script.onload = () => { state.loadedLanguages.add(lang); resolve(); };script.onerror = () => reject(new Error(`加载语言包失败: ${lang}`));document.head.appendChild(script);});
}
步骤4: 核心预览更新函数
async function updatePreview() {// 1. 获取编辑器中的Markdown文本let markdownText = els.editor.value;// 2. 处理自定义颜色语法 ==颜色:文本==markdownText = markdownText.replace(/==([a-zA-Z#0-9#]+):([\s\S]+?)==/g, function (match, color, text) {return `<span style="color:${color};">${text}</span>`;});// 3. 设置预览区域样式els.previewContent.style.fontFamily = getComputedStyle(document.documentElement).getPropertyValue('--font-body');els.previewContent.style.fontSize = state.fontSize + 'px';// 4. 将Markdown转换为HTMLels.previewContent.innerHTML = marked.parse(markdownText);// 5. 查找所有代码块const codeBlocks = els.previewContent.querySelectorAll('pre code');const langsToLoad = new Set();// 6. 收集需要加载的编程语言codeBlocks.forEach(code => {const classes = code.className.split(/\s+/);for (const cls of classes) {if (cls.startsWith('language-')) {const lang = cls.replace('language-', '');langsToLoad.add(lang);}}});// 7. 动态加载需要的语言包try { await Promise.all([...langsToLoad].map(loadHighlightLanguage)); } catch (e) { console.warn(e); }// 8. 对每个代码块应用语法高亮codeBlocks.forEach(code => { try { hljs.highlightElement(code); } catch {} });
}

实际效果演示

用户输入:

# 我的代码示例这是一段JavaScript代码:```javascript
function greet(name) {console.log(`Hello, ${name}!`);return `Welcome, ${name}`;
}

转换后的效果:

  • functionconsolereturn 等关键字显示为蓝色
  • 字符串 "Hello, ${name}!" 显示为绿色
  • 注释显示为灰色
  • ==#ff0000:这是红色文字== 显示为红色文字

关键优势

  1. 按需加载: 只加载实际使用的编程语言包,节省带宽
  2. 自动检测: 可以自动识别代码语言类型
  3. 自定义语法: 支持特殊的颜色标记语法
  4. 性能优化: 避免重复加载,有缓存机制

八、结语:一步步复习要点

如果你要复习或复现这套方案,建议按这个顺序操作并验证:

  1. 在本地用 puppeteer 做个最简单的渲染:<h1>hello</h1> → PDF 成功。
  2. 把你页面的 HTML 发给后端,确认渲染的样式(注意字体是否生效)。
  3. 实现 CSS 的内联(先简单把 main.css 的内容 copy 到内联 <style>),再测试。
  4. 处理 url(...) 中的资源(图片/字体)为 data URI。
  5. 修复分页相关样式(移除 100vh、flex 等)。
  6. 处理字体(Google Fonts vs 本地自托管);如果要离线,转换为 base64 并 @font-face 内联。
  7. 添加安全校验、鉴权、速率限制。
http://www.dtcms.com/a/434458.html

相关文章:

  • lesson71:Node.js与npm基础全攻略:2025年最新特性与实战指南
  • 购买域名后 可以做网站么灰色关键词排名优化
  • 专业做家具的网站小程序注册方法
  • OWASP ZAP 工具安全测试介绍
  • Git介绍 常用命令
  • 教育机构作图:含拼团 / 课程封面模板,适配小程序
  • linux内核时间定时器延时
  • 远程安装vps相关软件记录
  • 山东高端网站建设方案宁波网站建设那家好
  • x86_64 centos7.2 上用aarch64-linux-gnu-gcc4.8.5交叉编译qt5.11.3
  • GLib线程池全面解析:原理、应用与性能优化
  • 东莞网站设计网址电影网站建设方案ppt模板下载
  • 51单片机-驱动LCD1602液晶显示屏教程
  • 【C++哲学】面向对象的三大特性之 多态
  • Python - 100天从新手到大师:第二十六天Python操作Word和PowerPoint文件
  • 算法基础 典型题 前缀和
  • 广告网站制作多少钱wordpress修改密码后还是登陆不了
  • 【MySQL】一篇讲透MySQL的MVCC机制!
  • 【开题答辩全过程】以 Web数据挖掘在电子商务中的应用研究为例,包含答辩的问题和答案
  • 网站界面美观度站长素材网站官网
  • 生活的方向,从来没有统一的标准答案——它不是一张固定的地图,也不是一条必须抵达的终点线,更像是你在行走中慢慢校准的“心之所向”。
  • 网站到期时间网站开发小图片
  • Git打tag标签
  • leetcode 494 目标和
  • 网站设置成灰色全球最牛的搜索引擎
  • Apache POI操作Docx文档时踩坑指南
  • K230基础-显示画面
  • 一级a做爰片免费网站 新闻wordpress用户修改文章
  • 从零起步学习Redis || 第五章:利用Redis构造分布式全局唯一ID
  • C++基础语法核心技术详解