标签: vue

  • 解决跨时区 Nuxt SSR 导致的页面离奇 Bug

    解决跨时区 Nuxt SSR 导致的页面离奇 Bug

    打开后台一看,如果本周再不写博客,就三周没更了,还是抽点时间分享点东西吧。今天结合近期解决的问题,分享一下出海服务全球用户,并且使用 SSR 时可能遇到的问题。

    问题

    我厂的 AI 日记产品 DailyLift 遇到一个非常奇怪的问题。合伙人发现,在午夜过后,打开我们的网页,界面会渲染得非常奇怪。

    这里的问题很多,不管是文字样式还是背景图案,都很奇怪,只看错误截图和代码完全没有思路。给不了解我厂产品的读者介绍一下上图中的 bug:

    1. No data 表示当天无数据,正常情况时字体很小。
    2. 只有具体的数字才应该显示成这么大的字体。
    3. 配图也不对,no data 对应的应该是其它图片。
    4. No data 自然不应该有弧形进度条。

    难以复现的 bug

    我们是远程工作,我一般会把工作时间固定在早上 9:30~晚上 6:30。当我次日开始工作,打开开发环境开始 debug,这个 bug 完全无法重现。对合伙人来说,这个 bug 也是时有时无。所以,这个 bug 在我们的任务列表里开了关关了开,反反复复,非常难搞。

    但是随着时间发展,我们还是总结了一些问题特征:

    1. 此问题只在午夜零点之后出现
    2. 此问题只在刚发布过日志之后出现(这个很关键,可惜我解决 bug 那天才发现)
    3. 此问题只在新加载页面时出现
    4. 此问题只在线上(生产 / 测试)环境中出现

    至此,我还是没有什么思路,只是觉得,可能要午夜的时候来调试一下。

    捕获 bug

    上上周,儿子考试结束,我们带他去长沙武汉各玩两天,逛街三天,赶车三次,日均 1w 步之后,我的膝盖很累。导致周三的力量举 PR 测试状态奇差,离既定目标差了一半。(感兴趣的同学可以看下:年中测试新极限,逛街三天赶车两次,状态很差,只完成目标的一边。下次继续努力吧

    晚上睡觉前,我心情比较低落,毕竟努力了半年,之前的运动表现估算起来,肯定不止这个数字。于是我就随手打开 DailyLift,写了篇日志。然后,我鬼使神差地想起这个 bug,随手刷新了一下页面,正所谓失之东隅,收之桑榆,竟然复现了这个 bug!

    简单介绍一下我们的技术选型:

    1. Nuxt
    2. Supabase
    3. 部署在 Vercel,非企业用户,只能选择特定区域,于是我们选择美西服务器

    开发环境无法复现,线上环境稳定复现。虽然表象无法推测问题所在,但也是巨大进步。我思来想去,突然想到,会不会跟服务器端渲染(SSR)有关?于是我查看源代码——注意,是源代码,不是 DOM 树;因为 DOM 树可能会被 Vue(Nuxt)修改过,不是初始状态——一看,果然,服务器端渲染的结果跟我页面的效果不一样。而这样的结果,是由于时区导致的。

    我的博客发表在 12 点之前,刷新页面在 12 点之后。如果大家观察上图,可以发现,我们是按照日期来分割数据的。我身处 +8 时区,所以我的 N 日 0 点,是 UTC N-1 日 16 点;Vercel 服务器位于美西,差不多是 -7 时区。于是,Vercel 在服务器端渲染页面的时候,他认为我的这篇日志,应该是 N-1 日 9 点钟写的,N-1 天还没结束,所以是当天(Today)的日志。 但是当我在本地运行 Vue 的时候,我已经是 N+1 日了,于是我本地的代码认为日志发表在昨天,今天还没写过日志。

    于是,Nuxt 尝试对 DOM 进行修改,但不知为何,它复用了服务器端传回来的部分 HTML,修改了部分 HTML,所以就出现了前面的 bug。

    解决 Bug

    找到问题根源之后,解决问题就没那么难了。唯一的难点就是克服测极限带来的身体疲劳,不过找到问题的兴奋感冲淡了这份疲劳。

    查阅文档之后我发现,Vercel 倒是早就帮我们准备了解决方案,我们只需要用useRequestHeader('x-vercel-ip-timezone') 就可以获取到用户所在的时区,接下来,只要保证整个 SSR 在这个时区里进行就好。

    我使用的 dayjs 来处理时间,它自带 timezone 功能,所以给所有 dayjs() 后面加上 dayjs().tz(tzHeader) 就可以。

    总结

    最后,困扰我们一个月之久的 Bug 终于解决。我也意识到处理时区在全球化应用开发时的重要性。合伙人问我有没有办法确保类似的问题不再发生,我回他:我这辈子都不会忘了处理时区了……

    希望这篇博客对你的出海应用开发有帮助。如果你对 Nuxt、服务器端渲染、云服务器等有问题,欢迎留言讨论。

  • 【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    这次的视频更偏理论,重点讲解 Nuxt3 如何处理 useAsyncData,以及为了兼顾 SSR 和前端开发所做的渲染策略设计。这部分知识我其实很晚才掌握,因为文档里说的也不太详细;所以既是好消息也是坏消息。好消息是,哪怕你没有掌握,也不太耽误使用 Nuxt3 开发项目;坏消息是,保不齐就会遇到一些奇怪的问题,难以复现和排错。

    视频要点:

    1. 现代化 SSR 的优势
    2. 深入理解 useAsyncData
    3. 使用 Pinia 传递数据
    4. 理解生命周期钩子变化

    如果你对 Vue3 开发、Nuxt3 开发、SSR 感兴趣,欢迎关注我的本系列。如果你对这些话题有疑问,欢迎留言讨论。

  • 【视频】技术栈大升级:Vue3 到 Nuxt3(1)基础知识篇

    【视频】技术栈大升级:Vue3 到 Nuxt3(1)基础知识篇

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临到很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    本期视频主要介绍 SSR 所需的知识和概念,为下一阶段正式重构项目做准备。

    1. 什么是 SSR?为什么要用 SSR?
    2. SSR 的一般构成
    3. Nuxt3 的 SSR 组件
    4. Nuxt3 的渲染规则与缓存处理
    5. 如何鉴别用户身份

    视频中的课件:从 SPA 到 SSR,从 Vue3 到 Nuxt3

    有任何问题、意见、建议,欢迎留言弹幕私信与我交流。如果你觉得视频对你有所帮助,还请留下宝贵的一键三连,并完播分享,谢谢。

  • Nuxt3 如何传递 Token

    Nuxt3 如何传递 Token

    昨天我在思否上看到这个问题:nuxt3 请求 token 要怎么传?我之前也被这个问题困扰过,我猜测不少刚刚从 Vue3(SPA) 转向 Nuxt3(SSR) 开发的同学也会受困于这个问题,所以就把答案扩展一下,写成博文,分享给大家。

    Vue3(SPA)里使用 Token

    在 Vue3 开发的单页应用(Single Page Applicati,SPA)里使用 Token 比较简单。一般来说,我们会在项目初始化的时候读取 localStorage,拿到标记用户身份的 token,然后放到内存当中,在请求时放到 header 里面发出。如果找不到 token,就认为用户没有登录,那么就通过路由将用户指引到登录页,或者不需要登录也能看的页面。

    使用 Token 可以避免 CSRF 攻击,因为从外部网站无法同时满足访问 localStorage,和将信息放入 http header 这两个条件。所以在单页应用里,使用 token 验证用户身份非常常见,有着各种开源、强壮、高效的实现。

    这个过程相信做过 SPA 的同学都很熟悉,不多说了。

    Nuxt3 请求的特点

    从 Vue3(SPA) 到 Nuxt3(SSR),主要涉及一个思路的转换。

    在 Vue(SPA)里,所有请求都是  请求,即完成网页加载、JS 执行完毕后,再发起请求。这些请求,有以下特点:

    1. 可以认为完全由开发者控制,即开发者确定请求发起的条件,然后在基本确定的时候发起请求。
    2. 这些请求大部分只请求数据,且有明显的特征,比如 HTTP Header 里包含 Authorization
    3. 这些请求里基本不包含静态资源。

    在 Nuxt 里,因为 SSR 的存在,所以请求至少可以分成两类:

    1. 数据交互类,跟 SPA 里基本一致。
    2. 页面渲染类,SSR 核心所在。

    后者最大的变化是:它会影响到 HTML 的内容,而通常来说,HTML 会被视作静态资源。我们知道,在网络环境里,存在大量缓存节点,假如跟用户相关的敏感数据渲染成 HTML,缓存到 CDN 当中,会是非常严重的安全问题。所以 Nuxt 在 SSR 时,不会携带 cookie;只有当页面渲染完成,由于用户操作而主动发起的请求里,才会携带 cookie。

    怎么识别 SSR 请求

    Nuxt2 时期,使用 Option API 的时候,有一个很明确的 asyncData() 函数,它会在 SSR 阶段被调用,发起请求获取数据。

    Nuxt3 时期,Composition API 会稍嫌模糊一些。这个时候,整个 setup() 函数都会在 SSR 期间执行,而 useFetchuseAsyncData 则负责在这个阶段发起请求,获取数据。因为这个阶段是在服务器的 node.js 环境里执行的,所以自然无法使用用户的 localStorage,也无法使用用户本地存储的 token。

    但是因为打开网页的请求是由用户发起的,所以其实用户有把自己的 cookie 发给服务器。如果我们希望 SSR 阶段用 cookie 鉴别用户身份,可以手动将 cookie 放在 useFetchuseAsyncData 产生的请求当中,参考 官方文档 useRequestHeaders

    基本上,假如我们不了解 Nuxt3 的设计思路,简单看了眼文档就上手写代码,大概率所有页面数据相关的请求都会是 SSR 请求。至少我是如此。

    使用 server: false 仅在网页里发起请求

    当然,Nuxt3 提供了只在网页里发起请求的方法,也就是基本等效于我们之前在 SPA 里发起请求的方法,那就是在 useFetchoptions 里标记 server: false

    const { pending, data: posts } = useFetch('/api/comments', {
    server: false
    })

    再演示个更扣题的方式

    const { pending, data: posts } = useAsyncData(
    'posts',
    async () => {
    const token = localStorage.getItem('token');
    const data = await $fetch('/api/comments', {
    headers: {
    Authorization: `Bearer ${token}`,
    },
    });
    return data;
    },
    {
    server: false,
    }
    );

    (代码高亮插件还是崩的,大家凑合看,等我回头修……)

    详情请看 官方文档

    这样的请求就会在页面完成渲染后,才会发起请求。在这些请求里,如果需要使用 token,就按照以往的方式使用,即可。

    server: falselazy: true 的区别

    如果大家仔细看 Nuxt3 官方文档,会发现还有一个参数:lazy: true,以及两个语法糖函数:useLazyFetchuseLazyAsyncData

    这两个函数也是先加载页面,再加载数据。它们与 server: false 的区别在于,后者是把全部请求都放在网页端进行;而 lazy: true 则只在网页跳转的时候起作用。

    不使用 lazy: true 的情况下,当我们从 A 页面进入 B 页面时,如果 B 页面里需要加载数据,Nuxt3 就会等待数据加载完成之后,再进行页面渲染。如果你使用一个比较慢的网络,页面切换就会卡卡的,体验非常明显。

    如果使用 lazy: true,页面切换一下就完成了,但是数据可能还没加载到,此时我们需要手动处理加载状态。比如放个转转的菊花在页面中间。但是如果我们直接打开 B 页面,Nuxt3 还是会先把数据请求回来,完成页面渲染,然后呈现完整的 HTML 给我们,这样可以确保 SEO 效果。

    server: false,则会彻底放弃这部分数据的 SSR,对于用户身份相关的数据,就比较合适。

    总结

    其实就跟当年从前端学 Node.js 一样,新技术最大的挑战在于:使用场景不同、运行环境不同,思路需要很大变化,需要不同的理解,不能照搬以前的做法。

    Nuxt3 涉及到网页渲染,涉及到网络链路上的缓存层,为了防止出现信息泄漏,它会主动放弃一些数据传输。同时,由于它的一部分运行环境在服务器上,所以有一些用户数据它没法使用。于是在开发 Nuxt3 应用的时候,我们必须搞清楚这些变化,然后作出适当的调整。

    希望上面文章对大家有所帮助。如果文中有问题,请不吝指出。如果你对 Vue3、Nuxt3 开发有兴趣,有疑问,欢迎留言评论。

  • 使用 `postMessage` 跨域名迁移 `localStorage`

    使用 `postMessage` 跨域名迁移 `localStorage`

    朋友的网站有个需求:要从 A 域名迁移到 B 域名。所有内容不变,只是更改域名。这个需求不复杂,理论上改下配置然后 301 即可。但这个网站是纯静态网站,用户数据都存在 localStorage 里,所以他希望能够自动帮用户把数据也迁移到新域名。

    我们知道,localStorage 是按照域名存储的,B 网站无法访问 A 网站的 localStorage。所以我们就需要一些特殊的手段来实现需求。经过一些调研,我们准备使用 postMessage() 来完成这个需求。

    大体的方案如下:

    首先,增加 migrate.html 页面。这个页面不需要具体的功能,只要侦听 message 事件,并且把 localStorage 发出即可。

    <script>
      window.addEventListener('message', function (message) {
        const { origin, source } = message;
        // 验证来源,只接受我们自己的域名发来的请求
        if (origin !== 'https://wordleunlimited.me') return;
    
        const local = localStorage.getItem('wordle');
        // `source` 是浏览器自动填充的,即调用 `postMessage` 的来源。作为跨域 iframe 里的页面,拿不到外层 window,只能通过这种方式往回传递数据。
        source.postMessage({
          type: 'migrate',
          stored: local,
        }, 'https://wordleunlimited.me');
      });
    </script>

    然后,在应用里增加 <iframe>,因为我用 Vue3,所以这里也用 Vue 组件的方式处理。

    <template lang="pug">
    migrate-domain(v-if="needMigrate")
    </template>
    
    <script setup>
    // 我会把迁移的状态持久化,以免反复提示打扰用户
    const needMigrate = location.hostname === 'wordleunlimited.me' && !store.state.migrated19;
    </script>
    <script lang="ts" setup>
    import {ref} from "vue";
    // 项目启动得早,还在用 vuex
    import {useStore} from "vuex";
    import {key} from "@/store";
    
    const store = useStore(key);
    const migrateIFrame = ref<HTMLIFrameElement>();
    
    // migrate.html 接到请求后,验证来源,然后会把 localStorage 的数据发回。我们用这个函数接收。
    window.addEventListener('message', message => {
      const { origin, data } = message;
      // 同样验证来源
      if (origin !== 'https://mywordle.org' && origin !== 'https://mywordgame.com') return;
      const { type, stored } = data;
      if (type !== 'migrate') return;
      if (stored) {
        // 迁移数据时,需加入特殊标记,用来标记“已迁移”状态
        localStorage.setItem('wordle', stored.replace(/}$/, ', "migrated19": true}'));
        // 很奇怪,直接 reload 可能会迁移失败,所以这里稍微等一下
        setTimeout(() => {
          if (confirm('Data migrated, reload the page?')) {
            location.reload();
          }
        }, 100);
      }
    });
    
    // iframe 加载完即执行这段 JS,向 iframe 内的页面传递迁移请求
    function onLoad() {
      const contentWindow = migrateIFrame.value?.contentWindow;
      if (contentWindow) {
        contentWindow.postMessage('migrate', 'https://mywordle.org');
      } else {
        console.warn('no content window');
      }
    }
    </script>
    
    <template lang="pug">
    iframe(
      ref="migrateIFrame"
      src="https://mywordle.org/migrate.html"
      frameborder="no"
      width="0"
      height="0"
      @load="onLoad"
    )
    </template>
    
    <style scoped>
    iframe {
      width: 0;
      height: 0;
    }
    </style>

    至此,功能完成。

    如此一来,老用户打开网站后,会被跳转到新域名。然后应用 JS 会检查 localStorage 里存储的数据,如果没有迁移过,就会使用 <iframe> 加载老域名下的 migrate.html。等待目标页面加载完成之后,调用 postMessage() 发送迁移请求。接下里,migrate.html 接到请求后,返回之前存储的数据。新域名储存之后,提示刷新。

    主要的坑在于 <iframe> 里的页面无法直接跟跨域页面通信,所以需要父页面先找到子页面,发起请求;然后子页面再把数据回传给父页面。其它方面应该就是一般的 API 调用,以及体验性问题。

    希望本文对大家有帮助。如果各位对 postMessage() 或者其它相关技术有问题的话,欢迎留言交流。

    本文参与了「SegmentFault 思否写作挑战赛」,欢迎正在阅读的你也加入。

  • 在 Code.fun 做 Code Review(四)

    在 Code.fun 做 Code Review(四)

    时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。

    关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:


    (更多…)
  • Vite 项目里启动 PWA

    Vite 项目里启动 PWA

    很简单,使用 vite-plugin-pwa 插件,Antfu 出品,品质保证。零配置,简单易用。

    0. 安装插件

    pnpm i vite-plugin-pwa -D

    1. 启动插件

    修改 vite.config.ts

    import { VitePWA} from 'vite-plugin-pwa';
    import { definePlugin } from 'vite';
    import vue from '@vitejs/plugin-vue';
    
    export default definePlugin(({ command }) => {
      const isDev = command === 'serve';
      return {
        plugins: [
          vue(),
          new VitePWA({
            disable: isDev, // 开发环境不启动 pwa
            includeAssets: [
              // 非直接加载,但是需要预缓存的内容
            ],
          }),
        ],
      };
    })

    2. 可脱机提示及可更新提示

    原则上来说,Vite、Vite 插件都是开发脚手架,不限定框架。不过我用的最多的还是 Vue。这里以 Vue3 为例示范一下如何使用插件快速实现 PWA 组件:

    1. pwa 完成缓存后,提示可脱机使用
    2. 线上版本更新后,提示有新版本可用
    3. 更新时,给出视觉反馈
    <script setup lang="ts">
    import { useRegisterSW } from 'virtual:pwa-register/vue'
    import {ref} from "vue";
    const {
      offlineReady,
      needRefresh,
      updateServiceWorker,
    } = useRegisterSW({
      immediate: true,
      onRegistered(r) {
        // 每小时自动检查一次,是否有新版本
        r && setInterval(async() => {
          await r.update()
        }, 60 * 60 * 1000)
      },
    });
    const isRefreshing = ref<boolean>(false);
    function doRefresh() {
      isRefreshing.value = true;
      updateServiceWorker();
    }
    const close = () => {
      offlineReady.value = false
      needRefresh.value = false
    }
    </script>
    
    <template lang="pug">
    .pwa-toast.fixed.right-4.bottom-4.p-3.border.border-gray-200.rounded.bg-white.z-index-10.shadow-md(
      v-if="offlineReady || needRefresh"
      role="alert"
    )
      p.message.mb-2
        span(v-if="offlineReady") App ready to work offline
        span(v-else-if="isRefreshing") Refreshing...
        span(v-else) New content available, click on reload button to update.
      button.border.border-gray-200.rounded.py-1.px-2(
        v-if="needRefresh",
        type="button",
        :disabled="isRefreshing",
        @click="doRefresh",
      )
        .spinner(v-if="isRefreshing")
        template(v-else) Reload
      button.border.border-gray-200.rounded.py-1.px-2.ml-2(type="button" @click="close") Close
    </template>

    3. 一些坑

    1. PWA 会拦截所有请求,以便缓存到本地。所以,打开网站,注册完 service worker,再请求其它文件,比如 ads.txt,也可能会看不到。这并不影响广告,因为广告商服务器不会受 PWA 影响;但是广告商运营人员可能只会操作浏览器,她们可能会认为你的广告文件没准备好。此时,请告诉她们使用匿名窗口。
    2. PWA 会自动预缓存 dist 目录内的东西,所以一定要注意 build.emptyOutDir,不要让目录过分膨胀,影响新用户体验。
    3. 有新版本后,上面的组件会提示用户刷新,但是刷新过程可能很慢(清理缓存,下载新内容等),所以点完之后可能没有反应。所以最好加上 spinner。

    4. 总结&扩展阅读

    整体来说,这个插件很好用,没什么特别需求的话,几乎可以零配置。

    建议感兴趣的同学好好阅读下 官方文档,尤其是 examples 目录里的内容,会有很大帮助。

  • Vue 使用 Provide/Inject 向子组件内注入数据

    Vue 使用 Provide/Inject 向子组件内注入数据

    前阵子做厂里的需求,允许用户编辑算法生成的 CSS,以便将我厂的产品应用到生产环境。我们允许用户使用可视化编辑器编辑某条特定规则,大约如下图所示:

    我们知道,CSS 规则有优先级,优先级高的规则会覆盖优先级低的规则中的同名属性;也会继承低优先级规则里没有被覆盖的属性。反应到我们的可视化编辑器里,就是:

    1. 有些属性用户不能删除,因为那是从低优先级的样式里继承来的
    2. 有些属性用户修改了也不会生效,因为会有更高优先级的规则把它覆盖掉

    所以我们希望给用户一些提示,避免他们实际操作的时候感到疑惑。那么问题来了:怎么实现呢?

    从图上可以看出,CSS 可视化编辑器挺复杂的,有些属性可以直接编辑,比如 visibility,独立生效,那随便写个 <input type="checkbox"> 就行;有些则与其它属性一起发生作用,比如 align-items,那我们就需要比较复杂的组件,里面会嵌套别的组件。

    所以一般的 v-bind 方式就不适用:我计算出优先级之后,需要传递多次才能穿透组件间的嵌套关系,太复杂,很难用。我们需要更简单的传递方案。好在 Vue 提供了 provide/inject 方式。

    Vue 官方称这个功能为“依赖注入”,我们在父组件上使用 provide 暴露一些属性,然后在子组件里用 inject 把这些属性拿进来使用。不管子组件和父组件之间嵌套了几层,都可以获取到:

    provide() {
      return {
        foo: this.foo
      }
    }
    inject: ['foo']

    用法比较简单,就这么两步,但是有一些注意事项:

    1. 注入的变量默认没有响应式,Vue 就是这么设计的,这样可以避免一些问题。如果需要响应式,那就需要传入包含响应式的变量,比如 data 或者 computed
    2. 如果你用 vue-property-decorator,那么需要用 ProvideReactiveInjectReactive
    3. 这个依赖注入和设计模式里的 DI 不是一回事,面试时不要乱讲。
    4. 不要滥用这个设计,只有不特定层级子组件需要用到属性,才这么做。

    扩展阅读:

  • 理解 Vue3 里的 defineProps 和 defineEmits

    理解 Vue3 里的 defineProps 和 defineEmits

    大家请先看这个问题:https://segmentfault.com/q/1010000041497872/a-1020000041498716,看看你们能不能给出答案。

    Vue3 增加了 Composition API,是一个很大的改进。一方面可以提升代码复用效率,另一方面通过更好的 tree-shaking,打包体积也会小很多:作为参考,前面博客中提到 mywordle.org,打包后 vendor.js 只有区区 96kB,里面可是用到了 vue3 全家桶。

    最初的 Composition API 是在 Options API 基础上改进的,不仅需要使用 setup() 函数,还要在 setup() 末尾返回所有模版需要用到的变量和函数,使用起来相当繁琐。于是后面就增加了 <script setup> 语法糖:

    1. 从生命周期来讲,相当于 created
    2. 支持顶层 await(因为实际上这还是个 setup() 函数)
    3. 所有 import 的内容、声明的变量和函数默认都返回
    4. 至少省了两层缩进

    但是由于少了 export,没法传参,也不方便暴露接口,所以作者就增加了三个工具方法:

    • defineProps
    • defineEmits
    • defineExpose

    注意,这三个工具方法只是帮助 Vue 编译器构建组件,它们不会出现在最终代码里,我们也不能预期它们会像普通函数那样工作。比如下面这段代码,就得不到常见的结果:

    const props = defineProps({
      userMenu: {
        type: Array,
        default() {
          return []
        }
      }
    })
    console.log(props) // 该对象中的 userName 总是有值
    console.log(props.userMenu) // 该对象始终是一个空数据

    因为 Vue 是 MVVM 框架,它的视图会在数据变化后自动渲染,于是通常情况下,props 里的值什么时候被填充并不重要,Vue 开发团队也不想追求 defineProps 工作的一般化。所以使用目前版本,上面这段代码,访问到的 props 是的 reactive 对象,数据被填充后就能看到 userName 里有值;而 props.userMenu 在访问时还没有被填充,所以得到的是 default() 返回的默认值,一直是空的。

    同时大家还要知道,console.log() 输出的是对象的指针,而非快照。所以里面的值只跟你展开时有关,跟运行时关系不大。

  • 近期帮一个朋友做的 Vue 网站优化方案

    近期帮一个朋友做的 Vue 网站优化方案

    前几天有个朋友找到我,说他们公司的网站产品打开速度不太理想,加载的数据量很大,想优化一下。并且询问我,是不是用微前端会好一些。

    分析

    我看了一下,大概有几个点:

    1. 他们是做自动化运维的,提供多个工具,所有工具都是独立的 Vue 项目
    2. 用户一般先登录 dashboard 页面,然后导航到具体的工具页
    3. 因为每个工具都独立开发、独立打包,所以每个页面都会用到不同的 app.[hash].jschunk.vendors.[hash].js
    4. 于是,用户每次切换工具,都要完整加载一遍公共资源,如 vue 全家桶和 antdv
    5. 已经对一些模块做了 lazy-loading

    于是我得出一些结论:

    1. 首先,因为所有项目的技术栈高度趋同,所以并不需要微前端(微前端的解释在后面)
    2. 现在重复加载最多的应该是 vue 全家桶和 antdv,重复加载的原因是没有拆包。适当拆分打包后资源,应该可以大大提高代码复用率,减轻不同项目间的切换成本
    3. 同时还应开启 http/2,提高连接复用率

    方案

    于是我给朋友提出了以下改进方案:

    1. 整站启用 http/2
    2. 所有项目手动分 chunk
      1. vue 全家桶
      2. antdv
      3. 其它好统计的、全站都在使用的仓库
      4. 其它仓库
    3. 统一所有项目的依赖,提交 lock 文件入库
    4. 所有项目对公共仓库的引用顺序需保持一致,保证 webpack 打包之后的序号能维持一致
    5. 替换页面中的资源位置,指向同一个资源
    6. 延长资源缓存时间,提高利用率

    下一步

    他们需要一些时间来消化和实施这些方案,所以这一次咨询先到这里。

    接下来,我可能会建议他们调整支持的浏览器、增加 ESM build。以及使用 npm workspace 创建一个核心项目,把所有工具项目放到一起构建,减少前面的(2)(4)(5)环节。

    总结和广告

    希望上述方案对大家也有启发和帮助。

    顺便帮他们打个广告吧:

    OpsAny,云原生场景下的智能化运维平台。我们倡导“以资源为中心”和“以应用为中心”相融合的运维理念,提高运维效率、保障业务连续性。

    OpsAny, make ops perfect
    (更多…)