论博客的进化与前端发展史
概述
这次的标题十分标题党了,这篇文章实际上想要聊聊我的个人博客的技术栈变更与我感受中的前端技术的发展。
事实上,博客这个词,对我来说似乎是上个世纪的东西,在我小时候微博已经流行起来,我是在微博之后才知道博客这个东西的。大约在2017年左右吧,突然想找个输出文字的地方,做一些记录,当时注册了一个微信公众号,直到19年才开始搭建了这个个人博客。其实搭建博客可以说是醉翁之意不在酒,成熟的博客生成应用挺多的,没必要自己折腾,但是当时趁着促销买了个阿里云服务器,正好又想学习一些前端技术,这个博客就应运而生了。
有时为了实现一些功能,了解到一些新的技术,有时又正好相反,将一些新东西应用到博客上,致使博客成了个完全的“冗余工程”,我的博客的技术栈改变,也就恰好变成了我个人的技术栈成长历程,,正好在这里记录一下,顺带要记录一下NextJS
的使用体验。
模板渲染、Session与jQuery
一开始,做为一名主要使用Python
的后端程序员,我尝试使用Django
来开发博客。Django
是一个遵循MVC
模式的Web框架,Python
相关的应用在性能上一般都不强,所以Django
主打的卖点也是快速交付,内置的用户系统、管理后台、模型迁移等,最初版本的博客没花多久就做好了。
数据库采用MySQL
,在Django
中使用内置的模板引擎处理前端页面:
在约定的文件夹内放置静态文件,使用nginx
代理。UI
样式主要是通过BootStrap
,毕竟我对CSS
实在是不熟悉,对于一些页面动效也是通过框架或者一些搜索到的jQuery
代码来解决,这里参考了不少杜塞的博客。
Django
内置了用户与组模型,管理后台也是内置的,登录验证也通过默认的Session中间件,利用第三方库实现了OAuth
登录,在这些方面基本上没花时间,一开始博客开放了登录注册与评论功能,可以通过Github
登录,但后来觉得没必要又去掉了。这里UI与业务代码是完全紧密联系在一起的。
RESTful、Docker与TLS
因为工作的原因,我开始向全栈的方向发展,开始学习JavaScript
,在这之前就常常听到前端三大框架的名字了,因为偶然间加了一个React
群,于是开始学习React
。在这期间我先把博客的后端部分提取了出来,借助Django REST framework
这个库快速地完成了这部分工作,尽量使接口符合RESTful
规范,毕竟应用简单,没什么需要妥协而违反规范的地方,评论功能已经移除,但是登录接口仍然保留着,所以做了个JWT
登录认证的接口,后来还是删除了。剩下的Django
部分不再关心如何呈现用户界面,仅仅根据请求将需要的数据通过JSON
返回给前端,也并不关心前端是什么。
我的服务器操作系统是Ubuntu 18.04
,在最初的部署过程中,需要考虑Python
版本,以及nginx
,MySQL
的安装问题,未来如果服务器要迁移(毕竟阿里云学生机活动只有两年),也比较麻烦,于是用上了Docker
,写好镜像文件后,运行容器即可。
在安全问题与钱包的权衡中,我选择了Let's Encrypt的免费证书实现HTTPS
,安装很方便,每三个月自动续签一次。
SPA与SSR
顺利地在项目中使用了React
做了管理后台之后,开始考虑在博客中用上。但是平时开发的都是SPA
单页应用,怎么解决SEO问题呢?
之前我使用Django
的模板引擎来做服务端渲染,也就是在返回响应之前,已经将文章内容等数据插入HTML
文件中,最终用户在浏览器得到的都是静态文件,如果用户请求了另一个页面,那么他得到的是完全不同的静态文件。
而用React
写的SPA
则是在客户端渲染,使用虚拟DOM
,整个应用往往只有单个HTML
文件,切换页面也不再重新请求新的页面,而是更换需要改变的组件。
那么这两者的优缺点也就很明显了,传统的服务端渲染(SSR),首页打开是较快的,因为不会一次加载过多内容,对搜索引擎也是友好的,毕竟爬虫可以直接获取到静态的、包含数据HTML
文件,但是切换页面则需要更换整个页面重新渲染,并且前后端,表现层与业务层紧耦合;单页应用在客户端渲染,页面切换快,异步获取数据,前后端分离,但是由于首次访问就要获取整个应用资源,因此首屏加载慢,并且经由JS
在客户端操作渲染,爬虫难以获得需要的数据,对SEO
不利。
NextJS:pages、route与定制app
通过React
与SEO
这两个关键字,我发现了NextJS这个React
脚手架,在其官网我又看到了关键词SSR
服务端渲染,难道前端经过多年发展又绕回去了吗?当然不是,历史的发展总是螺旋上升的,这里的SSR
与传统的方式已经不同了,准确点说,这种技术应该叫SSG
静态站点生成或Jamstack
。
NextJS
采用约定式路由,在pages
目录下的文件名,如about.js
,则对应/about
这个URL
,其中默认导出的组件就是页面组件。特殊的是index.js
对应的是/
。除此之外还有动态路由,要求文件名用方括号括起来,如[tag].js
。可以匹配到/a
、/someThing
、/?tag=crumb
等。pages
目录下默认是页面级组件,共享组件则放到src/component
中。
框架提供了useRoute
这个Hook
让我们便捷地使用路由api
,这里我主要希望在页面切换的时候监听切换事件,改变loading
状态,改善用户弱网环境下体验(实际上很难看到这个切换过程,原因后面再说)。监听路由切换是挺方便的,但是如果每个页面都需要注册一次监听,组件卸载时取消监听,重复代码未免太多了。
NextJS
提供了修改容器组件的功能,在pages
文件夹下新建_app.js
:
上面代码中使用了内置的css-in-component
,一种内联式的样式写法。当时我使用的React
版本已经有了Hooks
,整个博客代码里我全部使用的函数式组件,事实上这里用类组件也是可以的。
Data Fetch
前面提到NextJS
可以让搜索引擎获取到预渲染的,拥有数据的静态页面,那么Next
中具体怎么获取数据呢?
-
SSR:服务端渲染,通过在页面级组件中导出
getServerSideProps
函数,在这个函数内访问API
,最后返回一个{props: {...}}
对象,返回值的props
将被注入到页面组件的props
中,这个方法的运行时机是每次客户端请求时(这种形式下NextJS
会默认用户使用Node
做服务器,但仅限于UI层,仍然是前后端分离的),适合页面数据变化多的情况。- 可以渐进加载,数据完全获取之前用户首先得到较少的、轻量的页面
- 前端需要一个
Node
服务做一个中间层,前后端分离 - 每次请求都需要向后端获取一次数据
-
SSG:静态站点生成,在页面中导出
getStaticProps
方法,这个方法只会在build
时运行一次,不会出现在客户端,所以甚至可以在这里访问数据库和文件系统,如果所有页面都是SSG
,构建之后的应用可以直接以Serverless
方式部署,只需一个CDN
就可以部署项目。同一个页面下SSR
与SSG
是互斥的,但不同页面可以根据需要来做。对于[id].js
这样的动态路由,则可以配合getStaticPaths
这个方法,返回所有可能的路径,Next
会自动生成所有页面。- 部署便捷
- 仅在构建时在服务端调用数据获取函数
-
ISG:增长式静态再生成,前面说了对于页面数据变化频繁的应用,可以使用
getServerSideProps
,但是较新版本的NextJS
变得更加强大,对于getStaticProps
,可以在返回的对象中加上revalidate
属性,值以秒为单位,Next
会在有新请求进入后的固定时间后验证后端数据,如果确实有新的更改,将先返回由新数据组成的页面,在这之后再自动重新build
。而getStaticPaths
则可以通过在返回值中设置fallback
属性为true
,这样例如posts/[uuid].js
上次build
后有24篇文章,那么访问/posts/25
将可以不用直接返回404,而是重新验证请求是否有新文章(详情查看文档)。这两种方式可以配合使用。- 增长式构建
- 快速响应,结合了传统服务端渲染与SPA的优点
-
客户端请求:在以上任何一种形式下,我们都仍然可以在客户端执行请求,并且
Next
提供了一个非常优秀的基于Hooks
的请求库swr。
CI/CD、Serverless与GraphQL
一开始我打算利用Github Actions来做自动部署,不过恰好碰上Next
更新,Next
所属的Vercel
云服务更加好用了,于是我就选择了官方推荐的方式,只要提供一个git
仓库的链接,Vercel
会在每次push
后自动以Serverless
的形式部署,并且提供域名与HTTPS
证书(其实就是Let's Encypt的证书)。
这时候我想把后端也改造成Serverless
应用,顺带发现了hasura,可以提供免费的GraphQL
服务,只要有一个postgresql
数据库就可以生成一个基于GraphQL
的后端,简单的CRUD
是完全没问题的,所以也没有继续折腾了。但对于前端来说,我仅仅是要在构建时获取数据,Apollo
对我来说太重了,没必要,于是我找到了graphql-request
这个库,基本上只是对fetch
的简单封装,足够使用了。
Link、Shallow Routing与筛选分页
前面提到服务端渲染在切换页面的速度上有缺陷,因为请求新页面需要返回完整的新页面的静态文件,哪怕页面大部分布局都没变。Next
提供的Link
组件,在默认情况下,会在闲置时自动请求JSON
数据,这样等到用户点击链接时,就可以做到快速更换内容,渲染新页面,也是因为这个在非弱网环境下我看不到页面loading
效果。
在我的博客中,为文章模型设置了不少外键,像文章栏目、标签这些,还有分页,想要为这些设置页面,Next
提供了一种不用重新抓取数据更新页面的方式Shallow Routing
:
在useEffect
这个Hook
中根据route.query.column
的变化决定是否更新文章数据源,就可以做到筛选,并且不需要重新获取数据,页面只有部分更新。
但是我非常贪心,既想使用静态模式,又想每次筛选只拿到筛选所需的数据,而不是一次取得所有数据,在客户端筛选,这可以借助动态路由来做(这里我使用的TypeScript,Next全面支持TS):
pages
├── about.tsx
├── _app.tsx
├── posts
│ └── [id].tsx
├── series.tsx
└── [column]
│ └── [page].tsx
├── [tag]
│ └── [page].tsx
└── [page].tsx
但是这种形式,实质上对于栏目、标签、页数的筛选,其实都是首页列表页,这造成仅仅只是getStaticPaths
函数不同,剩下全是重复代码,这是令人无法接受的,并且这只能接受**/[column]/[page这样的路由,而不能是/column=Python&page=2**这样的query
形式。
那么有没有办法一次接受所有的动态路由呢?实际上是有的,[...slug]
这种命名的页面组件就可以,但是参数只能是一个数组,例如['a', 'b']
对应/a/b
,这样我没法分辨column
与tag
,并且与前面说的一样,没法以query
参数的形式访问。
关于这方面,有一个issue,有可能会在某一个版本实现getStaticParams
这样的API
,对于博客这种数据量小的应用使用Shallow Routing
完全没问题,但是对于如知乎这样的大型平台,筛选、搜索、分页功能都是必不可少的。
下一步
现在在我的博客中最初的Django
部分已经完全废弃了,自然Django提供的管理后台也就不能用了,在上一篇文章中介绍了Blazor
,接下来预计会花费几个周末来搭建一个SPA
的后台,毕竟后台应用不需要SEO
,SPA
会更合适,用CSharp
来写前端,过去JS
向桌面端、移动端渗透,反过来,静态语言也开始染指Web
前端了。
总的来说,这个博客的不断重新构建的过程,也是我学习一些前端技能的试验过程,在这个过程中倒有一种经历了前端技术发展变迁的感觉,从“切图仔”,慢慢地工程化、体系化,随着Node
以及一些框架的发展,前端开发体验也在不断提高。博客本身成了一个实验室,各种东西轮番体验了一遍,这个过程暂时还不会停止,毕竟业余时间写写代码还是挺有意思的。博客已开源。