vite了解

434 阅读14分钟

vite官网:cn.vitejs.dev/guide/why.h…

学习链接:www.bilibili.com/video/BV1GN…

构建工具到底承担什么责任:

  • 模块化开发及多种模块化;
  • 处理代码兼容性,比如babel语法降级,语法转换等;
  • 提高项目性能,在打包的过程中,帮我们压缩文件,代码分割等;
  • 优化开发体验

1.它会帮助我们自动监听文件变化,调用相应的集成工具进行重新打包,然后在浏览器重新运行;

2.开发服务器,解决跨域问题;

构建工具让我们可以不用每次都关心代码在浏览器如何运行,我们只需要首次给构建工具提供一个配置文件即可。

那么vite相较于webpack的优势是什么呢?

当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。

image.png

如上面引自vite官网的话所述,当项目越来越大时,所要处理的js代码便会越来越多,便会造成需要很长时间才会将服务器成功启动。

原因:

webpack是支持多种模块化格式,就得抓取并构建整个应用,形成统一的格式(​__webpack_require__​​​),再提供服务。

vite是只支持ESM,就不存在上述过程了,Vite 只需要在浏览器请求源码时进行转换并按需提供源码就好了。

------------------------------------------12.15--------------------------------------------

vite启动项目

首先呢,新建一个文件目录,在里面新建以下文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>

</head>
<body>
    <script src="./main.js" type="module"></script>
</body>
</html>
import { count } from './counter.js';

console.log(count);
export const count = 0;

然后在浏览器上打开index.html文件,会发现控制台成功的输出了count的值,接着我们初始一下项目,再下载一下lodash,并在counter.js里引入一下lodash

1.yarn init -y

初始化文件设置

2.yarn add lodash

安装lodash

3.导入lodash

import _ from 'loash';

export const count = 0;

这时你就会发现浏览器报错了,提示你要以ESM导入资源的格式引入,而在默认情况下,esmodule导入资源的时候,要么是绝对路径,要么是相对路径。

image.png

报错的原因是ESM不会帮我们去搜索node_modules,便导致lodash加载不出来。

那么,接下来就让我们安装vite来帮我们解决这个问题。

1.yarn add vite -D

安装vite

2.更改配置文件

{
  "name": "vite-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "vite"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

3.yarn dev

启动项目

打开本地项目链接,就会发现lodash被成功加载了。

由此可以看出,vite和webpack一样是开箱即用的,不需要任何额外的配置就可以使用vite来帮助我们处理构建工作。

vite的预加载

vite是通过路径补全来解决上一小节lodash加载不了的问题的。

image.png

找寻依赖的过程是自当前目录依次向上查找的过程。

在找到包之后,会发现包会通过立即函数,commonjs等形式导出,当用commonjs规范导出时ESM便不认识了,那这时vite便提出了依赖预构建。

依赖预构建调用esbuild,将其他规范的代码转换为ESM,然后放在node_modules/.vite/deps,同时对ESM规范的各个模块进行统一集成。

各个模块统一集成便是指将多层模块依赖集成生成一个或多个模块,浏览器只需要请求几次就好,而不是遇到一个导入便请求一次。

第三方库源码

image.png

经过vite处理过的请求资源

image.png

------------------------------------------12.16--------------------------------------------

Vite语法提示及环境变量

关于语法提示方面:

  • webstorm无需特殊处理,会自动将vite配置项的语法进行补全。
  • 如是其他编辑器,则需要进行特殊处理。

特殊处理方式:

1.从vite里引入defineConfig

import { defineConfig } from 'vite'

export default defineConfig({
    optimizeDeps: {
        exclude: [] //不进行依赖预构建
    }
})

2.@type 类型标记注释

/** @type {import('vite').UserConfig} */
const config = {
    optimizeDeps: {
        exclude: [] //不进行依赖预构建
    }
}

3.官网推荐

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

关于环境变量方面:

项目根目录下新建三个文件夹:

1.vite.base.config.js

import { defineConfig } from 'vite'

export default defineConfig({
    
})

2.vite.dev.config.js

import { defineConfig } from 'vite'

export default defineConfig({
    
})

3.vite.prod.config.js

import { defineConfig } from 'vite'

export default defineConfig({
    
})

接下来在vite.config.js文件里将上述文件抛出的配置引入

import { defineConfig } from 'vite';
import BaseViteConfig from'./vite.base.config';
import DevViteConfig from'./vite.dev.config';
import ProdViteConfig from'./vite.prod.config';

const EnvResolver = {
    "serve": () => Object.assign({}, BaseViteConfig, DevViteConfig),
    "build": () => Object.assign({}, BaseViteConfig, ProdViteConfig),
}

export default defineConfig(({command}) => {
    return EnvResolver[command]();
})

command就是表示当前所处环境的字段,通过判断这个字段来导出不同的配置。

image.png

而command是build还是server是根据我们终端所敲入的命令决定的。

环境变量的配置:

环境变量:根据当前环境产生质的变化的变量,比如请求路径、参数及baseUrl等。可通过配置化环境变量,降低本地开发及线上频繁更改字段值而导致事故的风险。

vite是通过内置的第三方库dotenv解析对应的环境变量,并将其注入到process对象下。(但vite考虑到和其他配置的一些冲突问题,不会直接将变量注入到process对象下)

冲突问题:vite的配置文件可以配置root与envDir然后去读取环境变量的文件,如果process对象下的环境变量读取地址与envDir不一致,便会导致precess对象下的环境变量不会被用到。

所以vite便为我们提供了loadEnv方法来手动确认env文件,这时我们便可以在服务端对环境变量进行处理了

import { defineConfig, loadEnv } from 'vite';
import BaseViteConfig from'./vite.base.config';
import DevViteConfig from'./vite.dev.config';
import ProdViteConfig from'./vite.prod.config';

const EnvResolver = {
    "serve": () => {
        return Object.assign({}, BaseViteConfig, DevViteConfig)
    },
    "build": () => {
        return Object.assign({}, BaseViteConfig, ProdViteConfig)
    },
}

export default defineConfig(({command, mode}) => {
    const env = loadEnv(mode, process.cwd(), "");
    console.log(command, env, mode);
    
    return EnvResolver[command]();
})

接下来让我们在与vite配置文件同级的目录下新建以下配置文件:

  • .env文件,可配置全局变量。
  • .env.development文件,可配置开发环境变量(默认vite将开发环境命名为development)。
  • .env.production文件,可配置生产环境变量(默认vite将生产环境命名为production)。
test1 = 111
test2 = 222
test3 = 333

若我们不想用默认的环境名,我们也可以通过mode来自定义环境的名字

yarn dev --mode devlop

启动服务后,我们便会看到控制台将环境变量打印出来啦

  • 开发环境

image.png

  • 生产环境

image.png

其具体原理便是,当我们调用loadEnv时,他会做如下几件事:

  • 直接找到.env文件并解析环境变量,放进一个对象里
  • 将mode传进来的这个变量值进行拼接(.env.[mode]),并根据目录(process.cwd)取对应的文件并进行解析,放进一个对象里
  • 最后对象进行合并,我们可以理解为
const baseEnvConfig = .env的配置;
const modeEnvConfig = .env.mode的配置;

const lastEnvConfig = {...baseEnvConfig, ...modeEnvConfig};

以上是在服务端用到环境变量,如果我们在客户端的话,vite会将对应的环境变量注入到import.meta.env里,

但是vite在这里做了一层拦截,防止我们将隐私的变量直接注入到import.meta.env里,所以只有已VITE开头的变量才可以成功注入到客户端。

若我们不想用VITE前缀,可以在对vite进行配置,更改envPrefix字段的值。

结下来然我们在counter.js文件里将环境变量引入并打印

import _ from 'lodash-es';

console.log(_, import.meta.env);

export const count = 0;

启动服务后,我们可以看到,环境变量并没有打印出来,这是因为我们没有将环境变量的前缀更改为VITE。

VITE_test1 = 111
  • 未更改前缀

image.png

  • 更改前缀

image.png

自定义前缀

对vite进行配置,并更改.env文件里的环境变量配置后,便会得到我们想要的环境变量啦

import { defineConfig } from 'vite'

export default defineConfig({
    envPrefix: "ENV_"
})
ENV_test1 = 111

控制台打印:

image.png

------------------------------------------12.26--------------------------------------------

vite服务器搭建原理

实现简单的服务器

当执行yarn dev命令行时,都做了些什么事情,以下便对服务器的基础原理进行简单的认识。

首先新建一个文件夹vite-dev-server,对项目进行初始化,同时下载node框架koa。

mkdir vite-dev-derver

cd vite-dev-derver

yarn init -y

yarn add koa

接下来就是在当前目录下新建index.html文件.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>

然后在当前目录下新建index.js文件,将koa导入并起一个后端服务,并将index.html文件内容返回给前端。

const Koa = require("koa");
const fs = require("fs");
const path = require("path")

const app = new Koa();

app.use(async (ctx) => {
    console.log("ctx", ctx.request, ctx.response);

    if (ctx.request.url === '/') {
        const indexContent = await fs.promises.readFile(path.resolve(__dirname, "./index.html"), {encoding: "utf-8"});

        console.log("indexContent", indexContent);

        ctx.body = indexContent;
    }
})

app.listen(5174, () => {
    console.log("listen on 5174")
})

配置package.json文件,使得可以用通过yarn dev的方式将服务启动。

{
  "scripts": {
    "dev": "node index.js"
  }
}

服务启动后,在浏览器访问5714端口,请求成功,就会发现页面成功渲染后端传过来的html了。

通过以上操作,我们的前后端流程就走通了。

那么vite如何让浏览器识别.vue文件呢

首先呢,我们需要在当前目录下新建main.js文件,App.vue文件,并在index.html内将main.js导入

import App from './App.vue';

console.log(App)
<body>
    <script type="module" src="./main.js"></script>
</body>

这时重启服务,就会发现main.js资源请求失败,这个因为服务端没有对/main.js路径做处理。

那么,我们就需要在index.js文件内做出以下修改,来响应main.js的请求。现在是手动对每个路径做处理,等项目做大了之后,会通过中间件做处理的。

if (ctx.request.url === '/main.js') {
    const mainContent = await fs.promises.readFile(path.resolve(__dirname, "./main.js"), {encoding: "utf-8"});

    console.log("mainContent", mainContent);

    ctx.body = mainContent;
    ctx.response.set("Content-Type", "text/javascript")
}

重启服务,我们就又会发现App.vue资源请求报错了,其原因同上请求main.js报错,所以就又要更改index.js文件,配置相应路径的处理。

到这里,就引出了,浏览器是怎么认识.vue文件的原理了。

if (ctx.request.url === '/App.vue') {
    const vueContent = await fs.promises.readFile(path.resolve(__dirname, "./App.vue"), {encoding: "utf-8"});
    中间包含一系列操作
    console.log("vueContent", vueContent);

    ctx.body = vueContent;
    ctx.response.set("Content-Type", "text/javascript")
}

中间包含的一系列操作:获取到.vue文件内容后,并将其中间vue文件内容替换成js文件内容。(AST语法分析:通过createElemtent将vue模版构建成原生dom);

最后通过Content-Type告诉浏览器,即使是.vue文件,也要当成js文件解析。

综上所述,vite 当我们敲下 yarn dev 命令后就做了以上操作,将项目跑了起来。

同时我们也可以看出来浏览器不会管文件后缀名是什么,它是通过后端返回的Content-Type来决定用什么方式解析的。

css处理

vite是天生支持对css文件的直接处理的。

接下来回到vite-demo目录下,新建index.css文件,并在main.js文件内引入。

html, body {
    width: 100%;
    height: 100%;
    background-color: aquamarine;
}
import "./index.css"

启动服务,可以看到页面的背景颜色改变了,在此之前我们并没有对vite配置进行更改,可以看出vite是天生就支持对css文件进行处理的。

大体原理便是:

  • vite读取main.js时,引用了css文件
  • 直接去使用fs模块读取index.css的内容
  • 穿件style标签,将index.css文件的内容复制到style标签内
  • 将style标签嵌入到index.html的header标签中
  • 最后将css文件内容转换为js脚本(方便热更新或css模块化),同时设置Content-Type为js,让浏览器以js脚本的形式解析css后缀的文件

css模块化

多人协同开发的场景下,可能会导致css类名重复,造成样式覆盖,最后导致样式错乱的情况。而cssmodule便是解决这个问题的。

在vite-demo文件夹下新建四个文件,分别是A.js,B.js,A.module.css,B.module.css,并在main.js中将A.js,b.js引入。

import ACss from  "./A.module.css";

console.log("ACss", ACss);

const div = document.createElement("div");

document.body.appendChild(div);

div.className = ACss.footer;
import BCss from "./B.module.css";

console.log("BCss", BCss);

const div = document.createElement("div");

document.body.appendChild(div);

div.className=BCss.footer;
 .footer {
    width: 200px;
    height: 200px;
    background-color: beige;
}
 .footer {
    width: 100px;
    height: 200px;
    background-color: lightblue;
}
import './A'
import './B'

通过查看元素我们可以发现class的名字拼接上了一串哈希值,

通过控制台打印发现,导入的css变成了一个对象,其key及value值分别为我们原本的class类名与拼接哈希值后的class类名。

通过以上规律,我们将calss类名的赋值改为div.className=BCss.footer变量的方式,就会发现css样式被完全隔离开来了。这就是css的模块化

而css模块化的大致原理如下(基于node):

  • 读取文件发现时已moudule.css命名,便会开启模块化(module是模块化的约定)
  • 将所有类名进行一定规则的替换
  • 创建映射对象(footer: "_footer_1w47u_1")
  • 将替换后的内容塞到style标签内,然后将style嵌入到header标签内
  • 将moudle.css内容替换为js脚本
  • 将映射对象在脚本中进行倒出

------------------------------------------12.27--------------------------------------------

vite中的css配置

以上是默认的vite对css的处理,接下来通过手动更改vite的css配置,来进行其他额外的处理。

modules模块化

  • localsConvention:更改映射对象里的key的展现形式(驼峰/中划线)。
export default defineConfig({
    css: {
        modules: {
            localsConvention: 'camelCase'
        }
    }
})

配置生效后,就会发现,对象里多个驼峰式的类名及其映射。

此时,在js向dom赋值类名时,便可采用驼峰命名了,而不是中划线footer-content,符合代码编写时的规范。

div.className = ACss.footerContent;
  • scopeBehaviour:配置当前模块时模块化还是全局化(有无哈希值)
export default defineConfig({
    css: {
        modules: {
            scopeBehaviour: 'local'
        }
    }
})

local:模块化

global:全局化

  • generateScopedName:生成类名规则(映射对象value的展示形式,可配置为函数或字符串)
export default defineConfig({
    css: {
        modules: {
            generateScopedName: '[name]_[local]_[hash:5]'
        }
    }
})

配置生效后就会看到value的展现形式变成了本地文件名+本地类名+哈希值了

通过该网址https://github.com/webpack/loader-utils#interpolatename ,查询'[name][local][hash:5]'配置项及规则释义。

generateScopedName除了上述的方式外,还可配置成函数。

该函数会有三个入参:

函数reture的值便是映射对象的value值。

export default defineConfig({    css: {
        modules: {
            generateScopedName: (name) => {
                return name
            }
        }
    }
})
  • hashPrefix:配置的字符串会参与到哈希值的生成中去
export default defineConfig({
    css: {
        modules: {
            hashPrefix: 'demo',
        }
    }
})
  • globalModulePaths:不想参与到css模块化的路径(比如通用的css样式或第三方库样式)
export default defineConfig({
    css: {
        modules: {
            globalModulePaths: [],
        }
    }
})

preprocessorOptions预处理器配置

配置形式为key(预处理器的名less/sass等)+config(配置对象)

less官方文档:less.bootcss.com/usage/#less…,在这里可以知道config可以配置哪些参数。

  • math:支持计算
export default defineConfig({
    css: {
        preprocessorOptions: {
            less: {
                math: 'always',
            }
        }
    }
})
  • globalVars:定义全局变量
export default defineConfig({
    css: {
        preprocessorOptions: {
            less: {
                globalVars: {
                    mainColor: 'red'
                }
            }
        }
    }
})

devSourcemap文件索引

当代码被压缩和编译后,如果程序出错,则不会提示正确的错误信息位置,设置了sourcemap后,会生成一个索引文件,然后找出报错信息的正确位置。

export default defineConfig({
    css: {
        devSourcemap: true
    }
})

postcss

postcss的作用便是保证css在制定起来万无一失。

流程:css--->postcss(编译--->语法降级--->前缀补全)--->浏览器

我们知道css本身也是支持变量的,通过var()的方式,但是他的兼容性不太好,在低版本的浏览器中可能不会生效。

但是我们的预处理器(less/sass等)并不能处理这个问题,来进行语法降级。

这时postcss便发挥了作用,它会直接将var(变量名)替换为对应的值,来兼容不同的浏览器。

那么postcss做了哪些预处理器不能做的事情呢:

  • 对未来css属性的语法降级
  • 前缀补全

demo案例

新建postcss-demo文件夹,然后初始化项目

yarn init -y

安装依赖postcss,postcss-cli

yarn add postcss postcss-cli -D

当前目录下新建index.css文件

:root {
    --globalColor: 'blue'
}

div {
    background-color: var(--globalColor);
}

使用命定行,对css进行编辑,编译后的结果存到result.css文件内

(--o是指输出的意思,可以查看更多的命令在该地址:www.npmjs.com/package/pos…

npx postcss index.css --o result.css

运行成功后,打开result.css文件我们可以发现,css被并没有被降级,这是因为我们没有书写配置文件

安装预设(编译/语法降级等插件)

yarn add postcss-preset-env -D

配置文件postcss.config.js

const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
    plugins: [postcssPresetEnv()]
}

接下来运行编译命令后,打开result.css文件就会发现css语法被成功降级了

vite的postcss配置

回到vite-demo目录下,安装依赖

yarn add postcss-preset-env -D  

编辑vite.base.config.js文件

const postcssPresetEnv = require("postcss-preset-env");

export default defineConfig({
    css: {
        postcss: {
            plugins: [postcssPresetEnv()]
        }
    }
})

更改A.mobule.less样式,使用css新语法clamp及user-select

.footer-content {
    width: clamp(100px, 30%, 300px);
    height: 200px / 2;
    background-color: beige;
    user-select: none;
}

服务启动成功后,可以看到语法被成功降级和补全了

也可通过配置postcss.config.js来实现vite的postcss配置相同的功能,但是postcss.config.js优先级大于vite.base.config.js.

以上便是vite对postcss的一个简单配置的两种方法。

------------------------------------------12.28--------------------------------------------