分类: vite

  • 使用 Vite 建立灵活的外部仓库

    使用 Vite 建立灵活的外部仓库

    0. Vite 与 ESM

    与 Webpack 不同,Vite 以 ESM 为其唯一的模块管理规范。首先,在开发环境,它会把每个文件编译成独立的 ESM 模块,实现非常快速的热加载。其次,编译打包时,它默认的目标环境是 ES2019,支持 ESM,所以模块打包后,也会使用 ESM 加载。

    这给我们带来一个好处:如果用 Vite 开发项目,并且对其进行分包,构建之后放到线上(比如发布到 NPM);接下来我们就可以在其它项目中,使用 ESM 方式加载这个项目的代码。

    举个简单的例子。lodash 有一个同步发布的 lodash-es 包,功能完全一致,只是使用 ESM 构建,我们可以直接在代码中 import forEach from 'https://unpkg.com/lodash-es@4.17.21/forEach.js' 引用。一方面可以节省我们自己的带宽;另一方面如果用户在其它应用里使用过同一个库,就可以提高速度。

    1. Vite 分包

    Vite 把构建过程委托给 Rollup,所以构建时分包需要传参给 rollupOptions

    在某个项目中,我需要整合一批 codepen 上的绘图效果,这些效果都放在 /src/effects/ 目录下,所以我就要检查这个目录,并且生成对应的分包配置。

    export default defineConfig(async () => {
      // 从 v10.10 开始,node.js 的 `fs.readdir()` 函数支持 `withFileTypes` 参数,使用这个参数可以直接返回 `fs.Dirent` 对象,类似使用 `fs.stat()` 得到的 `fs.Stats` 对象。方便我们判断对象类型
      const files = await readdir(effectsDirectory, {withFileTypes: true});
      // 把目录下的内容分为两类,一个是基础类库,一个是不同特效
      const [effects, baseFiles] = files.reduce(([effects, base], file) => {
        const {name} = file;
        if (file.isDirectory()) {
          effects.push(name);
        } else if (file.isFile()) {
          base.push(name);
        }
        return [effects, base];
      }, [[], []]);
    
      return {
        build: {
          rollupOptions: {
            manualChunks(id) {
              // effects/some-effect 下的文件按目录分别打包
              const effect = effects.find(effect => id.includes(`/${effect}/`));
              if (effect) {
                return effect;
              }
              // 效果基类打包成一个文件,因为效果只需要基类,所以从主体剥离
              const baseFile = baseFiles.find(base => id.endsWith(base) && !/p5/i.test(id));
              if (baseFile) {
                return 'BaseEffect';
              }
              // p5 是个很大的效果库,官方不提供 esm 包,只能单独打一个
              if (/p5/i.test(id)) {
                return 'p5';
              }
              // 其它依赖正常打包,只在本项目中使用,不会被引用
              return id.includes('node_modules') ? 'vendor' : 'chuck';
            },
          },
          // 这个第3节会解释
          target: 'es2020',
        },
      },
    }
    

    2. 去掉文件名中的 hash

    Vite 很贴心的帮我们给生成的文件都加上了 hash。在独立项目中,给文件名加 hash 可以有效避免缓存问题;但是作为外部仓库的话,无法确定的 hash 会增加业务项目的开发难度,所以我希望构建时输出到特定版本号的目录里,然后去掉文件名中的 hash。

    这个操作同样需要修改 rollupOptions。rollup 有三个不同的选项分别处理不同的命名,这里我们可以忽略入口文件(entryFileNames),只改剩下两个。

    export default defineConfig(() => {
      return {
        build: {
          rollupOptions: {
            output: {
              // 资源文件,包括 css
              assetFileNames: 'assets/[name].[ext]',
              // 分包文件
              chunkFileNames: '[name].js',
            },
          },
        },
      };
    });

    3. 动态加载 CSS

    使用 Vite 开发时,我们同样可以在代码里 import 样式等非 JS 素材。构建时,Vite 会把它们处理后放在合适的地方。

    可惜的是,Vite 并不会帮我们自动加载分包后的素材。需要我们手动处理。这时就要利用 import.meta.url,它会返回当前模块的 URL,配合前面的的文件名策略,我们就可以完成动态加载,而不需要业务项目的开发者手动处理。

    但是 Vite 默认的版本基线是 ES2019,并不支持 import.meta.url,所以我们需要把 build.target 设置成 ES2020 或以上。

    let isCssLoaded = false;
    
    // 只有未加载且处于生产环境才加载 css。
    if (!isCssLoaded && __IS_PROD__) {
      const link = document.createElement('link');
      link.rel = 'stylesheet';
      // 这一步非常重要,因为 vite/rollup 有 bug,会把 `import.meta.url` 翻译成 `self.location`,导致出错
      const baseUrl = import.meta.url;
      link.href = new URL('./assets/particle-orb.css', baseUrl).toString();
      document.head.appendChild(link);
      link.onload = () => {
        isCssLoaded = true;
      }
    }

    4. 总结

    新的技术选型总能给我们带来新的可能,ESM 之后,我们在项目之间复用代码也有了新的选择,赶紧用起来吧。

    如果你在使用 Vite 或者 ESM 时遇到什么问题,欢迎提问。如果有什么经验,也欢迎分享。

  • 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 目录里的内容,会有很大帮助。

  • 使用 Vite JavaScript API 构建多语言静态网站

    使用 Vite JavaScript API 构建多语言静态网站

    静态化真是爽,不仅操作简单,还有很多羊毛可以薅,比如 Vercel、Digital Ocean、Cloudflare,除去开发成本,运维支持成本几乎为零。我用 Vite 搭建了一个静态网站,然后需要多语言,最简单的做法就是多编译几次,输出不同语言到不同目录。我实操了一下,大体上还算顺利,略有小坑,分享一下。

    Vite JavaScript API

    Vite 除了命令行工具,还提供了插件 API、HMR(模块热加载)API、JS API,方便开发者从各种角度去丰富 Vite 的生态和使用场景。

    这里我们要使用的就是 Vite JavaScript API。从 官网 来看,build,即我们要用到的构建功能,也是开放的,很好。

    这个 API 支持一个参数,即 vite config,然后就能完成构建。不过我实测这里并不能使用 defineConfig() 方法,不知道是否与我的使用方式有关,相关的范例代码也不多、文档也不详细,就先这么着吧。

    Demo code

    经过调试后,完成的代码如下:

    for (const language of languages) {
      // 一些 SEO 相关属性
      const {
        title = '',
        description = '',
        content = '',
      } = data[language];
      if (!language) {
        continue;
      }
      const config = {
        // 注意,`configFile` 属性非常关键,如果不设为 false,vite 还会加载默认配置文件 vite.config.js
        configFile: false,
        root,
        base: language === 'en' ? '/' : `/${language}/`,
        build: {
          // 这个属性也比较关键,不设置的话,vite 会自动清理掉其它语言
          empty: false,
          outDir: resolve(root, language === 'en' ? './dist' : `./dist/${language}`)
        },
        define: {
          // 放一些根据语言自定义的变量
        },
        plugins: [pugPlugin.default({}, {
          title,
          description,
          lang: language,
          version,
          content: marked.parse(content),
        })],
      };
      await build(config);
    }

    注意事项写在上面的代码里了,大家留意一下。

    TailwindCSS 及其它工具

    TailwindCSS 会在当前工作目录(即 cwd)里查找配置文件。如果你像我一样,把构建文件放在 build 目录里,执行的时候可能就会报错,说 TailwindCSS 找不到配置文件。

    此时,只能通过 NPM script 比如 npm run build 执行 ./build/build.js

    总结

    慢慢适应 Vite 之后,我开始逐步把个人小项目向 Vite 迁移。新技术的体验提升很大,不过文档和范例的确有所欠缺。

    下一步要尝试 vitest。