在前端工程化建设中,静态资源是必须处理的一个问题,前端的静态资源通常包括图片、JSON、Worker 文件、Web Assembly 文件等等。由于静态资源本身并不是一个标准意义上的模块,因此在处理静态资源和代码时是需要区别对待的。
对于资源加载问题,Vite需要处理的就是如何将静态资源解析出来并加载为一个 ES 模块;另一方面,我们还需要考虑在生产环境下,静态资源的部署问题、体积问题、网络性能等问题。本文将结合Vite自身的能力及其生态,来解决Vite项目中静态资源处理的各个疑难点。
一、图片加载
图片是前端项目中最常用的静态资源之一,本身包括的图片格式也非常的多,比如png、jpeg、webp、avif、gif、svg都是图片的范畴。本小节主要讨论的是如何加载图片,也就是说怎么让图片在页面中正常显示。
1.1 使用场景
在平时开发中,图片的加载主要有以下几种场景。
1,在HTML或者JSX中,使用img 标签来加载图片:
<img src="../../assets/a.png"/>
2,在 CSS 文件中通过 background 属性加载图片:
background: url('../../assets/b.png') norepeat;
3,在 JavaScript 中,通过脚本的方式动态指定图片的src属性:
document.getElementById('hero-img').src = '../../assets/c.png'
1.2 在Vite中使用
接下来,我们看一下如何在Vite项目中使用图片。首先,我们需要在 Vite配置文件vite.config.ts中配置一下图片资源,比如:
import path from 'path';
{resolve: {//别名配置alias: {'@assets': path.join(__dirname, 'src/assets')}}
}
经过上面的配置后,当遇到@assets路径的时候,Vite便会自动定位至根目录下的src/assets目录。值得注意的是,alias 别名配置不仅在 JavaScript 的 import 语句中生效,在 CSS 代码的 @import 和 url导入语句中也同样生效。
接下来,我们就可以在代码中引入assets的图片,比如:
import React, { useEffect } from 'react';
import { devDependencies } from '../../../package.json';
import styles from './index.module.scss';
// 1. 导入图片
import logoSrc from '@assets/vite.png';// 方式一
export function Header() {return (<div className={`p-20px text-center ${styles.header}`}><!-- 使用图片 --><img className="m-auto mb-4" src={logoSrc} alt="" /></div>);
}// 方式二
export function Header() {useEffect(() => {const img = document.getElementById('logo') as HTMLImageElement;img.src = logoSrc;}, []);return (<div className={`p-20px text-center ${styles.header}`}><!-- 省略前面的组件内容 --><!-- 使用图片 --><img id="logo" className="m-auto mb-4" alt="" /></div>);
}
需要说明的是,使用@assets方式引入资源文件时,需要安装一下craco插件:
npm i @craco/craco -D
接着,运行项目就可以看到效果了。
接下来,我们尝试一下在样式文件中添加background属性,看看是否能够正常显示:
.header {// 前面的样式代码省略background: url('@assets/background.png') no-repeat;
}
1.3 SVG 方式加载
除了png、jpeg、webp等常见的图片格式,svg也是开发中常见的,并且svg格式的图片具有使用灵活、不失真等特性。因此,我们望能将 svg 当做一个组件来引入,这样我们可以很方便地修改 svg 的各种属性。
默认情况下,svg格式的图片是不被支持的,如果需要在前端项目中使用svg图片,需要先安装对应的插件。不过还好社区中也已经了有了对应的插件支持:
- Vue2:使用 vite-plugin-vue2-svg插件
- Vue3:引入 vite-svg-loader插件
- React:引入 vite-plugin-svgr插件
首先,我们在使用Vite构建的React项目中安装vite-plugin-svgr插件:
npm i vite-plugin-svgr -D
然后,在 vite 配置文件添加这个插件:
import svgr from 'vite-plugin-svgr';
{plugins: [svgr()]
}
接着,还需要要在 tsconfig.json添加如下配置,否则会有类型错误提示。
{"compilerOptions": {//省略其它配置"types": ["vite-plugin-svgr/client"]}
}
经过上面的处理之后,我们就可以在项目中使用 svg 格式的图片了。
import { ReactComponent as ReactLogo } from '@/assets/react.svg'
export default Demo() {return <ReactLogo />
}
1.4 Web Worker
Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,允许我们在 js 主线程之外开辟新的 Worker 线程,并将一段 js 脚本运行其中,它赋予了开发者利用 js 操作多线程的能力。
Vite 中使用 Web Worker 也非常简单,我们在新建Header/example.ts文件,代码如下:
const start = () => {let count = 0;setInterval(() => {// 给主线程传值postMessage(++count);}, 2000);
};start();
然后,在组件中引入上面的文件,引入的时候注意加上?worker后缀,相当于告诉 Vite 这是一个 Web Worker 脚本文件。
import Worker from './example.ts?worker';const worker = new Worker();
worker.addEventListener('message', (e) => {console.log(e);
});
接着,重新运行项目,然后打开浏览器的控制面板就可以看到 Worker 传给主线程的信息。
1.5 Web Assembly
Vite 对于 .wasm 文件也提供了开箱即用的支持。我们拿一个斐波拉契的 .wasm 文件(原文件已经放到Github 仓库中)来进行一下实际操作,对应的 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;
}
接下来,我们在组件中导入fib.wasm文件:
import init from './fib.wasm';
type FibFunc = (num: number) => number;
init({}).then((exports) => {const fibFunc = exports.fib as FibFunc;console.log('Fib result:', fibFunc(10));
});
回到浏览器,在项目中执行上面的代码如果看到计算结果,说明 .wasm 文件已经被成功执行。
1.6 其他静态资源
除了上述的资源格式外,Vite 也对下面几类格式提供了内置的支持:
- 媒体类文件,包括mp4、webm、ogg、mp3、wav、flac和aac。
- 字体类文件。包括woff、woff2、eot、ttf 和 otf。
- 文本类。包括webmanifest、pdf和txt。
也就是说,可以在 Vite 项目中将这些类型的文件当做一个 ES 模块来导入使用。如果你的项目中还存在其它格式的静态资源,也可以通过assetsInclude配置让 Vite 来支持加载。
二、生产环境
在开发环境,我们对于Vite项目进行了具体的编码实践。那对于生产环境,我们又会遇到哪些问题呢:
- 部署域名怎么配置
- 资源打包成单文件还是作为 Base64 格式内联
- 图片太大了怎么处理
- svg 请求数量太多了怎么优化
2.1 自定义域名部署
一般来说,当我们访问线上的站点时,站点里面一些静态资源的地址都包含了相应域名的前缀。
<img src="https://baidu.com/flower.png" />
其中,“https://baidu.com/”就是CDN 地址前缀。那如果要在线上环境访问这些静态的图片资源,我们需要怎么处理呢?事实上,对于Vite构建的项目来说,只需要在配置文件中指定base参数的路径即可。
// 是否为生产环境,在生产环境一般会注入 NODE_ENV 这个环境变量,见下面的环境变量文件配置
const isProduction = process.env.NODE_ENV === 'production';
// 填入项目的 CDN 域名地址
const CDN_URL = 'xxxxxx';
{base: isProduction ? CDN_URL: '/'
}
// .env.development
NODE_ENV=development
// .env.production
NODE_ENV=production
注意,为了方便读取项目的配置文件,我们在项目根目录新增的两个环境变量文件.env.development和.env.production,顾名思义,即分别在开发环境和生产环境注入一些环境变量。
当然,有时候可能项目中的某些图片需要存放到另外的存储服务,一种直接的方案是将完整地址写死到 src 属性中,如:
<img src="https://my-image-cdn.com/logo.png">
不过,显然这种方式不太灵活也不太优雅。对于这种问题,我们可以通过定义环境变量的方式来解决这个问题,在项目根目录新增.env文件。
// .env 文件
VITE_IMG_BASE_URL=https://my-image-cdn.com
然后,在src/vite-env.d.ts配置文件增加类型声明:
interface ImportMetaEnv {readonly VITE_APP_TITLE: string;// 自定义的环境变量readonly VITE_IMG_BASE_URL: string;
}interface ImportMeta {readonly env: ImportMetaEnv;
}
值得注意的是,如果某个环境变量要在 Vite 中通过 import.meta.env 访问,那么它必须以VITE_开头,如VITE_IMG_BASE_URL。接下来,我们就可以在组件中来使用这个环境变量:
<img src={new URL('./logo.png', import.meta.env.VITE_IMG_BASE_URL).href} />
最后,当我们启动项目之后,就可以在开发环境启动项目或者生产环境打包后可以看到环境变量已经被替换。
2.2 单文件 or 内联
在Vite项目中,所有的静态资源都有两种构建方式,一种是打包成一个单文件,另一种是通过 base64 编码的格式内嵌到代码中。
通常,对于比较小的资源,适合内联到代码中,一方面对代码体积的影响很小,另一方面可以减少不必要的网络请求,优化网络性能。而对于比较大的资源,就推荐单独打包成一个文件,而不是内联了,否则可能导致代码体积瞬间庞大,页面加载性能直线下降。并且,Vite给出了内置的优化方案:
- 静态资源体积 >= 4KB,则提取成单独的文件
- 静态资源体积 < 4KB,则作为 base64 格式的字符串内联
上述的4 KB即为提取成单文件的临界值,当然,这个临界值你可以通过build.assetsInlineLimit自行配置。
{build: {// 8 KBassetsInlineLimit: 8 * 1024}
}
2.3 图片压缩
图片资源的体积在前端项目往往是项目产物体积的大头,如果能尽可能精简图片的体积,那么对项目整体打包产物体积的优化将会是非常明显的。
在 JavaScript 领域,有一个非常知名的图片压缩库imagemin,作为一个底层的压缩工具,前端的项目中经常基于它来进行图片压缩,比如 Webpack 中大名鼎鼎的image-webpack-loader就是使用的它。当然,对于Vite项目,我们也可以使用开箱即用的Vite插件——vite-plugin-imagemin。
使用前,我们需要在项目中先安装vite-plugin-imagemin插件。
npm i vite-plugin-imagemin -D
接着,在 Vite 配置文件中引入如下配置:
import viteImagemin from 'vite-plugin-imagemin';{plugins: [// 忽略前面的插件viteImagemin({// 无损压缩配置,无损压缩下图片质量不会变差optipng: {optimizationLevel: 7},// 有损压缩配置,有损压缩下图片质量可能会变差pngquant: {quality: [0.8, 0.9],},// svg 优化svgo: {plugins: [{name: 'removeViewBox'},{name: 'removeEmptyAttrs',active: false}]}})]
}
最后,我们尝试执行npm run build进行打包,就可以看到执行了压缩:
2.4 svg优化
在实际的项目中,我们还会经常用到各种各样的 svg 图标,虽然 svg 文件一般体积不大,但 Vite 中对于 svg 文件会始终打包成单文件,大量的图标引入之后会导致网络请求增加,大量的 HTTP 请求会导致网络解析耗时变长,页面加载性能直接受到影响。比如,我们需要在某个页面中引入5个 svg 文件。
import Logo1 from '@assets/icons/logo-1.svg';
import Logo2 from '@assets/icons/logo-2.svg';
import Logo3 from '@assets/icons/logo-3.svg';
import Logo4 from '@assets/icons/logo-4.svg';
import Logo5 from '@assets/icons/logo-5.svg';
顺便说一句,Vite 中提供了import.meta.glob的语法糖来解决这种批量导入的问题,如上述的 import 语句可以写成下面这样。
const icons = import.meta.glob('../../assets/icons/logo-*.svg');
接下来,我们稍作解析,将 svg 应用到组件当中:
const iconUrls = Object.values(icons).map(mod => mod.default);
// 组件返回内容添加如下
{iconUrls.map((item) => (<img src={item} key={item} width="50" alt="" />
))}
重新运行项目,会发现浏览器分别发出了 5 个 svg 的请求:
那我们能不能把这些 svg 合并到一起,从而大幅减少网络请求呢?答案是可以的,通过vite-plugin-svg-icons即可实现合并请求操作。首先,我们需要安装一下这个插件:
npm i vite-plugin-svg-icons -D
接着,在 Vite 配置文件中增加如下内容:
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
{plugins: [// 省略其它插件createSvgIconsPlugin({iconDirs: [path.join(__dirname, 'src/assets/icons')]})]
}
然后,在src/components目录下新建SvgIcon组件。
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>);
}
接着,我们在Header 组件中稍作修改。
const icons = import.meta.globEager('../../assets/icons/logo-*.svg');
const iconUrls = Object.values(icons).map((mod) => {// 如 ../../assets/icons/logo-1.svg -> logo-1const fileName = mod.default.split('/').pop();const [svgName] = fileName.split('.');return svgName;
});// 渲染 svg 组件
{iconUrls.map((item) => (<SvgIcon name={item} key={item} width="50" height="50" />
))}
最后,在src/main.tsx文件中添加一行代码:
import 'virtual:svg-icons-register';
回到浏览器的页面中,就可以发现svg图片已经生成,然后通过 use 属性来引用svg的对应内容即可。