前言

vite 的笔记


Vite介绍

构建工具

  1. 市面上主流的构建工具有哪些:

    • webpack
    • vite
    • parcel
    • esbuild
    • rollup
    • grunt
    • gulp
  2. 什么是构建工具

    浏览器他只认识html、css、js文件,当项目出现其他文件需要手动编译,而构建工具会自动编译。

vite相较于webpack的优势

  1. 构建速度快

    webpack支持多种模块化, 开始必须要统一模块化代码, 意味着需要将所有的依赖全部读一遍

    vite的上手难度更低, webpack的配置是非常多的, loader, plugin。

    vite是基于es modules的,webpack更多的关注兼容性, 而vite关注浏览器端的开发体验。

webpack 不能改的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// webpack支持多种模块,这一段代码最终会到浏览器里去运行
const lodash = require("lodash"); // commonjs 规范
import Vue from "vue"; // es6 module
// webpack支持多种模块化,所有的依赖全部读一遍编译,如:上面的变成下面的
(function(modules) {
function webpack_require(
) {}
// 入口是index.js
// 通过webpack的配置文件得来的: webpack.config.js ./src/index.js
modules[entry](webpack_require);
}, ({
"./src/index.js": (webpack_require) => {
const lodash = webpack_require("lodash");
const Vue = webpack_require("vue");
}
}))

vite 简介

  1. 模块引入

    1
    2
    3
    4
    import _ from "lodash";  // 直接引入文件,而不使用相对路径或绝对路径
    // 浏览器不支持这种引入(如果实现会导致,包里import其他的,会无穷无尽)
    // 构建工具支持(不加后缀):找寻依赖的过程是自当前目录依次向上查找的过程,
    // 直到搜寻到根目录或者搜寻到对应依赖为止
  2. 依赖预构建

    首先vite会找到对应的依赖, 然后调用esbuild(对js语法进行处理的一个库), 将其他规范的代码转换成esmodule规范, 然后放到当前目录下的node_modules/.vite/deps, 同时对esmodule规范的各个模块进行统一集成

    解决的3个问题:

    1. 不同的第三方包会有不同的导出格式(这个是vite没法约束人家的事情)
    2. 对路径的处理上可以直接使用.vite/deps, 方便路径重写
    3. 叫做网络多包传输的性能问题(也是原生esmodule规范不敢支持node_modules的原因之一), 有了依赖预构建以后无论他有多少的额外export 和import, vite都会尽可能的将他们进行集成最后只生成一个或者几个模块
  3. vite脚手架和vite区别

    yarn create vite如vue cli

    vite对应 webpack


vite 项目体验

1
2
3
4
5
6
7
yarn init -y // 初始化一个项目,生成package.json文件
// 不加 y 就需要自己手动配置
yarn add vite -D
// package.json 创建script,如:
"scripts": {
"dev": "vite"
},

vite 环境变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import {defineConfig, loadEnv} from "vite";
// dotenv会自动读取.env文件, 并解析这个文件中的对应环境变量 并将其注入到process对象下
// 但是vite考虑到和其他配置的一些冲突问题, 他不会直接注入到process对象下,使用loadEnv解决
import viteBaseConfig from "./vite.base.config";
import viteDevConfig from "./vite.dev.config";
import viteProdConfig from "./vite.prod.config";

// 策略模式
const envResolver = {
"build": () => {
console.log("生产环境");
return ({ ...viteBaseConfig, ...viteProdConfig })
},
"serve": () => {
console.log("开发环境");
return ({ ...viteBaseConfig, ...viteProdConfig }) // 新配置里是可能会被配置envDir .envA
}
}

export default defineConfig(({ command, mode }) => {
// 是build 还是serve主要取决于我们敲的命令是开启开发环境还是生产环境

// 第二个参数不是必须要使用process.cwd(),当前node 工作目录
// 第三个参数,文件名如:.env (.env是默认值)
const env = loadEnv(mode, process.cwd(), ".env");
// env 就可以访问到环境变量的值了,服务端就可以解决访问环境变量的值
return envResolver[command]();
})

// 其他配置 vite.prod.config 与 vite.dev.config
import { defineConfig } from "vite";
export default defineConfig({})

// 环境变量命令
// 使用mode指定环境
yarn dev --mode development(vite --mode test)
// .env 公共环境 .env.development 开发环境 .env.production 生产环境
// 先读取.env 其后就是对应的环境文件,相同的后覆盖前面的
// 使用 import.meta.env
// 必须使用VITE_开头,使用envPrefix配置,更改这个前缀

vite原理

1
2
3
4
5
6
7
8
9
10
11
mkdir vite-dev-server // 创建 vite 项目文件夹
// vite是怎么让浏览器可以识别.vue文件的,
// 将vue文件转化为js文件,返回给浏览器识别

// vite 对 css 的处理 (如引入一个 index.css 文件)
// 1. vite在读取到main.js中引用到了index.css
// 2. 直接去使用fs模块去读取index.css中文件内容
// 3. 直接创建一个style标签, 将index.css中文件内容直接copy进style标签里
// 4. 将style标签插入到index.html的head中
// 5. 将该css文件中的内容直接替换为js脚本(方便热更新或者css模块化), 同时设置Content-Type为js 从而让浏览器以JS脚本的形式来执行该css后缀的文件
// 注意:不是直接请求那个css文件

css module

1
2
3
4
5
6
1. module.css (module是一种约定, 表示需要开启css模块化)
2. 他会将你的所有类名进行一定规则的替换(将footer 替换成 _footer_i22st_1)
3. 同时创建一个映射对象{ footer: "_footer_i22st_1" }
4. 将替换过后的内容塞进style标签里然后放入到head标签中 (能够读到index.html的文件内容)
5. 将componentA.module.css内容进行全部抹除, 替换成JS脚本
5. 将创建的映射对象在脚本中进行默认导出

css 其他配置

1
2
3
4
5
// vite配置 preprocessorOptions(配置预处理器)

// sourceMap:文件之间的索引,开启后样式会直接找到源文件,
// 未开启将不会产生正确的错误位置信息 如果设置了sourceMap, 他就会有一个索引文件map
// sourceMap解决的问题极其的微小, 但是他的实现过程非常的复杂

postcss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用postcss
yarn add postcss-cli postcss -D // 安装依赖
// postcss-cli让脚手架我们可以输入命令,安装postcss
postcss.config.js // 书写描述文件

// css代码 --> postcss ---> less --> 再次对未来的高级css语法进行降级 --> 前缀补全 --> 浏览器客户端

// 使用 postcss 预设,包含一系列的插件(如:语法降级、编译模板等)
// 目前来说 less和sass等一系列预处理器的postcss插件已经停止维护了less插件
// 用less和sass编译完了, 然后你把编译结果给postcss,新的说法: postcss是后处理器

// 直接单独js文件 postcss.config.js
const postcssPresetEnv = require("postcss-preset-env");
const path = require("path"); // 做路径处理的

module.exports = {
plugins: [
postcssPresetEnv({
importFrom: path.resolve(__dirname, "./variable.css"),
// 就好比你现在让postcss去知道 有一些全局变量他需要记下来
})
]
}

vite加载静态资源

1
2
3
4
5
6
7
8
9
10
11
12
13
import sylasPicUrl from "sylas.png"; // 图片引入
import sylasPicUrl from "/sylas.png?raw"; // 图片引入(Buffer),默认是url

// json 文件引入
{
"name": "alice",
"age": "18"
}

import json from "a.josn"; // json引入,默认引入所以,并且打印是自动解析了json
import {name} from "a.josn"; // json可以直接按需引入(摇树优化)

// 注意:导入 json,需要添加 ./ 路径否则报错;webpack 也支持按需引入

直接使用svg

1
2
3
4
5
6
7
8
// vite 可以直接使用svg
import svgRaw from "./assets/svgs/fullScreen.svg?raw";
document.body.innerHTML = svgRaw;

const svgElement = document.getElementsByTagName("svg")[0];
svgElement.onmouseenter = function() {
this.style.fill = "red"; // 修改颜色
}

vite 插件

1
2
3
4
5
6
7
8
// 插件的定义
// vite会在生命周期的不同阶段中去调用不同的插件以达到不同的目的

// 插件的使用
import { ViteAliases } from "vite-aliases"; // vite别名生成
plugins: [
ViteAliases() // 一般是个函数调用,其中可以有配置对象
],

插件原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 如:手写Vite-aliases其实就是抢在vite执行配置文件之前去改写配置文件
// 通过vite.config.js 返回出去的配置对象以及我们在插件的config生命周期中返回的对象都不是最终的一个配置对象
// vite会把这几个配置对象进行一个merge合并 {...defaultConfig, ...specifyConfig}

// vite 的生命周期 https://cn.vitejs.dev/guide/api-plugin.html#vite-specific-hooks

// --------------
// viteAliases.js
// vite的插件必须返回给vite一个配置对象
const fs = require("fs");
const path = require("path");
function diffDirAndFile(dirFilesArr = [], basePath = "") {
const result = {
dirs: [],
files: []
}
dirFilesArr.forEach(name => {
// 我直接用异步的方式去写的
const currentFileStat = fs.statSync(path.resolve(__dirname, basePath + "/" + name));
console.log("current file stat", name, currentFileStat.isDirectory());
const isDirectory = currentFileStat.isDirectory();

if (isDirectory) {
result.dirs.push(name);
} else {
result.files.push(name);
}

})

return result;
}

function getTotalSrcDir(keyName) {
const result = fs.readdirSync(path.resolve(__dirname, "../src"));
const diffResult = diffDirAndFile(result, "../src");
console.log("diffResult", diffResult);
const resolveAliasesObj = {}; // 放的就是一个一个的别名配置 @assets: xxx
diffResult.dirs.forEach(dirName => {
const key = `${keyName}${dirName}`;
const absPath = path.resolve(__dirname, "../src" + "/" + dirName);
resolveAliasesObj[key] = absPath;
})

return resolveAliasesObj;
}

module.exports = ({
keyName = "@"
} = {}) => {
return {
config(config, env) {
// 只是传给你 有没有执行配置文件: 没有
console.log("config", config, env);
// config: 目前的一个配置对象
// production development serve build yarn dev yarn build
// env: mode: string, command: string
// config函数可以返回一个对象, 这个对象是部分的viteconfig配置【其实就是你想改的那一部分】
const resolveAliasesObj = getTotalSrcDir(keyName);
console.log("resolve", resolveAliasesObj);
return {
// 在这我们要返回一个resolve出去, 将src目录下的所有文件夹进行别名控制
// 读目录
resolve: {
alias: resolveAliasesObj
}
};
}
}
}
// 使用插件 vite.config.js
import MyViteAliases from "./plugins/ViteAliases";
plugins: [
MyViteAliases(),
]

vite-plugin-html 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 作用:改变 HTML 的 title ,控制整个 html 文件中内容
webpack --> webpack-html-plugin / clean-webpack-plugin (clean: true)
// vite内置了非常多的插件,开发者不需要承担这么高的心智负担
// 如集成了 css-loader less-loader ts-loader 等

// ejs在服务端会用的比较频繁 因为服务端可能经常会动态的去修改index.html的内容
<title>
<!-- ejs语法,类似于 vue 的插值语法 -->
<%= title %>
</title>
// 手写 CreateHtmlPlugin.js
module.exports = (options) => {
return {
// 转换html的
// 将我们插件的一个执行时机提前
transformIndexHtml: {
enforce: "pre",
transform: (html, ctx) => {
// ctx 表示当前整个请求的一个执行期上下文: api /index.html /user/userlist json get post headers
console.log("html", html);
return html.replace(/<%= title %>/g, options.inject.data.title);
}
}
}
}
// 使用 vite.config.js
import CreateHtmlPlugin from "./plugins/CreateHtmlPlugin";
plugins: [
createHtmlPlugin({
inject: {
data: {
title: "主页2"
}
}
})
]

vite-plugin-mock插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 在 vite 中使用 mock
yarn add vite-plugin-mock mockjs -D // 安装依赖
// 使用 vite.config.js
import { viteMockServe } from "vite-plugin-mock"; // 引用依赖
plugins:[
viteMockServe()
]

// 根目录新建 mock文件夹
// mock/index.js
const mockJS = require("mockjs"); // 用来生成假数据,并不是用来启动服务

const userList = mockJS.mock({
"data|100": [{
name: "@cname", // 表示生成不同的中文名
// ename: mockJS.Random.name(), // 生成不同的英文名
"id|+1": 1, //
time: "@time",
date: "@date"
}]
})

module.exports = [
{
method: "post",
url: "/api/users",
response: ({ body }) => {
return {
code: 200,
msg: "success",
data: userList
};
}
},
]

vite-plugin-mock插件实现原理(手写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const fs = require("fs");
const path = require("path");

export default (options) => {
// 做的最主要的事情就是拦截http请求
// D当我们使用fetch或者axios去请求的
// axios baseUrl // 请求地址
// 当打给本地的开发服务器的时候 viteserver服务器接管

return {
configureServer(server) {
// 服务器的相关配置
// req, 请求对象 --> 用户发过来的请求, 请求头请求体 url cookie
// res: 响应对象, - res.header
// next: 是否交给下一个中间件, 调用next方法会将处理结果交给下一个中间件
const mockStat = fs.statSync("mock");
const isDirectory = mockStat.isDirectory();
let mockResult = [];
if (isDirectory) {
// process.cwd() ---> 获取你当前的执行根目录
mockResult = require(path.resolve(process.cwd(), "mock/index.js"));
console.log("result", mockResult);
}

server.middlewares.use((req, res, next) => {
console.log("req", req.url);
// 看我们请求的地址在mockResult里有没有
const matchItem = mockResult.find(mockDescriptor => mockDescriptor.url === req.url);
console.log("matchItem", matchItem);

if (matchItem) {
console.log("进来了", );
const responseData = matchItem.response(req);
console.log("responseData", responseData);
// 强制设置一下他的请求头的格式为json
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(responseData)); // 设置请求头 异步的
} else {
next(); // 你不调用next 你又不响应 也会响应东西
}
}) // 插件 === middlewares
}
}
}

vite 与 TypeScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Vite 默认支持 TS
vite-plugin-checker // 安装插件,在开发时校验ts语法(直接浏览器弹框报错)
import checker from "vite-plugin-checker"
plugins:[
checker({typescript : true})
]
// 根目录创建 ts.config.json 配置文件
{
"compilerOptions":{
"skipLibCheck": false , // 是否跳过 node_modules 检查
"module": "esNext" // 需改默认转化成es3的配置
}
}
// package.json 文件中修改打包命令,TS语法错误将不能正常打包完成
script:{
"build": "tsc --noEmit && vite build"
}

// 三斜线文件 vite-env.d.ts 语法提示
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PROXY_TARGET: string;
}

性能优化optimize

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 分包策略
// 就是把一些不会常规更新的文件,进行单独打包(如:使用lodash的forEach)
// 浏览器缓存相同文件,文件名不变就不会重新请求(每次打包后端hash后缀不同)
// 使用相关方法后,生成的js文件就会有很长lodash的代码(每次文件名变化就会重新请求)

// gizp压缩
yarn add vite-plugin-compression // 安装插件
// 正常插件使用 一个函数即可

// 动态导入
import() // es法语自带的,返回一个promise
import().then() // 一般在路由使用比较多

// CND 加速
// 将第三方的库使用cdn加载
yarn add vite-plugin-cdn-import // 安装插件

// 跨域
// 后端处理:ngnix代理(与前端vite服务器一样原理)、配置身份标记(Access-Control-Allow-Origin)

Vite 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
optimizeDeps: {
exclude: [], // 将指定数组中的依赖不进行依赖预构建
},
envPrefix: "ENV_", // 配置vite注入客户端环境变量校验的env前缀
css: {
// 对css的行为进行配置
// modules配置最终会丢给postcss modules
modules: {
// 是对css模块化的默认行为进行覆盖
localsConvention: "camelCaseOnly", // 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
scopeBehaviour: "local", // 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖)
// generateScopedName: "[name]_[local]_[hash:5]" // https://github.com/webpack/loader-utils#interpolatename
// generateScopedName: (name, filename, css) => {
// name -> 代表的是你此刻css文件中的类名
// filename -> 是你当前css文件的绝对路径
// css -> 给的就是你当前样式
// console.log("name", name, "filename", filename, "css", css); // 这一行会输出在哪??? 输出在node
// 配置成函数以后, 返回值就决定了他最终显示的类型
// return `${name}_${Math.random().toString(36).substr(3, 8) }`;
// }
hashPrefix: "hello", // 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
globalModulePaths: ["./componentB.module.css"], // 代表你不想参与到css模块化的路径
},
preprocessorOptions: {
// key + config key代表预处理器的名
less: {
// 整个的配置对象都会最终给到less的执行参数(全局参数)中去
// 在webpack里就给less-loader去配置就好了
math: "always",
globalVars: {
// 全局变量
mainColor: "red",
},
},
},
devSourcemap: true, // 开启css的sourceMap(文件索引)
postcss: {}, // 配置postcss相关
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"), // 设置别名, 以后我们在其他组件中可以使用@来代替src这个目录
},
},
build: {
minify: false, // 设置后打包js会没有压缩,禁用最小化混淆
// 构建生产包时的一些配置策略
rollupOptions: {
// 配置rollup的一些构建策略
output: {
// 控制输出
// 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果
assetFileNames: "[hash].[name].[ext]",
manualChunks(id) {
if (id.includes("node_modules")) {
return id
.toString()
.split("node_modules/")[1]
.split("/")[0]
.toString();
}
},
},
},
assetsInlineLimit: 4096000, // 4000kb
outDir: "dist", // 配置输出目录
assetsDir: "static", // 配置输出目录中的静态资源目录
emptyOutDir: true, // 清除输出目录中的所有文件,默认为true
},
});