【记一忘三二】Vite笔记

90 阅读16分钟

什么是构建工具

开发者不用关心代码的编译和打包过程,只需要按照官方文档书写配置就可以畅通写代码

也可以把构建工具想成汽车工厂,通过各种其他工具把零散的零件拼接成一辆汽车

主要任务

  • 支持模块化(各种模块化之间转换)

  • 处理兼容性问题(tsless等文件的编译,babel处理es6语法降级)

  • 提高代码性能(代码压缩、多模块分割)

  • 优化开发体验(热更新、跨域)

vite相较于webapck的优势

使用webpack构建项目时,如果项目很大,项目启动和热更新花费的时间很长

image-20240727225711931

vite为什么不能全面代替webpack

webpack可以处理多种模块化,也就是CommonJsEs模块化webpack项目中可以同时使用,因为在webpack打包的过程中会处理,但是vite项目只支持Es模块化,所以vite项目并不能运行在node环境中

vite脚手架和vite的区别

vite脚手架可以根据开发者的需求快速搭建vite项目

vue-cli也就是webpack的脚手架

依赖预构建

为什么浏览器不能直接搜寻node_modules中的模块?

import _ from 'lodash-es';

image-20240728000103746

这是因为lodash-es依赖包可能内部还加载更多的模块,浏览器为了避免产生过多请求,所以禁止这样引入

commonJS之所以支持搜寻node_modules,这是因为commonJS允许在node厚度按环境中,为什么浏览器不能直接搜寻node_modules中的模块?

import _ from 'lodash-es';

image-20240728000103746

发现浏览器只支持以/./../开头的路径

那改用绝对目录就可以吗?

import _ from "/node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/lodash.js";

console.log(_);

发现确是加载了,但是暴露一个问题

image-20240728002304891

这里可以看到,依赖于lodash-es的文件有很多,全部加载这些文件,会造成网络负载

这也是浏览器不支持搜寻node_modules的原因

使用vite解决浏览器搜寻node_modules中的模块的问题

在使用vite构建项目时,会自动对替换项目中浏览器不能识别的路径,方便了开发者

比如

import _ from 'lodash-es';

替换为

import _ from "/node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/lodash.js";

并且在vite中提供多包传输的解决方案,就是依赖预构建

依赖预构建做了什么?

只会处理项目中所需的依赖包

  • 统一模块化,全部转为es模块化的方式
  • 对模块进行打包,多个文件合并成单个文件
  • 预构建之后的包放到/node_modules/.vite/deps

image-20240728003311335

依赖预构建带来的好处

  • 不同的第三包导出格式可以实现统一,转为浏览器能够处理的模块化方式,也就是es模块化
  • 模块中多个文件打包成的单个,浏览器加载时,只需要加载一个文件
  • 所有预构建完毕的包都放在/node_modules/.vite/deps中,方便vite路径替换
import _ from "/node_modules/.vite/deps/lodash-es.js?v=5e5bfe74";

image-20240728003358335

配置文件语法提示

给配置文件添加语法提示

import { defineConfig } from "vite";
export default defineConfig({
  
})


/** @type {import('vite').UserConfig} */
export default defineConfig({

})

defineConfig方法原理时采用jsDoc注释

vite.config.js可以使用 esModule语法,vite在读取ite.config.js会自动转为commonjs语法

开发环境和生成模式

export default defineConfig(({ command }) => {
 
});

配置文件可以传递函数

command的值有两个值,"build" | "serve"

"serve"对应命令是vite

"build"对应命令是vite build

环境变量

vite会根据不同的mode值加载不同的环境变量文件

比如执行vite build,那么默认的mode 的值是development,也就会加载.env.development文件

因为vite在准备配置文件时,并没有预先挂载环境变量,所以这里只能提供loadEnv函数提前查看环境变量

import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), "");

  return {};
});

loadEnv函数三个参数的含义

  • modevite 构建的模式
  • cwd:当前工作目录。这个参数是必需的,用于指定 .env 文件所在的目录
  • prefix:环境变量名称的可选前缀。这个参数用于指定从 .env 文件加载的所有环境变量的前缀。例如,如果将 prefix 设置为 "VITE_",则所有加载的环境变量都会以 "VITE_" 为前缀

注意:在vite.config.js中可以查看到所有的环境变量,但是在浏览器端只能查看以VITE开头的环境变量

vite工作原理

  1. 根据vite.config.js开启node的后端服务
  2. 设置当前项目为静态资源目录,方便后续浏览器根据路径访问文件
  3. 设置index.html为首页
  4. 加载index.js入口文件,根据文件中的import的路径浏览器向后端发起请求加载文件数据
  5. ,根据不同的文件类型,后端采用不同的解析方式,比如js类型的文件就会处理文件的内部路径和语法兼容,图片类型的请求,就会返回图片的具体可以请求到路径
const express = require("express");
const { post } = require("./vite.config.js");
const app = express();

app.use(express.static("../express"));

app.listen(post, () => {
  console.log("Server started on port " + post);
});

最重要的是app.use(express.static("../express"));,设置当前项目为静态资源目录,浏览器可以访问当前项目的任何文件

注意:import只能加载js文件,所以在vite中,所以请求都是请求的js文件

为什么浏览器能够识别.vue后缀的文件

vite会把vue文件的内容替换成js内容,然后修改content-typeapplication/javascript,浏览器就会按照js解析vue文件了

const Express = require("express");
const path = require("path");
const fs = require("fs");
const app = Express();

app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "index.html"));
});

app.get("/src/index.js", (req, res) => {
  res.sendFile(path.join(__dirname, "src/index.js"));
});

app.get("/src/index.vue", (req, res) => {
  res.setHeader("Content-Type", "application/javascript; charset=utf-8");
  res.sendFile(path.join(__dirname, "src/index.vue"));
});

app.listen(3000, () => {
  console.log("Server started on port 3000");
});

浏览器如何解析css文件

解析非模块化css

  1. vite 服务器根据路由地址访问到css文件
  2. 使用fs读取模块读取css文件的内容
  3. 修改已经读取的css文件内容,将内容改成js代码
    1. 内容动态创建一个style标签,把css样式插入到style标签中
    2. style标签插入到body标签中
  4. 修改响应的content-type属性为application/javascript,并返回到浏览器端
  5. 浏览器执行js代码

解析模块化css

css文件名称设置为文件名称.module.css就可以开启css模块化

  1. vite 服务器根据路由地址访问到css文件

  2. 使用fs读取模块读取css文件的内容

  3. 修改已经读取的css文件内容,将内容改成js代码

    1. 替换css选择器的类名,并且记录预设关系

    2. 内容动态创建一个style标签,把css样式插入到style标签中

    3. style标签插入到body标签中

  4. 修改响应的content-type属性为application/javascript,并返回到浏览器端

  5. 浏览器执行js代码,返回选择器名称映射关系,动态创建style标签和插入body标签

vite.config.js关于css模块化的配置

import { defineConfig } from "vite";

export default defineConfig({
  css: {
     // modules的配置最终会交给postcss中
    modules: {
      // 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
      localConvention: "dashes",
      // 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖)
      scopeBehaviour: "local",
      // 生成的类名的规则(可以配置为函数, 也可以配置成字符串规则: https://github.com/webpack/loader-utils#interpolatename)
      generateScopedName: "[name]_[local]_[hash:base64:5]",
      //  生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
      hashPrefix: "my-custom-hash",
      // 代表你不想参与到css模块化的路径
      globalModulePaths: [],
    },
  },
});

预处理器的配置

需要开发者安装lessscss的编译器

import { defineConfig } from "vite";

export default defineConfig({
  css: {
    preprocessorOptions: {
      // less 预处理器的配置参数,会传递给lessc编译器
      less: {
      },
      scss:{  
      }
    },
  },
});

postcsss

用于处理css编码,让加载再浏览器的css代码能够最优运行

vite内置了postcss

作用

  • 语法降级
  • 前缀补全
  • 屏幕尺寸兼容

配置

import { defineConfig } from "vite";

export default defineConfig({
  css: {
    // postcss配置
   postcss: {}
  },
});

不仅可以写在vite配置文件中,还可以单独新建postcss.config.js配置文件

css变量的语法降级无法全局识别

因为vite的编译器是按需加载的,所以就编译会记住以前文件书写css全局变量,这里需要告诉postcss,让他在编译其他css文件时先加载已存在的css全局变量

import { defineConfig } from "vite";
import postcssPresetEnv from "postcss-preset-env";
import path from "path";

export default defineConfig({
  css: {
    // postcss配置
    postcss: {
      plugins: [
        postcssPresetEnv({
          importFrom: path.resolve(__dirname, "./src/var.css"),
        }),
      ],
    },
  },
});

path

process.cwd()

process.cwd()是项目的工作目录,一般就是命令行的执行目录

相对路径

node端读取文件或者操作文件时,如果发现你使用的相对地址,那就会与process.cwd()绝对路径进行拼接,而不是以当前使用文件的绝对路径进行拼接,也就是并不会和index.js文件的绝对路径和./index.css进行拼接 image-20240729212954024

比如上面这样的项目项目结构,如果cmd目录在/node项目执行index.js,将会报index.css找不到

node ./src/index.js

image-20240729213426916

这是因为相对路径index.cssprocess.cwd()产生拼接结果是node/index.css,显然路径是错误的

如果需要正确执行,需要把命令行执行目录改到src目录中,就可以正确读取文件数据了

cd ./src

image-20240729213828882

__dirname__filename

注意:无论项目的工作目录如何改变,都不会影响__dirname__filename

__dirname

当前模块所在的目录的绝对路径

console.log(__dirname);		// C:\node\src

__filename

当前模块所在的绝对路径

console.log(__filename);	// C:\node\src\index.js

path.resolvepath.join

path.join

将多个路径拼接成一个路径,它会自动处理路径中的斜杠和路径分隔符

path.resolve('/path', 'file.txt');  // \path\file.txt
path.join('file.txt');				// file.txt

path.resolve

将多个路径拼接成一个绝对路径,它会自动处理路径中的斜杠和路径分隔符

注意:如果参数中存在一个或多个绝对路径,就会取最后一个绝对路径当做基础路径拼接后续路径,如果没有参数中没有绝对路径就会根据process.cwd()作为根路径进行拼接

path.resolve('/path', 'file.txt'); // C:\path\file.txt
path.resolve('file.txt'); 		   //  C:\node\index.js

拼接可访问路径

fs.readFileSync(path.resolve(__dirname, "./index.css"));

无论工作目录如何改变,都不会影响到结果

vite加载静态资源文件

图片、SVG

图片地址

import imgPath from "./assets/img.jpg";
// 和 import  imgPath from "./assets/img.jpg?url"; 一致

console.error(imgPath); // /src/assets/img.jpg

raw后缀

图片

import imgPath from "./assets/img.jpg?raw";

console.error(imgPath); // 二进制数据

svg

import svgImage from "./assets/a.svg?raw";

document.body.innerHTML = svgImage;  // svg标签

document.body.querySelector("svg").onmouseenter = () => {
    document.body.querySelector("svg").style.fill = "red";
}

json文件

//index.json
{
    "name": "vite_base",
    "version": "1.0.0"
}

// index.js
import { name } from "./index.json";

console.log(name);

注意:在读取json文件时最好只读取所需的,这样在打包阶段才会树摇

只有采用esModule规范才会进行树摇

resolve.alias原理

node服务器在接受到请求时,读取文件,然后根据alias配置替换文件内容中的地址

// index.js
const express = require("express");
const path = require("path");
const fs = require("fs");
const viteConfig = require("./vite.config.js");

const app = express();

app.get("/", (req, res) => {
  res.sendFile(path.resolve(__dirname, "index.html"));
});

app.get("*.(js|vue)", (req, res) => {
  const filePath = path.resolve(__dirname, path.join("./", req.path));

  let data = fs.readFileSync(filePath).toString();
  let alias = viteConfig.resolve.alias;

  Object.keys(alias).forEach((key) => {
    data = data.replace(key, alias[key].replace(process.cwd(), "").replace("\\", "/"));
  });

  res.setHeader("Content-Type", "text/javascript");
  res.send(data);
});

app.listen(3000, () => {
  console.log("Server started on port 3000");
});



// vite.config.js
const path = require("path");

module.exports = {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
  },
};

// index.js
import "@/index.vue";

console.log("hello world");

build打包配置

import { defineConfig } from "vite";

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 静态资源的打包路径规则
        assetFileNames: `[name].[hash].[ext]`,
        // chunk打包的路径规则
        chunkFileNames: `js/[name].[hash].js`,
        // 入口打包的路径规则
        entryFileNames: `[name].[hash].js`,
      },
    },
    // 静态资源文件最小化打包体积,小于此阈值的资源会被转为base64格式嵌入到js中
    assetsInlineLimit: 1 * 1024,
    // 打包输出目录
    outDir: "dist",
    // 静态资源文件输出目录(注意:如果配置了assetFileNames选项,那么这个选项会失效)
    assetsDir: "static",
    // 清除打包目录
    emptyOutDir: true,
  },
});

vite插件

vite执行的不同生命周期中调用不同的插件(执行代码)以达到不同的效果

生命周期钩子

esbuild的生命周期

config

vite配置生效之前执行,可以用来修改默认配置。返回一个配置对象,会和原本的配置对象进行合并,生成最终配置

configResolved

vite配置全完解析完毕时触发,可以用做留存配置和读取配置使用

configureServer

类似于espress服务,可以在这里设置浏览器请求vite服务器的中间件

configurePreviewServer

configureServer功能一样,只是它是服务于pnpm run preview指令的

transformIndexHtml

转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文,然后返回转换后的html结构,在每次请求html文件时都会在node端触发

vite-aliases

检测你当前目录下包括 src 在内的所有文件夹, 并帮助我们去生成别名

使用

pnpm install vite-aliases@0.9.2 -D
import { ViteAliases } from "vite-aliases";

export default defineConfig({
    plugins: [ViteAliases()],
})
import imgPath from "@assets/img.jpg";

手写

import fs from "fs";
import path from "path";

const findDirList = (dir) => {
  let allDir = fs.readdirSync(path.join(process.cwd(), dir));

  let dirList = allDir.filter((item) => {
    return fs.statSync(path.join(process.cwd(), dir, item)).isDirectory();
  });

  return dirList;
};

export default function ({ dir = "src", prefix = "@" } = {}) {
  return {
    config() {
      const dirList = findDirList(dir);

      let aliases = dirList.reduce((acc, item) => {
        acc[`${prefix}${item}`] = path.join(process.cwd(), dir, item);
        return acc;
      }, {});

      return {
        resolve: {
          alias: aliases,
        },
      };
    },
  };
}

vite-plugin-html

用于处理HTML 文件,自动自动注入资源、模板渲染、自定义 HTML 头部和尾部、自定义 HTML 文件路径

使用

pnpm install vite-plugin-html -D
import { createHtmlPlugin } from "vite-plugin-html";

export default defineConfig({
  plugins: [
    createHtmlPlugin({
      inject: {
        data: {
          title: "Vite学习",
        },
      },
    }),
  ],
});

手写

export const createHtmlPlugin = (config) => {
  return {
    transformIndexHtml: {
      order: "pre",
      handler(html) {
        return html.replace(
          /<title>(.*?)<\/title>/,
          `<title>${config.inject.data.title}</title>`
        );
      },
    },
  };
};

vite-plugin-mock

vite提供mockJs假数据

使用

pnpm install vite-plugin-mock -D
import { defineConfig } from "vite";
import { viteMockServe } from "vite-plugin-mock";

export default defineConfig({
  plugins: [
    viteMockServe(),
  ],
});

export default [
    {
        url: '/user/list',
        method: 'get',
        response: () => {
            return {
                code: 0,
                msg: 'success',
                data: {
                    "data|10": [
                        {
                            "id|+1": 1,
                            name: "@cname",
                            "age|18-30": 18,
                            address: "@county(true)",
                        },
                    ]
                }
            }
        }
    }
]

手写

import fs from "fs";
import path from "path";
import { bundleRequire } from "bundle-require";

const mockResponseList = [];

export const viteMockServe = ({ mockPath = "mock" } = {}) => {
  const mockDir = path.join(process.cwd(), mockPath);
  const isDirectory = fs.statSync(mockDir).isDirectory();
  if (isDirectory) {
    const mockDirList = fs.readdirSync(mockDir);
    const resolveModulePromiseList = [];
    for (let i = 0; i < mockDirList.length; i++) {
      const item = mockDirList[i];
      const itemPath = path.join(mockDir, item).replaceAll("\\", "/");
      const isFile = fs.statSync(itemPath).isFile();
      if (isFile) {
        resolveModulePromiseList.push(
          bundleRequire({
            filepath: itemPath,
          }).then((res) => {
            return res.mod.default[0];
          })
        );
      }
    }
    Promise.all(resolveModulePromiseList).then((res) => {
      mockResponseList.push(...res);
    });
  }
  return {
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        const url = req.url;
        const mockData = mockResponseList.find((item) => {
          return url.includes(item.url);
        });
        if (mockData) {
          res.setHeader("Content-Type", "application/json");
          res.end(JSON.stringify(mockData.response()));
        } else {
          next();
        }
      });
    },
  };
};

vite使用ts

vite天生内置ts,也就是自带tsc编译器,但是只能编译,不能类型检查,所以往往需要其他工具进行报错提示

{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.4.31"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.5",
    "typescript": "^5.2.2",
    "vite": "^5.3.4",
    "vue-tsc": "^2.0.24"
  }
}

如果你想开启类型检测,可以加上tsc --noEmit指令

  • 对于生产构建,您可以tsc --noEmit除了 Vite 的构建命令之外还运行。
  • 在开发过程中,如果您需要的不仅仅是 IDE 提示,我们建议tsc --noEmit --watch在单独的进程中运行,或者如果您希望直接在浏览器中报告类型错误,请使用vite-plugin-checker 。

性能优化

开发性能优化

在使用vite开发过程中,因为vite是按需加载的、按需编译,因此一般无需性能优化

生产性能优化

分包策略

第三方依赖分包

浏览器对于同一个名称的静态资源文件会启用缓存,这样在项目每次进行上线时,都会在打包文件后面加上hash值,防止升级前后文件名称相同,浏览器不加载新的升级文件

注意:hash根据文件内容改变而改变

image-20240803125710625

但是在使用依赖包时,如果也打包到index.js入口文件中,那么在每次仅有业务代码修改时,依赖包的代码的也要重新打包入index.js中,这样会造成用户端浏览器需要更新并未发生改变的依赖包的代码,首页加载缓慢

但是我们知道依赖包一般都无需更新,这里可以把第依赖包单独打在一个文件,并且因为依赖包的代码并不会改变,所打包之后的文件名称也不会发生改变,在项目上线时,用户并不需要更新加载依赖模块的代码

import { defineConfig } from "vite"

export default defineConfig({
    build: {
        rollupOptions: {
            output: {
                manualChunks(id) {
                    // id是文件的加载
                    if (id.includes("node_modules")) {
                        return "vendor"
                    }
                }
            }
        }
    },
})
动态加载

在使用import加载其他模块时,使用import a from './a.js';静态加载,必须要资源加载完毕才能执行代码,但是入口index.js文件往往会把所有的业务代码全部打包至其中,这样在加载inde.js时会非常的缓慢,也就是首屏加载速度很慢

import a from './a.js';

console.log(a);

image-20240803132326558

a.js被打包进入了index.js文件中

vue-router使用过程中,经常会提及一个名词动态路由,也就是在使用路由模块时,才去加载该部分的代码,这样可以提高首屏加载的速度,其实也就是把index.js分成很多块,对于展示不需要加载代码放到以后加载

import("./a.js").then((a) => {
    console.log(a.default);
})

image-20240803132300068

多入口分块

当业务出现多个入口时,不同入口的代码都需要单独打包,这样的好处互不干扰,并且打包体积更少

import { defineConfig } from "vite"

export default defineConfig({
    build: {
        rollupOptions: {
           input: {
               index: "./index.html",
               index2: "./index2.html"
           }
        }
    },
})

image-20240803133015604

gzp压缩

在生产项目构建时,大文件进行GZP压缩生成压缩文件,在项目部署到线上时,当浏览器请求资源文件,如果有GZP压缩文件,就给浏览器返回GZP压缩文件,然后浏览器再解压

import { defineConfig } from "vite"
import { compression } from 'vite-plugin-compression2'

export default defineConfig({
    plugins: [compression()]
})

image-20240803144405590

  • 浏览器检测请求的文件响应如果设置了content-encoding:gzip,那么浏览器就会先解压,再执行

  • nginx也可以进行GZP压缩,但是是动态解压,也就是读取到服务器文件再压缩,这样过程会比较缓慢,但是开发项目构建时就提前进行静态压缩,会减少请求时间

  • 因为浏览器解压需要时间,所以应该只对大文件进行GZP压缩

CDN加速

  • ·减少项目体积
  • 加快加载依赖模块的速度,
    • 不同地区加载最近的服务器
    • 浏览器对于同一个域名同时加载的资源数量有限制
import { defineConfig } from "vite"
import vitePluginCdnImport from "vite-plugin-cdn-import"

export default defineConfig({
    plugins: [
        vitePluginCdnImport({
            modules: [
                {
                    name: "lodash",
                    var: "_",
                    path: "https://unpkg.com/lodash@4.17.21/lodash.js"
                },
                {
                    name: "vue",
                    var: "Vue",
                    path: "https://unpkg.com/vue@3.2.45/dist/vue.global.js"
                }
            ]
        })
    ]
})

vite-plugin-cdn-import 插件主要做了两件事情

  1. 在构建生产项目index.html时,在里面加入cdn加载

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <!-- cnd加载地址 -->
      <script src="https://unpkg.com/lodash@4.17.21/lodash.js" crossorigin="anonymous"></script>
      <script src="https://unpkg.com/vue@3.2.45/dist/vue.global.js" crossorigin="anonymous"></script>
    
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
      <script type="module" crossorigin src="/assets/index-XSBODc05.js"></script>
    </head>
    <body>
    
    </body>
    </html>
    
  2. 替换代码中的lodashvue包的引入方式,改成全局变量的方式

    image-20240803161750336

    image-20240803161738959