重学 Vite

759 阅读14分钟

Vite

依赖预构建

解决三个问题

1、不同的第三方包会有不同的模块规范(commonjs,esmodule)

2、对包路径的处理,统一使用**.vite/deps**,方便路径重写

3、网络多包传输性能问题(也是原生es module规范不支持node_modules的原因之一),有了依赖预构建以及 以后包中无论有多少额外的export import,vite 都会尽可能将这些modules集成,最后生成一个或几个module

关闭预构建

optimizeDeps:不参与依赖预构建的包名数组

Type:string[ ]

// 配置项...
optimizeDeps: {
    exclude: ['xxx'] // 数组元素:不参与依赖预构建的包名
 }

vite 配置文件

vite.config.ts

import { defineConfig } from 'vite'
import viteBaseConfig from './vite.base.config'
import viteDevConfig from './vite.dev.config'
import viteProdConfig from './vite.prod.config'

// 策略模式
const devResolver = {
  build: () => {
    console.log('build', 'production');
    return ({ ...viteBaseConfig, ...viteProdConfig })
  },
  serve: () => {
    console.log('serve', 'development');
    return Object.assign({}, viteBaseConfig, viteDevConfig)
  },
}

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

环境变量与模式

环境变量:会根据当前代码环境产生值的变化的变量

代码环境:1、开发环境,2、测试环境,3、预发布环境,4、灰度环境,5、生产环境等

vite 中的环境变量

vite 内置了dotenv module

dotenv: 自动读取项目下的.env文件,并解析,.env文件内的环境变量,并将其注入 node全局对象 process 下,但是vite考虑到和其他配置的一些冲突问题,不会直接注入到 process 对象上

此时涉及到 vite.config.js 的配置

—envDir:用来配置当前环境变量所在的文件地址

Type:string

Vite 提供了补偿措施:vite 原生方法 loadEnv 可以帮助我们确认是否是正确的env文件

/**
 * @description: vite 配置文件
 * @param {string} mode 环境名称
 */

export default defineConfig(({ mode }) => {
  // console.log(process.env); // 这里是没有我们设置的环境变量
  
   /**
   * @description: 加载环境变量
   * @param {string} mode 环境名称
   * @param {string} root env环境变量所在的文件地址(绝对路径)
   * @param {string} [prefixes] env文件名
   * @return {Record<string, string>} 环境变量
   * @example
   * loadEnv('development', process.cwd())
   * // => { VITE_PORT: '3000', VITE_PUBLIC_PATH: '/' }
   */
  
  const env = loadEnv(mode, process.cwd(),'')
  console.log(env); // { VITE_APP_BASE_URL: '/api', ...}
})

process.cwd( ) 返回当前node进程的工作目录(绝对路径

环境变量文件名:

  • .env: 所有环境都用到的环境变量

  • .env.development : 开发环境用到的环境变量(vite 默认的开发环境 development)

  • .env.production: 生产环境用到的环境变量(vite 默认的生产环境 development)

  • .env.[mode]: mode 模式下用到的环境变量

环境变量生效方式

package.json

  "scripts": {
    "dev": "vite --mode development", //开发环境
    "build": "vite --mode production", // 生产环境
    "xxx":"vite --mode xxx", // xxx环境
  },

此时

export default defineConfig(({ mode }) => {
  // mode 与 package.json scripts -> vite --mode xxx强相关
  // mode = 'development' || 'production' || 'xxx'
})

此时,调用loadEnv 会发生如下事情:

1、直接读取.env 文件,并解析其中的环境变量,并放在一个对象里面(假定:baseEnvConfig)

2、读取.env.[mode] 文件,并解析其中的环境变量,并放在一个对象里面(假定:modeEnvConfig)

3、合并两个envConfig 并生成的最终的 lastEnvConfig 对象并使用

const baseEnvConfig = .env 文件里的环境变量对象
const modeEnvConfig = .env.[mode] 文件里的环境变量对象
const lastEnvConfig = {...baseEnvConfig,...lastEnvConfig}

我们如何在自己的代码里面去使用环境变量呢?

1、如果是客户端,vite 会将此时对应环境的变量注入到 import.meta.env 对象中,全js文件可访问,此时似乎也访问不了,我们需要的环境变量,是因为vite考虑到我们也许会将隐私性的变量,直接放在env文件中,我们需要以VITE_为前缀的变量命名,才能在import.meta.env 对象访问。

例如:

.env

APP_KEY = 110
VITE_BASE_URL=http://test.api/

此时,import.meta.env 对象中只能访问 VITE_BASE_URL,不能访问APP_KEY

前缀可修改

envPrefix: 可访问变量的前缀 默认值: VITE_

Type:string

  // 配置项...
    envPrefix: 'CHERRY_'

小知识:为什么vite.config.js,可以书写成es module 规范 ?

这是因为,vite 在读取 vite.config.js 时,会率先用node 去解析 vite.config.js 文件语法(字符串),如果发现时es module 规范,会直接将 es module 规范,进行替换成commonjs规范(String.prototype.replace( ))

Vite中的css

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

  • 1、vite在读取main.js 中引入的index.css

  • 2、直接使用node fs内置模块去读取index.css文件内容

  • 3、直接创建一个style标签,将index.css中读取的内容直接copy进style标签内

  • 4、将style 标签 直接插入index.html的head中

  • 5、将改css文件中的内容直接替换成js脚本(方便热更新,css模块化),并将文件响应头Content-Type 设置为text/js,浏览器可直接将其解析成js脚本执行.

但是当我们协同开发的时候,我们的class类名也许会冲突!

我们可以用css module 来解决这个问题

原理:

  • 1、.module.css( module 表示一种约定,表示开启css模块化 )

  • 2、模块化会将你的所有类名进行一定规则的替换(将footer 替换成 footer[hash值])

  • 3、同时构建一个映射关系对象 { footer: footer[hash值] }

  • 4、将替换后的内容塞进style标签里面此时style 中的类名footer[hash值] 类似,并插入head中

  • 5、将改.module.css文件中的内容直接替换成js脚本(方便热更新,css模块化),并将文件响应头Content-Type 设置为text/js,浏览器可直接将其解析成js脚本执行

  • 6、将创建的的映射关系对象在脚本中进行默认到处

vite.config.js 中 css配置

在vite.config.js 我们可以通过css option 去控制整个vite中css的处理行为

Modules

Modeles 选项用于配置css模块化的行为,最终会交给postcss处理

localsConvention

localsConvention 表示到处的映射关系对象的key 也就是类名的语法形式(中划线 或者 驼峰命名),默认值:dashes (中划线)

默认导出的映射关系对象

{
  footer-content:footer_sst01
}

当localsConvention 的值为camelCase,允许css 类名为驼峰命名法

  css: {
    modules: {
       // 生成的映射关系对象的key
      localsConvention: 'dashes', // "camelCase" | "camelCaseOnly" | "dashes" | "dashesOnly"
    }
  }

映射关系对象:

{
  footer-content:footer_sst01,
  footerContent:footer_sst01
}

这里可以做一个性能优化,[ camelCase|dashes] Only, 仅生成其中一种类名的语法形式

scopeBehaviour

scopeBehaviour 表示是否开启模块化样式 默认值:local(开启)

Type : 'local' | 'global' default: 'local'

  css: {
    modules: {
      // 生成的映射关系对象的key的语法形式
       scopeBehaviour: 'local' // 'local' | 'global'  default: 'local' 开启模块化样式 会影响全局样式
    }
  }

generateScopedName

generateScopedName: 构建出来的样式 类名的规则 可以传函数类型 详情见 postcss

Type : string | ((name: string, filename: string, css: string) => string)

 css: {
    modules: {
      // 生成的映射关系对象的value的格式
       generateScopedName: "[name]_[local]_[hash:5]" // ()=>(string)
    }
  }

hashPrefix

hashPrefix: 将你给的字符串打乱到生成的hash class 类名中去,增强类名唯一性

Type : string

 css: {
    modules: {
       hashPrefix: "cherry"
    }
  }

globalModulePaths

globalModulePaths: 表示不想参与css模块化的css文件路径(绝对路径

Type : string[ ]

 css: {
    modules: {
       globalModulePaths: [path.resolve(__dirname, 'src/styles/global.css')]
    }
  }

preprocessorOptions

preprocessorOptions css 预处理器的配置

Type : Record<string, any>

 css: {
    preprocessorOptions: {
      less: {
        // ... 该less配置会传递给less-loader
      },
      scss: {
        // ... 该scss配置会传递给sass-loader
      },
    },
  }

less 常见配置

math: 表示是否开启less的数学运算

Type : 'always' | 'parens-division' | 'parens' | 'strict' 例: 100px / 2 -> 50px

  css: {
      preprocessorOptions: {
        less: {
          math: 'always', // 默认值:always 该配置会启用less的数学运算
        },
      },
    }

globalVars: 表示全局样式变量

Type : Record<string, string>

  css: {
      preprocessorOptions: {
        less: {
          globalVars: {
            primary: '#333',
          },
        },
      },
    }

devSouseMap

devSouseMap: 表示是否开启css的sourcemap,即开启css文件索引,方便调试,默认值:false

Type : boolean

  css: {
      preprocessorOptions: {
        // ...
        devSouseMap: true, // 默认值:false
      },
    }

postcss

  • 1、对未来css属性的兼容性处理(降级)

  • 2、前缀补全 (-webkit- -moz- -ms- -o-)

  • 3、丰富的插件系统 供给流水线的每个阶段使用

  • 4、css 语法检查

小知识:在node中读取文件的时候,如果文件中是相对路径,那么会以当前node进程中的工作的文件目录( process.cwd())为基准,去拼接相对路径,去查找文件,如果是绝对路径,那么会以根目录为基准,去查找文件

Vite中的静态资源处理

Vite中的静态资源是开箱即用的,不需要任何配置,只需要在代码中引入静态资源,Vite会自动处理

静态资源的引入

性能优化点:Tree Shaking ( 万不得已不要全体导入 ,最好使用解构导入 )摇树 优化

Tree shaking是 JavaScript 上下文中用于消除死代码的常用术语。它依赖于 ES2015 模块语法的静态结构,如果当你整体导入一个模块的时候,Vite无法知道你到底用了模块中的哪些内容,所以就无法进行Tree Shaking

// 1、导入整体
import * as _ from 'lodash'
// 2、导入默认
import _ from 'lodash'
// 3、导入解构
import { debounce } from 'lodash' // 有 Tree Shaking 效果
  // 导入图片路径 xxx.png?url
 import logo from './assets/logo.png'  // 此时logo就是一个图片的路径 /src/assets/logo.png
 // 原始数据
  import logo from './assets/logo.png?raw' // 此时logo就是一个图片的二进制数据

路径别名

  // vite.config.ts
  import { defineConfig } from 'vite'
  import path from 'path'

  export default defineConfig({
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
      },
    },
  })
  // 使用路径别名
  import logo from '@/assets/logo.png'

生产环境的打包

Vite在生产环境下,会自动对静态资源进行打包,打包后的文件会放在dist目录下

打包后的资源为啥会有hash值?

  • 1、浏览器缓存(浏览器的缓存机制:当静态资源名字不变,他就会直接使用缓存)

  • 2、防止文件名重复

  • 3、防止文件内容被篡改

当我们文件名不变,但是文件内容变化了,那么浏览器就会认为文件没有变化,就会直接使用缓存,这样就会导致我们的页面不会更新,所以我们需要在文件名中加入hash值,这样就可以保证文件名不变,文件内容变化了,那么hash值就会变化,浏览器就会认为文件变化了,就会重新请求文件

Vite中的打包配置

兼容rollup的配置

    build: { // 打包配置
      rollupOptions: { // 用于rollup配置最终打包的选项
        output: { // 用于配置最终打包的输出
          assetFileNames: '[hash]_[name].[ext]', // 静态资源文件名
        }
      },
      assetsInlineLimit: 4096, // 小于4kb的静态文件会被转成base64
      // brotliSize: false, // 是否开启brotli压缩
      outDir: 'dist', // 打包输出目录
      assetsDir: 'assets', // 静态资源目录
  }

rollupOptions

rollupOptions是用于rollup配置最终打包的选项

  // rollupOptions
  rollupOptions: {
    output: {
      assetFileNames: '[hash]_[name].[ext]', // 静态资源文件名
    }
  }

assetsInlineLimit

assetsInlineLimit是用于配置小于多少kb的静态文件会被转成base64

  build:{
    assetsInlineLimit: 4096, // 小于4kb的静态文件会被转成base64
  } 

brotliSize

brotliSize是用于配置是否开启brotli压缩

  build:{
    brotliSize: false, // 是否开启brotli压缩
  } 

outDir

outDir是用于配置打包输出目录

  build:{
    outDir: 'dist', // 打包输出目录
  } 

assetsDir

assetsDir是用于配置静态资源目录

  build:{
    assetsDir: 'assets', // 静态资源目录
  } 

minify

minify 是否开启压缩

Type: boolean | "esbuild" | "terser" | undefined

  build:{
    minify: false, //关闭压缩
  } 

Vite中的插件

插件是什么?

插件是Vite的一个扩展,可以用来扩展Vite的功能 vite 会在生命周期中,根据配置的插件,依次执行插件中的方法以达到不同的目的

vite中的插件是一个对象

插件的使用

  // vite.config.ts
  import { defineConfig } from 'vite'
  import path from 'path'

  export default defineConfig({
    plugins: [
      // 插件
    ]
  })

热更新

热更新是什么?

热更新是指在不刷新页面的情况下,更新页面的内容, 也就是说,我们在开发过程中,修改了代码,页面不用刷新,就会直接更新页面的内容

热更新的原理

热更新的原理是通过websocket来实现的,当我们修改了代码,vite会将修改的代码的文件(此时hash name 已经改变)通过websocket发送给浏览器,浏览器接收到文件后,会直接使用此文件

Vite 构建优化

分包策略

浏览器华缓存策略: 当我们的文件名未发生变化时,浏览器并不会去重新请求资源,而是直接使用这个文件,此时引入hash值为文件名,当内容未发生改变,文件名就不会变化,反之,文件名会发生变化,浏览器就会重新请求资源。

所以当我们不会常规更新的文件,进行单独打包,这样就可以减少打包的体积,同时也可以减少浏览器的多次请求该资源(文件名字未变化)

    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            lodash: ['lodash'], // lodash是一个包名,后面是一个数组,数组中是需要单独打包的文件
          }
        }
      }
    }

  // 或者 写成一个函数调用
    build: {
      rollupOptions: {
        output: {
          manualChunks(id) { // id是每个文件(module)的路径
            if (id.includes('node_modules')) {
              return id.toString().split('node_modules/')[2].split('/')[0].toString()
            }
            // ...
          }
        }
      }
    }

gizp压缩

gizp压缩是一种压缩文件的方式,它可以将文件进行压缩,从而减少文件的大小,同时也可以减少浏览器的请求时间

  // vite.config.ts
  import { defineConfig } from 'vite'
  import { compression } from 'vite-plugin-compression2'

  export default defineConfig({
    plugins: [
      // ...your plugin
      compression()
    ]
  })

代码分割(动态导入)

代码分割是指将代码分割成多个文件,从而减少文件的大小,同时也可以减少浏览器的请求时间 常用于路由 懒加载 vite 中 直接使用import函数动态导入即可,webpack中需要使用webpack_require的魔法注释 webpack中使用魔法注释

  import(/* webpackChunkName: "about" */ './views/about.vue')

原理: webpack会将代码分割成多个文件,当我们使用魔法注释时,webpack会将这个文件打包成一个单独的文件,并且把代码塞入一个 script 标签中,从而实现代码分割,并且在进入这个页面时,才会去请求这个文件,也就是,webpack 会在内部 webpack_require.e().then(()=>{ webpack_require('./views/about.vue') }),当我们没有进入页面,这个promise永远是pending状态,所以不会去请求这个文件,当我们进入这个页面时,promise会变成resolve状态,把构建出来的script 标签,插入到body,就会去请求这个文件

 import home from './views/home.vue'

 const routes = [
   {
     path: '/',
     component: home
   },
   {
      path: '/about',
      component: () => import('./views/about.vue') // dynamic import 代码分割
   }
 ]

tree shaking

Tree Shaking ( 万不得已不要全体导入 ,最好使用解构导入 )摇树 优化 Tree shaking是 JavaScript 上下文中用于消除死代码的常用术语。它依赖于 ES2015 模块语法的静态结构,如果当你整体导入一个模块的时候,Vite无法知道你到底用了模块中的哪些内容,所以就无法进行Tree Shaking

scope hoisting

scope hoisting是指将代码中的函数和变量进行提升

CDN 加速(内容分发网络)

CDN加速是指将静态资源放到CDN上,从而减少浏览器的请求时间

  npm i vite-plugin-cdn-import -d
  // vite.config.ts
  import { defineConfig } from 'vite'
  import importToCDN, { autoComplete } from 'vite-plugin-cdn-import'
  export default defineConfig({
    plugins: [
      // ...your plugin
      importToCDN({
        modules: [
          autoComplete('lodash'),
        ]
      })
    ]
  })

Vite 跨域

跨域是指浏览器的同源策略,同源策略是指协议,域名,端口都相同(一般发生在浏览器请求的响应阶段)

Server代理

  // vite.config.ts
  import { defineConfig } from 'vite''

  export default defineConfig({
    server: {
      proxy: {
        '/api': {
          target: 'http://baidu.com',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, '')
        }
      }
    }
  })

例子:


fetch('/api/user').then(res => res.json()).then(data => console.log(data))

vite开发服务器 在匹配 req.url 为 /api 开头的请求时,会将其代理到目标服务器 baidu.com 此时 path 为baidu.com/api/user 同时会执行server.proxy.rewrite(path) (此处是将 /api 替换为空字符串),最终请求的地址为 baidu.com/user

代理解决跨域的原理: 因为跨越问题是在浏览器发出请求的响应阶段发生的, 浏览器先拼接完整的URL地址:http://http://127.0.0.1:5173/api/user,此时浏览器去请求的服务器地址是本地的vite开发服务器,开发服务器会根据vite.config.js 中 server.proxy 的配置,生成一个 目标服务器的网络请求地址 baidu.com/user 转发到目标服务器,此时 是服务器与服务器的通信故不会发生跨域问题,目标服务器返回的数据会再次经过开发服务器,然后再响应给浏览器( 这里为什么没有出现跨域问题?是因为网页的地址 与开发服务器地址 同源 )

此解决方式,仅适用开发环境

生产环境的处理

  • ngnix 代理服务 : 类似 vite 开发服务器

  • 配置身份标记

    • Access-Control-Allow-Origin : 表示那些源是允许访问的

    • xxx

未完待续...