使用 Vite 重写博客
Back就使用体验来说,感觉 create-react-app 还是不太好用,不是真正的“开箱即用”,最近 Vite 比较火,看了眼文档感觉确实不错(比 webpack 文档强多了),于是想试试 Vite 用起来咋样。
之前的 repo 里也有些自己觉得写得很离谱的地方(虽然整个 repo 只能算是“极小”的级别),这次也算是重构一下吧。
几个月以来也一直听说 Tailwind CSS,很多人推荐,于是也试了一下。虽然最开始会觉得有些颠覆之前的样式书写习惯,但是整个目录下只有一个 .css
文件还是挺爽的一件事。
这里记录的主要是感觉需要记录的一些点吧。
markdown 文件导入问题 ​
之前使用的 create-react-app 已经在 webpack 层面配置了 .md
文件的模块化,然后使用以一个 adapter 文件做转接,然后在博文组件里根据 pathname
动态导入文本内容。
Vite 在静态资源导入方面有更多选择:
- 使用
?url
后缀将资源导入为 URL 形式 - 使用
?raw
后缀将资源导入为字符串形式
最开始的想法是用 ?raw
后缀配合 import()
函数在博文页组件动态导入 markdown 文本,没有任何像 adapter.ts
这种看起来很冗余的处理。但是现实比较骨感,这一方式在本地调试环境可行,但是在 build 完成后再 serve 就报错了。
在 Vite 的 issue 里有提到这个问题,但是似乎尚未有有效的解答:Production Build Failed for Dynamic Import of ?raw Files #3222。
等这个 issue 被解决了写起来应该就方便多了。但是现在我还是选择了跟原来差不多方案,使用了一个 adapter.ts
的文件,负责导出文件的 url 信息,然后在博客页组件动态 fetch。虽然还是很麻烦,但是相比之前的做法,少了一步动态 import adapter,简洁了一点点。
// adapter.js
export { default as ReactMarkdownTest } from '../markdown/react-markdown-test.md?url';
export { default as VueNative } from '../markdown/vue-native.md?url';
export { default as JavascriptNoteForBadMemory } from '../markdown/javascript-note-for-bad-memory.md?url';
...
// 动态 fetch
fetch(mdUrl)
.then(res => {
// Vite converts files smaller than 4kb to base64 by default, so the
// url of markdown file may be a base64 string that start with 'data:text'
// or the filename that end with '.md'.
if (!(/^data:text/.test(res.url) || /\.md$/.test(res.url))) {
throw new Error(`Wrong path: '${location.pathname}'. Redirect to 404 page.`);
}
return res.text();
})
.then(text => {
setMarkdownText(text);
})
.catch(err => {
setIsNoContent(true);
console.error(err);
});
这里有一个值得注意的地方是,在验证 markdown 资源是否存在的同时,我也验证了这个 url 是否是以 data:text
为开头的。起初并没有做这步处理,结果发现打包后有几个 markdown 文件不翼而飞,博文也加载不出来,直到我看到文档里的这句话:
较小的资源体积小于 assetsInlineLimit 选项值 则会被内联为 base64 data URL。
默认的 assetsInlineLimit 是 4kb,所以比较小的 markdown 文件的 url 被转化为了 base64 格式。不好好看文档就会踩坑。
代码高亮问题 ​
这次换了个代码高亮方案,用了 rehype-highlight
插件,这个插件依赖了 lowlight.js
库,lowlight.js
是对 highlight.js
的封装。然后 CDN 引入一个主题 CSS 文件即可。
有一个问题是 build 的时候会报错:
node_modules/rehype-highlight/index.d.ts:11:59 - error TS2694: Namespace '"node_modules/lowlight/lib/core"' has no exported member 'LowlightRoot'.
手动把 node_modules
里的 LowlightRoot
改成 Root
才可以。这个问题有点怪。
2021-12-19 更: 十几天之前
rehype-highlight
官方解决了这个问题,并发布了新版本。 将版本从5.0.0
升级为5.0.1
之后解决
目录功能 ​
之前的做法是用 react-markdown 的 components
props 在处理标题 tag 的同时向外传输各级标题信息,之前遇到的问题现在也遇到了,调试时回调函数会执行两次,非常奇怪。
这次换了一个思路,分成两步执行:
- 在 markdown 文本更新后,逐行读取文本,将 '#{1,6}' 开头的字符串认为是标题信息,逐渐构建一棵目录树结构
- 然后再 通过
Selector API
获取所有标题 tag 元素,与之前目录树的结构通过 id 一一对应
useEffect(() => {
setCatalogList(getHeadingInfo(markdownText));
const markdownBody = document.querySelector('.markdown-body');
if (markdownBody) {
for (let i = 1; i <= 6; i++) {
(markdownBody as unknown as Element).querySelectorAll('h' + i).forEach((el, index) => {
el.id = 'h' + i + '-' + (index + 1);
});
}
}
}, [markdownText]);
其实 id 的标记思路是用的 h + level + 该级别标题出现的次数
,这样的话其实并不需构建树结构,直接遍历就行了。构建树是因为最开始想在 id 上体现目录层级结构,后来觉得比较麻烦就放弃了,但是我还是觉得树结构可能后面会用到吧。
Tailwind CSS 预设样式 ​
Tailwind CSS 预设了一些样式,比如列表项默认没有样式:
ol,
ul {
list-style-type: none;
}
这会和我们期望的 markdown 展示效果有所冲突,需要手动添加一些修改。使用 @layer
可以防止样式顺序先后引发的问题。
// index.css
@layer base {
.markdown-body ol,
.markdown-body ul {
list-style-type: unset;
}
}
react-router 版本升级 ​
原本用的 v5,这才没过多久就是 v6 了,而且重要的 API 都发生了改变。匹配也都是 exact 匹配了,而不是原来的前缀匹配。使用体验上还不错。
评论 ​
使用 Waline
做了评论系统,基于 Valine
,目测更新频率还不错,而且有很友好的文档。照着文档配合 Vercel 和 LeanCloud 做了一个效果很好的评论系统。
支持 markdown 和微博表情,好看的:
Vercel 部署 ​
原本一直都是用 Github Page 做静态页面部署,但是最近发现了更为优秀的 Vercel,使用体验和访问速度都可以说有了巨大提升。
在 Vercel 上需要做的事情非常简单,只需关联上 github 仓库就好,build 和 deploy 工作全部是自动完成的。每次仓库的更新都会自动触发部署的更新,极其方便。
另外一个很好的点竟然是自定义域名竟然也可以被生成 SSL,体验很好。
同时 Vercel 通过 rewrite 配置也可以让我们用上 BrowserRouter
(Why does react-router not works at vercel?)。之前把网站托管在 github page 上的时候只能被迫用 HashRouter
,总觉得有点蹩脚。
代码分割 ​
Vite 配置中有一项 build.chunkSizeWarningLimit
,默认值为 500,这意味着编译完成后第三方包大于 500 KB 的时候就会触发 Warning,告知用户需要做一些代码分割之类的工作了。最开始我图省事把这个值改成了 1000,后来装了几个包之后发现 vendor.js 又已经超过 1000 KB 了。所以开始着手开搞 code split。
由于 Vite 支持使用 Rollup 插件,所以在前人的铺垫下这件事做起来并不困难。
首先,rollup-plugin-analyzer
插件可以检测打包时各个 bundle 的大小,给出一个包大小降序排列的统计表:
Rollup File Analysis
-----------------------------
bundle size: 1.281 MB
original size: 1.428 MB
code reduction: 10.25 %
module count: 421
███░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
file: D:/repo/banqinghe.github.io/node_modules/parse5/lib/tokenizer/named-entity-data.js
bundle space: 7.26 %
rendered size: 93.089 KB
original size: 73.717 KB
code reduction: 0 %
dependents: 2
- D:/repo/banqinghe.github.io/node_modules/parse5/lib/tokenizer/named-entity-data.js?commonjs-proxy
- D:/repo/banqinghe.github.io/node_modules/parse5/lib/tokenizer/index.js
...
通过这个插件可以找到是哪些包占据了主要的第三方包体积,我现在比较占地方的包主要是:
react
,react-dom
waline
(评论插件)katex
(数学公式支持)
减小包体积地主要思路有两个:
- 引入 CDN
- 用
React.lazy()
做组件懒加载
代码分割方法也比较好实现,引入 CDN 再做一些配置就好了。这里主要用到了 rollup-plugin-external-globals
插件,可以比较方便地把代码中的模块引用转化为对全局变量的引用。
rollupOptions: {
plugins: [
externalGlobals({
'react': 'React',
'react-dom': 'ReactDOM',
katex: 'katex',
'@waline/client': 'Waline',
'highlight.js/lib/core': 'hljs',
}),
],
}
组件懒加载在路由处配置即可,将静态引用组件更改为使用 React.lazy()
引入:
import PostPage from './PostPage';
↓
const PostPage = React.lazy(() => import('./PostPage'));
同时需要配合 React.Suspense
组件来规定当组件未加载完成时展示的加载页。
一通操作之后终于让每个包的体积都降低到了 500 KB 以下,剩下体积较大的包都来自 parse5
,并没有找到 CDN 资源,就先不管了。