LOADING

vite

2024/2/26

介绍

没有 vite 之前,打包基本上都是 webpack

vite 的本质是用来代码处理的

vite 开箱即用(你不需要做任何额外的配置就可以使用 vite 来帮你处理构建工作)

安装 vite

yarn add vite

vite 与 create vite

create-vite 内置了 vite

create-vite 是 vite 的脚手架,是 vite 的最佳配置
create-vite(开发商)给你一套精装修模板(给你一套预设): 下载 vite, vue, post-css, less, babel 好了, 并且给你做好了最佳实践的配置。

当我们敲了yarn create vite

  1. 帮我们全局安装一个东西: create-vite (vite 的脚手架)
  2. 直接运行这个 create-vite bin 目录的下的一个执行配置

一般会使用这个命令快速创建一个项目

yarn create vite my-vue-app --template vue

vite 使用

开箱即用 vite

现代浏览器支持的 es 写法

<!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>Vite dev server</title>
</head>
<body>
    //esmodule
    <script type="module" src="./main.js"></script>
</body>
</html>


//在默认情况下, 我们的esmodule去导入资源的时候,
要么是绝对路径, 要么是相对路径
//main.js
import { count } from "./count.js"
console.log(count)


//count.js
export const count = 0;

安装依赖模块化引用

//初始化package.json
yarn init -y

yarn add lodash
//安装lodash

//main.js
import { count } from "./count.js"
console.log(count)

//在没有构建工具的情况会报错,因为浏览器认识这个路径
import _ from "lodash"
console.log(_)

使用 vite 来搭建开发服务器

//安装vite
yarn add vite

//配置package.json
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "test": "vite --mode test"
}

配置之后启动通过使用 yarn dev 命令之后 import _ from “lodash”可以正常引入

问:
既然我们现在的最佳实践就是 node_modules, 那为什么 es 官方在我们导入非绝对路径和非相对路径的资源的时候不默认帮我们 搜寻 node_modules 呢?
答:
服务端找文件(模块)不是通过网络请求去找的,而是找本地文件(可以做)
浏览器找依赖是通过网络请求,将会去加载许多的依赖包,非常消耗性能(不能去做),比如要通过网络引入一个 lodash,而 lodash 又会引入成千上万的 js,浏览器害怕有性能问题

他是怎么处理的?

vite 处理的过程中如果说看到了有非绝对路径或者相对路径的引用, 他则会尝试开启路径补全

import _ from "lodash"; // lodash可能也import了其他的东西

它会先找当前模块下的 node_modules,没找到会找上级目录,然后一直到 user/node_modules

之所以 vite 这么做,是因为 vite 会进行依赖预构建

生产和开发

yarn dev —> 开发(每次依赖预构建所重新构建的相对路径都是正确的)

yarn dev === yarn dev -mode develop

vite 会全权交给一个叫做 rollup 的库去完成生产环境的打包

依赖预构建(重要)

依赖预构建指的是在 DevServer 启动之前,Vite 会扫描使用到的依赖从而进行构建,之后在代码中每次导入(import)时会动态地加载构建过的依赖这一过程。

  • 依赖部分 更多指的是代码中使用到的第三方模块,比如 vuelodashreact 等。

Vite 将会使用 esbuild 在应用启动时对于依赖部分进行预构建依赖。

  • 源码部分 比如说平常我们书写的一个一个 jsjsxvue 等文件,这部分代码会在运行时被编译,并不会进行任何打包。

Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

注意:只有第三方模块才会进行依赖预构建

因为不同的库有些的导出的方式是 commonjs,有些是 esmodule,有些是 es3,es5 立即执行函数的方式所以 vite 要进行依赖预构建

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

他解决了 3 个问题:

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

export default defineConfig({
  optimizeDeps: {
    exclude: [], // 将指定数组中的依赖不进行依赖预构建
  },
});

element-plus 的地方不要对样式使用依赖预构建

optimizeDeps: {
    include: ['schart.js'],
    exclude: ['element-plus/es/components/popconfirm/style/index', 'element-plus/es/components/tabs/style/index',
      'element-plus/es/components/image/style/index', 'element-plus/es/components/tag/style/index', 'element-plus/es/components/input-number/style/index',
      'element-plus/es/components/radio-button/style/index',
      'element-plus/es/components/tab-pane/style/index', 'element-plus/es/components/rad', 'element-plus/es/components/link/style/index']
  },

因为可能用到再加载 样式会闪烁

所以在引入 main.js 的时候直接把 element-plus 的样式加载进去就好了

env 文件(环境变量)

vite 读取.env 文件

服务端

const baseEnvConfig = 读取.env的配置
const modeEnvConfig = 读取env相关配置
const lastEnvConfig = { ...baseEnvConfig, ...modeEnvConfig }

loadEnv会根据环境的不同,读取不一样的env配置项

// 当前env文件所在的目录
// 第二个参数不是必须要使用process.cwd(),
const env = loadEnv(mode, process.cwd(), "");

客户端

vite 会将对应的环境变量注入到 import.meta.env 里去

vite 做了一个拦截, 他为了防止我们将隐私性的变量直接送进 import.meta.env 中, 所以他做了一层拦截, 如果你的环境变量不是以 VITE 开头的(默认参数是 VITE), 他就不会帮你注入到客户端中去, 如果我们想要更改这个前缀, 可以去使用 envPrefix 配置

export default defineConfig({
  envPrefix: "ENV_", // 配置vite注入客户端环境变量校验的env前缀
});

css 配置(css)

vite 天生就支持对 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 后缀的文件

场景:

  • 一个组件最外层的元素类名一般取名 : wrapper
  • 一个组件最底层的元素明明我们一般取名: .footer

你取了 footer 这个名字, 别人因为没有看过你这个组件的源代码, 也可能去取名 footer 这个类名

最终可能会导致样式被覆盖(因为类名重复), 这就是我们在协同开发的时候很容易出现的问题

css module 就是来解决这个问题的

大概说一下原理:

全部都是基于 node

  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 脚本
  6. 将创建的映射对象在脚本中进行默认导出

做法和 webpack 的 css 模块化一样

less(预处理器): less 给我们提供了一些方便且非常实用的方法,使用 less 同样可以采用这种方式去实现模块化

将一个 css 模块化的默认命名规则

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
    css: { // 对css的行为进行配置
        // modules配置最终会丢给postcss modules
        localConvention: 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
        scopeBehaviour: 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖)
        generateScopedName: 生成的类名的规则(可以配置为函数, 也可以配置成字符串规则: https://github.com/webpack/loader-utils#interpolatename)
        hashPrefix: 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
        globalModulePaths: 代表你不想参与到css模块化的路径
        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相关
    },
});

处理静态资源

可以拿到加载图片的地址,加载 json 的地址等

// 主要是来帮助我们学习怎么加载静态图片资源
import sylasPicUrl from "@assets/images/sylas.png"; // 原始 字符串的replace操作

// 服务端 他会去读取这个图片文件的内容 ---> Buffer  二进制的字符串
// Buffer

console.log("sylasPicUrl", sylasPicUrl);
const img = document.createElement("img");
img.src = sylasPicUrl;
document.body.append(img);

处理 svg

import svgIcon from "./assets/svgs/fullScreen.svg?url"; //路径
import svgRaw from "./assets/svgs/fullScreen.svg?raw"; //源文件内容

console.log("svgIcon", svgIcon, svgRaw);

document.body.innerHTML = svgRaw;

const svgElement = document.getElementsByTagName("svg")[0];

svgElement.onmouseenter = function () {
  // 不是去改他的background 也不是color
  // 而是fill属性
  this.style.fill = "red";
};

// 第一种使用svg的方式
// const img = document.createElement("img");

// img.src = svgIcon;

// document.body.appendChild(img);

// 第二种加载svg的方式

处理路径(resolve)

原理:

先读取配置

然后当请求路径的时候,匹配字符进行替换,匹配的时候 path.resolve()里面得到的是绝对路径,需要进行截取从 src 开始

使用

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"), // 设置别名, 以后我们在其他组件中可以使用@来代替src这个目录
    },
  },
});

打包配置(build)

export default defineConfig({
  build: {
    rollupOptions: {
      // 配置rollup的一些构建策略
      output: {
        // 控制输出
        // 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果
        assetFileNames: "[hash].[name].[ext]",
      },
    },
    // 配置图片是单独文件还是base64
    assetsInlineLimit: 4096, //4kb
    outDir: "dist", // 配置输出目录
    assetsDir: "static", // 配置输出目录中的静态资源目录
    emptyOutDir: true, // 清除输出目录中的所有文件
  },
  //配置资源引用的路径
  base: "./",
});

插件配置(plugins)

vite 会在生命周期的不同阶段中去调用不同的插件以达到不同的目的,可以引入社区插件或者自己写插件来进行自定义配置

生命周期: 其实就和我们人一样, vite 从开始执行到执行结束, 那么着整个过程就是 vite 的生命周期

比如 webpack 输出 html 文件的一个插件 清除输出目录: clean-webpack-plugin

有一些钩子只有 vite 里才能使用,有些钩子是会在 vite 和 rollup 都使用(rollup 是用来生产打包的)

一共展示四个配置

  1. config(额外配置 vite.config.js)
  2. enforce(执行的时机)
  3. transform(动态配置静态 html)
  4. configureServer(配置 vite 服务器中间件)

其他

configResolved

使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用

configResolved(resolvedConfig) {
  // 存储最终解析的配置
  config = resolvedConfig
},

vite-aliases

可以帮助我们自动生成别名: 检测你当前目录下包括 src 在内的所有文件夹, 并帮助我们去生成别名

类似这样,当我们代码中使用@的时候,他就会帮助我们替换字符串

{
"@": "/**/src",
"@aseets": "/**/src/assets",
"@components": "/\*\*/src/components",
}
import { defineConfig } from "vite";
import { ViteAliases } from "vite-aliases";

export default defineConfig({
  plugins: [ViteAliases()],
});

原理手写

插件内部可以形成新的配置去覆盖 vite.config.js,所以我们做的事情就是往 vite.config.js 去添加这个配置。

本质就是我们去递归遍历每个文件

export default defineConfig({
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"), // 设置别名, 以后我们在其他组件中可以使用@来代替src这个目录
    },
  },
});

手写插件,将 src 目录下的所有文件夹可以通过@文件夹名来访问

// 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-plugin-html

动态控制 html 中的内容

动态控制 title(ejs 语法)

  plugins: [
      CreateHtmlPlugin({
          inject: {
              data: {
                  title: "主页2"
              }
          }
      }), // serve === 开启开发服务器
  ],

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>
        <%= title %>
    </title>
</head>
<body>
    <%= title %>
<script src="./main.js" type="module"></script>
</body>
</html>

手写

module.exports = (options) => {
  return {
    transformIndexHtml: {
      // 将我们插件的一个执行时机提前
      enforce: "pre",
      // 转换html的
      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-plugin-mock

mock 数据: 模拟数据

前后端一般是并行开发 用户列表 ( 接口文档 )

mock 数据 去做你前端的工作

  1. 简单方式: 直接去写死一两个数据 方便调试
  • 缺陷:

axios: http 请求库

- 没法做海量数据测试
- 没法获得一些标准数据
- 没法去感知http的异常
  1. mockjs: 模拟海量数据的, vite-plugin-mock 的依赖项就是 mockjs

使用

import { defineConfig } from "vite";
import { viteMockServe } from "vite-plugin-mock";

export default defineConfig({
  plugins: [
    //引入插件
    viteMockServe(),
  ],
});

在工程目录下新建文件夹 mock

//index.js

const mockJS = require("mockjs");

//使用mock语法模拟数据
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,
      };
    },
  },
];

原理与手写(主要涉及服务器)

VitePluginMock.js

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 开发服务器会读取我们 plugins 里面的内容,然后如果要修改配置,就会执行,如果要修改 html,也会挨个执行

完整配置(vite.config.js)

vite 运行在服务器,为什么 vite.config.js 可以书写成 esmodule 的形式?

这是因为 vite 他在读取这个 vite.config.js 的时候会率先 node 去解析文件语法, 如果发现你是 esmodule 规范会直接将你的 esmodule 规范进行替换变成 commonjs 规范,服务器拿到你的 js 是字符串他可以通过 replace 方法去替换

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
    optimizeDeps: {
        exclude: [], // 将指定数组中的依赖不进行依赖预构建
    },
    envPrefix: "ENV_", // 配置vite注入客户端环境变量校验的env前缀
    css: { // 对css的行为进行配置
        // modules配置最终会丢给postcss modules
        localConvention: 修改生成的配置对象的key的展示形式(驼峰还是中划线形式)
        scopeBehaviour: 配置当前的模块化行为是模块化还是全局化 (有hash就是开启了模块化的一个标志, 因为他可以保证产生不同的hash值来控制我们的样式类名不被覆盖)
        generateScopedName: 生成的类名的规则(可以配置为函数, 也可以配置成字符串规则: https://github.com/webpack/loader-utils#interpolatename)
        hashPrefix: 生成hash会根据你的类名 + 一些其他的字符串(文件名 + 他内部随机生成一个字符串)去进行生成, 如果你想要你生成hash更加的独特一点, 你可以配置hashPrefix, 你配置的这个字符串会参与到最终的hash生成, (hash: 只要你的字符串有一个字不一样, 那么生成的hash就完全不一样, 但是只要你的字符串完全一样, 生成的hash就会一样)
        globalModulePaths: 代表你不想参与到css模块化的路径
        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: { // 构建生产包时的一些配置策略
        rollupOptions: { // 配置rollup的一些构建策略
            output: { // 控制输出
                // 在rollup里面, hash代表将你的文件名和文件内容进行组合计算得来的结果
                assetFileNames: "[hash].[name].[ext]"
            }
        },
        assetsInlineLimit: 4096000, // 4000kb
        outDir: "dist", // 配置输出目录
        assetsDir: "static", // 配置输出目录中的静态资源目录
        emptyOutDir: true, // 清除输出目录中的所有文件
    }
    plugins: [
        MyViteAliases(),
        viteCompression(),
        // ViteAliases()
        // createHtmlPlugin({
        //     inject: {
        //         data: {
        //             title: "主页"
        //         }
        //     }
        // })
        CreateHtmlPlugin({
            inject: {
                data: {
                    title: "主页2"
                }
            }
        }), // serve === 开启开发服务器
        // viteMockServe()
        VitePluginMock()
    ],
});

开发服务器

当使用 yarn start 的命令的时候实际上是本地启动了一个开发服务器

vite 开发服务器浅析工作原理(处理 vue 文件)

当我们敲下一个域名,等于是在问终端服务器要东西,后端最频繁要做的事情就是在处理请求和操作文件,服务器就是在解析 url 来给你返回 html,json 数据等。

在服务器和浏览器眼里这些文件数据都是字符串

.vue 文件会被编译成.js 文件来执行

当使用 yarn dev 的使用其实 vite 是使用 koa(node 框架)开启了一个本地服务器

注:vite 里会采用依赖预构建(es build 打包),比如要使用 vue 模块,他会把要读取的文件打包完放入 node_module/.vite/deps 目录下进行读取

const Koa = require("koa"); // 不能用esmodule 必须使用commonjs
const fs = require("fs");  // ./ / npm install yarn add
const path = require("path");

// 我们不用返回给客户端的吧 而且我们这里约定的名字就叫做vite.config.js
const viteConfig = require("./vite.config");
const aliasResolver = require("./aliasResolver");

console.log("vite.Config", viteConfig)

const app = new Koa();  // const vue = new Vue();

//读取首页,读取vite.config,读取js
app.use(async (ctx) => { //context 上下文 request --> 请求信息 响应信息 get请求 /
    //浏览器不同的请求就进行不同的处理
    console.log("ctx", 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");
    }
    // 如果当前文件的url是以js后缀结尾的
    // 我们的root js文件一定是和html文件在一个目录的对不对
    if (ctx.request.url.endsWith(".js")) {
        const JSContent = await fs.promises.readFile(path.resolve(__dirname, "." + ctx.request.url)); // 在服务端一般不会这么用
        console.log("JSContent", JSContent);
        // 直接进行alias的替换
        const lastResult = aliasResolver(viteConfig.resolve.alias, JSContent.toString());
        ctx.response.body = lastResult; // 作为响应体发给对应的请求的人
        ctx.response.set("Content-Type", "text/javascript");
    }
})
W
function rewriteImport(content) {
  return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1){
    if (s1.startsWith('./') || s1.startsWith('/') || s1.startsWith('../')) {
      return s0
    } else {
      return ` from '/@modules/${s1}'`
    }
  })
}

//读取模块(重写裸模块)
app.use(async (ctx) => {
  const url = ctx.request.url
  if (url === '/') {
    // ...
  } else if (url.endsWith('.js')) {
    // ...
    const ret = fs.readFileSync(p, 'utf-8')
    // 重写裸模块导入部分,字符串替换
    ctx.body = rewriteImport(ret)
  }
    //处理第三方包,也就是裸模块的请求
    else if (url.startsWith('/@modules')) {
      const moduleName = url.replace("/@modules/", "");
      const prefix = path.join(__dirname, "../node_modules", moduleName);
      //获取真正要加载的js文件,等加载完以后里面可能还有裸模块请求
      //无限递归下去
      const module = require(prefix + "/package.json").module;
      const filePath = path.join(prefix, module);
      const ret = fs.readFileSync(filePath, "utf8");
      ctx.type = "text/javascript";
      ctx.body = rewriteImport(ret);
  }
//识别.vue文件,把他变成浏览器认识的js语法

//用到两个方法
const compilerSfc = require("@vue/compiler-sfc");
const compilerDom = require("@vue/compiler-dom");

    else if (url.indexOf('.vue') > -1) {
       // SFC路径
            const p = path.join(__dirname, url.split("?")[0]);
        const ret = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
            // SFC文件请求
        if (!query.type) {
          const scriptContent = ret.descriptor.script.content
          const script = scriptContent.replace('export default ', 'const __script = ')
          // 返回App.vue解析结果
          ctx.type = 'text/javascript'
          ctx.body = `
            ${rewriteImport(script)}
            //让template再发一次请求
            import { render as __render } from '${url}?type=template'
            __script.render = __render
            export default __script
          `
        }
        //模板请求(转化为render函数)
        //模板预编译(其实在启动服务器的时候,模板已经不存在了,模板的编译不是在运行时)
        else if (query.type === 'template') {
          // 模板内容
          const template = ret.descriptor.template.content
          // 编译为render
          const render = compilerDom.compile(template, { mode: 'module' }).code
          ctx.type = 'text/javascript'
          ctx.body = rewriteImport(render)
        }
        //把样式解析加入到页面的head中
        else if(query.type === 'sttle'){

        }
    }

})




app.listen(5173, () => {
    console.log("vite dev serve listen on 5173");
})

打开第三方库的 package.json 的 module 字段中会写要真正读取或者加载的 js 文件地址

想要增加 css 解析

热更新(仅开发阶段)

//用if (import.meta.hot)包一下,在生产阶段会被移除
if (import.meta.hot) {
  //当这两个文件改变的时候,会重新运行函数
  import.meta.hot.accept(["./render.ts", "./state.ts"], (modules) => {
    const [renderModule, stateModule] = modules;

    if (renderModule) {
      renderModule.render();
    }

    if (stateModule) {
      stateModule.initState();
    }
  });
}

生产与开发打包

开发环境(Develop)

开发环境使用的是 es build,生产环境使用的是 rollup,但我们又需要干预开发环境,又需要干预生产环境的时候就需要在文档中查找共享配置

生产环境(Production)

构建应用程序:运行 npm run build 或类似的命令,Vite 会执行构建过程。

Rollup 打包:Vite 在生产环境中使用 Rollup 进行打包。Rollup 是一个强大的 JavaScript 模块打包器,它将应用程序中的所有模块打包成一个或多个 bundle 文件,以便在生产环境中使用。

代码压缩和优化:Rollup 可以对代码进行压缩和优化,例如删除未使用的代码、合并模块等,以减少最终生成的代码的大小。

生成最终的静态资源:在打包完成后,Vite 会生成最终的静态资源文件,包括 JavaScript 文件、CSS 文件和其他静态文件,这些文件可以直接部署到 Web 服务器上。

总结起来,Vite 在开发环境中使用即时编译,实现了快速的启动和热重载。而在生产环境中,Vite 使用 Rollup 进行打包和优化,生成最终的静态资源文件,以便于部署到 Web 服务器上。这种开发和构建的方式使得 Vite 在开发过程中具有高效和快速的特性,同时在生产环境中提供了优化的打包结果。

Rollup 是什么?

Rollup 是一个现代化的 JavaScript 模块打包器,专注于打包 JavaScript 库和应用程序。它采用了 ES 模块的标准,并支持各种 JavaScript 模块格式,包括 ES modules、CommonJS、AMD 等。Rollup 的主要目标是生成高效、精简的代码包,以便于在浏览器环境中运行。

对于 Vue 文件的打包,Rollup 结合了其他插件和工具来处理不同类型的模块和资源。以下是 Rollup 打包 Vue 文件的一般过程:

解析 Vue 文件:Rollup 通过特定的插件(如 rollup-plugin-vue)解析 Vue 文件。这个插件能够识别 Vue 文件的特殊语法和结构,提取其中的模板、样式和脚本部分。

处理模板:Vue 的模板部分通常是使用 Vue 的模板语法编写的。Rollup 会将 Vue 模板转换为 JavaScript 代码,并生成可执行的函数。这样,在运行时,模板就可以被渲染成实际的 HTML。

处理样式:Vue 的样式部分通常是使用 CSS 预处理器编写的,如 Less、Sass 等。Rollup 会使用相应的插件(如 rollup-plugin-less、rollup-plugin-sass 等)来处理样式文件,并将其转换为浏览器可识别的 CSS。

处理脚本:Vue 的脚本部分通常是使用 ES6+的 JavaScript 代码编写的。Rollup 会使用 Babel 等工具来处理脚本文件,将其转换为向后兼容的 JavaScript 代码,以确保在目标浏览器中的兼容性。

打包输出:在处理完 Vue 文件的各个部分后,Rollup 会将它们组合成一个或多个 JavaScript 文件,并根据配置进行代码的合并、压缩和优化。这样就生成了最终的打包文件,包含了 Vue 组件的代码和相关依赖。

总结起来,Rollup 是一个现代化的 JavaScript 模块打包器,通过插件和工具的组合来处理 Vue 文件的各个部分(模板、样式、脚本),将它们转换为浏览器可识别的格式,并最终生成最小化、优化的打包文件。这样,Vue 应用程序可以在浏览器中加载和运行。

vite 的 css module 和 Scoped CSS 的两者区别

CSS Modules Scoped CSS
需要在 vue.config.js 中额外配置 Vue Loader 默认支持,无需额外配置
通过根据配置的类命名规则,为元素生成独一无二的类名来实现作用域隔离 通过给元素自定义 hash 属性,再使用属性选择器选中元素来实现作用域隔离
在 style 标签中声明 module 在 style 标签中声明 scoped
支持导入其他 module 的样式,支持样式组合 /
通过:global()来解除作用域的隔离,使样式在全局生效 1. 可以定义全局样式,使样式不受作用域约束
2. 可以通过深度作用选择器命中子组件,从而控制子组件的样式

css module

<template>
  <div class=".src-views-login-Index__example">hi</div>
</template>

vue scoped css

<style>
.example[data-v-f3f3eg9] {
  color: red;
}
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

webpack 与 vite

预设场景两者不一样

webpack 支持多种模块化,你的工程可能不只是跑在浏览器端,关注更多的兼容性,他是一个纯打包工具

vite 是基于 es modules 的, 只关注浏览器端的开发体验,是一个更上层的工具链方案

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

原代码
const lodash = webpack_require("lodash");
const Vue = webpack_require("vue");

// 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 为什么比 webpakck 快

webpack 原理图

vite 原理图

webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。
vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。(vite 是用 koa 启动的一个服务器)

由于现代浏览器本身就支持 ES Module,会自动向依赖的 Module 发出请求。vite 充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 webpack 那样进行打包合并。(webpack 是在浏览器中执行打包以后的 js,这就导致了利用 webpack 构建工具去开发一定比较慢)
由于 vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。(修改下路径,解析下 vue 文件)这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite 的优势越明显。(他会把 css 变成一个字符串,vue 文件会变成一个纯 js)
虽然不打包,请求的文件会比不打包要多,但请求的东西都在本地,所以依然非常快,而且依赖 es module 现代浏览器的情况也只是在开发人员的电脑上开发服务器的要求,大大增加了开发效率。

在 HMR 方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高。

vite 的主要优势在开发阶段,当需要打包到生产环境时,vite 使用传统的 rollup 进行打包,这样也使得开发环境的配置和打包环节完全解耦

由于 vite 利用的是 ES Module,因此在代码中不可以使用 CommonJS