Vite 是什么?
Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:
- 一个开发服务器,它基于 原生
ES模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。 - 一套构建指令,它使用
Rollup打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。
为什么选用Vite?
-
知道了
Vite是什么以及可以做什么之后,相信你们会有一些疑问,这么多构建工具,诸如Webpack、Rollup、ESbuild、Babel等等,为什么要用Vite?首先看一张构建时间图:
如上图所示,ESbuild构建是最快的,Webpack5这个版本虽然做了比较大的改动和优化,但是性能依然是比较糟糕。
- 为什么不直接用
ESbuild?反而要用Vite呢?
💡 Vite 官方文档: 虽然 ESbuild快得惊人,并且已经是一个在构建库方面比较出色的工具,但一些针对构建 应用的重要功能仍然还在持续开发中 —— 特别是代码分割和 CSS 处理方面。就目前来说,Rollup 在应用打包方面更加成熟和灵活。尽管如此,当未来这些功能稳定后,我们也不排除用 ESbuild作为生产构建器的可能。
总结一下,其实就是因为 ESbuild 生态不完成,无法满足复杂业务场景下的打包要求。
如何使用?
💡 当前项目的结构是Lerna Monorepo + example 的形式,构建方式采用的是由lerna 管理的核心包(引擎包),采用Rollup + Babel + Typescript 构建,example 则采用Webpack5进行构建。
由于项目的基础包使用的是
Rollup进行构建,Vite内部就是通过Rollup进行构建的,所以,改造起来工作量并不算大,但是要踩一些坑。
👉 假设当前的项目结构如下: example src index.ts packages a1 src index.ts b1 src index.ts
坑一:Rollup的配置文件支持多入口导出,Vite 仅支持单入口导出
-
Rollup-
配置
// rollup.config.js function configure(name) { return { input: `packages/${name}/src/index.ts`, output: [ { file: `packages/${name}/dist/index.js`, format: 'cjs', exports: 'named', }, { file: `packages/${name}/dist/index.es.js`, format: 'es', exports: 'named', }, ], plugins: [ resolve({ extensions: ['.js', '.jsx', '.ts', '.tsx'], browser: true, }), ...xxx ], // 忽略部分 warning 信息 onwarn: function (warning) { if (warning.code === 'THIS_IS_UNDEFINED' || warning.code === 'CIRCULAR_DEPENDENCY') { return; } console.warn(warning.message); }, }; } // 导出的是 config[] export default [ factory('a'), factory('b'), ]; -
启动
// packages.json scripts: { "dev": "rollup -c ./rollup.config.js -w" }
-
-
Vite//
Vite配置声明 export declare function defineConfig(config: UserConfigExport): UserConfigExport; 这就导致书写Vite的配置不可能像Rollup一样,可以导出config[],然后在package.json 中以 vite dev 的形式启动,只能通过Vite提供的build方法,自己写脚本去构建。-
配置
-
生成构建函数
// vite.config.ts import { defineConfig } from 'vite'; import { esbuildPluginBabel } from 'vite-plugin-babel'; import injectStyle from '../plugins/injectStyle'; import autoExternal from 'rollup-plugin-auto-external'; import image from '@rollup/plugin-image'; import progress from 'rollup-plugin-progress'; import sizes from '@atomico/rollup-plugin-sizes'; const buildConfig = (name) => defineConfig({ plugins: [ injectStyle(), esbuildPluginBabel(), ], mode: 'development', css: { preprocessorOptions: { less: { // 支持内联 JavaScript javascriptEnabled: true, }, }, }, build: { cssCodeSplit: false, watch: { }, lib: { entry: resolve(`${name}/src/index.ts`), formats: ['es'], name: 'index', fileName: 'index', }, assetsDir: '', outDir: resolve(`${name}/dist`), emptyOutDir: false, sourcemap: true, rollupOptions: { plugins: [ autoExternal({ packagePath: resolve(`${name}/package.json`), }), image(), progress(), sizes(), ], output: { inlineDynamicImports: true, exports: 'named', }, }, }, }); export { buildConfig } -
调用
Vite提供的build方法,依次构建子项目// build.ts import { build, UserConfig } from 'vite'; import { buildConfig } from './config'; const packageNames = ['a', 'b']; const buildPackages = () => { let buildIndex = 0; const total = packageNames.length; return new Promise((resolve, reject) => { const next = () => { const config = buildConfig(packageNames[buildIndex]) as UserConfig; build(config).then(() => { buildIndex++; if (buildIndex < total) { next(); } else if (buildIndex === total) { resolve(null); } }).catch((error => { reject(error); })); }; next(); }); }; buildPackages()
-
-
启动
// packages.json "script":{ "dev":"ts-node ./build.ts" }
-
坑二:Vite 提供了两种输出模式
-
库模式,即在
build.lib中做一些输出配置build:{ lib: { entry: resolve(`${name}/src/index.ts`), formats: ['es'], name: 'index', fileName: 'index', }, } -
**
build.rollupOptions中的output,这块大家应该比较了解build:{ ****rollupOptions:{ output:{ { file: `packages/${name}/dist/index.js`, format: 'es', }, } }**** }
目前项目采用的是第一种,采用第一种会有一个问题,打包生成的css文件并不会内联到相关联的js文件中,作为一个基础包,在业务方式用的时候,需要跟antd的使用规则一样,单独引入样式文件,这对于业务来说,并不友好(当前基础包是一个文本编辑器,并未提供自定义样式的功能,严一旦样式缺失,整个排版全部乱了,而之前采用Rollup形式构建的包是将样式内联在js,为了保持一致性,也需要达到相同的效果)。
-
如何做?
-
查找是否有相关的插件可以做这样的事情
-
github.com/vitejs/vite… 从这个
issue中可以看到,这个问题存在有一段时间,目前没有官方的解决方案,仅有民间版本,原理大致是拿到打包之后的css,插入到编译之后js文件中,做到内联/* eslint-disable import/no-extraneous-dependencies */ import fs from 'fs' import esbuild from 'esbuild' import { resolve } from 'path' const fileRegex = /.(css).*$/ const injectCode = (code) => `function styleInject(css,ref){if(ref===void 0){ref={}}var insertAt=ref.insertAt;if(!css||typeof document==="undefined"){return}var head=document.head||document.getElementsByTagName("head")[0];var style=document.createElement("style");style.type="text/css";if(insertAt==="top"){if(head.firstChild){head.insertBefore(style,head.firstChild)}else{head.appendChild(style)}}else{head.appendChild(style)}if(style.styleSheet){style.styleSheet.cssText=css}else{style.appendChild(document.createTextNode(css))}};styleInject(`${code}`)` const template = `console.warn("__INJECT__")` let viteConfig const css = [] async function minifyCSS(css, config) { const { code, warnings } = await esbuild.transform(css, { loader: 'css', minify: true, target: config.build.cssTarget || undefined }); if (warnings.length) { const msgs = await esbuild.formatMessages(warnings, { kind: 'warning' }); config.logger.warn(source.yellow(`warnings when minifying css:\n${msgs.join('\n')}`)); } return code; } export default function libInjectCss(){ return { name: 'lib-inject-css', apply: 'build', configResolved(resolvedConfig) { viteConfig = resolvedConfig }, async transform(code, id) { if (fileRegex.test(id)) { const minified = await minifyCSS(code, viteConfig) css.push(minified.trim()) return { code: '', } } if ( // @ts-ignore // true || id.includes(viteConfig.build.lib.entry) || id.includes(viteConfig.build.rollupOptions.input) ) { return { code: `${code}; ${template}`, } } return null }, async writeBundle(_, bundle) { for (const file of Object.entries(bundle)) { const { root } = viteConfig const outDir = viteConfig.build.outDir || 'dist' const fileName = file[0] const filePath = resolve(root, outDir, fileName) try { let data = fs.readFileSync(filePath, { encoding: 'utf8', }) if (data.includes(template)) { data = data.replace(template, injectCode(css.join('\n'))) } fs.writeFileSync(filePath, data) } catch (e) { console.error(e) } } }, } }
-
-
上述的
民间版本插件是否满足当前业务的需求?不满足当前的需求,存在以下问题:
- 最终
inline的css可能并不是最终生成的css(中间态,导致样式缺失) - 最终会生成
style.css本地文件(既然已经内联到js文件中了,该文件没有存在的必要性)
如何解决?
-
原理已经知道,看下
Vite插件的执行顺序和hook有哪些-
执行顺序
-
一个 Vite 插件可以额外指定一个
enforce属性(类似于 webpack 加载器)来调整它的应用顺序。enforce的值可以是pre或post。解析后的插件将按照以下顺序排列:-
Alias
-
带有
enforce: 'pre'的用户插件 -
Vite 核心插件
-
没有 enforce 值的用户插件
-
Vite 构建用的插件
-
带有
enforce: 'post'的用户插件 -
Vite 后置构建插件(最小化,manifest,报告)
上述民间版本插件并没有显示指定
enforce,处于构建中但是未生成静态文件之前。 -
-
-
改造插件
import * as fs from 'fs'; import * as path from 'path'; import { PluginOption } from 'vite'; const injectCode = (code) => `function styleInject(css, ref){ if ( ref === void 0 ) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } };styleInject(`${code}`);`; const template = 'console.warn("__INJECT__");'; let viteConfig; let cssData; export default function injectStyle(): PluginOption { return { name: 'lib-inject-css', // 在生成磁盘文件阶段 enforce: 'post', apply: 'build', configResolved(resolvedConfig) { viteConfig = resolvedConfig; }, transform(code, id) { if (id.includes(viteConfig.build.lib.entry)) { return { code: `${template}${code}`, }; } return null; }, generateBundle(this, options, bundle, isWrite) { for (const file in bundle) { const chunk = bundle[file]; // 如果当前的chunk是style.css,则读取数据,并从bundle删除该引用,确保不会生成磁盘文件 if (chunk.fileName === 'style.css' || chunk.name === 'style.css') { cssData = (bundle[chunk.fileName || chunk.name] as any).source; delete bundle[chunk.fileName || chunk.name]; } } }, async writeBundle(_, bundle) { for (const file of Object.entries(bundle)) { const { root } = viteConfig; const outDir = viteConfig.build.outDir || 'dist'; const fileName = file[0]; const filePath = path.resolve(root, outDir, fileName); if (!fs.existsSync(filePath)) { return; } try { let data = fs.readFileSync(filePath, { encoding: 'utf8', }); if (data.includes(template) && cssData) { data = data.replace(template, injectCode(cssData.replace(/\/g, '\\'))); fs.writeFileSync(filePath, data); } } catch (e) { console.error(e); } } }, }; }
-
- 最终
-
总结
以上是本次使用Vite 的一次实战经验,目前还存在一些问题,如:
- 仅处理了开发环境,未做生产环境等处理,包括生产环境的*
prolifill* d.ts文件的生成问题