什么是构建工具
开发者不用关心代码的编译和打包过程,只需要按照官方文档书写配置就可以畅通写代码
也可以把构建工具想成汽车工厂,通过各种其他工具把零散的零件拼接成一辆汽车
主要任务
-
支持模块化(各种模块化之间转换)
-
处理兼容性问题(
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
包的引入方式,改成全局变量的方式