Vite 多入口踩坑
想给项目加多一个入口,并可以通过 /admin 请求(与前端的路由保持一致),按照 Vite 的目录结构,应该在 projectRoot 创建一个 admin 目录。
于是我创建了 projectRoot/admin/index.html 文件,并在浏览器中通过 /admin 测试,屡屡失败,但请求 /admin/index.html 却可以。
这一旦让我怀疑是自己配置的不对,因为 文档 确实是这么写的,我开始了在配置与配置之间反复调整,然后陷入不断的自我怀疑。
直到 ── 这时时间已经过去很久,我在偶然中发现,/admin 与 /admin/ 是不同的,请求 /admin/ 竟然可以!!这时我既喜悦,又气愤,这 TM 算什么 J8 设计??
此前我确实反复读了文档,文档也确实写的 `/nested/`,这没毛病,但却没写 /nested/ 和 /nested 是不同的,就连顺带一提 “注意最后的 /” 都没有,这让人下意识觉得只是书写风格的不同 ── 毕竟其它“常规”的 webserver 都应该默认认为这二者是等价的。
退一步说,如果 /nested/ 表示目录,/nested 表示文件,但是你能在同一个目录里建两个完全同名,但类型不同的东西?所以添加 “/” 的意义是什么,我是真的不理解。
---
补充内容:
写完这篇 post,我找到了 这个5个月前的 issue,它被标记为 bug,目前还是 open 状态。然而我并不确定这是不是 bug,因为 Vite 确实被实现为区分 “/”,而且文档也与这种行为保持了一致(最后带了 “/”),因此至少在写文档时,是很清楚 Vite 会有这种行为的。
所以我更倾向这是“先天性设计问题”,直到后来有像我这样的傻瓜踩到坑,被提出来,才被定性为“bug”。但是因为这个,我浪费了大量宝贵时间,实属不值!
想给项目加多一个入口,并可以通过 /admin 请求(与前端的路由保持一致),按照 Vite 的目录结构,应该在 projectRoot 创建一个 admin 目录。
于是我创建了 projectRoot/admin/index.html 文件,并在浏览器中通过 /admin 测试,屡屡失败,但请求 /admin/index.html 却可以。
这一旦让我怀疑是自己配置的不对,因为 文档 确实是这么写的,我开始了在配置与配置之间反复调整,然后陷入不断的自我怀疑。
直到 ── 这时时间已经过去很久,我在偶然中发现,/admin 与 /admin/ 是不同的,请求 /admin/ 竟然可以!!这时我既喜悦,又气愤,这 TM 算什么 J8 设计??
此前我确实反复读了文档,文档也确实写的 `/nested/`,这没毛病,但却没写 /nested/ 和 /nested 是不同的,就连顺带一提 “注意最后的 /” 都没有,这让人下意识觉得只是书写风格的不同 ── 毕竟其它“常规”的 webserver 都应该默认认为这二者是等价的。
退一步说,如果 /nested/ 表示目录,/nested 表示文件,但是你能在同一个目录里建两个完全同名,但类型不同的东西?所以添加 “/” 的意义是什么,我是真的不理解。
---
补充内容:
写完这篇 post,我找到了 这个5个月前的 issue,它被标记为 bug,目前还是 open 状态。然而我并不确定这是不是 bug,因为 Vite 确实被实现为区分 “/”,而且文档也与这种行为保持了一致(最后带了 “/”),因此至少在写文档时,是很清楚 Vite 会有这种行为的。
所以我更倾向这是“先天性设计问题”,直到后来有像我这样的傻瓜踩到坑,被提出来,才被定性为“bug”。但是因为这个,我浪费了大量宝贵时间,实属不值!
😁1
Rollup plugin 可能会为 virtual module id 添加 \0
Rollup 约定如果一个插件使用了 virtual module,则它的 id 需要以 \0 开头。
我发现这个,是因为在配置 manualChunks 时,出现了一些莫名其妙的问题,比如本是 dynamic import 的 bundle,却被提前 import 了。
在我的项目里,这是由于 commonjs 插件的 commonjsHelpers.js 被打包在了这个毫不相关的 bundle 里。而根本原因是我使用 id === 'commonjsHelpers.js' 比较 id,但实际的 id 是 '\0commonjsHelpers.js'。
这意味着,最好使用 id.includes('commonjsHelpers.js') 比较,或至少是 id.endsWith('commonjsHelpers.js') 而不是 startsWith。
话虽如此,某些插件创建的,如以 react/ 或 vite/ 开头的 module,依旧可以使用 startsWith,因为它们并不包含 \0。
#experience
Rollup 约定如果一个插件使用了 virtual module,则它的 id 需要以 \0 开头。
我发现这个,是因为在配置 manualChunks 时,出现了一些莫名其妙的问题,比如本是 dynamic import 的 bundle,却被提前 import 了。
在我的项目里,这是由于 commonjs 插件的 commonjsHelpers.js 被打包在了这个毫不相关的 bundle 里。而根本原因是我使用 id === 'commonjsHelpers.js' 比较 id,但实际的 id 是 '\0commonjsHelpers.js'。
这意味着,最好使用 id.includes('commonjsHelpers.js') 比较,或至少是 id.endsWith('commonjsHelpers.js') 而不是 startsWith。
话虽如此,某些插件创建的,如以 react/ 或 vite/ 开头的 module,依旧可以使用 startsWith,因为它们并不包含 \0。
#experience
Rollup 插件开发:让 Service Worker 中的 import 支持 Firefox
我有一个 packet module,它是一个实现了 binary communication protocol 的 common module,这意味着它不只在 SW 中使用,还会在通常的 browser context 中使用,因此可以将该 module 打包为独立的 bundle,以便能在这两个不同的地方分别 import 它。
但是,import statement in workers 在 Chrome、Edge、Safari 都得到了支持,而 Firefox 却不支持。要想让 SW 在 Firefox 也能正常工作,就必须考虑另外一个方案:将 packet module 分别为 SW 和 browser context 打包两份,也就是说 packet 一份代码抽象为两份存在,此时便不再需要 import。这没关系,因为它足够小且足够关键。
那么,问题是,在 Rollup 中每个 module 由一个 unique id 标识,这意味着 不可能让一份 id 产出两份代码。在这里,我选用更加灵活的 virtual module 实现这一目的。
操作步骤是,在 transform hook 中 parse SW entry 的 AST,traverse 里面所有的 import nodes,并分别将它们替换,如
替换后的模块名会进入 resolveId hook,在这里需要为其补全 extension,如 `sw-import:../utils/packet.ts`,并在随后的 load 中将其从 fs 加载即可。最后还需要在 manualChunks 中,检查 id 是否包含 `sw-import:`,如若包含,将其与 SW scripts 打包到一起,否则打包到 common bundle 并服务于常规的 browser context。
#experience
我有一个 packet module,它是一个实现了 binary communication protocol 的 common module,这意味着它不只在 SW 中使用,还会在通常的 browser context 中使用,因此可以将该 module 打包为独立的 bundle,以便能在这两个不同的地方分别 import 它。
但是,import statement in workers 在 Chrome、Edge、Safari 都得到了支持,而 Firefox 却不支持。要想让 SW 在 Firefox 也能正常工作,就必须考虑另外一个方案:将 packet module 分别为 SW 和 browser context 打包两份,也就是说 packet 一份代码抽象为两份存在,此时便不再需要 import。这没关系,因为它足够小且足够关键。
那么,问题是,在 Rollup 中每个 module 由一个 unique id 标识,这意味着 不可能让一份 id 产出两份代码。在这里,我选用更加灵活的 virtual module 实现这一目的。
操作步骤是,在 transform hook 中 parse SW entry 的 AST,traverse 里面所有的 import nodes,并分别将它们替换,如
import {pack, unpack} from '../utils/packet' 替换为 import {pack, unpack} from 'sw-import:../utils/packet'。替换后的模块名会进入 resolveId hook,在这里需要为其补全 extension,如 `sw-import:../utils/packet.ts`,并在随后的 load 中将其从 fs 加载即可。最后还需要在 manualChunks 中,检查 id 是否包含 `sw-import:`,如若包含,将其与 SW scripts 打包到一起,否则打包到 common bundle 并服务于常规的 browser context。
#experience
👍1
2202 年我的 React 搭配
状态管理:局部 jotai、全局 zustand
网络请求:swr
路由:React Router
CSS:emotion + Tailwind CSS
UI:MUI、Mantine
动画:react-spring
表单验证:React Hook Form + zod
日期:Day.js
状态管理:局部 jotai、全局 zustand
网络请求:swr
路由:React Router
CSS:emotion + Tailwind CSS
UI:MUI、Mantine
动画:react-spring
表单验证:React Hook Form + zod
日期:Day.js
汇总:强制创建 layer 的属性,及 side effects
一般使用 backface-visibility 或 will-change: opacity,副作用相对最小。
See also: https://t.me/handrush/151
#learning
will-change: transform:会创建新的 containing block,导致 position: fixed 或 position: absolute 异常transform: translateZ(0):与 will-change: transform 有同样问题。早期的 Chrome、Safari 会引发闪烁,因此使用 backface-visibility: hidden 作为替代backface-visibility: hidden:会把 backface 的内容隐藏,在 3D 空间旋转元素时会有问题will-change: opacity:会创建新的 stacking context,影响元素的呈现顺序,但可以通过 z-index 恢复到预期状态一般使用 backface-visibility 或 will-change: opacity,副作用相对最小。
See also: https://t.me/handrush/151
#learning
FPS 不是流畅动画的全部
今天测试 CSS 与 JS 动画的性能差异时,发现一个有趣的现象:使用 JS 创建的动画,虽然能持续跑满 60fps,但动画本身“看起来”并不流畅;而 CSS 动画很流畅,但它却并不总是保持在 60fps。
稍微研究了下,发现这跟一个叫 subpixel 的东西有关,具体来说,对于 sub-pixel animations,它会在像素与像素之间添加模糊,使人眼看起来更平滑。
由于并不存在“半个像素”,一些更细小的动画效果就需要通过 subpixel 达到,它让人看起来动画是在“动”,实际上物体并没有动,而只是像素之间的交替变换。
因此,动画应尽量使用 CSS,或 Animations API。CSS 动画不仅能更充分利用 GPU,还能为动画提供诸如 subpixel 的优化,以减少 paint/composite 次数,并让视觉感到流畅。此外,它还不会破坏现有文档结构,而招致的频繁 lay out。
#learning
今天测试 CSS 与 JS 动画的性能差异时,发现一个有趣的现象:使用 JS 创建的动画,虽然能持续跑满 60fps,但动画本身“看起来”并不流畅;而 CSS 动画很流畅,但它却并不总是保持在 60fps。
稍微研究了下,发现这跟一个叫 subpixel 的东西有关,具体来说,对于 sub-pixel animations,它会在像素与像素之间添加模糊,使人眼看起来更平滑。
由于并不存在“半个像素”,一些更细小的动画效果就需要通过 subpixel 达到,它让人看起来动画是在“动”,实际上物体并没有动,而只是像素之间的交替变换。
因此,动画应尽量使用 CSS,或 Animations API。CSS 动画不仅能更充分利用 GPU,还能为动画提供诸如 subpixel 的优化,以减少 paint/composite 次数,并让视觉感到流畅。此外,它还不会破坏现有文档结构,而招致的频繁 lay out。
#learning
👍1
CSS Pixel 与 Device Pixel
CSS Pixel 不与真实的 Device Pixel 一一对应。在具有高 Device Pixel Ratio (DPR) 的设备上,一个 CSS Pixel 可能由多个 Device Pixel 表示,如被排列为 2x2 的方格。
DPR 为方格的高度,或宽度,2x2 grid 的 DPR 是 2。对于图像,其宽高与 Device Pixel 相关,若 image 元素 width 为 200px,对应 Device Pixel 为 400px,则图像的清晰显示宽度应为 400px。
#learning
CSS Pixel 不与真实的 Device Pixel 一一对应。在具有高 Device Pixel Ratio (DPR) 的设备上,一个 CSS Pixel 可能由多个 Device Pixel 表示,如被排列为 2x2 的方格。
DPR 为方格的高度,或宽度,2x2 grid 的 DPR 是 2。对于图像,其宽高与 Device Pixel 相关,若 image 元素 width 为 200px,对应 Device Pixel 为 400px,则图像的清晰显示宽度应为 400px。
#learning
https://featurepolicy.info/
一个 Permissions-Policy 可用值的列表。在里边我发现了
#tools
一个 Permissions-Policy 可用值的列表。在里边我发现了
sync-script 这个从来没见过的东西,不过目前也还没任何浏览器支持#tools
👍1
原来 MIME 还定义了 'example' media type,如 image/example、text/example,一般作为例子占位用。
还发现
#learning
还发现
Content-type: text/plain; charset=utf-8 中的 charset 原来是 MIME 的一部分,并不是特定于 Content-type 的。#learning
👍1
Service Worker 最佳缓存实践
SW script 的文件名应该是固定的,而不该包含 hash 或其它任何动态内容,如
此外,在 SW 的 installation lifecycle,一般会将 index.html 加入缓存,这意味着 index.html 由 SW 提供(依赖于 SW),若 SW 具有非固定文件名,这需要体现在 index.html 中(依附于 index.html),这会引发 circular dependency。
因此最佳策略是,将整个项目构建的 unique id 添加到 SW script,并在下次请求时,浏览器检查到 SW script 变更,重新执行完整的 SW lifecycle,并对包括 index.html 在内的所有静态资源重新缓存。当然也可以配合
#experience
SW script 的文件名应该是固定的,而不该包含 hash 或其它任何动态内容,如
sw.5b6aeb1b.js 是不被提倡的。因为 Chrome 68 及更高版本,在检查 SW 更新时会忽略缓存。此外,在 SW 的 installation lifecycle,一般会将 index.html 加入缓存,这意味着 index.html 由 SW 提供(依赖于 SW),若 SW 具有非固定文件名,这需要体现在 index.html 中(依附于 index.html),这会引发 circular dependency。
因此最佳策略是,将整个项目构建的 unique id 添加到 SW script,并在下次请求时,浏览器检查到 SW script 变更,重新执行完整的 SW lifecycle,并对包括 index.html 在内的所有静态资源重新缓存。当然也可以配合
skipWaiting 使其提前进入 activation lifecycle。#experience
👍1