ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从零实现抖音表白源码:HTML5 Canvas粒子动画与原生JS交互实战

从零实现抖音表白源码:HTML5 Canvas粒子动画与原生JS交互实战 简介在Web前端开发中HTML5、CSS3和JavaScript是构建动态网页和交互体验的三大核心技术。HTML5 Canvas元素提供了强大的图形绘制能力允许开发者通过脚本实时渲染图形、动画和视觉效果其原理是基于像素级的绘图API。结合原生JavaScript的事件驱动模型可以创建出高性能、响应式的交互应用这在数据可视化、游戏开发和创意动效领域具有重要价值。例如利用Canvas粒子系统模拟自然现象或创建吸引眼球的视觉特效是前端工程实践中常见的技术应用。本文以近期热门的“biu biu biu”表白页面为例详细解析如何运用Canvas粒子动画和原生JS事件处理实现爱心发射、动态渐变背景等浪漫视觉效果并深入探讨代码结构优化与常见问题排查为开发者提供一份可复现、易定制的实战指南。1. 项目缘起从“biu biu biu”到可复现的浪漫最近在技术社区和社交媒体上一个名为“抖音最火表白源码biu biu biu”的项目热度居高不下。点开相关讨论你会发现一个有趣的现象很多人都在求源码但真正拿到手、能跑起来、并且理解其背后逻辑的却不多。这背后反映的其实是很多朋友对前端技术特别是HTML、CSS和JavaScript如何协同创造视觉奇观的强烈兴趣以及一种“想亲手为重要的人做点什么”的浪漫需求。这个所谓的“biu biu biu”表白源码本质上是一个纯前端网页。它不涉及后端服务器、数据库或复杂的业务逻辑其核心魅力在于利用浏览器作为画布通过代码创造出动态、互动、充满心意的视觉效果。当你在搜索框里输入“html表白代码”、“七夕情人节html代码”时你寻找的不仅仅是一段可以复制粘贴的文本更是一个能承载情感、制造惊喜的数字化载体。这类项目之所以能火正是因为它将技术人的浪漫以一种极低门槛一个浏览器就能运行的方式展现了出来。然而网络上流传的版本质量参差不齐。有些只是简单的静态页面有些则包含了复杂的动画和交互。更常见的问题是代码缺乏注释结构混乱或者依赖一些过时或不可靠的外部资源导致新手下载后根本无法运行更别提进行个性化修改了。本文的目的就是为你彻底拆解一个高质量、可运行的“biu biu biu”表白页面的实现过程。我们将从零开始不仅给你可以“抄作业”的完整代码更重要的是我会带你理解每一行代码背后的意图让你能真正掌握它并把它变成属于你自己的独特创作。2. 核心效果拆解与技术选型在动手写代码之前我们必须先想清楚一个打动人的表白页面应该具备哪些要素根据我对大量类似项目的观察和实际开发经验可以总结出以下几个关键点这也是我们技术实现的蓝图2.1 视觉与动效核心氛围营造需要一个温馨、浪漫或梦幻的背景。这可以通过纯色渐变、星空粒子、动态心形背景或一段柔和的视频来实现。核心动画“biu biu biu”这个拟声词本身就暗示了动态效果。通常这表现为爱心图案从屏幕底部或一侧“发射”出来划过屏幕可能伴有光晕、轨迹或缩放效果。文字呈现表白的话语需要以引人注目的方式出现。可能是打字机效果逐字出现也可能是优雅的淡入或者是配合动画节奏的显隐。交互反馈当用户点击屏幕或按下按键时应该有即时反馈比如点击处绽放一个爱心或者触发一段新的动画序列增强参与感。音乐与音效一段契合主题的背景音乐和恰到好处的音效如爱心发射的“biu”声能极大提升沉浸感。2.2 我们的技术实现方案基于以上需求我们选择最纯粹、兼容性最好的Web技术栈HTML5作为页面结构骨架。我们将使用canvas画布元素来实现复杂的粒子动画如发射的爱心因为它能提供极高的绘制性能和控制粒度。CSS3负责静态样式和部分简单动画。比如页面的布局、背景渐变、基础按钮样式以及文字的颜色、字体和过渡效果。对于性能要求不高、定义简单的动画用CSS的keyframes会更方便。Vanilla JavaScript (原生JS)作为大脑驱动所有复杂的逻辑和交互。我们将用JS来控制canvas上爱心的生成、运动轨迹和消失。监听用户的鼠标点击、移动等事件。管理背景音乐的播放与暂停。实现打字机效果等动态文本更新。为什么不直接用现成的动画库如Anime.js、GSAP或游戏引擎对于这个特定项目原生实现足以胜任且能让代码更透明、依赖更少、体积更小方便你理解和修改。理解了原生实现你以后使用任何高级库都会更加得心应手。3. 从零搭建完整的代码实现与逐行解析接下来我们进入实战环节。我会提供一个完整的、可运行的HTML文件代码并对其中的关键部分进行详细拆解。你可以将以下所有代码保存为一个.html文件直接用浏览器打开即可看到效果。3.1 基础HTML结构与资源准备!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleTo My Special One ❤️/title link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css link hrefhttps://fonts.googleapis.com/css2?familyDancingScript:wght700familyPoppins:wght300;400;600displayswap relstylesheet style /* CSS样式将在下一部分详细列出 */ /style /head body !-- 背景音乐 -- audio idbgm loop !-- 你需要准备一个.mp3格式的浪漫纯音乐文件替换下面的src -- source srcYOUR_ROMANTIC_MUSIC.mp3 typeaudio/mpeg 您的浏览器不支持音频元素。 /audio !-- 主画布用于绘制发射的爱心 -- canvas idheartCanvas/canvas !-- 内容容器 -- div classcontainer header h1i classfas fa-heart/i For You i classfas fa-heart/i/h1 p classsubtitleA message written in code, from the heart./p /header main !-- 动态显示的表白文字 -- div classmessage-box p idtypedText/p div classcursor/div /div !-- 控制按钮 -- div classcontrols button idstartBtn classbtn i classfas fa-play/i 开启心动模式 /button button idmusicBtn classbtn i classfas fa-volume-up/i 播放音乐 /button button idfireBtn classbtn i classfas fa-heart-broken/i Biu Biu Biu! /button /div !-- 隐藏的完整信息在动画后显示 -- div idfullMessage classhidden p从遇见你的那一刻起我的代码世界就多了一个永远无法被捕获的异常——那就是想你。/p p你是我人生算法中最优美的递归每一次思念都让结果更靠近“永远”。/p p—— 一个用二进制思考却用十进制爱你的程序员/p /div /main footer p点击屏幕任意处会有惊喜哦 ❤️/p /footer /div script // JavaScript代码将在第三部分详细列出 /script /body /html代码解析与注意事项meta name”viewport”: 这行代码确保页面在手机和电脑上都能正确缩放和显示对于移动端表白至关重要。字体图标 (Font Awesome) 和 Google Fonts: 我们通过CDN引入了两个外部资源。Font Awesome提供了漂亮的心形等图标Google Fonts引入了Dancing Script手写体和Poppins现代无衬线体两种字体来提升视觉美感。注意如果你的使用环境无法访问这些CDN需要下载字体文件到本地并修改链接或者使用系统默认字体。audio标签: 用于嵌入背景音乐。关键点loop属性使音乐循环播放。你需要将YOUR_ROMANTIC_MUSIC.mp3替换为你准备好的音乐文件的实际路径。请确保你拥有该音乐文件的使用权或者使用免版税音乐。canvas标签: 这是我们实现动态爱心发射效果的“舞台”。它的所有绘制内容都由JavaScript控制。内容结构我们用.container包裹了所有可见的文本和按钮内容与Canvas动画层分开便于管理和样式控制。#typedText是打字机效果的目标元素#fullMessage是最终完整显示的信息。3.2 精心设计的CSS样式将以下CSS代码放入上面HTML的style标签中。* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Poppins, sans-serif; min-height: 100vh; background: linear-gradient(-45deg, #ffafcc, #ffc8dd, #cdb4db, #bde0fe); background-size: 400% 400%; animation: gradientBG 15s ease infinite; overflow: hidden; color: #590d22; display: flex; justify-content: center; align-items: center; } /* 动态渐变背景动画 */ keyframes gradientBG { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } #heartCanvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; /* 关键让画布不拦截鼠标事件 */ z-index: 1; } .container { position: relative; z-index: 2; background-color: rgba(255, 255, 255, 0.85); backdrop-filter: blur(10px); border-radius: 24px; padding: 2.5rem 3rem; max-width: 800px; width: 90%; box-shadow: 0 20px 60px rgba(89, 13, 34, 0.2); text-align: center; border: 2px solid rgba(255, 175, 204, 0.3); } header h1 { font-family: Dancing Script, cursive; font-size: 3.5rem; color: #ff4d6d; margin-bottom: 0.5rem; } header .subtitle { font-size: 1.1rem; color: #a4133c; margin-bottom: 2rem; font-weight: 300; } .message-box { min-height: 180px; border: 2px dashed #ffafcc; border-radius: 15px; padding: 2rem; margin: 2rem 0; background: rgba(255, 255, 255, 0.7); position: relative; text-align: left; } #typedText { font-size: 1.8rem; line-height: 1.6; color: #800f2f; min-height: 1.6em; } .cursor { display: inline-block; width: 3px; height: 1.8rem; background-color: #ff4d6d; margin-left: 4px; animation: blink 0.7s infinite; vertical-align: text-bottom; } keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } .controls { display: flex; justify-content: center; gap: 1.5rem; flex-wrap: wrap; margin: 2.5rem 0; } .btn { padding: 1rem 2rem; font-size: 1.1rem; font-family: Poppins, sans-serif; border: none; border-radius: 50px; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; gap: 10px; font-weight: 600; } #startBtn { background: linear-gradient(to right, #ff758c, #ff7eb3); color: white; } #musicBtn { background: linear-gradient(to right, #a663cc, #cdb4db); color: white; } #fireBtn { background: linear-gradient(to right, #ffafcc, #ffc8dd); color: #590d22; } .btn:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); } .btn:active { transform: translateY(-2px); } #fullMessage { margin-top: 2rem; padding: 1.5rem; background: rgba(255, 245, 248, 0.9); border-radius: 15px; border-left: 5px solid #ff4d6d; } #fullMessage p { margin-bottom: 1rem; font-size: 1.3rem; line-height: 1.7; color: #800f2f; } #fullMessage p:last-child { text-align: right; font-style: italic; color: #a4133c; font-weight: 600; } footer { margin-top: 2rem; font-size: 0.9rem; color: #a4133c; opacity: 0.8; } .hidden { display: none !important; }样式设计要点与避坑指南pointer-events: none;这是Canvas层 (#heartCanvas) 样式中最关键的一行。它让画布“穿透”鼠标事件这样你点击画布上的爱心动画时事件会传递到下层的按钮和文字上确保交互正常。没有这个按钮可能无法点击。z-index层级管理我们通过z-index建立了清晰的层级Canvas动画层在最底层z-index: 1内容容器在上层z-index: 2。这样动画不会遮盖文字。背景与容器body的动态渐变背景 (gradientBG) 创造了流动的梦幻感。.container使用了backdrop-filter: blur(10px)实现毛玻璃效果这在现代浏览器上观感极佳但需注意旧版浏览器兼容性。响应式设计使用了width: 90%、max-width、flex-wrap和百分比单位让页面在不同屏幕尺寸下都能良好适配。交互反馈按钮的:hover和:active伪类定义了悬停和点击时的动画这是提升用户体验的细节。3.3 驱动一切的JavaScript逻辑将以下JavaScript代码放入上面HTML的script标签中。这是整个项目的“大脑”代码量稍大我会分模块解释。// 1. 全局变量与DOM元素获取 const canvas document.getElementById(heartCanvas); const ctx canvas.getContext(2d); const typedTextEl document.getElementById(typedText); const cursorEl document.querySelector(.cursor); const startBtn document.getElementById(startBtn); const musicBtn document.getElementById(musicBtn); const fireBtn document.getElementById(fireBtn); const fullMessageEl document.getElementById(fullMessage); const bgm document.getElementById(bgm); // 调整Canvas尺寸为全屏 function resizeCanvas() { canvas.width window.innerWidth; canvas.height window.innerHeight; } window.addEventListener(resize, resizeCanvas); resizeCanvas(); // 初始化 // 爱心粒子数组 let hearts []; // 打字机相关变量 let typewriterIndex 0; let typewriterText “嘿有一个bug我一直解不掉... 那就是想你。; let isTyping false; // 音乐播放状态 let isMusicPlaying false; // 2. 爱心粒子类与动画引擎 class HeartParticle { constructor(x, y) { this.x x; this.y y; this.size Math.random() * 15 8; // 随机大小 8px ~ 23px this.speedX Math.random() * 3 - 1.5; // 水平速度 -1.5 ~ 1.5 this.speedY Math.random() * -5 - 3; // 垂直速度向上 -8 ~ -3 this.color hsl(${Math.random() * 30 330}, 100%, 65%); // 粉色系 this.alpha 1; // 透明度 this.decay Math.random() * 0.02 0.005; // 消失速度 } update() { this.x this.speedX; this.y this.speedY; this.speedY 0.1; // 模拟重力让爱心下落 this.alpha - this.decay; // 如果爱心太小或透明度为0标记为待删除 return this.size 0.5 this.alpha 0; } draw() { ctx.save(); ctx.globalAlpha this.alpha; ctx.fillStyle this.color; ctx.beginPath(); // 绘制一个简易爱心路径 (使用贝塞尔曲线近似) const topCurveHeight this.size * 0.3; ctx.moveTo(this.x, this.y this.size / 4); ctx.bezierCurveTo( this.x, this.y - topCurveHeight, this.x - this.size, this.y - topCurveHeight, this.x - this.size, this.y this.size / 4 ); ctx.bezierCurveTo( this.x - this.size, this.y this.size * 0.7, this.x, this.y this.size * 1.2, this.x, this.y this.size * 1.2 ); ctx.bezierCurveTo( this.x, this.y this.size * 1.2, this.x this.size, this.y this.size * 0.7, this.x this.size, this.y this.size / 4 ); ctx.bezierCurveTo( this.x this.size, this.y - topCurveHeight, this.x, this.y - topCurveHeight, this.x, this.y this.size / 4 ); ctx.closePath(); ctx.fill(); ctx.restore(); } } // 动画循环函数 function animateHearts() { // 清空画布用半透明黑色实现尾迹效果 ctx.fillStyle rgba(0, 0, 0, 0.05); ctx.fillRect(0, 0, canvas.width, canvas.height); // 更新并绘制每一个爱心粒子 for (let i 0; i hearts.length; i) { if (hearts[i].update()) { hearts[i].draw(); } else { // 移除已经消失的粒子 hearts.splice(i, 1); i--; } } requestAnimationFrame(animateHearts); // 循环调用形成动画 } // 启动动画循环 animateHearts(); // 3. 事件监听与交互函数 // 创建一簇爱心用于按钮点击和屏幕点击 function createHeartCluster(x, y, count 15) { for (let i 0; i count; i) { // 在点击位置周围随机散布 const scatterX x (Math.random() - 0.5) * 100; const scatterY y (Math.random() - 0.5) * 100; hearts.push(new HeartParticle(scatterX, scatterY)); } } // 打字机效果函数 function typeWriter() { if (typewriterIndex typewriterText.length) { isTyping true; cursorEl.style.animationPlayState running; // 光标闪烁 typedTextEl.innerHTML typewriterText.charAt(typewriterIndex); typewriterIndex; // 随机打字速度更自然 setTimeout(typeWriter, Math.random() * 80 50); } else { isTyping false; cursorEl.style.animationPlayState paused; // 打字结束光标常亮 cursorEl.style.opacity 1; // 显示完整信息 setTimeout(() { fullMessageEl.classList.remove(hidden); fullMessageEl.style.animation fadeIn 1.5s ease forwards; }, 800); } } // 4. 绑定事件 // “开启心动模式”按钮开始打字机效果 startBtn.addEventListener(click, () { if (isTyping) return; // 防止重复触发 typedTextEl.innerHTML ; // 清空原有文字 typewriterIndex 0; fullMessageEl.classList.add(hidden); // 隐藏完整信息 typeWriter(); // 同时发射一些爱心 createHeartCluster(canvas.width / 2, canvas.height / 2, 30); }); // “播放音乐”按钮控制背景音乐 musicBtn.addEventListener(click, () { if (isMusicPlaying) { bgm.pause(); musicBtn.innerHTML i classfas fa-volume-mute/i 播放音乐; } else { // 注意现代浏览器通常禁止自动播放音频必须由用户交互触发 bgm.play().catch(e console.log(音频播放失败:, e)); musicBtn.innerHTML i classfas fa-volume-up/i 暂停音乐; } isMusicPlaying !isMusicPlaying; }); // “Biu Biu Biu!”按钮连续发射爱心 let fireInterval; fireBtn.addEventListener(mousedown, () { // 按下时开始连续发射 fireInterval setInterval(() { createHeartCluster(Math.random() * canvas.width, canvas.height 20, 5); }, 100); // 每100毫秒发射一簇 }); fireBtn.addEventListener(mouseup, () { // 松开时停止 clearInterval(fireInterval); }); fireBtn.addEventListener(mouseleave, () { // 鼠标移出按钮也停止 clearInterval(fireInterval); }); // 屏幕任意位置点击发射爱心 window.addEventListener(click, (event) { createHeartCluster(event.clientX, event.clientY, 10); }); // 5. 页面加载后初始化 // 页面加载后先预加载一些爱心营造氛围 window.addEventListener(load, () { for (let i 0; i 20; i) { setTimeout(() { hearts.push(new HeartParticle( Math.random() * canvas.width, canvas.height Math.random() * 100 )); }, i * 100); } // 给完整信息添加一个简单的淡入动画CSS中需定义 const style document.createElement(style); style.textContent keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } ; document.head.appendChild(style); });JavaScript核心逻辑深度解析Canvas动画原理requestAnimationFrame(animateHearts)是核心循环。它告诉浏览器“在下次重绘前执行animateHearts函数”从而实现平滑动画。每一帧我们并不完全清空画布 (clearRect)而是用半透明的黑色 (rgba(0,0,0,0.05)) 覆盖这样上一帧的图案会留下淡淡的痕迹形成漂亮的拖尾效果。HeartParticle类封装了单个爱心的所有属性位置、大小、颜色、速度、透明度和行为更新位置、绘制自身。这是面向对象思想在前端动画中的典型应用。贝塞尔曲线绘制爱心这是代码中的一个难点。我们通过四次bezierCurveTo调用用贝塞尔曲线近似勾勒出爱心轮廓。你可以通过调整控制点的参数来改变爱心的形状。如果觉得复杂一个更简单的方法是使用爱心符号❤作为文本在Canvas上绘制 (ctx.fillText(‘❤’, x, y))但自定义路径可以更好地控制颜色和渐变。音频播放的“坑”现代浏览器如Chrome的自动播放策略要求音频播放必须由用户手势点击、触摸触发。这就是为什么我们把播放逻辑放在musicBtn的点击事件里而不是在window.onload中自动播放。代码中的.catch用于处理可能因策略导致的播放失败。事件处理的细节fireBtn使用了mousedown、mouseup和mouseleave三个事件来实现“按住连续发射松开停止”的效果这比单纯点击交互感强得多。全局的click事件监听实现了“点击屏幕任意处发射爱心”的惊喜功能。性能优化点粒子系统 (hearts数组) 会定期清理已经消失的粒子 (splice(i, 1))防止数组无限膨胀导致内存泄漏和性能下降。使用requestAnimationFrame而非setInterval来做动画循环前者与浏览器刷新率同步更节能、更流畅。4. 个性化定制与高级玩法拿到能运行的代码只是第一步让它变成独一无二的表白才是精髓所在。这里分享几个深度定制方向和避坑经验。4.1 如何修改表白文字和样式文字内容直接修改typewriterText变量的字符串值。如果你想显示多段话可以将其改为数组然后修改typeWriter函数逻辑进行循环播放。字体和颜色在CSS中修改#typedText和#fullMessage p的font-family、color、font-size等属性。记得如果要用非系统字体需要在head部分引入对应的字体文件或CDN链接。打字速度修改setTimeout(typeWriter, Math.random() * 80 50);中的数字。50是最小间隔毫秒80是随机范围。数字越小打字越快。4.2 如何更换背景音乐和音效背景音乐准备一个MP3格式的音乐文件替换audio标签中src属性的路径。重要提醒确保文件路径正确。如果HTML文件和音乐文件在同一目录直接写文件名即可如果在子文件夹需要写相对路径如”music/romantic.mp3″。添加音效你可以在createHeartCluster函数里添加音效。首先在HTML中新增一个audio标签用于音效并设置preload”auto”。然后在JS中获取该元素并在创建爱心簇时调用soundEffect.currentTime 0; soundEffect.play();。4.3 爱心的外观与行为定制改变颜色修改HeartParticle类中this.color的赋值。我们使用了HSL色彩模式hsl(色调, 饱和度%, 亮度%)。Math.random() * 30 330生成的是330-360度的色调属于红色-粉色系。你可以固定一个颜色如’#ff4d6d’或者生成随机彩色hsl(${Math.random() * 360}, 100%, 65%)。改变运动轨迹这是最好玩的部分。this.speedX和this.speedY决定了爱心初始的运动方向。this.speedY 0.1;模拟了重力。你可以尝试无重力漂浮注释掉重力加速度这行。螺旋运动引入角度变量让this.x和this.y根据正弦/余弦函数变化。鼠标吸引力在update()中计算粒子与鼠标位置的距离和方向让爱心向鼠标位置缓慢移动。4.4 常见问题与排查踩坑实录问题Canvas动画非常卡顿。排查打开浏览器的开发者工具F12进入“性能(Performance)”或“渲染(Rendering)”面板勾选“帧率(FPS meter)”查看。如果帧率很低如低于30fps。解决减少粒子数量createHeartCluster的count参数不要太大尤其是移动端。优化绘制我们的爱心是用路径绘制的如果数量过多会吃力。可以改为使用drawImage绘制预先生成的爱心图片精灵性能会好很多。检查循环确保animateHearts中只做必要的绘制和更新避免复杂的计算。问题点击按钮没反应或者音乐无法播放。排查首先打开开发者工具的“控制台(Console)”看是否有红色报错信息。常见错误是Uncaught TypeError: Cannot read property ‘addEventListener’ of null这表示JS在DOM元素加载完成前就执行了。解决确保JS代码放在HTML body的底部或者用window.addEventListener(‘DOMContentLoaded’, function() { … })包裹你的初始化代码。对于音乐播放错误查看控制台是否有NotAllowedError这通常是浏览器自动播放策略阻止确保音频播放是由按钮点击等真实用户手势触发的。问题在手机上显示错乱。排查检查CSS中的viewport设置是否正确。检查所有尺寸是否使用了相对单位rem,%,vw/vh而非固定的px。解决使用Chrome开发者工具的“设备模式”进行模拟调试。对于Canvas确保resizeCanvas函数被正确调用并且canvas.width/height设置的是设备的物理像素尺寸。4.5 部署与分享——如何让对方看到这是最后也是最关键的一步。你有几种选择本地文件直接发送将完整的.html文件和相关资源音乐、图片打包成一个ZIP压缩包发送给对方。对方解压后双击.html文件即可在浏览器中打开。缺点路径依赖强如果资源引用是相对路径必须保证文件结构不变。使用GitHub Pages免费且推荐在GitHub上创建一个新的代码仓库。将你的index.html、style.css、script.js和资源文件上传。在仓库设置中找到 “Pages” 或 “GitHub Pages” 选项选择源分支通常是main或master。几分钟后你会获得一个https://[你的用户名].github.io/[仓库名]/的链接这个链接就是你的在线表白页面可以分享给任何人。使用其他静态网站托管服务如 Vercel, Netlify 等操作类似通常只需拖拽文件上传即可获得访问链接。我个人更推荐GitHub Pages方案。它不仅免费、稳定而且整个过程本身也像一次“用代码构建世界”的浪漫隐喻。当你把项目链接发给她/他时可以说“这是我为你部署的一个小小世界。”本文还有配套的精品资源点击获取
返回列表