跳到主要内容

3 篇博文 含有标签「vite」

查看所有标签

· 阅读需 6 分钟

指标

FCP(First Contentful Paint):白屏时间(第一个文本绘制时间)

Speed Index:首屏时间

TTI(Time To Interactive): 第一次可交互的时间

lighthouse score(performance):Chrome浏览器审查工具性能评分(也可以npm install -g lighthouse,编程式调用)

打包产物优化

  1. tree shaking

清除未使用的代码

Vite: 只要遵循 ESM,Vite 自动树摇优化

Webpack:mode 改为 production;babel 的 modules 改为 false,不要转为 cjs;启用 optimization 的 usedExports 选项

  1. 代码压缩

Vite 自动压缩 css,js 可以靠 terser 插件来处理,rollupOptions.plugins 配置,例如删除 clg 和注释

Webpack 靠 loader 和插件,例如 css-loader-minimizeTerserPlugin,UglifyJsPlugin

  1. 分包

Vite: build.rollupOptions.output.manualChunks

Webpack: optimization.splitChunks

不怕包多,因为 http2 多路复用

  1. 排查冗余依赖、静态资源

传输(网络/请求)优化

  1. 开启 http2
server {
listen 443 ssl http2; # 启用 HTTPS 和 HTTP/2
server_name yourdomain.com;

ssl_certificate /path/to/your/cert.pem;
ssl_certificate_key /path/to/your/key.pem;

location / {
proxy_pass http://backend_service; # 后端服务地址
proxy_http_version 1.1; # 继续使用 HTTP/1.1 与后端通信
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
  1. gzip 压缩传输

nginx 配置 gzip

#开启和关闭gzip模式
gzip on;
#gizp压缩起点,文件大于1k才进行压缩
gzip_min_length 1k;
# gzip 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 6;
# 进行压缩的文件类型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
# nginx对于静态文件的处理模块,开启后会寻找以.gz结尾的文件,直接返回,不会占用cpu进行压缩,如果找不到则不进行压缩
gzip_static on
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
# 设置gzip压缩针对的HTTP协议版本
gzip_http_version 1.1;
  1. prefetch preload

<link> 标签的 rel 属性的两个可选值。 Prefetch,预请求,是为了提示浏览器,用户未来的浏览有可能需要加载目标资源,所以浏览器有可能通过事先获取和缓存对应资源,优化用户体验。 Preload,预加载,表示用户十分有可能需要在当前浏览中加载目标资源,所以浏览器必须预先获取和缓存对应资源。

首屏字体、大图加载,CSS中引入字体需要等CSS解析后才会加载,这之前浏览器会使用默认字体,当加载后会替换为自定义字体,导致字体样式闪动,而我们使用Preload提前加载字体后这种情况就好很多了,大图也是如此

类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload

Vite: 用插件

Webpack: 注释 /* webpackPrefetch: true */

  1. cdn 内容分发网络

将静态资源托管到 cdn,就近派发

图片优化

  1. 图标优化

雪碧图:将多张比较小的图片,合并到一张大的图片上面,大的图片背景透明,使用的时候通过不同的 background-position定位来展示的那部分图片。

iconfont

svg:将图片转换为svg文件,一个 svg 文件可能会存在若干图标

  1. 小图变成 dataUrl

Vite 自带

Webpack:使用 url-loader

  1. thumbnail

图片不用原图,而是使用分辨率低的小图占位

  1. 懒加载

对于图片很多的情况下,可以使用 Intersection Observer 、模糊图作为 div 父元素的背景等懒加载方案

交互优化

  1. loading 首屏和路由跳转

通常会在 index.html 上写简单的 CSS 动画,直到应用挂载后替换挂载节点的内容

  1. 骨架屏

代码优化(React)

针对使用 antd 的 ProTable 组件的页面进行优化,主要方案如下

  1. 减少 State

检查代码行文逻辑,尤其是 request 中的清理或重置状态的逻辑

可以使用 if 减少无意义的 setState

  1. 使用 setEditableKeys(ids) 替代 action?.startEditable(id)

  2. 减轻表格渲染压力:只负责展示文本

  3. 利用缓存减少数据处理过程(空间换时间)

使用 useRef 存储数据处理结果,作为后续处理过程的参考或者恢复时的备份

尽可能将多个需求的数据处理过程合并,减少时间复杂度

  1. 虚拟滚动

  2. 使用 useMemo 处理 tableComponents

当需要重写 table 的组件例如 cell 或者 body 时,可以使用 useMemo 缓存这个组件,减少不必要的计算与更新

使用 useCallback 缓存复杂回调函数,对于作为回调函数传入 memo 组件的函数,非常好用

  1. columns 配置中适当添加 shouldCellUpdate

  2. 避免使用第三方高度封装的组件。。例如 EditableProTable

· 阅读需 6 分钟

记账项目部署

将项目前端、后端、数据库这三个服务打包的镜像,push 到 docker hub,再利用 docker compose 统一拉取新版本镜像进行服务端部署

后端

源码:https://github.com/GSemir0418/account-app-gin

镜像:https://hub.docker.com/r/gsemir/account-app-backend

  • 流程

容器共需两个基础镜像,一个用来构建golang:1.21-alpine3.20 AS builder),一个用来运行项目(alpine:latest

构建时先复制依赖文件,安装依赖,之后再将源码复制到容器进行构建

构建产物及环境变量文件复制到运行时的镜像中,启动项目即可

  • 区别环境变量

在构建运行时镜像配置中,添加容器环境变量;启动项目之前将环境变量文件复制到运行时容器中

# ...
FROM alpine:latest
ENV GIN_ENV prod
ENV GIN_MODE release
# ...
WORKDIR /app
COPY --from=builder /app/main /app/main
COPY --from=builder /app/.env.prod /app/.env.prod
# ...

然后代码中通过 GIN_ENV 区别加载的文件,注意是运行时才会区别环境而不是构建时

// 获取环境变量 GIN_ENV,默认值为 "dev"
env := os.Getenv("GIN_ENV")
if env == "" {
env = "dev"
}

// 确定要加载的 .env 文件
envFile := filepath.Join(basepath, "..", ".env."+env)
// 加载环境文件
err := godotenv.Load(envFile)
  • 数据库连接要配置容器名而不是 localhost 或 ip
# env
DB_DSN=username:password@tcp(mysql-container-name:3306)/db_name?charset=utf8mb4&parseTime=True&loc=Local
  • 记得配置 go 代理,加速依赖包
FROM golang:1.21-alpine3.20 AS builder
WORKDIR /app
ENV GOPROXY=https://goproxy.cn,direct
# ...
  • docker build 报错
failed commit on ref "layer-sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1": "layer-sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1" failed size validation: 0 != 32: failed precondition

解决方法:使用 docker pull 手动拉取镜像

  • 先同步数据库,再启动项目
CMD ["sh", "-c", "./main db migrate:create && ./main server"]
  • 镜像构建脚本
#!/bin/bash
# push.sh

VERSION=$1
if [ -z "$VERSION" ]; then
echo "Error: No version specified."
exit 1
fi

docker build -t account-app-backend:$VERSION .
docker tag account-app-backend:$VERSION gsemir/account-app-backend:$VERSION
docker push gsemir/account-app-backend:$VERSION

先构建镜像,再打标签,最后 push 到 dockerhub。在执行脚本时必须传递版本号 sh push.sh 0.1.0

前端

源码:https://github.com/GSemir0418/account-app-vite

镜像:https://hub.docker.com/r/gsemir/account-app-frontend

  • 流程

与后端差不多,需要两个基础镜像,一个用来构建(node:20-alpine3.20 AS build),一个用来运行(nginx:alpine

构建时同样先复制 package.json 与 package-lock.json 到镜像,安装依赖,之后再打包

启动时使用 nginx 开启服务即可

  • 跨域

使用 nignx 反向代理浏览器发出的 api 接口请求

location /api/ {
proxy_pass http://account-app-backend:8080/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

此时前端的 axios 的 baseURL 要使用本地的 origin,即 window.location.origin,不能写死

写死的话 nginx 根本不会匹配到对应的 url 并转发,只能靠后端改 CORS 响应头

  • Vite 多环境配置

根目录定义 .env.development.env.production 等多环境环境变量文件

页面中使用 import.meta.env.XXX_XXX 来访问环境变量

注意 Vite 会在构建时替换对应的环境变量的值,因此在 build 项目前要声明 NODE_ENV 环境变量

  • NODE_ENV 与依赖

这里还存在一个问题,就是如果设置了 NODE_ENV=production 环境变量,npm 在安装依赖时会忽略掉 devDependencies

但是通常情况下,构建阶段应包含所有开发依赖。所以这里我们把设置 NODE_ENV 的步骤放到安装依赖之后、打包项目之前

  • npm ci 替换 npm install

npm ci 会严格安装 lock 文件安装依赖,忽略对 package.json 中版本范围的解析等过程,且安装依赖前会删除现有的 node_modules 目录

总的来说,npm ci 更适合 CICD 的流程中,更严格、更、更干净地安装依赖

数据库

数据库使用官方的 mysql:latest 镜像,使用 docker run 命令直接启动也可以

docker run -d \
--name account-app-mysql \
--network network1 \
-e MYSQL_ROOT_PASSWORD=xxx \
-e MYSQL_DATABASE=account_app_db \
-e MYSQL_USER=xxx \
-e MYSQL_PASSWORD=xxx \
-p 3307:3306 \
-v mysql-data:/var/lib/mysql \
mysql:latest

但是后面会集成到 docker compose 中

docker-compose

源码:https://github.com/GSemir0418/account-app-deploy

Docker compose 的配置实际上就是整合多个容器服务的 docker run 命令

  • network

注意配置网络环境,保证三个服务在同一网络环境下

services:
frontend:
image: gsemir/account-app-frontend:0.1.3
networks:
- account-app-network

backend:
image: gsemir/account-app-backend:0.1.0
networks:
- account-app-network

db:
image: mysql:latest
networks:
- account-app-network

networks:
account-app-network:
driver: bridge
  • 端口

注意端口映射,ports: 70:80 表示宿主环境的(外部)的 70 端口对应容器内部环境的 80 端口,即服务启动(暴露)的端口

  • 环境变量与持久化

会自动读取根目录中的 .env 文件中定义的数据库连接所需的环境变量

services:
db:
image: mysql:latest
container_name: account-app-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- mysql-data:/var/lib/mysql

volumes:
mysql-data:

· 阅读需 2 分钟

Vite 插件开发

插件可以运行在构建期间的一些特殊节点,从而扩展构建工具的能力

插件本质上就是一个工厂函数,返回一个对象

Rollup 官方将钩子分为两类:构建钩子输出生成钩子

  • 构建钩子在构建阶段运行。它们主要涉及在 Rollup 处理输入文件之前定位、提供和转换输入文件。
  • 输出生成钩子可以提供有关生成的产物的信息并在构建完成后修改构建。

文档对于这些 hook 的声明与运行时机有详细说明,这里不再赘述

下面我们实现一个根据路由生成 html 文件结构的简单插件

// plugins/generateHTMLFromRoutes.ts
import fs from 'node:fs'
import path from 'node:path'

// 暂时手动维护一份路由表
const routes = [
{
path: 'basic-data',
children: [
{ path: 'material' },
{ path: 'resource' },
{ path: 'switch-rule' },
],
},
{ path: 'overview-today' },
{ path: 'overview-month' },
]
interface Options {
outDir?: string
}
/*
* options: 目前仅支持 outDir 配置
* */
export default function generateHtmlWithRoutes(options: Options = {}) {
const outputDir = options.outDir || 'dist'
return {
name: 'generateHtmlWithRoutes',
apply: 'build' as const,
writeBundle() {
const sourceFile = path.join(outputDir, 'index.html')
const makeDir = (url: string) => fs.mkdirSync(url, { recursive: true })
const cpHtml = (target: string) => fs.copyFile(
sourceFile,
target,
(err) => {
if (err)
throw err
})
// 按路由表生成文件夹并复制 html
for (const m of routes) {
if (m.children) {
for (const m2 of m.children) {
makeDir(path.join(outputDir, m.path, m2.path))
cpHtml(path.join(outputDir, m.path, m2.path, 'index.html'))
}
}
else {
makeDir(path.join(outputDir, m.path))
cpHtml(path.join(outputDir, m.path, 'index.html'))
}
}
},
}
}

使用

// vite.config.ts
export default defineConfig({
plugins: [generateHtmlWithRoutes()],
// ...
})