nuxt3使用笔记

:)

  没记错的话,这是第5次重写blog,没有其他的点子,只好拿博客开刀了呀sticker   差不多花了一周时间,大部分时间用在实践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的思想,诸如useHeaddefinePageMetauseFetch,均可以随处使用,只需在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后依旧近500KGzip后依旧近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.8KGzip后66.8K

本地后端服务

  实在想不到用什么词来描述这个概念,简单地说,就是在运行npm run dev时,把原本使用github graphql的请求,换成“直接修改本地文件”。这样本站就既支持在线更新,又支持本地更新了。   我最先想到的是找找vite的热更新接口,查了下文档,看起来挺简单。我们先写一个本地版deleteList函数,它的函数签名和utils/manage/github.tsdeleteList一模一样:

/** 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,我现在的见解和笔记尚显浅薄,至于值不值得写下来,就交给时间去判断吧sticker

标签:前端
2年前
0