Vite

什么是 vite

为什么用 vite

  • 开发环境启动快,热更新快。 Vite 以 原生 ESM 方式提供源码。这实际上是让浏览器接管了打包程序的部分工作

缺点

  • 为什么生产环境仍需打包 尽管原生 ESM 现在得到了广泛支持,但由于嵌套导入会导致额外的网络往返,在生产环境中发布未打包的 ESM 仍然效率低下(即使使用 HTTP/2)。为了在生产环境中获得最佳的加载性能,最好还是将代码进行 tree-shaking、懒加载和 chunk 分割(以获得更好的缓存)。

  • 为何不用 ESBuild 打包,目前使用 rollup 打包 虽然 esbuild 快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用 的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除使用 esbuild 作为生产构建器的可能。

vite 配置

export default defineConfig({
  server: {
    host: true,
    proxy: {
      '/icons': {
        target: 'http://xxx:3000', // 图标服务
        changeOrigin: true, //是否允许跨域
        rewrite: (path) => path.replace(/^\/icons/, ''),
      },
    },
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@use "./styles/variables.scss" as *;', // 自动注入 scss 变量
      },
    },
  },
})

插件

加载 svg 资源

const path = require('path')
const svgIconPlugin = require('vite-plugin-svg-icons')

// 创建 svg 图标,传入 svg 集合文件路径
function createSvgIcon(svgPath = 'src/icons/svg') {
  return svgIconPlugin.createSvgIconsPlugin({
    // 指定要缓存的图标文件夹
    iconDirs: [path.resolve(process.cwd(), svgPath)],
    // 执行icon name的格式
    symbolId: 'icon-[dir]-[name]',
  })
}

export default defineConfig({
  plugins: [ createSvgIcon() ]
})

自动导入/自动注册全局组件

// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    AutoImport({
      dts: './src/auto-imports.d.ts',
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      dts: './src/components.d.ts',
      resolvers: [ElementPlusResolver()],
    }),
  ]
})

打包优化

如果你需要在嵌套的公共路径下部署项目,只需指定 base 配置项,然后所有资源的路径都将据此配置重写。这个选项也可以通过命令行参数指定,例如 vite build --base=/my/public/path/

可视化分析 rollup-plugin-visualizer

// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'stats.html',
      gzipSize: true, // 收集 gzip 大小并将其显示
      brotliSize: true, // 收集 brotli 大小并显示
      // emitFile: true, // stats.html 是否生成在 dist 目录里
    })
  ],
})

分离公共依赖,提高打包速度 (提供在内网环境下的 CDN 加载方式)

  • 方式一: vite-plugin-cdn-import
// vite.config.ts
import importToCDN from 'vite-plugin-cdn-import'

export default defineConfig({
  plugins: [
    vue(),
    importToCDN({
      modules: [
        {
          name: 'vue',
          var: 'Vue',
          path: 'http://xxx/vue/vue@latest/dist/vue.global.prod.js',
        },
        {
          name: 'vue-demi',
          var: 'VueDemi',
          path: 'http://xxx/-/pkg/vue-demi/vue-demi@latest/dist/index.iife.js',
        },
        {
          name: 'element-plus',
          var: 'ElementPlus',
          path: 'http://xxx/-/pkg/element-plus/element-plus@latest/dist/index.full.js',
        },
      ],
    }),
    // 自定义插件:打包后将 index.html 文件中静态资源链接域名部分去掉,方便在内网场景部署,根据需要添加
    (function () {
      return {
        name: "vite:set-index-base",
        apply: 'build',
        writeBundle (options, bundle) {
          for (const chunkName in bundle) {
            if(Object.prototype.hasOwnProperty.call(bundle, chunkName) && chunkName === 'index.html') {
              const chunk: any = bundle[chunkName]
              if(chunk.source && options.dir) {
                chunk.source = chunk.source.replace(new RegExp(devCDNBase, 'g'),'')
                const fullPath = join(options.dir, chunk.fileName)
                writeFileSync(fullPath, chunk.source)
              }
            }
          }
        },
      }
    })(),
  ]
})
  • 方式二

1、build 分离依赖

// vite.config.js
import externalGlobals from 'rollup-plugin-external-globals'
const externalDepends = ['vue', 'vue-demi']

build: {
  rollupOptions: {
    external: externalDepends,
    plugins: [
      externalGlobals:({
        vue: 'Vue',
        'vue-demi': 'VueDemi',
      })
    ]
  }
}

2、生产打包时在 index.html 中注入 script 标签

// vite.config.js
import { createHtmlOlugin } from 'vite-plugin-html'
const externalDepends = ['vue', 'vue-demi']
const externalSource = {
  vue: {
    url: '/-/pkg/vue/vue@latest/dist/vue.global.prod.js', // 资源地址
    global: 'Vue', // 全局名称
  },
  'vue-demi': {
    url: '/-/pkg/vue-demi/vue-demi@latest/dist/index.iife.js',
    global: 'VueDemi',
  }
}

export default defineConfig(({ mode }) => {
  const vitePlugins = []
  let injectScript = '<script></script>'
  if(mode !== 'development') {
    externalDepends.forEach(v => {
      if(externalSource[v] && externalSource[v].url) {
        injectScript += `<script src="${externalSource[v].url}"></script>`
      }
    })
  }
  vitePlugins.push(
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          title: 'index',
          injectScript
        }
      }
    })
  )
  return {
    plugins: vitePlugins
  }
})

3、index.html 中插入 script 语句变量

<head>
  <%- injectScript %>
</head>

按资源类型分类打包

// vite.config.js
build: {
  rollupOptions: {
    output: {
      chunkFileNames: 'assets/js/[name]-[hash].js',
      entryFileNames: 'assets/js/[name]-[hash].js',
      assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
      manualChunks(id) {
        // 该方法会将每个依赖打包成单独文件,按需求判断是否需要
        if (id.includes('node_modules')) {
          let splitKey = 'node_modules/'
          if (id.includes('.pnpm')) {
            splitKey = 'node_modules/.pnpm/'
          }
          return id.toString().split(splitKey)[1].split('/')[0].toString()
        }
      },
    },
  }
}

gzip 压缩 vite-plugin-compression

// vite.config.js
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    viteCompression({
      threshold: 1024 * 10,  // 超过 10kb 的压缩
      algorithm: 'gzip',
      ext: '.gz',
    })
  ],
})

打包时注入构建信息

// vite.config.ts
function InjectBuildInfo(): Plugin {
  return {
    name: 'define-version',
    transform(code, id) {
      const timeStr = '__VITE_BUILD_TIME'
      const versionStr = '__VITE_BUILD_VERSION'
      const hashStr = '__VITE_BUILD_GIT_HASH'
      let version = 'unknown'
      let hash = 'unknown'
      try {
        const packageJson = JSON.parse(readFileSync('./package.json', 'utf-8'))
        version = packageJson.version
      } catch (error) {
        console.error(error)
      }
      const isProd = process.env.NODE_ENV === 'production'
      if(isProd) {
        try {
          // 按需配置,是否需要 git hash 值
          hash = execSync('git rev-parse HEAD').toString().trim().slice(0, 8)
        } catch (error) {
          console.error(error)
        }
      }
      const time = new Date().toLocalString('zh-CN', { timeZone: 'Asia/Shanghai' })
      if (id.endsWith('.vue') || id.endsWith('.ts')) {
        code = code.replace(timeStr, time)
        code = code.replace(versionStr, version)
        code = code.replace(hashStr, hash)
      }
      return {
        code
      }
    }
  }
}
// 使用: App.vue
window.__build_info = {
  time: '__VITE_BUILD_TIME',
  version: '__VITE_BUILD_VERSION',
  hash: '__VITE_BUILD_GIT_HASH',
}