什么是构建工具
浏览器他只认识html, css, js
构建工具具体做了哪些事情:
- typescript: 如果遇到ts文件我们需要使用tsc将typescript代码转换为js代码
- React/Vue: 安装react-compiler / vue-complier, 将我们写的jsx文件或者.vue文件转换为render函数
- less/sass/postcss/component-style: 我们又需要安装less-loader, sass-loader等一系列编译工具
- 语法降级: babel ---> 将es的新语法转换旧版浏览器可以接受的语法
- 体积优化: uglifyjs ---> 将我们的代码进行压缩变成体积更小性能更高的文件
- 热更新: 稍微改一点点东西, 非常麻烦 将App.tsx ---> tsc ---> App.jsx ---> React-complier ---> js文件 (构建工具会帮你自动监听文件的变化, 当文件变化以后自动帮你调用对应的集成工具进行重新打包, 然后再浏览器重新运行(整个过程叫做热更新, hot replacement)
- 代码分割:按需加载、代码分割、防止重复
- 模块化开发支持: 支持直接从node_modules里引入代码 + 多种模块化支持
- 开发服务器: 跨域的问题, 用react-cli create-react-element vue-cli 解决跨域的问题,
> 注意:这些功能不是构建工具自己做的,构建工具将这些语法对应的处理工具集成进来自动化处理
构建工具他让我们可以不用每次都关心我们的代码在浏览器如何运行, 我们只需要首次给构建工具提供一个配置文件(这个配置文件也不是必须的, 如果你不给他 他会有默认的帮你去处理), 有了这个集成的配置文件以后, 我们就可以在下次需要更新的时候调用一次对应的命令就好了, 如果我们再结合热更新, 我们就更加不需要管任何东西, 这就是构建工具去做的东西, 他让我们不用关心生产的代码也不用关心代码如何在浏览器运行, 只需要关心我们的开发怎么写的爽怎么写就好了
tsc xxx babel xx less xx
webpack
...
打包:将我们写的浏览器不认识的代码 交给构建工具进行编译处理的过程就叫做打包, 打包完成以后会给我们一个浏览器可以认识的文件, 这个东西就叫做构建工具
市面上主流的构建工具有哪些:
- webpack
- vite
- parcel
- esbuild
- rollup
- grunt
- gulp
vite相较于webpack的优势
官方文档: cn.vitejs.dev/guide/why.h…
当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。我们开始遇到性能瓶颈 —— 使用 JavaScript 开发的工具通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用 HMR(热更新),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
起因: 我们的项目越大 ----> 构建工具(webpack)所要处理的js代码就越多 【跟webpack的一个构建过程(工作流程)有关系】
造成的结果: 构建工具需要很长时间才能启动开发服务器 (启动开发服务器 ---> 把项目跑起来)
yarn start
yarn dev
npm run dev
npm run start
webpack支持多种模块化: 你的工程可能不只是跑在浏览器端
// index.js
// 这一段代码最终会到浏览器里去运行
const lodash = require("lodash"); // commonjs 规范
import Vue from "vue"; // es6 module
// webpack是允许我们这么写的
webpack的编译原理, AST 抽象语法分析的工具 分析出你写的这个js文件有哪些导入和导出操作 构建工具是运行在服务端的
// webpack的一个转换结果
const lodash = webpack_require("lodash");
const Vue = webpack_require("vue");
(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相较于webpack的优势 总结:
因为webpack支持多种模块化, 他一开始必须要统一模块化代码, 所以意味着他需要将所有的依赖全部读一遍,而vite是基于es modules的 其他模块没有。所有不需要将所有的依赖全部读一遍 如下:
-
webpack 是将所有的依赖全部读一遍(为什么全部读一遍?因为,一开始必须要统一模块化代码), 并进行打包,后才会启动服务器:
-
vite 先开启服务器, 加载当前页面,并且按需加载当前依赖, 未加载页面不会加载其所需的依赖:
-
webpack更多的关注兼容性, 而vite关注浏览器端的开发体验
-
vite的上手难度更低, webpack的配置是非常多的, loader, plugin
vite开发环境
- 利用浏览器原生的
ES Module
编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server
只提供轻量服务。 - 浏览器执行ESM的
import
时,会向dev server
发起该模块的ajax
请求,服务器对源码做简单处理后返回给浏览器。 Vite
中HMR是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。- 使用
esbuild
处理项目依赖,esbuild
使用go编写,比一般node.js
编写的编译器快几个数量级。
vite生产环境
- 集成
Rollup
打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制。
导入资源 (CommonJS & ES Module)
-
Commonjs 重复加载处理原理:
-
首先加载之后的文件的
module
会被缓存到Module
上 (Module
缓存每一个模块加载的信息) -
其他文件再次使用此
module
时,会直接从Module
直接读取缓存值
-
-
Commonjs 避免循环引用:
- 对每一个模块都存在缓存,可以有效的解决循环引用问题。
(例如:a.js b.js 互相引用,main.js 调用 a.js, 会先执行a.js文件,a.js文件顶部引入b.js,会直接进入b.js, b.js文件顶部引入a.js, 此时,读取的a.js 就是缓存。执行完b.js,会返回a.js 继续执行下面未执行代码)
- 对每一个模块都存在缓存,可以有效的解决循环引用问题。
-
Commonjs 总结:
-
CommonJS 模块由 JS 运行时实现。
-
CommonJs 是单个值导出,本质上导出的就是 exports 属性。
-
CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
-
CommonJS 在模块缓存中还记录着导出的变量的拷贝值,并且此变量是放在一块新的内存中,用的时候会直接读取此内存值。
-
CommonJS 模块同步加载并执行模块文件。
-
-
Es module 重复加载处理原理:
-
首先加载之后的文件的
module
会被缓存到Module
上 (Module
缓存每一个模块加载的信息) -
其他文件再次使用此
module
时,会直接从Module
直接读取缓存值
-
-
Es module 避免循环引用:
- ES Module借助模块地图,已经进入过的模块标注为获取中,遇到import语句会去检查模块地图,已经标注为获取中的则不会进入,地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。
-
Es module 总结:
-
ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
-
ES6 Module 的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果。
-
ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
-
ES6 模块提前加载并执行模块文件,
-
ES6 Module 导入模块在严格模式下。
-
ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。
-
ES6 Module 模块地图中的每一个节点是一个模块记录,上面有导出变量的内存地址,导入时会做一个连接——即指向同一块内存。
-
vite 初始化项目
-
理解vite脚手架 & vite
-
vite 脚手架 官方文档【原理篇】
当我们敲了
yarn create vite
首先:帮我们全局安装一个东西:create-vite(vite的脚手架)
其次:直接运行这个create-vite bin目录下的一个执行配置
误区:认为官网中使用对应yarn create 构建项目的过程也是vite在做的事情
create-vite和vite的关系是什么呢? --- create-vite内置了vite
-
vite 自己搭建
yarn init -y yarn add vite -D "scripts": { "dev": "vite", // 启动开发服务器,别名:`vite dev`,`vite serve` "build": "vite build", // 为生产环境构建产物 "preview": "vite preview" // 本地预览生产构建产物 },
创建并配置 vite.config.js 创建环境变量
-
功能一: vite 依赖预构建
-
目的:
-
CommonJS 和 UMD 兼容性: 在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块,预构建这一步由 esbuild 执行。
在转换 CommonJS 依赖项时,Vite 会进行智能导入分析,这样即使模块的导出是动态分配的(例如 React),具名导入(named imports)也能正常工作:
// 符合预期 import React, { useState } from 'react'
-
性能: 为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。
有些包将它们的 ES 模块构建为许多单独的文件,彼此导入。例如,
lodash-es
有超过 600 个内置模块!当我们执行import { debounce } from 'lodash-es'
时,浏览器同时发出 600 多个 HTTP 请求!即使服务器能够轻松处理它们,但大量请求会导致浏览器端的网络拥塞,使页面加载变得明显缓慢。通过将
lodash-es
预构建成单个模块,现在我们只需要一个HTTP请求!
-
【原理篇】CommonJS 和 UMD 兼容性 原理:
-
首先vite会找到对应的依赖, 然后调用esbuild(对js语法进行处理的一个库), 将其他规范的代码转换成esmodule规范, 然后放到当前目录下的node_modules/.vite/deps, 同时对esmodule规范的各个模块进行统一集成
例子:
// a.js export default function a() {}
// b.js export { default as a } from "./a.js"
vite依赖预构建:
function a() {}
-
-
预构建 缓存:
-
文件系统缓存:
-
Vite 将预构建的依赖项缓存到
node_modules/.vite
中。它会基于以下几个来源来决定是否需要重新运行预构建步骤: -
包管理器的锁文件内容,例如
package-lock.json
,yarn.lock
,pnpm-lock.yaml
,或者bun.lockb
; -
补丁文件夹的修改时间;
-
vite.config.js
中的相关字段; -
NODE_ENV
的值。
只有在上述其中一项发生更改时,才需要重新运行预构建。
-
-
浏览器缓存:
- 已预构建的依赖请求使用 HTTP 头
max-age=31536000, immutable
进行强缓存,以提高开发期间页面重新加载的性能。
- 已预构建的依赖请求使用 HTTP 头
-
vite 配置文件处理细节
-
vite 配置文件的语法提示
-
因为 Vite 本身附带 TypeScript 类型,所以你可以通过 IDE 和 jsdoc 的配合来实现智能提示
/** @type {import('vite').UserConfig} */ export default { // ... }
- 另外你可以使用
defineConfig
工具函数,这样不用 jsdoc 注解也可以获取类型提示:
import { defineConfig } from 'vite' export default defineConfig({ // ... })
-
环境变量和模式
-
环境变量:
-
Vite 在一个特殊的
import.meta.env
对象上暴露环境变量 -
内建变量:
-
import.meta.env.MODE
: {string} 应用运行的模式。 -
import.meta.env.BASE_URL
: {string} 部署应用时的基本 URL。他由base
配置项决定。 -
import.meta.env.PROD
: {boolean} 应用是否运行在生产环境(使用NODE_ENV='production'
运行开发服务器或构建应用时使用NODE_ENV='production'
)。 -
import.meta.env.DEV
: {boolean} 应用是否运行在开发环境 (永远与import.meta.env.PROD
相反)。 -
import.meta.env.SSR
: {boolean} 应用是否运行在 server 上。
-
-
自定义变量:
-
只有以
VITE_
为前缀的变量才会暴露给经过 vite 处理的代码 -
例子:
.env.production VITE_SOME_KEY=123 DB_PASSWORD=foobar
其他文件中使用: console.log(import.meta.env.VITE_SOME_KEY) // "123" console.log(import.meta.env.DB_PASSWORD) // undefined
只有
VITE_SOME_KEY
会被暴露为import.meta.env.VITE_SOME_KEY
提供给客户端源码,而DB_PASSWORD
则不会**注意**:
环境变量解析会返回的类型都变成
string
类型,使用时候请注意环境变量类型转换
-
-
-
.env 文件:
.env # 所有情况下都会加载 .env.local # 所有情况下都会加载,但会被 git 忽略 .env.[mode] # 只在指定模式下加载 .env.[mode].local # 只在指定模式下加载,但会被 git 忽略
-
模式:
-
默认情况下,开发服务器 (
dev
命令) 运行在development
(开发) 模式,而build
命令则运行在production
(生产) 模式 -
若想在
vite build
时运行不同的模式,可使用--mode (这边要执行文件名)
- 例如:想在 staging (预发布)模式下构建应用
vite build --mode staging
-
-
NODE_ENV and Modes:
-
需要注意的是
NODE_ENV
(process.env.NODE_ENV
) 和众数是两个不同的概念。以下是不同命令如何影响NODE_ENV
和 模式命令 节点环境 模式 vite build
"production"
"production"
vite build --mode development
"production"
"development"
NODE_ENV=development vite build
"development"
"production"
NODE_ENV=development vite build --mode development
"development"
"development"
-
-
将环境变量处理在打包过程中放入到node对象中原理(vite & webpack 同理):
-
使用了第三方库:dotenv
-
deotenv 会自动读取.env文件,并解析这个文件中的对应环境变量 并将其注入到process (这个是node环境中的)对象下,这样我们就可以在配置文件中使用.env文件环境变量
-
-
获取 环境变量 两种方式:
-
import.meta.env
:是在运行时获取环境变量的值,适用于应用程序代码中需要动态获取环境变量的场合。(注意
:配置文件中获取不到,因为配置文件实在构建时被读取) -
loadenv(mode: string, envDir: string, prefixes: string | string[] = 'VITE_')
:是在构建时加载环境变量,适合用于打包时(构建时)
// 第一个参数是模式 // 第二个参数不是必须要使用process.cwd(), // 默认情况下只有前缀为 `VITE_` 会被加载,除非更改了 `prefixes` 配置 const env = loadEnv(mode, process.cwd(), ""); console.log("env:", env)
-
【原理篇】开发服务器 & vite是怎么让浏览器可以识别.vue文件的
-
开发服务器
- 第一步 创建并绑定端口
// 不能用esmodule 必须使用commonjs, 因为是 node const Koa = require("koa"); // node 端的框架 需要 yarn add koa // 创建并返回 HTTP 服务器,将给定的参数传递给 `Server#listen()` const app = new Koa(); // Koa 应用程序被绑定到 `5173` 端口 app.listen(5173, () => { console.log("vite dev serve listen on 5173"); })
- 第二步 读取文件并操作文件,返回给客户端
// 不能用esmodule 必须使用commonjs, 因为是 node const Koa = require("koa"); // node 端的框架 需要 yarn add koa const fs = require("fs"); // node 内置 无需安装 const path = require("path"); // node 内置 无需安装 const app = new Koa(); // 类似 const vue = new Vue(); // 注意:实际源码中用中间件去帮我们读文件! app.use(async (ctx) => { //ctx 是 context 上下文 //ctx.request --> 请求信息 ctx.response --> 响应信息 if (ctx.request.url === "/") { // 请求信息的路径 const indexContent = await fs.promises.readFile(path.resolve(__dirname, "./index.html")); // 注意:在服务端一般不会这么用 ctx.response.body = indexContent; // 响应主体 ctx.response.set("Content-Type", "text/html"); // 响应类型 } // 这边 是处理js if (ctx.request.url === "/main.js") { // 请求的url 就是 / const mainContent = await fs.promises.readFile( path.resolve(__dirname, "./main.js") ); // 在服务端一般不会这么用 ctx.response.body = mainContent; console.log("main.js indexContent:", mainContent.toString()); ctx.response.set("Content-Type", "text/javascript"); } // 这边 是处理vue if (ctx.request.url === "/App.vue") { const mainVueContent = await fs.promises.readFile( path.resolve(__dirname, "./App.vue") ); // 在服务端一般不会这么用 // 如果是 vue 文件,会进行: // AST 语法解析 ===> vue.createElement() ===> 构建原生dom,从而生成原生js // 这边没处理,详细请看源码 ctx.response.body = mainVueContent; console.log("main.js indexContent:", mainVueContent.toString()); ctx.response.set("Content-Type", "text/javascript"); } console.log("ctx", ctx.request, ctx.response); }) app.listen(5173, () => { console.log("vite dev serve listen on 5173"); })
【原理篇】vite中对 css & css模块化 处理
-
在vite中处理css
-
vite在读取到main.js中引用到了Index.css
-
直接去使用fs模块去读取index.css中文件内容
-
直接创建一个style标签, 将index.css中文件内容直接copy进style标签里
-
将style标签插入到index.html的head中
-
将该css文件中的内容直接替换为js脚本(方便
热更新
或者css模块化
), 同时设置Content-Type为js 从而让浏览器以JS脚本的形式来执行该css后缀的文件
-
-
CSS Modules 原理
-
当执行到文件 module.css (module是一种约定, 表示需要开启css模块化)
-
他会将你的所有类名进行一定规则的替换(例如:将footer 替换成 _footer_i22st_1)
-
同时创建一个映射对象(例如:{ footer: "_footer_i22st_1" })
-
将替换过后的内容塞进style标签里然后放入到head标签中 (能够读到index.html的文件内容)
-
将componentA.module.css内容进行全部抹除, 替换成JS脚本
-
将创建的映射对象在脚本中进行默认导出(可以在js中能够拿到最终的样式)
-
【构建优化】CSS 代码分割
-
Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个
<link>
标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行,避免发生 FOUC 。 -
如果你更倾向于将所有的 CSS 抽取到一个文件中,你可以通过设置
build.cssCodeSplit
为false
来禁用 CSS 代码分割。
【配置篇】vite 配置文件
-
vite.config.js 中 css 配置(modules篇)
- localConvention: 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
如果
css.modules.localsConvention
设置开启了 camelCase 格式变量名转换(例如localsConvention: 'camelCaseOnly'
),你还可以使用按名导入
。
// .apply-color -> applyColor import { applyColor } from './example.module.css' document.getElementById('foo').className = applyColor
-
generateScopedName: 配置的类名的规则(可以配置为函数, 也可以配置成字符串规则: github.com/webpack/loa…)
例如:
-
hashPrefix: 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
-
globalModulePaths: 代表你不想参与到css模块化的路径
-
scopeBehaviour: 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖) (
作用不大,不用考虑
)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", }, }, scss: { // 整个的配置对象都会最终给到scss的执行参数(全局参数)中去 }, }, devSourcemap: true, // 开启映射 postcss: { plugins: [ // 插件 postcssPresetEnv({ // 配置postcss }), ], }, }
- localConvention: 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
如果
-
vite.config.js 中 css 配置(preprocessorOptions篇) 也就是 CSS 预处理器
- Vite 也同时提供了对
.scss
,.sass
,.less
,.styl
和.stylus
文件的内置支持。没有必要为它们安装特定的 Vite 插件,但必须安装相应的预处理器依赖
# .scss and .sass npm add -D sass # .less npm add -D less # .styl and .stylus npm add -D stylus
- preprocessorOptions: 指定传递给 CSS 预处理器的选项
- Vite 也同时提供了对
-
vite.config.js 中 css 配置(postcss篇)
-
实际上,PostCSS 的主要功能只有两个:
-
第一个就是前面提到的把 CSS 解析成 JavaScript 可以操作的 AST,
-
第二个就是调用插件来处理 AST 并得到结果(简单说:提供了一个庞大的插件生态系统来执行不同的功能,常见的功能:css语法降级、前缀补全、嵌套、函数、变量、自动补全 等等)。
-
-
在vite中使用postcss
-
-
vite.config.js 中 resolve 配置 alias ( 别名配置,将长的路径修改 改到固定某一个文件夹下 )
resolve: { alias: { "@": path.resolve(__dirname, "src"), components: path.resolve(__dirname, "src/components"), styles: path.resolve(__dirname, "src/styles"), plugins: path.resolve(__dirname, "src/plugins"), views: path.resolve(__dirname, "src/views"), layouts: path.resolve(__dirname, "src/layouts"), utils: path.resolve(__dirname, "src/utils"), apis: path.resolve(__dirname, "src/apis"), dirs: path.resolve(__dirname, "src/directives"), }, },
-
【原理篇】 vite.config.js 中 resolve 配置 alias
- 拿到 resolve 配置 alias,通过 entires = Object.entries(alias), 遍历 entires 数组, 将文件内容中自定义 alias 进行替换。
module.exports = function(aliasConf, JSContent) { // aliasConf 配置文件 alias // JSContent 文件内容 const entires = Object.entries(aliasConf); console.log("entires", entires, JSContent); let lastContent = JSContent; entires.forEach(entire => { const [alia, path] = entire; // 会做path的相对路径的处理 // 如果我用官方的方式去找相对路径的话 const srcIndex = path.indexOf("/src"); // alias别名最终做的事情就是一个字符串替换 const realPath = path.slice(srcIndex, path.length); lastContent = JSContent.replace(alia, realPath); }) console.log("lastContent..........", lastContent); return lastContent; }
-
打包后的静态资源为什么要有hash
- 浏览器是有一个缓存机制 静态资源名字只要不改, 那么他就会直接用缓存的,
- 当文件内容未发生改变那么 hash 值也不会改变。只要文件有点改变,打包后的文件hash值也会改变,从而浏览器不会去读取缓存,而是重新请求拿到最新修改后的文档
-
vite.config.js 中 build 配置 ( 更多配置:cn.vitejs.dev/config/buil… )
build: { rollupOptions: { // 配置rollup的一些构建策略 output: { // 控制输出 // 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果 // hash: 只要你的文件内容有一个字不一样, 那么生成的hash就会完全不一样, 但是只要你的文件内容完全一样, 生成的hash就会一样 // name: 文件初始文件名 // ext: 文件扩展名 assetFileNames: "[hash].[name].[ext]", }, }, assetsInlineLimit: 4096000, // 4000kb 当静态资源小于4000kb 则会转化为base64文件 outDir: "dist", // 配置打包的输出目录 assetsDir: "static", // 配置静态资源的存放目录 emptyOutDir: true, // 清除输出目录中的所有文件 },
-
vite.config.js 中 插件 配置 (社区:github.com/vitejs/awes… )
-
强制插件排序
pre
:在 Vite 核心插件之前调用该插件- 默认:在 Vite 核心插件之后调用该插件
post
:在 Vite 构建插件之后调用该插件
-
按需应用
apply
属性指明它们仅在'build'
或'serve'
plugins: [ { ...ViteAliases(), // 别名插件 enforce: 'pre', // 强制插件排序 }, createHtmlPlugin({ // 创建html插件 inject: { data: { title: "主页" } } }) { ...typescript2(), apply: 'build', // 插件在开发 (serve) 和生产 (build) 模式中调用 }, viteMockServe() // 插件模拟服务器 ],
-
-
【创建插件】 部分插件API
-
Vite 插件扩展了设计出色的 Rollup 接口,带有一些 Vite 独有的配置项。推荐在阅读下面的章节之前,首先阅读下 Rollup 插件文档
-
创建插件 通过(通用钩子 和 Vite独有的钩子)来创建不同的插件
-
以下钩子在服务器启动时被调用:
以下钩子会在每个传入模块请求时被调用:
以下钩子在服务器关闭时被调用:
-
-
实例:
import { Plugin } from 'vite'; export function myPlugin(): Plugin { return { name: 'my-plugin', // 插件名,在开发工具中显示 // 其钩子函数中参数,也是根据不同的钩子参数参数也不同 transform(code, id) { // 这边是钩子函数 可以是通用钩子 也可以 是vite独有钩子 if (!/\.js$/.test(id)) { return; // 只处理.js文件 } // 添加自定义逻辑,例如在文件末尾添加一行代码 return `${code}\nconsole.log('This file is processed by my plugin!');`; }, }; }
-
-
【手写篇】 vite.config.js 中 ViteAliases 插件(别名的插件) 配置
// 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; } // 获取文件夹 并 将配置文件中 resolve 里面 alias 所需对象参数返回 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; } // 返回 vite.config.js 配置信息,并替换上面配置信息 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 中 VitePluginMock(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 }) => { // body -> 请求体 // page pageSize body return { code: 200, msg: "success", data: userList }; } }, ]
- 手写 vite mock plugin 插件
const fs = require("fs"); const path = require("path"); export default (options) => { // 做的最主要的事情就是拦截http请求 // D当我们使用fetch或者axios去请求的 // axios baseUrl // 请求地址 // 当打给本地的开发服务器的时候 viteserver服务器接管 return { configureServer(server) { // 这边固定写法 const mockStat = fs.statSync("mock"); // 获取mock文件夹的文件状态 const isDirectory = mockStat.isDirectory(); // 判断是否是文件夹 let mockResult = []; if (isDirectory) { // 如果是文件夹 // process.cwd() ---> 获取你当前的执行根目录 mockResult = require(path.resolve(process.cwd(), "mock/index.js")); // 引入mock文件夹下的index.js console.log("result", mockResult); } // 服务器的相关配置 // req, 请求对象 --> 用户发过来的请求, 请求头请求体 url cookie // res: 响应对象, - res.header // next: 是否交给下一个中间件, 调用next方法会将处理结果交给下一个中间件 server.middlewares.use((req, res, next) => { // 这边固定写法 // 看我们请求的地址在mockResult里有没有 const matchItem = mockResult.find( (mockDescriptor) => mockDescriptor.url === req.url ); console.log("matchItem", matchItem); if (matchItem) { // 当前的请求地址在mockResult里 console.log("进来了"); const responseData = matchItem.response(req); // 调用mock 中 index.js 当前请求,并调用 response 返回响应体 console.log("responseData", responseData); // 强制设置一下他的请求头的格式为json res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(responseData)); // 设置请求头 异步的 } else { next(); // 你不调用next 你又不响应 也会响应东西 } }); // 插件 === middlewares }, }; };
-
在配置中调用创建的VitePluginMock插件
plugins: [ VitePluginMock(), ]
-
请求 mock 中 /api/users 网络地址,顺利获取到返回数据
fetch("/api/users", { method: "post" }).then(data => { console.log("data", data); }).catch(error => { console.log("error", error); })
-
vite.config.js 中 依赖优化选项 配置
-
vite.config.js 其他配置
-
typeScript 在 vite 相关配置
介绍: Vite 仅执行
.ts
文件的转译工作,并不执行 任何类型检查- 使用插件 vite-plugin-checker 该插件作用,将错误显示到控制台中
import checker from "vite-plugin-checker"; export default defineConfig({ plugins: [ checker({ typescript: true }) ] })
{ "compilerOptions":{ "skipLibcheck": fasle // 是否跳过 node_modules目录检查 } }
- 为了防止有错误的ts写法还能打包成功,我们可以在webpage.json中配置
tsc --noEmit
"scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", "test": "vite --mode test" },
- 解决在typeScript文件中使用环境变量报错和无提示问题
{ "compilerOptions":{ "skipLibcheck": fasle // 是否跳过 node_modules目录检查 "module": "ESNext" // 处理环境变量报错问题 } }
/// <reference typs= "vite/client" /> interface ImportMetaEnv { readoly VITE_PROXY_TARGET: string; // 自定义变量显示提示 }
【优化篇】vite 优化
-
分包策略
:分包策略就是把一些不会经常更新的文件,进行单独打包处理
-
浏览器的缓存策略
浏览器在请求静态资源时,只要静态资源的名称不变,它就不会重新请求,而是使用缓存。
使用Vite打包后的js文件是带有哈希值的,只要我们的代码内容有一点点变化,那么文件的hash值都会变化
-
前提介绍:
这里我们有一个vite工程,我们对它进行打包,打包完成后我们会发现,结果把所有的东西全部合并到一个js文件里面。包含咱们自己开发业务逻辑和第三方库。
-
问题:
随着项目越来越大,变动比较大的是我们自己的业务代码,而这些第三方库,变动比较小,是相对比较稳定的,每次咱们业务代码更新,第三方库未变,且全在一个文件中。导致打包后文件名变化,需要重新请求。为了解决这个问题,我们需要进行分包。
-
分包配置
1.第一种:分开打包每一个不变的库,每个都是单独的文件 优点:每一个库都一个单独文件相互不影响 可以防止一个文件导致包过大
缺点:独立文件太多,每个文件都是一个网络请求,会导致请求次数过多export default defineConfig({ build: { rollupOptions: { output: { // manualChunks 配置 manualChunks: { // 将 React 相关库打包成单独的 chunk 中 'react-vendor': ['react', 'react-dom'], // 将 Lodash 库的代码单独打包 'lodash': ['lodash-es'], // 将组件库的代码打包 'library': ['antd', '@arco-design/web-react'], }, }, } }, });
-
第二种:将静态包打包一个文件中,
优点:减少了请求次数,只需要请求一次就可以 缺点:静态库多,都打包到一个文件中,导致该文件容易太大
export default defineConfig({ build: { rollupOptions: { output: { // manualChunks 配置 manualChunks: ( filePath ) => { if(filePath.includes("node_modeules")){ return "vendor" } }, }, } }, }); ```
-
gzip压缩
-
当我们某一个文件打包后依然很大时候,这就会导致http传输性能会导致损耗,所有我们需要进行gzip压缩, 这样前端可以将一些大文件静态资源在前端提前打包成为gzip,这样可以帮助后端减少打包
-
后端服务器也可以压缩前端文件,
缺点会用服务器内存
-
服务器读取gzip文件设置一个响应头 content-encoding,浏览器收到响应结果 发现响应头中有gzip对应字段,会进行解压,得到原来文件。
注意:浏览器是要承担一定的解压时间
-
vite中可以使用
vite-plugin-compression
插件
import viteCompression from 'vite-plugin-compression'; export default defineConfig({ plugins:[viteCompression()] })
-
-
动态导入
- 可以将代码分割
-
一般使用在路由中
import()
-
路由中使用
import()
,【webpack原理】:// import 函数始终返回一个Promise function import(path){ // resolve 不被调用的话 Promise永远是pending状态 return new Promise((resolve)=>{ // 当进入到对应路由时将webpack__require.e这个Promise的状态设置为fullfulled 调用resolve() // 如果我从来没进入过某一路由页面(如home路由页面),我就让这个webpack__require.e这个Promise的状态为pending状态,从而永远无法执行.then()函数 webpack__require.e().then(()=>{ const result = await webpack__require(path) }) }) } // 解释一下 webpack__require.e(),其内容创造了一个promise.all 创建了一个script标签, src指向某一路由文件(如home路由页面) // 当进入到某一个页面或者组件的时候,将其script推到body,从而渲染出当前路由中的页面 // 其他未进入的页面,还是会创建script标签,并将src指向当前页面,但是这个script 标签不会塞入到body里面
-
2.
CDN 加速
主要思路: 尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
实现方法: 通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接和负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,加快访问速度。
目的: 使用户可
就近
取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度
。优势:
- CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
- 大部分请求在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载。
vite cdn 加速 代码:
-
使用cdn的库,不会打包到项目中,而是使用cnd地址请求,从而减少项目体积,提高打包速度
-
第一种:使用第三方插件,例如
vite-plugin-cdn-import
缺点
:进入首屏就加载全部 CDN 资源
import viteCDNPlugin from "vite-plugin-cdn-import"; export default defineConfig({ pulgins: [ viteCDNPlugin({ modules: [ { name: "lodash", // 当前需要使用cdn第三方库 var: "_", // 全局导出的变量 path: "https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" // cdn 地址 } ] }) ] })
-
第二种:alias 配置项加载
支持 ESM 编译的 CDN
优点
:按需加载;开发环境也可使用缺点
:import 时不能解构,只能直接引入,这也说明这种方式不适合那些需要解构资源的 CDNimport { defineConfig } from 'vite'; import { resolve } from 'path'; export default defineConfig({ // other config resolve: { alias: { '@': resolve(__dirname, 'src'), mitt: 'https://cdn.jsdelivr.net/npm/mitt@3.0.0/+esm', axios: 'https://esm.sh/axios@0.21.4', } } })
-
跨域
:
-
同源策略 【仅在浏览器发生,浏览器的规则】:http交互默认情况下只能在,同协议、同域名、同端口进行,否则就会产生跨域。
-
跨域限制时服务器已经响应了东西,但是浏览器不给你,不是说服务器没响应东西
-
vite 处理跨域方式
import {defineConfig} from "vite"; export default defineConfig({ server: { proxy: { // 字符串简写写法:http://localhost:5173/foo -> http://localhost:4567/foo '/foo': 'http://localhost:4567', // 带选项写法:http://localhost:5173/api/bar -> http://jsonplaceholder.typicode.com/bar '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, // 正则表达式写法:http://localhost:5173/fallback/ -> http://jsonplaceholder.typicode.com/ '^/fallback/.*': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/fallback/, ''), }, // 使用 proxy 实例 '/api': { target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, configure: (proxy, options) => { // proxy 是 'http-proxy' 的实例 } }, // 代理 websockets 或 socket.io 写法:ws://localhost:5173/socket.io -> ws://localhost:5174/socket.io '/socket.io': { target: 'ws://localhost:5174', ws: true, }, } } })
【跨域原理篇】
fetch("/api/getUserInfo").then(data=>{ console.log("data",data) })
import {defineConfig} from "vite"; export default defineConfig({ server: { // 开发服务器中的配置 proxy: { // 配置跨域解决方案 '/api': { // key + 描述对象 以后请求遇到/api开头的请求时,都将其代理到target属性对应的域中去 // :http://localhost:5173/api/getUserInfo -> http://jsonplaceholder.typicode.com/getUserInfo target: 'http://jsonplaceholder.typicode.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''),// 是否重写路径 }, } } })
在开发环境中,我们请求如:/api/getUserInfo 地址的时候:
-
浏览器会先给我们拼接成为:当前项目地址运行 + /api/getUserInfo 例如:( http://127.0.0.1:5173/api/getUserInfo ) ,拼接完成以后,会去找 vite , vite 发现这个path有配置过跨域代理策略,然后他会根据策略的描述对象,进行再次请求。根据上面代码路径会变成 ( jsonplaceholder.typicode.com/getUserInfo )
-
上面也就是
浏览器会先做一次请求地址处理
,因为有跨域配置,所有本地开发服务器会做第二次请求地址处理
。并在本地服务器中发起请求, 服务器与服务器之间请求资源(由于没有使用浏览器故不会发生跨域),然后开发服务器把获取到的资源 返回给浏览器 ,因为开发服务器和客户端是同源的,于是浏览器会把开发服务器请求到的资源传递给客户端器。
vite 开发服务器大概代码逻辑
if(ctx.request.url.includes("/api")){ // 当请求路径包含 "/api" const target = proxy.target; // 拿到配置信息的 target const rewrite = str => str; // 拿到配置信息的 rewrite 并执行 rewrite() const result = await request(target + rewrite("api")); // 将两个路径进行拼接,并在开发服务器发送请求,拿到数据; ctx.response.body = result; // 将获得的的数据发送给浏览器 }