什么是构建工具
开发者不用关心代码的编译和打包过程,只需要按照官方文档书写配置就可以畅通写代码
也可以把构建工具想成汽车工厂,通过各种其他工具把零散的零件拼接成一辆汽车
主要任务
-
支持模块化(各种模块化之间转换)
-
处理兼容性问题(
ts、less等文件的编译,babel处理es6语法降级) -
提高代码性能(代码压缩、多模块分割)
-
优化开发体验(热更新、跨域)
vite相较于webapck的优势
使用webpack构建项目时,如果项目很大,项目启动和热更新花费的时间很长
vite为什么不能全面代替webpack?
webpack可以处理多种模块化,也就是CommonJs和Es模块化再webpack项目中可以同时使用,因为在webpack打包的过程中会处理,但是vite项目只支持Es模块化,所以vite项目并不能运行在node环境中
vite脚手架和vite的区别
vite脚手架可以根据开发者的需求快速搭建vite项目
vue-cli也就是webpack的脚手架
依赖预构建
为什么浏览器不能直接搜寻node_modules中的模块?
import _ from 'lodash-es';
这是因为lodash-es依赖包可能内部还加载更多的模块,浏览器为了避免产生过多请求,所以禁止这样引入
commonJS之所以支持搜寻node_modules,这是因为commonJS允许在node厚度按环境中,为什么浏览器不能直接搜寻node_modules中的模块?
import _ from 'lodash-es';
发现浏览器只支持以/、./、../开头的路径
那改用绝对目录就可以吗?
import _ from "/node_modules/.pnpm/lodash-es@4.17.21/node_modules/lodash-es/lodash.js";
console.log(_);
发现确是加载了,但是暴露一个问题
这里可以看到,依赖于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中
依赖预构建带来的好处
- 不同的第三包导出格式可以实现统一,转为浏览器能够处理的模块化方式,也就是
es模块化 - 模块中多个文件打包成的单个,浏览器加载时,只需要加载一个文件
- 所有预构建完毕的包都放在
/node_modules/.vite/deps中,方便vite路径替换
import _ from "/node_modules/.vite/deps/lodash-es.js?v=5e5bfe74";
配置文件语法提示
给配置文件添加语法提示
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函数三个参数的含义
mode:vite构建的模式cwd:当前工作目录。这个参数是必需的,用于指定.env文件所在的目录prefix:环境变量名称的可选前缀。这个参数用于指定从.env文件加载的所有环境变量的前缀。例如,如果将prefix设置为 "VITE_",则所有加载的环境变量都会以 "VITE_" 为前缀
注意:在vite.config.js中可以查看到所有的环境变量,但是在浏览器端只能查看以VITE开头的环境变量
vite工作原理
- 根据
vite.config.js开启node的后端服务 - 设置当前项目为静态资源目录,方便后续浏览器根据路径访问文件
- 设置
index.html为首页 - 加载
index.js入口文件,根据文件中的import的路径浏览器向后端发起请求加载文件数据 - ,根据不同的文件类型,后端采用不同的解析方式,比如
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-type为application/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
vite服务器根据路由地址访问到css文件- 使用
fs读取模块读取css文件的内容 - 修改已经读取的
css文件内容,将内容改成js代码- 内容动态创建一个
style标签,把css样式插入到style标签中 - 将
style标签插入到body标签中
- 内容动态创建一个
- 修改响应的
content-type属性为application/javascript,并返回到浏览器端 - 浏览器执行
js代码
解析模块化css
css文件名称设置为文件名称.module.css就可以开启css模块化
-
vite服务器根据路由地址访问到css文件 -
使用
fs读取模块读取css文件的内容 -
修改已经读取的
css文件内容,将内容改成js代码-
替换
css选择器的类名,并且记录预设关系 -
内容动态创建一个
style标签,把css样式插入到style标签中 -
将
style标签插入到body标签中
-
-
修改响应的
content-type属性为application/javascript,并返回到浏览器端 -
浏览器执行
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: [],
},
},
});
预处理器的配置
需要开发者安装less、scss的编译器
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进行拼接
比如上面这样的项目项目结构,如果cmd目录在/node项目执行index.js,将会报index.css找不到
node ./src/index.js
这是因为相对路径index.css和process.cwd()产生拼接结果是node/index.css,显然路径是错误的
如果需要正确执行,需要把命令行执行目录改到src目录中,就可以正确读取文件数据了
cd ./src
__dirname和__filename
注意:无论项目的工作目录如何改变,都不会影响__dirname和__filename
__dirname
当前模块所在的目录的绝对路径
console.log(__dirname); // C:\node\src
__filename
当前模块所在的绝对路径
console.log(__filename); // C:\node\src\index.js
path.resolve和path.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根据文件内容改变而改变
但是在使用依赖包时,如果也打包到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);
a.js被打包进入了index.js文件中
在vue-router使用过程中,经常会提及一个名词动态路由,也就是在使用路由模块时,才去加载该部分的代码,这样可以提高首屏加载的速度,其实也就是把index.js分成很多块,对于展示不需要加载代码放到以后加载
import("./a.js").then((a) => {
console.log(a.default);
})
多入口分块
当业务出现多个入口时,不同入口的代码都需要单独打包,这样的好处互不干扰,并且打包体积更少
import { defineConfig } from "vite"
export default defineConfig({
build: {
rollupOptions: {
input: {
index: "./index.html",
index2: "./index2.html"
}
}
},
})
gzp压缩
在生产项目构建时,大文件进行GZP压缩生成压缩文件,在项目部署到线上时,当浏览器请求资源文件,如果有GZP压缩文件,就给浏览器返回GZP压缩文件,然后浏览器再解压
import { defineConfig } from "vite"
import { compression } from 'vite-plugin-compression2'
export default defineConfig({
plugins: [compression()]
})
-
浏览器检测请求的文件响应如果设置了
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 插件主要做了两件事情
-
在构建生产项目
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> -
替换代码中的
lodash和vue包的引入方式,改成全局变量的方式