可乐Cola

V1

2022/06/09阅读:59主题:默认主题

Vite Lerna Monorepo 项目改造初体验

Vite Lerna Monorepo 项目改造初体验

Vite 是什么?

仓库链接:https://github.com/vitejs/vite

Vite(法语意为 "快速的",发音 /vit/,发音同 "veet")是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成:

  1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
  2. 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。

为什么选用Vite

  1. 知道了Vite是什么以及可以做什么之后,相信你们会有一些疑问,这么多构建工具,诸如WebpackRollupESbuildBabel等等,为什么要用 Vite ?首先看一张构建时间图:
构建性能对比
构建性能对比

如上图所示,ESbuild构建是最快的,Webpack5这个版本虽然做了比较大的改动和优化,但是性能依然是比较糟糕。

  1. 为什么不直接用 ESbuild ?反而要用 Vite 呢?

总结一下,其实就是因为 ESbuild 生态不完成,无法满足复杂业务场景下的打包要求。

如何使用?

由于项目的基础包使用的是Rollup进行构建,Vite 内部就是通过Rollup进行构建的,所以,改造起来工作量并不算大,但是要踩一些坑。

坑一: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 提供了两种输出模式

  1. 库模式,即在build.lib中做一些输出配置

    build:{
       lib: {
            entry: resolve(`${name}/src/index.ts`),
            formats: ['es'],
            name: 'index',
            fileName: 'index',
          },
    }
    
  2. **build.rollupOptions 中的output ,这块大家应该比较了解

    build:{
     ****rollupOptions:{
    		output:{
    			{
            file: `packages/${name}/dist/index.js`,
            format: 'es',
          },
    		}
    	}****
    }
    

目前项目采用的是第一种,采用第一种会有一个问题,打包生成的css文件并不会内联到相关联的js文件中,作为一个基础包,在业务方式用的时候,需要跟antd的使用规则一样,单独引入样式文件,这对于业务来说,并不友好(当前基础包是一个文本编辑器,并未提供自定义样式的功能,严一旦样式缺失,整个排版全部乱了,而之前采用Rollup形式构建的包是将样式内联在js,为了保持一致性,也需要达到相同的效果)。

  • 如何做?
    1. 查找是否有相关的插件可以做这样的事情

      1. https://github.com/vitejs/vite/issues/1579 从这个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)
                }
              }
            },
          }
        }
        
    2. 上述的民间版本插件是否满足当前业务的需求?

      不满足当前的需求,存在以下问题:

      • 最终inlinecss 可能并不是最终生成的css (中间态,导致样式缺失)
      • 最终会生成style.css本地文件(既然已经内联到js文件中了,该文件没有存在的必要性)

      如何解决?

      • 原理已经知道,看下Vite 插件的执行顺序和 hook 有哪些
        • 执行顺序

          • 一个 Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整它的应用顺序。enforce 的值可以是pre 或 post。解析后的插件将按照以下顺序排列:

            • Alias
            • 带有 enforce: 'pre' 的用户插件
            • Vite 核心插件
            • 没有 enforce 值的用户插件
            • Vite 构建用的插件
            • 带有 enforce: 'post' 的用户插件
            • Vite 后置构建插件(最小化,manifest,报告)

            上述民间版本插件并没有显示指定 enforce ,处于构建中但是未生成静态文件之前。

        • hook

        • 改造插件

          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 文件的生成问题

分类:

前端

标签:

前端

作者介绍

可乐Cola
V1