:) 没记错的话,这是第5次重写blog,没有其他的点子,只好拿博客开刀了呀 差不多花了一周时间,大部分时间用在实践typescript和组织代码逻辑。相比于vue2SFC一把梭,vue3提供更灵活的代码书写方式,我也是偶然从一个视频里了解到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,我现在的见解和笔记尚显浅薄,至于值不值得写下来,就交给时间去判断吧
nuxt3使用笔记
:)
没记错的话,这是第5次重写blog,没有其他的点子,只好拿博客开刀了呀 差不多花了一周时间,大部分时间用在实践typescript和组织代码逻辑。相比于vue2
SFC
一把梭,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!
在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>
// 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,我现在的见解和笔记尚显浅薄,至于值不值得写下来,就交给时间去判断吧