草庐IT

如何在Vite项目中处理静态资源

xiangzhihong8 2023-10-12 原文

在前端工程化建设中,静态资源是必须处理的一个问题,前端的静态资源通常包括图片、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 KB
    assetsInlineLimit: 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-1
  const 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的对应内容即可。

有关如何在Vite项目中处理静态资源的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  3. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  4. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  5. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  6. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  7. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  8. ruby-on-rails - 项目升级后 Pow 不会更改 ruby​​ 版本 - 2

    我在我的Rails项目中使用Pow和powifygem。现在我尝试升级我的ruby​​版本(从1.9.3到2.0.0,我使用RVM)当我切换ruby​​版本、安装所有gem依赖项时,我通过运行railss并访问localhost:3000确保该应用程序正常运行以前,我通过使用pow访问http://my_app.dev来浏览我的应用程序。升级后,由于错误Bundler::RubyVersionMismatch:YourRubyversionis1.9.3,butyourGemfilespecified2.0.0,此url不起作用我尝试过的:重新创建pow应用程序重启pow服务器更新战俘

  9. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  10. ruby-on-rails - 新 Rails 项目 : 'bundle install' can't install rails in gemfile - 2

    我已经像这样安装了一个新的Rails项目:$railsnewsite它执行并到达:bundleinstall但是当它似乎尝试安装依赖项时我得到了这个错误Gem::Ext::BuildError:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcheckingforlibkern/OSAtomic.h...yescreatingMakefilemake"DESTDIR="cleanmake"DESTDIR="

随机推荐