vite 个人纪录备份new

80 阅读36分钟

什么是构建工具

浏览器他只认识html, css, js

构建工具具体做了哪些事情:

  1. typescript: 如果遇到ts文件我们需要使用tsc将typescript代码转换为js代码
  2. React/Vue: 安装react-compiler / vue-complier, 将我们写的jsx文件或者.vue文件转换为render函数
  3. less/sass/postcss/component-style: 我们又需要安装less-loader, sass-loader等一系列编译工具
  4. 语法降级: babel ---> 将es的新语法转换旧版浏览器可以接受的语法
  5. 体积优化: uglifyjs ---> 将我们的代码进行压缩变成体积更小性能更高的文件
  6. 热更新: 稍微改一点点东西, 非常麻烦 将App.tsx ---> tsc ---> App.jsx ---> React-complier ---> js文件 (构建工具会帮你自动监听文件的变化, 当文件变化以后自动帮你调用对应的集成工具进行重新打包, 然后再浏览器重新运行(整个过程叫做热更新, hot replacement)
  7. 代码分割:按需加载、代码分割、防止重复
  8. 模块化开发支持: 支持直接从node_modules里引入代码 + 多种模块化支持
  9. 开发服务器: 跨域的问题, 用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…

  • webpack 是将所有的依赖全部读一遍(为什么全部读一遍?因为,一开始必须要统一模块化代码), 并进行打包,后才会启动服务器:

    image.png

  • vite 先开启服务器, 加载当前页面,并且按需加载当前依赖, 未加载页面不会加载其所需的依赖:

    image.png

第一:开发模式不同

webpack

启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,这可满满都是 CPU、IO 操作啊,在 Node 运行时下性能必然是有问 题。

vite

  1. 而 Vite 是通过在一开始将应用中的模块区分为 依赖源码 两类,改进了开发服务器启动时间。

  2. 依赖 大多为纯 JavaScript 并在开发时不会变动。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模块中。

  3. Vite 将会使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。

  4. 源码 通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载。(例如基于路由拆分的代码模块)。

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

第二:打包效率不同

webpack

会把所有的模块打包成一个bundle,这会导致初次加载速度较慢

vite

而Vite则利用了浏览器对ES Module的原生支持,只打包和缓存实际改动的模块,从而极大提高了打包效率,不同于开发模式下使用的Esbuild, 而打包时使用的 rollup

第三:插件生态不同

webpack

Webpack的插件生态非常丰富,有大量社区和官方插件可以选择,覆盖了前端开发的各个方面

vite

而Vite的插件生态尽管在不断发展,但相比Webpack来说还显得较为稀少。

第四:配置复杂度不同

webpack

Webpack的配置相对复杂,适合复杂的自定义配置的项目,更灵活。

vite

而Vite在设计上更注重开箱即用,大部分场景下用户无需自己写配置文件,其大部分的配置文件都已经内置好了,所以无需配置也可直接使用。

第五:热更新机制不同

webpack

  • 全局刷新: Webpack 的 HMR 通常会在代码发生变化时替换整个模块,然后通过 HMR runtime 来触发页面级别的刷新或重新渲染,这可能导致部分页面状态的丢失。

  • 较慢的冷启动: Webpack 的开发模式下,由于整个应用程序需要被打包和构建,因此冷启动时间相对较长,尤其是随着项目规模的增大,冷启动时间会进一步延长。

  • 配置复杂: 在使用 Webpack 进行 HMR 时,需要进行一定的配置,包括设置 HMR 插件、编写 HMR 相关的代码等,相对来说略显复杂。

vite

  • 局部更新: Vite 的 HMR 实现了更细粒度的模块更新,可以实现局部模块的更新而无需刷新整个页面,从而减少页面状态的丢失。

  • 快速冷启动: Vite 利用现代浏览器的原生 ES 模块加载能力,实现了快速的冷启动时间,因为它不需要将整个应用程序打包成一个或多个文件。

  • 无需额外配置: 在 Vite 中,HMR 是开箱即用的,无需额外配置。Vite 会自动处理模块的更新和热替换,让开发者专注于业务逻辑的开发。

vite开发环境

  • 利用浏览器原生的ES Module编译能力,省略费时的编译环节,直给浏览器开发环境源码,dev server只提供轻量服务。
  • 浏览器执行ESM的import时,会向dev server发起该模块的ajax请求,服务器对源码做简单处理后返回给浏览器。
  • Vite中HMR是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块失活,使得无论应用大小如何,HMR 始终能保持快速更新。
  • 使用esbuild处理项目依赖,esbuild使用go编写,比一般node.js编写的编译器快几个数量级。

vite生产环境

  • 集成Rollup打包生产环境代码,依赖其成熟稳定的生态与更简洁的插件机制

vite 初始化项目

  • 理解vite脚手架 & vite

    1. vite 脚手架 官方文档

      当我们敲了yarn create vite

      首先:帮我们全局安装一个东西:create-vite(vite的脚手架)

      其次:直接运行这个create-vite bin目录下的一个执行配置

      误区:认为官网中使用对应yarn create 构建项目的过程也是vite在做的事情

      create-vite和vite的关系是什么呢? --- create-vite内置了vite

    2. 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 依赖预构建

  • 目的:

  1. CommonJS 和 UMD 兼容性:  在开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块,预构建这一步由 esbuild 执行。

    在转换 CommonJS 依赖项时,Vite 会进行智能导入分析,这样即使模块的导出是动态分配的(例如 React),具名导入(named imports)也能正常工作:

    // 符合预期
    import React, { useState } from 'react'
    
  2. 性能:  为了提高后续页面的加载性能,Vite将那些具有许多内部模块的 ESM 依赖项转换为单个模块。

    有些包将它们的 ES 模块构建为许多单独的文件,彼此导入。例如,lodash-es 有超过 600 个内置模块!当我们执行 import { debounce } from 'lodash-es' 时,浏览器同时发出 600 多个 HTTP 请求!即使服务器能够轻松处理它们,但大量请求会导致浏览器端的网络拥塞,使页面加载变得明显缓慢。

    通过将 lodash-es 预构建成单个模块,现在我们只需要一个HTTP请求!

第二:预构建 缓存

  • 文件系统缓存:

    • Vite 将预构建的依赖项缓存到 node_modules/.vite 中。它会基于以下几个来源来决定是否需要重新运行预构建步骤:

    • 包管理器的锁文件内容,例如 package-lock.jsonyarn.lockpnpm-lock.yaml,或者 bun.lockb

    • 补丁文件夹的修改时间;

    • vite.config.js 中的相关字段;

    • NODE_ENV 的值。

    只有在上述其中一项发生更改时,才需要重新运行预构建。

  • 浏览器缓存:

    • 已预构建的依赖请求使用 HTTP 头 max-age=31536000, immutable 进行强缓存,以提高开发期间页面重新加载的性能。

第三:vite 配置文件的语法提示

  • 因为 Vite 本身附带 TypeScript 类型,所以你可以通过 IDE 和 jsdoc 的配合来实现智能提示
/** @type {import('vite').UserConfig} */ 
export default { 
  // ... 
}
  • 另外你可以使用 defineConfig 工具函数,这样不用 jsdoc 注解也可以获取类型提示:
import { defineConfig } from 'vite'

export default defineConfig({
    // ...
})

第四:环境变量和模式

  • 环境变量:

    • Vite 在一个特殊的 import.meta.env 对象上暴露环境变量

    • 内建变量:

      1. import.meta.env.MODE: {string} 应用运行的模式

      2. import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。

      3. import.meta.env.PROD: {boolean} 应用是否运行在生产环境(使用 NODE_ENV='production' 运行开发服务器或构建应用时使用 NODE_ENV='production' )。

      4. import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。

      5. 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 文件:

    • Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量:
     .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_ENVprocess.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 配置文件

  • 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…)

    image.png

    image.png

    • 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
             }),
           ],
         },
     }
     ```
    
  • vite.config.js 中 CSS 配置【preprocessorOptions(预处理器)】

    • 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 预处理器的选项

    image.png

  • vite.config.js 中 css 配置【postcss(兼容)】

    • 实际上,PostCSS 的主要功能只有两个:

      • 第一个就是前面提到的把 CSS 解析成 JavaScript 可以操作的 AST,

      • 第二个就是调用插件来处理 AST 并得到结果(简单说:提供了一个庞大的插件生态系统来执行不同的功能,常见的功能:css语法降级、前缀补全、嵌套、函数、变量、自动补全 等等)。

    • 在vite中使用postcss

      image.png

      image.png

      image.png

  • 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"),
        },
      },
    
    
  • 打包后的静态资源为什么要有hash

    1. 浏览器是有一个缓存机制 静态资源名字只要不改, 那么他就会直接用缓存的,
    2. 当文件内容未发生改变那么 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() // 插件模拟服务器
      ],
    
  • vite.config.js 中 依赖优化选项 配置

  • vite.config.js 其他配置

  • 【自定义创建插件】 部分插件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!');`;
      
              },
      
          };
      
      }
      
  • typeScript 在 vite 相关配置

    介绍: Vite 仅执行 .ts 文件的转译工作,并不执行 任何类型检查

    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文件里面。包含咱们自己开发业务逻辑和第三方库。

    • 问题:

      随着项目越来越大,变动比较大的是我们自己的业务代码,而这些第三方库,变动比较小,是相对比较稳定的,每次咱们业务代码更新,第三方库未变,且全在一个文件中。导致打包后文件名变化,需要重新请求。为了解决这个问题,我们需要进行分包。

      image.png

  • 分包配置

    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'],
             },
           },
         }
       },
     });
    
     ```
    

image.png

  1. 第二种:将静态包打包一个文件中,

    优点:减少了请求次数,只需要请求一次就可以 缺点:静态库多,都打包到一个文件中,导致该文件容易太大

    export default defineConfig({
        build: {
            rollupOptions: {
              output: {
                // manualChunks 配置
                manualChunks: ( filePath ) => {
                   if(filePath.includes("node_modeules")){
                      return "vendor"
                   }
                },
              },
            }
          },
        });
       ```
    

image.png

  • CSS 代码分割 分包策略:

    • Vite 会自动地将一个异步 chunk 模块中使用到的 CSS 代码抽取出来并为其生成一个单独的文件。这个 CSS 文件将在该异步 chunk 加载完成时自动通过一个 <link> 标签载入,该异步 chunk 会保证只在 CSS 加载完毕后再执行,避免发生 FOUC 。

    • 如果你更倾向于将所有的 CSS 抽取到一个文件中,你可以通过设置 build.cssCodeSplit 为 false 来禁用 CSS 代码分割。

  • gzip压缩

    • 当我们某一个文件打包后依然很大时候,这就会导致http传输性能会导致损耗,所有我们需要进行gzip压缩, 这样前端可以将一些大文件静态资源在前端提前打包成为gzip,这样可以帮助后端减少打包

    • 后端服务器也可以压缩前端文件,缺点会用服务器内存

    • 服务器读取gzip文件设置一个响应头 content-encoding,浏览器收到响应结果 发现响应头中有gzip对应字段,会进行解压,得到原来文件。 注意:浏览器是要承担一定的解压时间

    • vite中可以使用 vite-plugin-compression插件

    import viteCompression from 'vite-plugin-compression';
    export default defineConfig({
        plugins:[viteCompression()]
    })
    
  • 动态导入

    • 可以将代码分割

    image.png

    • 一般使用在路由中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里面
      
  • CDN 加速

    主要思路: 尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。

    实现方法: 通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接和负载状况以及到用户的距离响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上,加快访问速度。

    目的: 使用户可就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度

    优势:

    1. CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低;
    2. 大部分请求在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 时不能解构,只能直接引入,这也说明这种方式不适合那些需要解构资源的 CDN

      import { 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 地址的时候:

    1. 浏览器会先给我们拼接成为:当前项目地址运行 + /api/getUserInfo 例如:( http://127.0.0.1:5173/api/getUserInfo ) ,拼接完成以后,会去找 vite , vite 发现这个path有配置过跨域代理策略,然后他会根据策略的描述对象,进行再次请求。根据上面代码路径会变成 ( jsonplaceholder.typicode.com/getUserInfo

    2. 上面也就是浏览器会先做一次请求地址处理因为有跨域配置,所有本地开发服务器会做第二次请求地址处理。并在本地服务器中发起请求, 服务器与服务器之间请求资源(由于没有使用浏览器故不会发生跨域),然后开发服务器把获取到的资源 返回给浏览器 ,因为开发服务器和客户端是同源的,于是浏览器会把开发服务器请求到的资源传递给客户端器。

    image.png

    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; // 将获得的的数据发送给浏览器
    }
    

第七:导入资源 (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比webpack快 的原因之一

前言:ES 模块的特点是,在script标签中设置type=module 之后,浏览器可以直接使用exportimport的方式实现导入模块和导出模块了。效果如下:

//a.js 
export const message = 'hello world'; 
<script type="module"> 
  import { message } from './a.js' 
</script>

script标签中的代码执行时,浏览器会发起http请求获得a.js

Vite是如何使用ES模块的呢?

image.png

image.png

image.png

Webpack的编译打包方式很难做到按需加载,会将所有的资源打包到一个boundle文件中。而随着项目的业务内容增多,打包的boundle文件也会越来越大。为了减小boundle的体积,引入import()的方式来实现按需加载,但是这样的方式引入的代码还是需要提前打包。后来使用webpack的tree shaking功能删除没有使用的代码块。

Vite 省去了打包的过程,直接从浏览器中按照ES 模块的方式获得了代码。

【原理篇】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中能够拿到最终的样式)

【原理篇】开发服务器 & vite是怎么让浏览器可以识别.vue文件的

  • 开发服务器

    1. 第一步 创建并绑定端口
    // 不能用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");
    })
    
    1. 第二步 读取文件并操作文件,返回给客户端
    // 不能用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");
    })
    

.vue 的文件拆成了二个请求及文件(分别对应 scripttemplate 和style

image.png

  • 【原理篇】Vite 热更新的实现

热更新大致可以分为一下步骤:

  • 服务端基于watcher监听文件改动,根据类型判断更新方式,并编译资源

  • 客户端通过WebSocket监听到一些更新的消息类型

  • 客户端收到资源信息,根据信息类型执行不同的更新逻辑

    1. .js文件 执行update逻辑,会热更新

    2. .html 和 (.js 文件未找到引用者)执行full-reload逻辑,会调用location.load() 让客户端重新加载页面, 不会在热更新, 这里做了协商缓存

    3. config文件/.env文件 执行restartServer,vite会直接重启服务器

image.png

热更新流程详细

根据 Vite 源码整理出了一个大致流程,总体就是 Vite Server 监听到文件变化,通过 WebSocket 向 Vite Client 发送通知,Client 根据通知内容解析,发起 HTTP 请求获取更新后的文件,最后局部更新页面。

image.png

话说回来,Vite Server 和 Vite Client 是指什么呢?

我们可以先看下 Vite 代码的目录结构:

src
├── client
│   └── ...
└── node
    └── ...

总体分为 client 和 node 两个目录结构,开发时我们是通过 vite dev 启动项目的,这里其实就是启动了一个 node 服务,其与 HMR 相关的功能主要有两个:

  1. 监听项目文件变化
  2. 启动 WebSocket 服务,向客户端主动推送消息

另外一个就是 Vite Client,比如项目中有入口 index.html 文件,在实际请求时返回的 html 内容与实际在项目中写的并不相同:

<script type="module" src="/@vite/client"></script>

Vite 通过这种方式向我们的代码中注入 Vite Client,其与 HMR 相关的功能主要有两个:

  1. 监听 WebSocket 消息,解析后发起文件请求
  2. 执行 import.meta.hot 中定义的钩子函数

client.js 相关代码

  • vite会创建一个client.js文件,合并UserConfig配置,通过transformIndexHtml钩子函数,在转换index.html的时候,把生成client的代码注入到index.html中,这样在浏览器端访问index.html就会加载client生成代码,创建client客户端与webSocket服务端建立connect链接,以便于接受webScoket服务器信息

  • 在Vanilla中 client.js image.png

  • 在vue中 client.js image.png

  • client.js 相关代码

    
    // Listen for messages
    socket.addEventListener('message', async({data})=>{
        handleMessage(JSON.parse(data));
    }
    
    // 不同文件热更新,执行不同相关代码
    async function handleMessage(payload) {
      switch (payload.type) {
        case 'connected':
            console.debug(`[vite] connected.`);
            hmrClient.messenger.flush();
            // proxy(nginx, docker) hmr ws maybe caused timeout,
            // so send ping package let ws keep alive.
            setInterval(()=>{
                if (socket.readyState === socket.OPEN) {
                    socket.send('{"type":"ping"}');
                }
            }
            , 30000);
            break;
        case 'update':
            notifyListeners('vite:beforeUpdate', payload);
            if (hasDocument) {
                // if this is the first update and there's already an error overlay, it
                // means the page opened with existing server compile error and the whole
                // module script failed to load (since one of the nested imports is 500).
                // in this case a normal update won't work and a full reload is needed.
                if (isFirstUpdate && hasErrorOverlay()) {
                    window.location.reload();
                    return;
                } else {
                    if (enableOverlay) {
                        clearErrorOverlay();
                    }
                    isFirstUpdate = false;
                }
            }
            await Promise.all(payload.updates.map(async(update)=>{
                if (update.type === 'js-update') {
                    return hmrClient.queueUpdate(update);
                }
                // css-update
                // this is only sent when a css file referenced with <link> is updated
                const {path, timestamp} = update;
                const searchUrl = cleanUrl(path);
                // can't use querySelector with `[href*=]` here since the link may be
                // using relative paths so we need to use link.href to grab the full
                // URL for the include check.
                const el = Array.from(document.querySelectorAll('link')).find((e)=>!outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl));
                if (!el) {
                    return;
                }
                const newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes('?') ? '&' : '?'}t=${timestamp}`;
                // rather than swapping the href on the existing tag, we will
                // create a new link tag. Once the new stylesheet has loaded we
                // will remove the existing link tag. This removes a Flash Of
                // Unstyled Content that can occur when swapping out the tag href
                // directly, as the new stylesheet has not yet been loaded.
                return new Promise((resolve)=>{
                    const newLinkTag = el.cloneNode();
                    newLinkTag.href = new URL(newPath,el.href).href;
                    const removeOldEl = ()=>{
                        el.remove();
                        console.debug(`[vite] css hot updated: ${searchUrl}`);
                        resolve();
                    }
                    ;
                    newLinkTag.addEventListener('load', removeOldEl);
                    newLinkTag.addEventListener('error', removeOldEl);
                    outdatedLinkTags.add(el);
                    el.after(newLinkTag);
                }
                );
            }
            ));
            notifyListeners('vite:afterUpdate', payload);
            break;
        case 'custom':
            {
                notifyListeners(payload.event, payload.data);
                break;
            }
        case 'full-reload':
            notifyListeners('vite:beforeFullReload', payload);
            if (hasDocument) {
                if (payload.path && payload.path.endsWith('.html')) {
                    // if html file is edited, only reload the page if the browser is
                    // currently on that page.
                    const pagePath = decodeURI(location.pathname);
                    const payloadPath = base + payload.path.slice(1);
                    if (pagePath === payloadPath || payload.path === '/index.html' || (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)) {
                        pageReload();
                    }
                    return;
                } else {
                    pageReload();
                }
            }
            break;
        case 'prune':
            notifyListeners('vite:beforePrune', payload);
            await hmrClient.prunePaths(payload.paths);
            break;
        case 'error':
            {
                notifyListeners('vite:error', payload);
                if (hasDocument) {
                    const err = payload.err;
                    if (enableOverlay) {
                        createErrorOverlay(err);
                    } else {
                        console.error(`[vite] Internal Server Error\n${err.message}\n${err.stack}`);
                    }
                }
                break;
            }
        default:
            {
                const check = payload;
                return check;
            }
        }
    }
    
  • 【原理篇】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.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;
        }
    
  • 【手写篇】 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的网络) 插件 配置

    1. 项目跟目录下面创建 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
             };
           }
         },
       ]
    
    1. 手写 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
           },
         };
       };
    
  1. 在配置中调用创建的VitePluginMock插件

         plugins: [
            VitePluginMock(),
        ]
    
  2. 请求 mock 中 /api/users 网络地址,顺利获取到返回数据

        fetch("/api/users", {
          method: "post"
        }).then(data => {
          console.log("data", data);
        }).catch(error => {
          console.log("error", error);
        })