Vite处理静态资源
前言
静态资源处理是前端工程经常遇到的问题,在真实的工程中不仅仅包含了动态执行的代码,也不可避免地要引入各种静态资源,
如 图片
、JSON
、Worker 文件
、Web Assembly 文件
等等。
而静态资源本身并不是标准意义上的模块,因此对它们的处理和普通的代码是需要区别对待的。
- 一方面我们需要解决资源加载的问题
- 对 Vite 来说就是如何将静态资源解析并加载为一个 ES 模块的问题;
- 另一方面在生产环境下我们还需要考虑静态资源的部署问题、体积问题、网络性能问题,并采取相应的方案来进行优化。
图片加载
这一部分我们主要讨论的是如何加载图片,也就是说怎么让图片在页面中正常显示。
图片是项目中最常用的静态资源之一,本身包括了非常多的格式,诸如 png、jpeg、webp、avif、gif,当然,也包括经常用作图标的 svg 格式。
使用场景
在日常的项目开发过程中,我们一般会遇到三种加载图片的场景:
- 在 HTML 或者 JSX 中,通过 img 标签来加载图片,如:
<img src="../../assets/a.png"></img>
- 在 CSS 中通过 background 属性加载图片,如:
background: url('../../assets/b.png') norepeat;
- 在 JavaScript 中,通过脚本的方式动态指定图片的
src
属性,如:
document.getElementById('hero-img').src = '../../assets/c.png'
当然一般这种会使用路径别名,比如地址前缀直接换成@assets
这样就不用开发人员手动寻址,降低开发时的心智负担。
在 Vite 中使用路径别名+VSCode别名插件配置
https://vitejs.dev/config/shared-options.html#resolve-alias
export default defineConfig({
resolve: {
alias: {
assets: path.resolve(__dirname, 'src/assets')
}
},
})
配置好了之后还要在编辑器中去配置一下别名路径,让编辑器也能知道我们配置的别名!不然开发的体验依然不是很好,比如下面的情况!并没有帮我们智能的提示我们的别名!
在vscode中!我们可以安装一个插件!path-intellisense
,然后在当前项目目录下创建.vscode/setting.json
来配置当前项目的配置文件
{
"path-intellisense.mappings": {
"assets": "${workspaceRoot}/src/assets",
}
}
配置好了 之后重启一下vscode
!在看一下,已经达成我们想要的效果啦!
模块化导入图片测试
而且我们的图片也可以正常的显示在页面上!
import React from 'react';
import style from './App.module.scss';
import s1 from 'assets/images/s1.png';
function App() {
return (
<div className={style.logo}>
<img width={'150px'} height={'150px'} src={s1} alt="" />
</div>
);
}
export default App;
然后我们去css中使用测试一下~
.logo {
width: 500px;
height: 500px;
background-image: url('assets/images/bg.png');
background-size: contain;
}
同样是OK的!
SVG 组件方式加载
刚才我们成功地在 Vite 中实现了图片的加载,上述这些加载的方式对于 svg 格式来说依然是适用的。
import React from 'react';
import style from './App.module.scss';
import s1 from 'assets/images/s1.png';
import s2 from 'assets/svgs/s1.svg'
function App() {
return (
<div className={style.logo}>
<img width={'150px'} height={'150px'} src={s1} alt="" />
<img src={s2} alt="" />
</div>
);
}
export default App;
不过,我们通常也希望能将 svg 当做一个组件来引入,这样我们可以很方便地修改 svg 的各种属性,而且比 img 标签的引入方式更加优雅。
SVG 组件加载在不同的前端框架中的实现不太相同,社区中也已经了有了对应的插件支持:
- Vue2 项目中可以使用 vite-plugin-vue2-svg插件。
- Vue3 项目中可以引入 vite-svg-loader。
- React 项目使用 vite-plugin-svgr插件。
现在让我们在 React 脚手架项目中安装对应的依赖:
pnpm i vite-plugin-svgr -D
然后需要在 vite 配置文件添加这个插件:
// vite.config.ts
import svgr from 'vite-plugin-svgr';
{
plugins: [
// 其它插件省略
svgr()
]
}
接下来让我们在项目中使用 svg 组件:
import React from 'react';
import style from './App.module.scss';
import { ReactComponent as MySvg } from 'assets/svgs/s1.svg';
function App() {
return (
<div className={style.logo}>
<MySvg />
</div>
);
}
export default App;
可以看到SVG已经渲染成功了!
但是此时编辑器会报类型错误!
我们需要在tsconfig中配置一下 这个包的类型!制定一下这个包的类型生命文件
可以看到ReactComponent 这个类型是 vite-plugin-svgr/client
这个文件声明的!所以我们要在 tsconfig添加下面的配置!
{
"compilerOptions": {
"types": ["vite-plugin-svgr/client"]
}
}
JSON 加载
Vite 中已经内置了对于 JSON 文件的解析
底层使用
@rollup/pluginutils
的dataToEsm
方法将 JSON 对象转换为一个包含各种具名导出的 ES 模块
使用姿势如下:
import React from 'react';
import style from './App.module.scss';
import { ReactComponent as MySvg } from 'assets/svgs/s1.svg';
import * as packageJSON from '../package.json';
function App() {
console.log(packageJSON);
return (
<div className={style.logo}>
<MySvg />
</div>
);
}
export default App;
你也可以在配置文件禁用按名导入的方式
这样会将 JSON 的内容解析为export default JSON.parse("xxx")
,这样会失去按名导出
的能力,不过在 JSON 数据量比较大的时候,可以优化解析性能。
Web Worker 脚本
当在 HTML 页面中执行脚本时,页面的状态是不可响应的,直到脚本已完成。
web worker 是运行在后台的 JavaScript,独立于其他脚本,不会影响页面的性能。
您可以继续做任何愿意做的事情:点击、选取内容等等,而此时 web worker 在后台运行。
const start = () => {
let count = 0;
setInterval(() => {
// 给主线程传值
postMessage(++count);
}, 2000);
};
start();
然后在组件中引入,引入的时候注意加上?worker
后缀,相当于告诉 Vite 这是一个 Web Worker 脚本文件:
import React from 'react';
import style from './App.module.scss';
import { ReactComponent as MySvg } from 'assets/svgs/s1.svg';
import Worker from './webWork.js?worker';
function App() {
// 1. 初始化 Worker 实例
const worker = new Worker();
// 2. 主线程监听 worker 的信息
worker.addEventListener('message', (e) => {
console.log(e);
});
return (
<div className={style.logo}>
<MySvg />
</div>
);
}
export default App;
打开浏览器的控制面板,你可以看到 Worker 传给主线程的信息已经成功打印:
说明 Web Worker 脚本已经成功执行,也能与主线程正常通信。
Web Assembly 文件
WebAssembly 是一种新的编码方式,可以在现代的网络浏览器中运行 - 它是一种低级的类汇编语言,具有紧凑的二进制格式,可以接近原生的性能运行,并为诸如 C / C ++等语言提供一个编译目标,以便它们可以在 Web 上运行。它也被设计为可以与 JavaScript 共存,允许两者一起工作。
Vite 对于 .wasm
文件也提供了开箱即用的支持,我们拿一个斐波拉契的 .wasm
文件来进行一下实际操作,对应的 JavaScript 原文件如下:
export function fib(n) {
var a = 0,
b = 1;
if (n > 0) {
while (--n) {
let t = a + b;
a = b;
b = t;
}
return b;
}
return a;
}
Vite 会对.wasm
文件的内容进行封装,默认导出为 init 函数,这个函数返回一个 Promise,因此我们可以在其 then 方法中拿到其导出的成员——fib
方法。
让我们在组件中导入fib.wasm
文件: 需要加 ?init
import init from './fibFunc.wasm?init';
Vite 会对.wasm
文件的内容进行封装,默认导出为 init 函数,这个函数返回一个 Promise,因此我们可以在其 then 方法中拿到其导出的成员——fib
方法。
import React from 'react';
import style from './App.module.scss';
import { ReactComponent as MySvg } from 'assets/svgs/s1.svg';
import init from './fibFunc.wasm?init';
function App() {
// 声明 init resove 结果的类型
type FibFunc = {
fib: (num: number) => number;
};
init({}).then((exports) => {
const fun = exports.exports as FibFunc;
console.log(fun); // {memory: Memory(0), fib: ƒ}
console.log(fun.fib(10)); // 55
});
return (
<div className={style.logo}>
<MySvg />
</div>
);
}
export default App;
回到浏览器,我们可以查看到计算结果,说明 .wasm 文件已经被成功执行:
其它静态资源
除了上述的一些资源格式,Vite 也对下面几类格式提供了内置的支持:
- 媒体类文件,包括
mp4
、webm
、ogg
、mp3
、wav
、flac
和aac
。 - 字体类文件。包括
woff
、woff2
、eot
、ttf
和otf
。 - 文本类。包括
webmanifest
、pdf
和txt
。
也就是说,你可以在 Vite 将这些类型的文件当做一个 ES 模块来导入使用。如果你的项目中还存在其它格式的静态资源,你可以通过assetsInclude
配置让 Vite 来支持加载:
// vite.config.ts
export default defineConfig({
assetsInclude: ['**/*.gltf']
})
特殊资源后缀
Vite 中引入静态资源时,也支持在路径最后加上一些特殊的 query 后缀,包括:
?url
: 表示获取资源的路径,这在只想获取文件路径而不是内容的场景将会很有用。?raw
: 表示获取资源的字符串内容,如果你只想拿到资源的原始内容,可以使用这个后缀。
比如之前写过的 glsl 着色器,这个只需要拿到字符串放到threejs去执行就好了,所以只需要读取源文件的原始内容,不用模块化!
import fragmentBlowUp from "./shaders/blowupfragment.glsl?raw";
import vertexBlowUp from "./shaders/blowupvertex.glsl?raw";
import fragment from "./shaders/fragment.glsl?raw";
...
const material = new THREE.RawShaderMaterial({
vertexShader: vertex,
fragmentShader: fragment,
...
})
?inline
: 表示资源强制内联,而不是打包成单独的文件。
生产环境处理
在前面的内容中,我们围绕着如何加载静态资源这个问题,在 Vite 中进行具体的编码实践。
但另一方面,在生产环境下,我们又面临着一些新的问题。
- 部署域名怎么配置?
- 资源打包成单文件还是作为 Base64 格式内联?
- 图片太大了怎么压缩?
- svg 请求数量太多了怎么优化?
1. 自定义部署域名
一般在我们访问线上的站点时,站点里面一些静态资源的地址都包含了相应域名的前缀,如:
<img src="https://sanyuan.cos.ap-beijing.myqcloud.com/logo.png" />
以上面这个地址例子,https://sanyuan.cos.ap-beijing.myqcloud.com
是 CDN 地址前缀,/logo.png
则是我们开发阶段使用的路径。
那么,我们是不是需要在上线前把图片先上传到 CDN,然后将代码中的地址手动替换成线上地址呢?这样就太麻烦了!
在 Vite 中我们可以有更加自动化的方式来实现地址的替换,只需要在配置文件中指定base
参数即可:
// vite.config.ts
// 是否为生产环境,在生产环境一般会注入 NODE_ENV 这个环境变量,见下面的环境变量文件配置
const isProduction = process.env.NODE_ENV === 'production';
// 填入项目的 CDN 域名地址
const CDN_URL = 'https://xxxxxx/';
// 具体配置
{
base: isProduction ? CDN_URL: '/'
}
// .env.development
NODE_ENV=development
// .env.production
NODE_ENV=production
注意在项目根目录新增的两个环境变量文件.env.development
和.env.production
,顾名思义,即分别在开发环境和生产环境注入一些环境变量,这里为了区分不同环境我们加上了NODE_ENV
,你也可以根据需要添加别的环境变量。
打包的时候 Vite 会自动将这些环境变量替换为相应的字符串。
npm run build
生产环境代码!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="https://xxxxxx/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<script type="module" crossorigin src="https://xxxxxx/assets/index.bc32fe14.js"></script>
<link rel="stylesheet" href="https://xxxxxx/assets/index.ca86ec27.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
当然难免有时候可能项目中的某些图片需要存放到另外的存储服务,更有甚一个项目接入多个平台的CDN资源
这里准备了一张真实的CDN图片~
http://qiniuyun.quancundexiwang.wang/R-C.0951e0e8a511181cb43bf8c4c3eb15e2.png
一种直接的方案是将完整地址写死到 src 属性中,如:
<img src="http://qiniuyun.quancundexiwang.wang/R-C.0951e0e8a511181cb43bf8c4c3eb15e2.png">
这样做显然是不太优雅的
我们可以通过定义环境变量的方式来解决这个问题
在项目根目录新增.env
文件:下面是 env 文件的优先级
开发环境优先级:
.env.development > .env
生产环境优先级:
.env.production > .env
那么我们就需要在.env
配置这个 CDN 的域名!
// .env 文件
VITE_QINIUYUN=http://qiniuyun.quancundexiwang.wang/
值得注意的是,如果某个环境变量要在 Vite 中通过 import.meta.env
访问,那么它必须以VITE_
开头,如VITE_IMG_BASE_URL
。接下来我们在组件中来使用这个环境变量:
import React from 'react';
import style from './App.module.scss';
function App() {
return (
<div className={style.logo}>
{/* new URL 第一个参数 是cdn域名后面的路径~ */}
<img src={new URL('/R-C.0951e0e8a511181cb43bf8c4c3eb15e2.png', import.meta.env.VITE_QINIUYUN).href} />
</div>
);
}
export default App;
不管是生产还是开发环境都可以加载成功!
2. import.meta.env 的类型配置
上面我们使用 import.meta.env 引入自定义的环境变量时,其实在编辑器中是不会有任何提示的!如果有很多变量其实写起来会增加很多寻找负担!
如何添加类型呢?进入 src/vite-env.d.ts
增加类型声明:这个文件专门可以应该定义一些vite定义的环境变量类型的!
对于使用 TypeScript 的开发者来说,请确保在
env.d.ts
或vite-env.d.ts
文件中添加类型声明,以获得类型检查以及代码提示。
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string;
// 自定义的环境变量
readonly VITE_QINIUYUN: string;
}
// ImportMeta.env => ImportMetaEnv
interface ImportMeta {
readonly env: ImportMetaEnv;
}
然后回到编辑器中去看看效果!可以看到已经有了我们自定义环境变量的提示了!
3.单文件 or 内联?
在 Vite 中,所有的静态资源都有两种构建方式,
- 一种是打包成一个单文件
- 另一种是通过 base64 编码的格式内嵌到代码中。
这两种方案到底应该如何来选择呢?
对于比较小的资源,适合内联到代码中,一方面对代码体积
的影响很小,另一方面可以减少不必要的网络请求,优化网络性能
。
而对于比较大的资源,就推荐单独打包成一个文件,而不是内联了,否则可能导致**上 MB 的 base64 字符串内嵌到代码中**,导致代码体积瞬间庞大,页面加载性能直线下降。
Vite 中内置的优化方案是下面这样的:
build.assetsInlineLimit 默认 4096(4kb)
- 如果静态资源体积 >= 4KB,则提取成单独的文件
- 如果静态资源体积 < 4KB,则作为 base64 格式的字符串内联
上述的4 KB
即为提取成单文件的临界值,当然,这个临界值你可以通过build.assetsInlineLimit
自行配置,如下代码所示:
// vite.config.ts
{
build: {
// 8 KB
assetsInlineLimit: 8 * 1024
}
}
值得注意的是~svg 格式的文件不受这个临时值的影响,始终会打包成单独的文件,因为它和普通格式的图片不一样,需要动态设置一些属性
4. 图片压缩
图片资源的体积往往是项目产物体积的大头,如果能尽可能精简图片的体积,那么对项目整体打包产物体积的优化将会是非常明显的。
在 JavaScript 领域有一个非常知名的图片压缩库imagemin,作为一个底层的压缩工具,前端的项目中经常基于它来进行图片压缩
比如 Webpack 中大名鼎鼎的image-webpack-loader
。
社区当中也已经有了开箱即用的 Vite 插件——vite-plugin-imagemin
,首先让我们来安装它: viteImagemin
这个包依赖着不同的图片格式的压缩包!
pnpm i vite-plugin-imagemin -D
//vite.config.ts
import viteImagemin from 'vite-plugin-imagemin';
{
plugins: [
// 忽略前面的插件
viteImagemin({
// 无损压缩配置,无损压缩下图片质量不会变差
optipng: {
optimizationLevel: 7
},
// 有损压缩配置,有损压缩下图片质量可能会变差
pngquant: {
quality: [0.1, 0.2], // 这里我们把质量压缩的狠一点!
},
// svg 优化
svgo: {
plugins: [
{
name: 'removeViewBox'
},
{
name: 'removeEmptyAttrs',
active: false
}
]
}
})
]
}
接下来我们可以尝试执行pnpm run build
进行打包:
Vite 插件已经自动帮助我们调用 imagemin
进行项目图片的压缩,可以看到压缩的效果非常明显,强烈推荐大家在项目中使用。
然后我们执行 npm run preview
来看看页面效果!
请求图片的大小的确小了!
5. 雪碧图优化
在实际的项目中我们还会经常用到各种各样的 svg 图标,虽然 svg 文件一般体积不大,但 Vite 中对于 svg 文件会始终打包成单文件
大量的图标引入之后会导致网络请求增加,大量的 HTTP 请求会导致网络解析耗时变长,页面加载性能直接受到影响。这个问题怎么解决呢?
HTTP2 的多路复用设计可以解决大量 HTTP 的请求导致的网络加载性能问题,因此雪碧图技术在 HTTP2 并没有明显的优化效果
这个技术更适合在传统的 HTTP 1.1 场景下使用(比如本地的
Dev Server
)。
比如在 组件 中分别引入 5 个 svg 文件:
import React from 'react';
import style from './App.module.scss';
import s1 from 'assets/svgs/s1.svg';
import s2 from 'assets/svgs/s2.svg';
import s3 from 'assets/svgs/s3.svg';
import s4 from 'assets/svgs/s4.svg';
import s5 from 'assets/svgs/s5.svg';
function App() {
return (
<div className={style.logo}>
<img width={'100px'} src={s1} alt="" />
<img width={'100px'} src={s2} alt="" />
<img width={'100px'} src={s3} alt="" />
<img width={'100px'} src={s4} alt="" />
<img width={'100px'} src={s5} alt="" />
</div>
);
}
export default App;
回到页面中,我们发现浏览器分别发出了 5 个 svg 的请求:
BTW( by the way ) Vite 中提供了import.meta.glob
的语法糖来解决这种批量导入的问题,如上述的 import 语句可以写成下面这样:
const icons = import.meta.glob('../../assets/icons/logo-*.svg');
import React from 'react';
import style from './App.module.scss';
// import s1 from 'assets/svgs/s1.svg';
// import s2 from 'assets/svgs/s2.svg';
// import s3 from 'assets/svgs/s3.svg';
// import s4 from 'assets/svgs/s4.svg';
// import s5 from 'assets/svgs/s5.svg';
const icons = import.meta.glob('assets/svgs/*.svg');
function App() {
console.log(icons);
return (
<div className={style.logo}>
</div>
);
}
export default App;
Icons
打印结果如下!
可以看到对象的 value 都是动态 import,适合按需加载的场景。在这里我们只需要同步加载即可,可以添加配置 eager: true
const icons = import.meta.glob('assets/svgs/*.svg',{ eager: true });
这样就是同步的模块化了!
接下来我们稍作解析,然后将 svg 应用到组件当中:
import React from 'react';
import style from './App.module.scss';
const icons = import.meta.glob('assets/svgs/*.svg', { eager: true });
function App() {
const iconUrls = Object.values(icons).map((mod) => mod.default);
return (
<div className={style.logo}>
{iconUrls.map((item) => (
<img src={item} key={item} width="50" alt="" />
))}
</div>
);
}
export default App;
这样就可以 完成 批量导入资源的问题!
言归正传~
假设页面有 100 个 svg 图标,将会多出 100 个 HTTP 请求,依此类推。我们能不能把这些 svg 合并到一起,从而大幅减少网络请求呢?
答案是可以的。这种合并图标的方案也叫雪碧图
,我们可以通过vite-plugin-svg-icons
来实现这个方案,首先安装一下这个插件:
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
{
plugins: [
createSvgIconsPlugin({
// 指定svg文件的目录可以指定多个所以是数组!
iconDirs: [path.resolve(__dirname, 'src/assets/svgs')]
})
]
}
在 src/components
目录下新建SvgIcon
组件:
// SvgIcon/index.tsx
import React from 'react';
export interface SvgIconProps {
name: string;
prefix: string;
color: string;
[key: string]: string;
}
export default function SvgIcon({
name,
prefix = 'icon',
color = '#333',
...props
}: SvgIconProps) {
const symbolId = `#${prefix}-${name}`;
return (
<svg {...props} aria-hidden="true">
<use href={symbolId} fill={color} />
</svg>
);
}
最后在src/main.tsx
文件中添加一行代码:
import 'virtual:svg-icons-register';
然后去组件使用 SvgIcon
import React from 'react';
import style from './App.module.scss';
import SvgIcon from './components/SvgIcon';
function App() {
return (
<div className={style.logo}>
<SvgIcon color="#f00" name="s1" prefix="icon" />
<SvgIcon color="#f00" name="s2" prefix="icon" />
<SvgIcon color="#f00" name="s3" prefix="icon" />
<SvgIcon color="#f00" name="s4" prefix="icon" />
<SvgIcon color="#f00" name="s5" prefix="icon" />
</div>
);
}
export default App;
回到浏览器看一下!如此一来,我们就能将所有的 svg 内容都内联到 HTML 中,省去了大量 svg 的网络请求。
小结
- 重点掌握在Vite 如何加载静态资源和如何在生产环境中对静态资源进行优化。
- 如何加载各种静态资源,如图片、svg(组件形式)、JSON、Web Worker 脚本、Web Asssembly 文件等等格式,并通过一些示例带大家进行实际的操作。
- 其次,我们会把关注点放到生产环境,对
自定义部署域名
、是否应该内联
、图片压缩
、svg 雪碧图
等问题 - 也穿插了一些 Vite 其他的知识点,比如如何
定义环境变量文件
、如何使用 Glob 导入
的语法糖
评论区