项目背景
产品类型
移动端政务管家
技术架构
UI 框架:Vue3
路由管理:VueRouter4
状态管理:Pinia3
组件库:Vant4
构建工具:Vite6
CSS:Tailwindcss3
备注:项目采用 unplugin-auto-import/vite、 'unplugin-vue-components/vite'、VantResolver 完成组件与样式的自动引入
实现步骤
由于是传统的 CSR 项目,首页是通过 JavaScript 动态生成的,直接使用 Vite 生态的 Critical CSS 插件无法达到效果,因此选用了 vite-ssg 插件,同时达到预渲染 + Critical CSS 的目的
安装依赖
pnpm add vite-ssg -D
首页改造
import { createPinia } from 'pinia'import { ViteSSG } from 'vite-ssg'import routes from '~pages'import './styles/index.css'import App from './App.vue'export const createApp = ViteSSG( App, { routes: routes }, ({ app, router, initialState }) => { const pinia = createPinia() app.use(pinia) if (import.meta.env.SSR) initialState.pinia = pinia.state.value else pinia.state.value = initialState.pinia || {} router.beforeEach((to, _from, next) => { if (to.path === '/') { next({ path: '/home' }) } if (to.name === 'index-home') { const urls = [ `${import.meta.env.VITE_BASE_PATH}img/home/psychological.webp`, ] urls.forEach(url => { preloadImage(url) }) } next() }) },)
Vite 配置
ssgOptions: { includedRoutes: (paths: string[]) => { return paths.filter(path => { return path === 'home' }) }, mock: true, // 模拟浏览器的全局变量,让代码能够正确地在 node 环境运行 beastiesOptions: { preload: 'media' },},
解决 Vant 样式报错
**报错根本原因:**项目中导入的样式是 Vant ESM 库的导出样式:vant/es/xxx/style/index.mjs
以 button 组件为例,index.mjs 文件的内容如下
import "../../style/base.css";import "../../badge/index.css";import "../../icon/index.css";import "../../loading/index.css";import "../index.css";
在打包后的 JS 文件中包含 import "vant/es/xxx/style/index"
在 Node 环境下,直接执行该 JS 代码会报错
解决方案:
- 让
import "vant/es/xxx/style/index"
执行不报错 - 让
import "vant/es/xxx/style/index"
不出现在打包的文件中
第一种方向没有找到合适的解决方案,第二种方向采用了直接导入 CSS 文件的方式,明细如下:
-
postinstall 阶段执行脚本,在 index.mjs 旁边新建 index.css 文件,css 文件的内容如下
@import "../../style/base.css"; @import "../../badge/index.css"; @import "../../icon/index.css"; @import "../../loading/index.css"; @import "../index.css";
动态生成的脚本这里就不贴了,可以使用 AI 直接生成,AI 已经可以高效地完成有规律地事情,比如这里,只需要发出指令:在 vant/es/xxx/style/index.mjs 旁边新建一个 vant/es/xxx/style/index.css 文件,css 文件与 mjs 文件地内容一致,xxx 代表具体的组件变量
-
改变模块解析方式,在 Vite 文件配置
resolve: { alias: [ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)), }, { // By AI find: /^vant/es/(.+)/style/index1/style/index.css', // 替换为 .css 路径 }, ],},
量化成果
- FCP 减少 3 秒
- Lighthouse 性能评分提高 10 分
落地学习路径
由于之前一直使用 CSR 方案开发传统页面,完全没有接触过服务端渲染、性能优化方面的知识,工程化方面的知识也很薄弱;因此在落地过程中遇到了很多问题,这个部分介绍了在落地过程中解决问题的路径以及新接触的知识点
Critical CSS 概念
内联关键 CSS,异步加载非关键 CSS
主流的 Critical CSS 方案
Critters、PostCSS Critical、Penthouse,Critters 包已经不再维护,可以采用 Beasties
在项目中引入没有成功达到效果,需要了解没有效果的原因,看文档的例子都是静态的 html,而 CSR 项目都是浏览器动态渲染的,于是带来一个需要解决的问题:
可以在动态渲染的 HTML 项目中生效吗,如果可以生效,是如何做到的,答案是不行,在查阅资料的过程中看到了一个新的概念预渲染
注意:查阅资料的过程中可能遇到很多干扰信息,比如 Penthouse 采用无头浏览器,而 Critters 没有采用,往深了研究这里的关注点可能会放在无头浏览器,插件如何是如何做到 Critical CSS 的,但这里的关键点其实时解决动态渲染 HTML 的问题,没必要往深了研究,只需要关注解决问题的关键路径
预渲染概念
在构建时生成静态 HTML
预渲染方案
- vite-ssg:静态站点生成 + Critical CSS 内置方案
- vite-plugin-ssr:服务端渲染方案
- vitepress
- vuepress
- nuxt
结合项目现状,对比下来选择了成本最低的 vite-ssg 方案
vite-ssg 包引入
引入后项目直接报错,报无法执行 css 文件的错,并且有一个模块化冲突的警告;由于对模块化相关知识、以及服务端渲染相关知识点的缺失,解决这个问题走了很大的弯路
CommonJS VS ESM
由于看到这个警告好几次了,随即决定先深入了解以下 CommonJS 以及 ESM 模块,CommonJS VS ESM 的文档总结,通过查阅相关资料,对浏览器以及 Node 运行环境有了深入的理解,于是有了一个初步的推测,之所以会报 CSS 相关的错误是因为 Node 在运行相关的代码?
新起项目引入 vite-ssg
新起 vite 项目,直接引入 vite-ssg 项目还是报错,看提示是找不到 window,先不解决了,直接找一个 vite-ssg 引入成功的项目吧
github 寻找成功引入 vite-ssg 的例子
通过这个例子,学到了很多解决问题的关键点,以下是一些关键点:
- md 文件可以直接转换成 html 文件
- vite-ssg 项目可以使用 css 样式,如果直接引入 css 样式是可以的,但是在单独的 JS 模块中引入 css 是会报相同的错的
在这里想到了两个大的解决问题的方向:
- 改变引入 css 的方式
- 让 vite-ssg 执行不报错
针对 1 可以直接手动引入 css 或者采用自动化的方案,自动化方案可以从安装依赖环节、自动引入 resolver、Vite 插件直接改写代码
针对 2 需要去了解 vite-ssg 是否提供了响应的配置,深入地需要去了解 vite-ssg 是如何处理 css 文件的,它是如何静态生成 html 文件的
其实通过手动引入 css 样式的方式是可以解决问题的,但是考虑到项目的页面比较多,挨着去手动导入样式比较麻烦,也不好维护,因此需要自动化的解决方案
先在项目中引入 vant 样式吧,然后去实践具体的自动化方案
在实践自动化方案的过程中没有在网上找到合适的插件
vite-ssg 配置项深入解读
没有什么有用的可以控制 css 执行或转换的配置项,不过了解到了模拟浏览器全局变量的核心配置项:mode
VantResolver 理解
提供 type + resolve 方法,能够实现自动引入的核心代码为:
if (name.startsWith('Van')) { const partialName = name.slice(3) if (!options.exclude?.includes(partialName)) { return { name: partialName, from: `vant/${moduleType}`, sideEffects: getSideEffects(kebabCase(partialName), options), } }} // 在这里首次通过改变源码 sideEffects 的方式,完成了 Vant css 的自动化解决方案,不过这不是一个好的解决方案
Vite plugin 插件机制(如何运行,包含哪些)
学习到 Vite 打包的流程,以及 Vite 插件的执行机制,以及 Vite 提供额钩子函数,包括 resolveId、load、transform 等
通过对 resolveId 的深度理解与实践,深入理解了构建过程的模块路径解析阶段,最终采用 resolve 配置的方式解决样式引入报错的问题
这里遇到了 load、transform 改写代码不生效的问题
vite-ssg 实现
秉着对静态文件如何生成的好奇,以及寻找更好的解决方案的目的,找到 load、transform 改写代码不生效的原因,深入阅读了 vite-ssg 的源码,在这个过程中了解到,vite-ssg 其实也没有做什么意想不到的事情,整体划分下来也就散步:构建客户端渲染文件 ——> 构建服务端渲染文件 ——>执行服务端渲染生成的文件 ——>以客户端渲染生成的 html 文件为入口,生成需要静态生成的页面,并且添加相应的脚本
服务端渲染深入理解
在看 vite-ssg 源码的过程中,对服务端渲染流程及代码实现有了深入的理解,包括 VueRouter 在 SSR 项目中的应用;客户端与服务端会生成两套代码,客户端的代码用于激活操作,只是纯静态的网站只需要一套代码
css 更好的解决方案
阅读了 vite-ssg 源码、理解了 VueRouter 在服务端的行为,了解了 manifest.json 文件,了解了在生成用于服务端渲染的 JS 过程中会忽略 CSS、图片等静态资源后没有找到更合适的解决方案,通过 manifest.json 文件准确定位到具体路由的代码,最终采用了比实现步骤中更好的解决方案:客户端打包时才自动引入 CSS
const importStyle = !ssrBuild
AutoImport({ imports: ['vue', 'vue-router', 'pinia'], resolvers: [VantResolver({ importStyle })], dts: '.typings/auto-import.d.ts', eslintrc: { enabled: true, filepath: '.typings/.eslintrc-auto-import.json', },}),Components({ resolvers: [ VantResolver({ importStyle }), IconsResolver({ customCollections: ['custom'], // 自动引入图标 对应的组件名:ICustomSvgname }), ], dts: '.typings/components.d.ts', globs: [ 'src/components/*.{vue,tsx}', 'src/components/*/index.{vue,tsx}', ], // 只把 components/xxx.vue components/floder/index.vue 作为全局组件}),
vite 打包机制
这里其实还有一个遗留的问题就是例子项目中非 main.ts 文件引入的样式会被 JS 异步加载,因此 Critical CSS 不会生效,而待优化项目默认会同步引入首屏用到的样式,没有找到原因,不过这暂时不影响 Critical CSS 在优化项目中落地的主任务,将它作为一个代办项吧
技术约束
动态生成 DOM 结构
待办项
为什么例子项目的 Critical CSS 样式没有生效?
项目使用 vue-router/auto-routes
自动生成的路由,每个页面都没有同步打包
QA
- 解决引入 Vant 样式报错的核心发现是什么?
- 在成功引入 vite-ssg 的项目中导入引入 css 模块的模块会报错