目 录CONTENT

文章目录

Vite处理静态资源

Hello!你好!我是村望~!
2022-12-07 / 0 评论 / 1 点赞 / 309 阅读 / 5,884 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

Vite处理静态资源

前言

静态资源处理是前端工程经常遇到的问题,在真实的工程中不仅仅包含了动态执行的代码,也不可避免地要引入各种静态资源,

图片JSONWorker 文件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!在看一下,已经达成我们想要的效果啦!

vscode alias 2

模块化导入图片测试

而且我们的图片也可以正常的显示在页面上!

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;

image-20221206170654546

然后我们去css中使用测试一下~

.logo {
  width: 500px;
  height: 500px;
  background-image: url('assets/images/bg.png');
  background-size: contain;
}

image-20221206171611005

同样是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;

image-20221206172028526

不过,我们通常也希望能将 svg 当做一个组件来引入,这样我们可以很方便地修改 svg 的各种属性,而且比 img 标签的引入方式更加优雅。

SVG 组件加载在不同的前端框架中的实现不太相同,社区中也已经了有了对应的插件支持:

现在让我们在 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已经渲染成功了!

image-20221206175510697

但是此时编辑器会报类型错误!

image-20221206175815406

我们需要在tsconfig中配置一下 这个包的类型!制定一下这个包的类型生命文件

image-20221206175913565

可以看到ReactComponent 这个类型是 vite-plugin-svgr/client 这个文件声明的!所以我们要在 tsconfig添加下面的配置!

{
  "compilerOptions": {
    "types": ["vite-plugin-svgr/client"]
  }
}

JSON 加载

Vite 中已经内置了对于 JSON 文件的解析

底层使用@rollup/pluginutilsdataToEsm 方法将 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;

image-20221207105251669

你也可以在配置文件禁用按名导入的方式

这样会将 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 传给主线程的信息已经成功打印:

webwor

说明 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 文件已经被成功执行:

image-20221207112631774

其它静态资源

除了上述的一些资源格式,Vite 也对下面几类格式提供了内置的支持:

  • 媒体类文件,包括mp4webmoggmp3wavflacaac
  • 字体类文件。包括woffwoff2eotttfotf
  • 文本类。包括webmanifestpdftxt

也就是说,你可以在 Vite 将这些类型的文件当做一个 ES 模块来导入使用。如果你的项目中还存在其它格式的静态资源,你可以通过assetsInclude配置让 Vite 来支持加载:

assetsInclude

// 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;

image-20221207121241023

不管是生产还是开发环境都可以加载成功!

2. import.meta.env 的类型配置

上面我们使用 import.meta.env 引入自定义的环境变量时,其实在编辑器中是不会有任何提示的!如果有很多变量其实写起来会增加很多寻找负担!

import env type

如何添加类型呢?进入 src/vite-env.d.ts增加类型声明:这个文件专门可以应该定义一些vite定义的环境变量类型的!

对于使用 TypeScript 的开发者来说,请确保在 env.d.tsvite-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;
}

然后回到编辑器中去看看效果!可以看到已经有了我们自定义环境变量的提示了!

Dec-07-2022 12-22-19

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进行打包:

image-20221207205618048

Vite 插件已经自动帮助我们调用 imagemin 进行项目图片的压缩,可以看到压缩的效果非常明显,强烈推荐大家在项目中使用。

然后我们执行 npm run preview 来看看页面效果!

image-20221207205827132

请求图片的大小的确小了!

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 的请求:

image-20221207210709537

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 打印结果如下!

image-20221207211811144

可以看到对象的 value 都是动态 import,适合按需加载的场景。在这里我们只需要同步加载即可,可以添加配置 eager: true

const icons = import.meta.glob('assets/svgs/*.svg',{ eager: true });

image-20221207212145465

这样就是同步的模块化了!

接下来我们稍作解析,然后将 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 的网络请求。

image-20221207215532836

小结

  • 重点掌握在Vite 如何加载静态资源如何在生产环境中对静态资源进行优化
  • 如何加载各种静态资源,如图片、svg(组件形式)、JSON、Web Worker 脚本、Web Asssembly 文件等等格式,并通过一些示例带大家进行实际的操作。
  • 其次,我们会把关注点放到生产环境,对自定义部署域名是否应该内联图片压缩svg 雪碧图等问题
  • 也穿插了一些 Vite 其他的知识点,比如如何定义环境变量文件如何使用 Glob 导入的语法糖
1

评论区