:)
没记错的话,这是第5次重写blog,没有其他的点子,只好拿博客开刀了呀
差不多花了一周时间,大部分时间用在实践typescript和组织代码逻辑。相比于vue2SFC
一把梭,vue3提供更灵活的代码书写方式,我也是偶然从一个视频里了解到vue3的理念:
一些想法
前段日子我忽然想:热衷于写博客网站,但却没有值得写的博客文章,那还有何意义呢?以后学习知识,可以不妨考虑一下——学到何种程度,值得记下来吗。无论是有用亦或是有趣,如果不值得,那是否还有必要去学? 本文就作为我第一篇值得写的文章。
学习笔记
也可以是typescript
+ vue3
+ nuxt3
学习笔记,因为之前我没有系统地学习并写typescript。
typescript
javascript本身是无类型检查的,代码量多了,写起来会忘记数据结构,函数参数。很明显ts可以解决这个问题,还有另一个很好的地方,ts只是js的超集,使用它,只会有优点不会有缺点。
我没有学习ts的最佳实践,自己慢慢摸索出自己的风格。例如,本站有三个tab:文章
、记录
、文化
,它们的数据都有共有点,于是我如下定义:
export type NeedsItem = {
id: number;
time: number;
_show: boolean;
};
export type ArticleItem = NeedsItem & {
title: string;
len: number;
tags: string[];
};
export type RecordItem = NeedsItem & {
images: { src: string; alt: string, id?: number }[];
};
export type KnowledgeItem = NeedsItem & {
title: string;
type: "book" | "film" | "game";
};
export type CommonItem = ArticleItem | RecordItem | KnowledgeItem;
CommonItem
便是通用的item对象,表示任何页面item。
Composition API
结合vue3提供的Composition API,我定义如下方法,用来封装列表页的基础逻辑:
export function useListPage<T extends CommonItem> () {
const githubToken = useGithubToken();
// 获取当前列表,当前路由,当前页面名称
const { targetList, activeRoute, activeName } = getTargetList();
useHead({
title: activeName
});
const resultList = reactive(cloneDeep(targetList).map((item) => {
return {
...item,
_show: true
};
})
) as T[];
// 根据github token状态控制item列表,因为没有token的话就没必要显示加密的item
watch(githubToken,() => {
resultList.forEach((item) => {
item._show = !item.encrypt || !!githubToken.value;
});
}, { immediate: true });
return {
list: resultList
};
}
于是,我可以这样使用useListPage
,其他页面也是类似:
<script setup lang="ts">
const { list: articlesList } = useListPage<ArticleItem>();
</script>
<template>
<div class="article-list">
<ul>
<li v-for="item in articlesList" v-show="item._show" :key="item.id">
<nuxt-link :to="'/articles/' + String(item.id)">
{{ item.title }}
</nuxt-link>
</li>
</ul>
</div>
</template>
我当时思考这样的代码时,头脑是很兴奋的——vue不再死板,我可以封装任何我想要的逻辑,并把它们组合到.vue
文件里。而拥有ts的加持,所有的外部封装都有了类型检查,这是多么好的事啊!
use in anywhere!
初学者可能有一个疑惑:为什么
useHead,onMounted,watch
可以随意地使用,而不限于在vue组件里呢?我同样有此疑惑,猜测是,vue在调用组件setup时,有一个context存储当前的组件对象,并把它传给setup,setup内部的函数调用均可以读取这个context,类似于在window上的对象。当然这仅是我的猜测,还需要仔细学习源码。
在nuxt3里,同样"继承"了vue3的思想,诸如useHead
、definePageMeta
、useFetch
,均可以随处使用,只需在setup里调用即可。这给我们提供了很高的自由度,代码逻辑可以四散封装,最终都进入setup。基于此的还有vueuse,提供一系列功能函数,可以理解为lodash。
使用nuxt3的plugin
,可以做到在任何界面预先检查token,并影响其他vue组件内部的useGithubToken()
export default defineNuxtPlugin(() => {
const localToken = localStorage.getItem("GithubTokenKey");
if (localToken) {
// 进入界面时,检查token
checkIsAuthor(localToken)
.then((res) => {
if (res) {
useGithubToken().value = localToken;
notify({
title: "Token验证成功!"
});
}
});
}
});
打包优化
vercel的速度很慢,目前nuxt3不支持纯静态站点,这意味着如果不优化bundle的话,访客将看到很长一段时间的白屏。下图是优化前,网络差的情况下,首屏可能需要数10秒加载:Gzip后依旧近500K
我对rollup知之甚少,但打包文件超过500K时有提示:请使用rollup的manualChunks或者动态引入import()
。可能是nuxt&vite的打包机制有问题,manualChunks
没有太大作用。我试过把showdown
加入manualChunks
(上图便是),showdown只在详情页有用到,但是列表页居然也需要引入它,我排查发现列表页完全没有用showdown,很奇怪。于是我决定换import()
,然后配合useFetch()
,把loading状态一并做了。
优化前,
articles.json
通过import引入,这会使articles.json被打包进entry.js
,结果就是:访问/articles时,由白屏直接变为加载完成。我们改成如下,entry的体积会减小,在articles.json加载前,展示一个loading状态:<script lang="ts" setup> // import articlesList from "~/public/rebuild/json/articles.json" const {pending, data: articlesList} = useFetch('/rebuild/json/articles.json'); </script> <template> <loading v-if="pending" /> <ul v-else> <li v-for="item in articlesList"> ... ... </li> </ul> </template>
优化前,showdown和highlight.js的体积占很多,而这两个库只会在详情页用到,本不必被列表页引入,但nuxt还是引了。以highlight.js为例,我们改成如下,hljs在调用时才被加载:
// import hljs from "highlight.js"; mdEl.querySelectorAll("pre>code:not(.hljs)").forEach(async (el: HTMLElement) => { const hljs = (await import("highlight.js")).default as any; hljs.highlightElement(el); });
通过以上操作,entry.js成功减小到66K(Gzip后):Gzip后66.8K
本地后端服务
实在想不到用什么词来描述这个概念,简单地说,就是在运行npm run dev
时,把原本使用github graphql
的请求,换成“直接修改本地文件”。这样本站就既支持在线更新,又支持本地更新了。
我最先想到的是找找vite的热更新接口,查了下文档,看起来挺简单。我们先写一个本地版的deleteList
函数,它的函数签名和utils/manage/github.ts
的deleteList
一模一样:
/** utils/manage/__github.ts */
export function deleteList (json, deletions) {
// ... ...
// 这里通过websocket发送update请求
import.meta.hot.send("rebuild:update", {
additions: [{
path: `public/rebuild/json/articles.json`,
content: JSON.stringify(json, null, 2)
}],
deletions,
});
// ... ...
}
以articles管理页为例,加上dev判断:
/** pages/manage/articles/index.vue */
import { deleteList } from "~/utils/manage/github";
import { deleteList as deleteListDev } from "~/utils/manage/__github";
function deleteItems() {
// ... ...
// 即 process.env.NODE_ENV === 'development'
if (useRuntimeConfig().public.dev) {
deleteListDev(json, selectedList);
} else {
deleteList(json, selectedList);
}
}
可以正常使用,但有一个问题:编译时,deleteListDev()
属于dev
下才会使用的代码,也会被打包进dist,显然是不够优雅的。换一种思路,我们的目标是:在不改变函数签名的情况下,“偷梁换柱”把函数调个包。也许在import时动动手脚就行了?在查询rollup的文档后,服务端插件这样写:
import { Plugin } from "vite";
const LOCAL_SERVER = "ls:";
export default {
name: "local-server-plugin",
resolveId (source, importer, options) {
if (source.startsWith(LOCAL_SERVER)) {
const realPath = source.slice(LOCAL_SERVER.length);
// 如果是dev环境,则在文件名前面加两个下划线:__
const id = process.env.NODE_ENV === "development" ? realPath.replace(/([^/]*)$/, "__$1") : realPath;
return this.resolve(id, importer, options).then(resolved => resolved || { id });
}
return null;
},
configureServer (server) {
// 响应import.meta.hot.send("rebuild:update")
server.ws.on("rebuild:update", (data, client) => {
try {
// 这里省略writeFile和removeFile函数
data.additions.forEach(writeFile);
data.deletions.forEach(removeFile);
client.send("rebuild:result", true);
} catch (e) {
client.send("rebuild:result", e.toString());
}
});
}
} as Plugin;
articles管理页改成这样:
/** pages/manage/articles/index.vue */
// * dev时: import { deleteList } from "~/utils/manage/__github";
// * build时: import { deleteList } from "~/utils/manage/github";
import { deleteList } from "ls:~/utils/manage/github";
function deleteItems() {
// ... ...
deleteList(json, selectedList);
}
不需要判断dev,只需在import时加上前缀ls:
即可,插件会进行判断,若当前是dev,则把文件改个名字。
typescript无法识别ls:
前缀,我们需要shim一下,我对此不熟悉,目前没找到更好的办法:
// Need a better way
declare module "ls:*github" {
export const deleteList: typeof import("./utils/manage/github")["deleteList"];
}
简论
对于vue3,我现在的见解和笔记尚显浅薄,至于值不值得写下来,就交给时间去判断吧