前言
阅读时长 | 用脑度 | 前置知识 |
---|---|---|
10min | 30% | React |
最近更新了一波个人主页,正好整理一篇文章来分享一些想法。这篇文章会聊一聊网站中每个部分的实现思路,以及会说到我对设计的一些想法和思路。
如果你也想写自己的个人主页,希望这篇文章可以给你一些灵感。
以前的版本
先给大家看看以前的版本。v1.0 还是 2018 年写的,年代真的太久远了,以至于本地跑 node-sass 都报错了。
v1.0(2018)
image.pngv2.0(2019)
image.png定位
首先来聊聊定位,我觉得这是做个人主页的最重要的部分。我见过太多人做着做着就偏离目标,最终放弃,俗称 “需求不明确”。
比如刚开始做文章博客,在 Markdown 样式各种纠结,然后放弃,又比如有的人喜欢啥都自己造,结果花时间太多在“造轮子”上,反而网页迟迟造不出来,最后放弃。
我以前做个人主页的目的只是为了收集在 简书 和 Medium 上的文章,顺便写写个人简介,就当个主页了。
当时比较迷恋:内容为王 这个思路。所以我的想法很简单:一切以博客文章为主。绝不手写轮子,轮子给我啥我用啥,就用默认的样式。
可以看到无论是 v1.0 和 v2.0 都是一股强烈的 Element UI 味,很多组件连改都不改,只在 v2.0 在首页上稍微做了点改进。
而且我还非常的 鸡贼,我偏不踩 Markdown 和编辑器的坑,所以文章链接都是 外链 形式,点进去就跳转到 简书 和 Medium 上。
image.png一来引流(天真了,有人点进我主页就有鬼了),二来不想浪费时间在 Markdown 的样式了,三来不想自己搞后端服务、博客管理后台。
而对于新版 v3.0,我不再是放各种文章了,因为只要在平台上写好文章,自然就有人关注了。另一方面,别人点进我的主页可能仅仅是因为好奇。
所以,为了满足各位观众老爷的好奇心,我将其定位为个人落地页,也即 Landing Page。主要内容:突出个人,且花里胡哨。
风格
落地页最重要的特点就是 大 和 爽,所以我看了市面上很多个人主页,主要归为几个类:微博类、大佬简约类和欧美巨大类。
中国微博类
第一类我称之为“中国微博类”,因为具有非常强烈的微博样式,主要元素有:文章主体、文章目录、文章分类、标签等。代表作:阮一峰的网络日志。
image.png这类主要以文章内容为主,元素非常多和杂。国内使用这类的非常多,导致同质化非常严重,很难搞出新意,而且要做好文章的样式是一件非常麻烦且复杂的事情,与我的定位不符,所以 pass~
大佬简约类
这类算是意义上的落地页,但是元素实在太少,只有几行的介绍就完事了。代表作:尤雨溪的主页。
image.png连个图片都没有,属于“爱看看,不看去学习”的风格。然而我不是大佬,pass~
“奇行种”类
这类一般非常的秀,几乎进去的就不是为了看内容的,而是为了 玩。比如这些:
欧美巨大类
国外的人更喜欢 大图,粗线条感 和 重口味颜色 的风格。这类的设计图在 dribbble 和 behance 上面非常多。
image.png主要特点就是:线条比较粗犷,色彩比较鲜艳或者淡雅,元素以“大”为主,同时能保持一种简约风格。
“欧美巨大类”是我比较喜欢的类型,跳脱了国内“类微博风”的博客主页,又能有好看的设计。唯一的难度就是自己设计不出来这么好看的,所以我在 dribbble 和 behance 上找了一个模板,再结合别的一些元素,边开发边融合。
为什么不?
这时有的老铁就会问了:
为什么你不去 Hexo 这些网站直接使用免费的模板呢?
主要还是因为上面的模板大多数都是“类微博风”,很少有 dribbble 网站上的设计风格。但是如果你没有十分特别的需求,我建议最好就用现成的,尽量不要“发明”轮子。
那 WordPress,Wix 国外的网站生成器也有“欧美巨大类”的呀?
一个原因是访问速度实在太慢了,不过,主要还是贵 🤣。
技术栈
- React
- TypeScript
- Sass
- Ant Design
我相信有很多人都会觉得:
个人网站就应该自己手写,这样才能吸引到面试官。当面试官问起的时候,才可以滔滔不绝地讲如何攻克某个技术点。
我的想法是:不建议这么搞,能用轮子用轮子,除非万不得已,千万别手写!
要时刻记住我们究竟是在 练习 还是在 做产品。对于前者没什么好说的,手写轮子 + 发技术文章,这在掘金上经常见到。而对于后者,则不应该纠结技术,而是想方设法把 产品 做完美的。
自己造轮子的缺点有很多:
- 喜欢 “发明” 轮子,而不是 “造” 轮子
- 做出来还要维护,而且并不比市面上的轮子好用
- 最重要的一点,很容易就钻入 “如何解决 XX 问题” 的牛角尖,然后忘了到底是来写主页还是来造轮子的
请相信另一句更真实的信条:
做出来就是 NB,做不出来就是 SB。
所以,放过自己,站在巨人肩膀上不香么?好了,废话不多说,下面就来说说我是怎么实现的吧。
导航栏 - Nav
Nav.jpg经典的左边 Logo,右边 List 布局,实现方法非常多。
在以前,用 float: left
和 float: right
来实现会比较流行,然而,这个方法缺点是得手动清除一下浮动,且 float 已经成为过去式了,所以 pass~
这里直接用 flex 的 space-between 收工了。
.nav {
display: flex;
align-items: center;
justify-content: space-between; // 分开左右两边
}
导航栏另一个需求点就是要适配手机端,不然所有 Nav 标签都被挤在一起了。
[图片上传失败...(image-450826-1625280925104)]
我的实现是:做两个导航栏,然后通过 @media 媒体查询来控制两者的显示。
<!-- 水平 -->
<ul class="horizontal">
...
<li class="navBtn" onclick="打开vertical list">垂直打开按钮</li>
</ul>
<!-- 垂直 -->
<ul class="vertical">
...
</ul>
// 小屏样式
@media screen and (max-width: 992px) {
ul.horizontal { // 水平的 Nav 别出来
li {
display: none;
&.navBtn {
display: block; // Toggle 按钮出来
}
}
}
ul.vertical {
display: flex; // 垂直的 Nav 出来
}
}
Very easy~ 导航栏还有一个需求点:点到哪个 Tab 就要下滑到对应的 Section。很多老哥的第一反应就是 <a href="#id">
标签 + div 里嵌入 id
,用 url hash 来导航。缺点是:下滑动作太生硬了,没有动画。
这里推荐使用 $el.scrollIntoView({ behavior: 'smooth' })
这个 API,在 PC 端滑动效果还不错。缺点是不能适配手机端,小细节,可忽略~
const scroll = (toEl: string) => {
const $toEl = document.querySelector(toEl);
if ($toEl) {
setActiveItem(toEl);
$toEl.scrollIntoView({ behavior: 'smooth' });
}
};
加上 position: fixed
将 Nav 组件固定在屏幕头部,会更有 整体感。
.nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 3;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.1);
}
最好在外层加个 box-shadow 阴影效果,会让导航栏更有 立体感。
但是注意,不要加得很明显,会让人感觉有点刻意而为之。最好的阴影效果是第一眼看不出来,仔细看才看出来。
像这种阴影效果、背景渐变效果,是很难纯手动调出来的,最好搭配 GUI 生成器来生成 CSS。Google 搜索 box-shadow generator,各种样式随便调!
image.png要是你说:我 GUI 还是调不出来好看的效果咋办?答案是:抄。自己不专业,就看专业的人怎么做,比如掘金就的导航栏阴影就不错:
image.png广告页 - Banner
home.gif左边部分,一个 <p/>
里面加个 <span/>
搞定了。划线部分,可以用伪元素 ::before
和 ::after
生成两条横线,再通过 position: absolute
来调整位置就可以实现划线效果。
.del {
position: relative;
&::before, &::after {
content: '';
position: absolute;
background: #bd0000;
width: 120%; //宽度超出一点点比较好看
height: 6px;
}
&::before {
left: -10%; // 处理宽度超出的部分
top: 40%;
}
&::after {
left: -10%; // 处理宽度超出的部分
bottom: 30%;
}
}
打字效果 可以使用 typed.js 这个小库,用起来非常简单,这是官方的小 Demo:
// Can also be included with a regular script tag
import Typed from 'typed.js';
var options = {
strings: ['<i>First</i> sentence.', '& a second sentence.'],
typeSpeed: 40
};
var typed = new Typed('.element', options);
打字速度、删除速度、DOM 元素的获取逻辑都是可以重复使用的,所以我封装成一个 hook:
import Typed, { TypedOptions } from 'typed.js';
const useTyped = (strings: string[], extra?: TypedOptions) => {
const el = useRef<HTMLElement | null>(null); // span 元素
const typed = useRef<Typed | null>(null); // Typed.js 对象
useEffect(() => {
const options = { // 默认属性
strings,
typeSpeed: 100,
backSpeed: 60,
...extra,
};
typed.current = new Typed(el.current || '', options);
return () => typed.current?.destroy(); // 擦屁股
}, [strings]);
return el;
};
使用的时候就很方便了:
const Home: FC = () => {
const el = useTyped(strings, { loop: true });
return (
<span ref={el} />
);
};
typed.gif
再来看右边,其实是一个使用 ::after
实现的动画效果 + Lottie 动画 效果。先来说这个 “波纹” 效果,其实英文名是 pulse。“波纹” 效果是另一种效果,叫 ripple。
@keyframes pulse {
0% {
transform: scale(1);
border-color: #e3342f;
}
100% {
transform: scale(1.2); // 放大
border-color: transparent; // 透明
}
}
.ball {
&::before {
content: "";
position: absolute; // 定位到同元素上
width: 100%;
height: 100%;
border: 4px solid #e3342f; // 波纹
border-radius: 50%; // 变圆
background: transparent; // 不要背景颜色
animation: pulse 2s cubic-bezier(.57, .06, .27, .84) infinite; // pulse 动画
z-index: 1; // 放在原来元素之上
}
}
sheep.gif
哦,是心动,不对,是心跳的感觉。
关于这个小羊的动画,它并不是一个 Gif 图片,而是 Lottie 动画。这是 Airbnb 开源的一套跨平台的完整的动画效果解决方案。说人话:就是高级版的 Gif。动画内容是通过 JSON 文件来驱动的,可以在 Lottie 官网 上找到免费的,自己找一下就可以了。
关于使用方式,我依然封装了一个 hook:
import lottie, { AnimationConfigWithData } from 'lottie-web';
const useLottie = (path: string, extra?: AnimationConfigWithData) => {
const lottieRef = useRef<HTMLDivElement | null>(null); // 动画元素
useEffect(() => {
if (lottieRef.current) { // 默认参数
lottie.loadAnimation({ // Load
container: lottieRef.current,
path,
renderer: 'svg',
loop: true,
autoplay: true,
...extra,
});
}
}, []);
return lottieRef;
};
使用的时候传入 JSON url 就可以了:
const sheepLottie = 'https://assets3.lottiefiles.com/private_files/lf30_lgesk2nm.json';
const Home: FC = () => {
const sheepLottieRef = useLottie(sheepLottie);
return (
<div ref={sheepLottieRef} />
);
};
至于下面的箭头,就是 Lottie + scrollIntoView({ behavior: 'smooth' })
的组合,不赘述。
关于 - About
image.png这一部分为个人介绍。左、中、右分别是 <p>
、<img>
、<p>
,简单。其中,左边中间那个正方体依然是个 Lottie 动画,右边的 “海怪” 用的是 HongLei 字体库。
Tip:强烈背影颜色情况下,依然可以使用阴影效果来突显页面的立体感,可以有效避免文字和背景在视觉上 “融合” 的问题。这里文字用了 text-shadow,头像用了 box-shadow。
下一个部分,Timeline 组件。
image.png之前研究过 Ant Design 的 Timeline 组件,它是用定位来实现的。我不太喜欢内容 div 脱离文档流的实现方式,会经常就出现 div 高度坍塌的问题。所以,我的 Timeline 组件并没有用 position,而是用 Flex 布局 来实现的。。
来复习一下 Flex 布局的 align-items 属性,flex-start 靠上,flex-end 靠下展示:
image.png如果把这个 div 加上 flex-direction: column
,然后左右的 div 设计宽度 50% 不就可以实现左右两边展示了么?
.timelineItem {
width: 50%;
&.right {
text-align: left;
align-self: flex-end;
}
&.left {
text-align: right;
align-self: flex-start;
}
}
image.png
然后就是轴体的实现,这里用了 ::before 伪元素 + position 来实现,但是如果设置宽度为 1px 的时候,会出现 “错位” 的问题。
image.png这也很容易理解,因为定位后的 “轴体” 就是在原有 div 突出来的。可以设置 left
和 right
的值来解决,但是 1px 又会产生心理上的不对称,所以我把宽度设置为 2px 就 OK 了,同时轴体看上去也更饱满。
中间的自定义节点就传入一个 ReactChild 就 OK 了,字体、背景也不赘述。
Project - 个人项目
image.png先说说瀑布流怎么实现的,你可以使用以下方式来实现:
- multi-column 多栏布局
- grid 布局
- flexbox 布局
- ...
还记得前面说的么?我们是在做产品,不是造轮子,所以不要纠结怎么实现,直接使用现有 NPM。这里推荐 react-masonry-css。顺便说一句,瀑布流的效果英文真不叫 Water Fall,而是 masonry。
const Project = () => {
return (
<Masonry
breakpointCols={{ default: 3, 992: 2, 786: 1 }}
className={styles.projectList}
columnClassName={styles.projectListColumn}
>
{projects.map((project) => (
<Item {...project} key={project.logo} />
))}
</Masonry>
);
};
.projectList {
display: flex;
list-style-type: none !important;
}
.projectListColumn {
background-clip: padding-box;
}
这个库有一个叫 breakpointCols
的 props,可以在不同屏幕宽度下展示不同列数,非常实用的一个功能。
项目卡片的样式就不废话了,无非就是 color
, font-size
, text-shadow
之类的。
来说说这些 stars 1.2K
的图标是怎么来的吧。首先,如果你是用 Travis 或者 Coveralls,官网是有地方自动生成的,比如在 Travis CI 上点这个图标就会有图标的 Image URL。
那我岂不是要上每个网站上弄一遍图标地址?而且有的网站还可能没有呢。这里推荐使用 shields.io 这个网站,几乎可以自动生成我们常见的所有 Shield 图标。
image.png唯一的缺点就是:点这个图标不会跳转到对应的网站。不过可以自动生成多类图标,还可以设定图标 style,还可以自定义 Shield,要啥自行车不是?
然后是这些小 icon 的获取,在 http://iconfont.cn/ 拿就行了,这个老铁们应该很熟了。
最后说一下这个动画效果。
jelly.gif英文名叫 jello,中文名是 啫喱(zhě lí )。
@keyframes jello {
0% {
transform: scale(1, 1);
}
25% {
transform: scale(0.9, 1.1);
}
50% {
transform: scale(1.2, 0.8);
}
75% {
transform: scale(0.95, 1.05);
}
}
Contact - 找到我
contact.gif因为上面几个 section 的背景都是跟随页面滚动的,而且这个页面没有太多的动画效果,所以我把这里的背景设置为 background-attachment: fixed
,让其不会显得那么单调。
然而 backgroud-attachment: fixed
在手机端是不能用的,会变成 cover 的样式,所以在手机端要设置为 initial
的值。
style={{ backgroundAttachment: isMobile ? 'initial' : 'fixed' }}
同时,背景这里我选择了 黑白 + 个人 比较单一的照片,并没有太多花里胡哨,也是因为这个 section 的元素太少了。
样式实现上很简单,就不多说了。
动画
上面把各个 section 都大概讲了一遍,这部分聊聊动画。
下面部分带有强烈主观色彩,不一定正确
我在主页里加入了很多入场动画,用到的库是 react-reveal。这个库的功能是:当滚动到当前元素时,使用动画入场效果展示元素。 很实用的一个库。虽然是个老库了,但是动画方面还是挺强的,用法也简单:
import Fade from 'react-reveal/Fade';
class FadeExample extends React.Component {
render() {
return (
<div>
<Fade left>
<h1>React Reveal</h1>
</Fade>
</div>
);
}
}
fade-left.gif
动画并不是越多越好的,好的设计不是花里胡哨,而是克制。虽然我不是设计师,但是也尽量遵循 Ant Design 的设计原则。
自然
尽量不用很夸张的动画。虽然 react-reveals 提供了很多花里胡哨的动画,但是都太夸张了。
image.png99% 我都用 fade-in 这个入场效果,过渡更自然点。
高效
对于 Banner 页面的文字,刚开始是想用 typed.js 将整段文字输出的:
complex.gif同事看了后,说:“一般面试官是没有耐心看完的”,这让我意识到这么太低效。所以我改成直接展示 90% 的文字,只有最后的词语循环打印。
另一个场景是,我原来是用 fade in from bottom 来展示每个项目:
[图片上传失败...(image-8f2558-1625280925105)]
每个 ProjectItem 里的文字、图片、图标是比较多的,所以使用了向上动画会让人很难第一时间注意到内容,必须等动画结束了才能“看清楚”内容,而且在手机上尤为明显,同样是 低效 的。所以,我后来都改成直接 fade-in,稍快加速动画,同时兼顾了入场动画。
对比 & 对称
如果所有入场都用 fade in from bottom 就会显得有点重复了,所以加点 对比 和 对称 可以稍微点缀一下。
首先是在小羊 Lottie 动画和向下箭头 Lottie 动画这里,前者向上渐入,后者向下渐入,形成对比。因为箭头是向下的,所以用 fade in from top 会更有 逻辑性。
banner-transition.gif另一个地方就是时间轴这里,左边内容使用向右入场,右边内容则向左入场,同时也遵循动画入场的 逻辑性。
timeline-compare.gif强调
唯一使用了夸张动画的地方,就是 “联系我” 的 “👍 求点赞,求关注,求转发,一键三连!”。
contact-transition.gif主要还是因为这个 section 真的太单一了,加一个夸张动画增加一点动感,这里用作强调动画应该不过分吧,哈哈 🤣。
颜色
颜色方面,国内的审美一般以 小清新 为主,而由于走了欧美的 粗犷 路线,所以颜色方面使用了重口味的颜色,以 红、黄、橙、黑 为主。
image.png这里的颜色值都是比较相近的。千万不要选颜色跨度太大的颜色,不然你的页面就变成东一块,西一块,有很强的割裂感。
如果你对选颜色不是很敏感,可以上 Adobe 的 主题色推荐,里面有超多的主题色任君选择。
image.png背景
背景真的太难选了!难点有:
- 不能太花里胡哨。不然会喧宾夺主,内容会被背景抢去关注
- 图片体积要小,很多人只会打开一次你的主页,这时是没有任何 HTTP 缓存的,所以体积大的背景加载时,会出现从头加载到脚的效果
虽然只说了两点,但是 90% 的图片都是不合格的:风景图、个人照、文艺照。然而,往往这些照片才能提升主页逼格的关键。那有什么图片,体积又小、又不那么单一的呢?答案是:
在 background-repeat: repeat 或者 background-size: cover|contain 情况下都不会有太多视觉上变化的图片,才能满足上面的要求。
这里推荐两个网站,自动生成高级 SVG 背景图:
- svgbackgrounds,有 48 个免费 SVG 背景,而且都可以自定义一些样式的,付费有 200 多种,我觉得 48 个就够用了
- loading.io,本来是个做 Loading 动画的网站,后面也做 SVG 背景图了,里面更多自定义的模板,免费版只能生成静态的,付费版可生成动态 SVG 背景,同样的,免费版就够用了
优化
代码层面,用 React 的 React.lazy 和 Suspense 做了分包:
const Home = React.lazy(() => import('./sections/Home'));
const About = React.lazy(() => import('./sections/About'));
const Project = React.lazy(() => import('./sections/Project'));
const Contact = React.lazy(() => import('./sections/Contact'));
const Footer = React.lazy(() => import('./sections/Footer'));
const App: FC = () => (
<div id="app">
<Suspense fallback={<Loading />}>
<Nav />
<main>
<Home />
<About />
<Project />
<Contact />
</main>
<Footer />
</Suspense>
</div>
);
对于 图片 的优化,本来想用 Webpack 的 imagemin 来做的,但是 creat-react-app 太坑了,试过 react-app-rewired 和 craco 都没什么效果,算了,还是手动自己压缩吧,反正没几个图。头像和背景图都转成 webp 格式,尽量减小体积。
最后是 字体库 的优化,上面说到我用了 HongLei 这个字体库,整个库有 1.1 MB,而我就用了“海怪”这两个字,有点划不来。所以,我用 fontmin 将字体库压缩到了 4 KB,能更快一点显示字体。
最后
整个主页在业余时间写了 2 周,大部分时间都是在试各种设计、颜色、背景,实现上也挺简单的。喜欢的话就在 我的 github 上点个 Star 吧,欢迎 fork 和魔改!
其实,还有 Github 部署和 CDN 加速这一块也非常重要。由于这期文章太长了,就放下一篇来讲吧。
对了,最近开了个公众号【写代码的海怪】,如果你觉得我写得不错也可以随缘关注一下喽~
网友评论