记录改造一个2018年的react老项目到vite4.3全过程

2,501 阅读7分钟

前言:

每个项目的改造成本是不一样的,您遇到的问题,可能恰巧我没有遇到,so,不喜勿喷~

本文忽略:

  1. eslint, prettier, 因为影响不大,改造成本相对较小。
  2. test & mock环节, 自己配很简单 (推荐使用vitest, msw)

原文链接:www.js-bridge.com/post/d5b89a…

老项目核心配置

实际提升:

  • 老项目打包时间 4分51秒左右
  • vite4.3.x 2分钟左右,上下15秒

改造过程

vite有个好处:

  1. 基本上xxx-loader不需要自己install,比如less-loader, ts-loader, sass-loader, 只需要装less, typescript, sass即可
  2. postcss/postcss-loader也不需要安装

用vite初始化一个空react-ts项目

此时配置很简洁,就一个react插件的配置

npm create vite@latest proj-to-vite -- --template react-ts

cd proj-to-vite

index.html改造

将老项目的index.html和vite的index.html进行合并, 并保留以下脚本

<!-- react是index.tsx -->
<script type="module" src="/src/index.tsx"></script>

同时后期运行项目,我遇到了:global/this.globalThis not defined的问题

解决办法:在index.html中添加

+ <script>
+    window.global = window;
+    this.globalThis || (this.globalThis = this);
+  </script>
<script type="module" src="/src/index.tsx"></script>

postcss配置

主要作用是css前缀添加/flexbox bugs修复等(online demo)...

npm i postcss-flexbugs-fixes postcss-preset-env -D

举例:比如我们想要给filter, animation属性添加-webkit前缀 (此时设定范围需要包含chrome52)

.browserslistrc

>0.2%
Chrome >= 52

修改postcss.config.js(或者直接在vite.config.ts中配置)

module.exports = {
  plugins: [
    // https://github.com/luisrudge/postcss-flexbugs-fixes#readme
    require('postcss-flexbugs-fixes'), 
    require('postcss-preset-env')({
      // postcss-preset-env依赖了autoprefixer, 所以不需要单独安装
      autoprefixer: {
        // 或者在这里覆盖.browserslistrc
        // overrideBrowserslist: ['Chrome >= 52'],
        grid: true,
      },
    }),
  ],
};

路由异步加载

为了提升开发和用户体验,请异步加载路由,我使用的是:

@loadable/component

vite 配置修改

请仔细阅读以下每一步修改:

base路径修改

有些项目并不是部署在服务器的根目录下, 有可能在二级目录下,base修改为 ./

/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  return {
    base: './',
    plugins: [react()],
  };
});

变量替换

慎用__DEV__这个名称, 因为有些第三方包源码中存在__DEV__判断,vite默认会匹配并执行所有替换,项目跑起来报错,如下:

image.png

换成不一样的名称即可,比如__ENV_DEV__

/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  const isBuild = command !== 'serve';
  // load .env vars
  const env = loadEnv(mode, process.cwd(), '');

  return {
    ...,
    define: {
      __ENV_DEV__: !isBuild,
      // __DEV__: !isBuild, // 慎用
      
      // === other vars ===
      
      // MOCK_DEV定义在.env文件中
      __MOCK_IN_PRODUCTION__: env.MOCK_DEV === '1',
    },
  };
});

alias路径映射

路径简写,和webpack中的alias一样

/// <reference types="vite/client" />

import path from 'path';
import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    resolve: {
      alias: [
        ...(
          Object.entries({
            '@': path.resolve('src'),
            '@config': path.resolve('src/config'),
            '@assets': path.resolve('src/assets'),
            '@components': path.resolve('src/components'),
          }).map(([key, val]) => ({ find: key, replacement: val }))
        ),
      ],
    },
  };
});

classnames/lodash改造

vite通过预处理,将多个模块进行合并,以减少开发环境模块请求数量, 从而提升开发体验

npm i classnames-es-ts lodash-es -S

# 代码还是正常 import { xx } from 'lodash';
/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    resolve: {
      alias: [
        ...(
          Object.entries({
            ...,
            'classnames': 'classnames-es-ts',
            'lodash': 'lodash-es',
          }).map(([key, val]) => ({ find: key, replacement: val }))
        ),
      ],
    },
  };
});

build配置修改

/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    build: {
      outDir: 'build', // 我们的项目输出文件夹要求是build
      assetsInlineLimit: 4096 * 2, // 转换base64的临界点(kb)
      chunkSizeWarningLimit: 3000, // 对于PC,500kb上限有点小
      sourcemap: !isBuild ? true : env.SOURCE_MAP === '1', // SOURCE_MAP定义在.env文件中
      manifest: true, // for PWA
    },
  };
});

react插件配置/html-plugin

项目使用了decorators, class-properties, reflect-metadata

切记:src/index.tsx入口处需要import 'reflect-metadata'

同时

# 因为使用了`decorators`, `class-properties`, `reflect-metadata`
# 请记得install:
# @babel/plugin-proposal-decorators
# @babel/plugin-proposal-class-properties
# babel-plugin-transform-typescript-metadata

npm i babel-plugin-transform-typescript-metadata @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    plugins: [
      createHtmlPlugin(),
      // 我测试了很多遍,babel无法读取.babelrc,需要写在插件里面
      // TIPS: 这里不在需要presets配置,否则你会很难受!
      react({
        babel: {
          plugins: [
            'babel-plugin-transform-typescript-metadata',
            ['@babel/plugin-proposal-decorators', { legacy: true }],
            ['@babel/plugin-proposal-class-properties', { loose: true }],
          ],
        },
      }),
    ]
  };
});

antd按需加载

/// <reference types="vite/client" />

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    css: {
      preprocessorOptions: { // antd 的基本配置
        less: {
          javascriptEnabled: true,
          modifyVars: {
            '@primary-color': 'blue',
            // ...
          },
        },
        scss: {
          // 解决postcss插件产生的warning
          charset: false,
        },
      },
    },
    plugins: [
      ...,
      createStyleImportPlugin({
        resolves: [
          AntdResolve(),
        ],
        libs: [
          {
            libraryName: 'antd' ,
            resolveStyle: (name) => {
              return `antd/es/${name}/style`;
            },
          },
        ],
      }),
    ]
  };
});

让vite读取.browserslistrc配置

当然你可以直接使用vite-plugin-legacy,but, 它的打包速度非常的慢, 我们的项目加上它,打包时间从2分钟直接变成了6分钟,坑爹啊,果断放弃, 转到browserslist-to-esbuild

TIPS: vite默认只处理语法转换,比如async await/??/?./??=,函数(例如padStart)需要自行添加polyfill方案

以下设定可以转换 “async await/??/?./??=”语法, online demo

> 0.2%
Chrome >= 52
...
import browserslistToEsbuild from 'browserslist-to-esbuild'

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    build: {
      ...
      // 可读取.browserslistrc文件
      target: browserslistToEsbuild(),
    },
  };
});

目前测试.browserslistrc配置可以影响css的输出 (js方面,默认只会处理语法转换,比如async await/??/?./??=,函数(例如padStart)需要自行添加polyfill方案):

css方面,我的配置是chrome >= 52,因为我的浏览器范围设定很大,包含低版本,所以如下样式会被转换:

background-color: #0006;

/** to **/

background-color: rgba(0, 0, 0, 0.4);

注意:若css需要添加前缀 / flexbugs修复,请配置postcss.config.js

详细请转到:本文“postcss配置”部分

第三方包 require not defined 问题

添加transformMixedEsModules配置即可

...
import browserslistToEsbuild from 'browserslist-to-esbuild'

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    build: {
      ...
      commonjsOptions: {
        transformMixedEsModules: true
      },
    },
  };
});

typescript静态类型实时检测

建议先不开启它,等改造成功后再开启(因为会产生一些ts相关的类型error, 但不影响程序运行,需要我们手动fix

...
import TsChecker from 'vite-plugin-checker';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    plugins: [
      ...,
      // 里面也有eslint的配置
      TsChecker({ typescript: true }),
    ]
  };
});

本地开发环境启用https

安装步骤请戳👇

...
import viteCopyPlugin from '@col0ring/vite-plugin-copy';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    server: {
      // VITE_HTTPS定义在.env文件中
      https: env.VITE_HTTPS === 'true' ? {
        key: fs.readFileSync('keys/cert.key'),
        cert: fs.readFileSync('keys/cert.crt')
      } : false,
    },
  };
});

拷贝文件功能

这里我们在构建成功后,将public下的文件拷贝到build文件夹下。

...
import viteCopyPlugin from '@col0ring/vite-plugin-copy';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    plugins: [
      ...,
      viteCopyPlugin([
        {
          src: './public/*',
          target: './build',
        },
      ]),
    ]
  };
});

开启webpack的魔法注释

magic-comments vite

// 使用方式和webpack魔法注释一样
() => import(/* webpackPrefetch: true */'./xxx/xx/xxx')
...
import { magicComments } from 'vite-plugin-magic-comments';

export default defineConfig(({ command, mode }) => {
  ...

  return {
    ...,
    plugins: [
      ...,
      !isBuild ? magicComments() : null,
    ]
  };
});

不过目前最新版本0.1.1版本还有bug,我们可以使用patch-package打补丁

改动如下 (你也可以向作者提个merge request):

patches/vite-plugin-magic-comments+0.1.1.patch

diff --git a/node_modules/vite-plugin-magic-comments/dist/index.mjs b/node_modules/vite-plugin-magic-comments/dist/index.mjs
index 79dfdfc..d795ca4 100644
--- a/node_modules/vite-plugin-magic-comments/dist/index.mjs
+++ b/node_modules/vite-plugin-magic-comments/dist/index.mjs
@@ -677,9 +677,9 @@ const magicComments = () => {
         htmlTags.push({
           tag: "link",
           attrs: {
+            rel: meta.preload ? "preload" : meta.prefetch ? "prefetch" : "",
+            as: "script",
             href: withLeadingSlash(fileName),
-            preload: meta.preload ? JSON.stringify(meta.preload) : void 0,
-            prefetch: meta.prefetch ? JSON.stringify(meta.prefetch) : void 0
           }
         });
       }

package.json修改

dependencies merge

将老项目的dependencies合并过来,并做适当升级, react升级17.0.2测试了下,很稳

devDependencies 改造

  • 移除所有webpack相关依赖包
  • 移除xxx-loader

scripts改造

使用vite提供的运行脚本

其他改造

  • 删除src目录

  • 将原来项目的src目录粘贴过来

  • postcss配置

    本文postcss配置中有说明

  • antd/lib/xx路径全局替换为antd/es/xx

  • 关于antd datepicker 国际化修改

    // index.tsx
    
    // import 'moment/locale/zh-cn'; // 需要改成以下方式,否则不生效
    import 'moment/dist/locale/zh-cn';
    
    moment.locale('zh-cn');
    
  • 项目使用了 reflect-metadata

    so,在src/index.tsx入口处需要import 'reflect-metadata'

  • 可能会出现interface xxx not defined, 解决办法很简单:

    import { type xxx } from 'xxx/xx/...';
    
  • class方式写的组件不支持hot reload,你需要在所在文件末尾添加 (但是效果还是不理想,因为页面还是会刷新)

    if (import.meta.hot) {
      import.meta.hot.accept()
    }
    
  • 项目本身的一些require语法改造,这个比较简单,改成import即可

    关于第三方包的修复,在本文vite配置 -> require not defined中有说明

  • src/vite-env.d.ts添加

    主要加一些全局的类型定义,比如.jpg,.png,....,还有vite配置中的环境变量声明

    /// <reference types="react" />
    /// <reference types="react-dom" />
    
    /**
     * process.env是否是development
     */
    declare const __ENV_DEV__: boolean
    
    // ...
    
    // CSS modules
    type CSSModuleClasses = { readonly [key: string]: string }
    
    declare module '*.module.css' {
      const classes: CSSModuleClasses
      export default classes
    }
    
    // ...
    

改造完成,项目也正常跑起来了,打包运行也没问题

好了,如果您有任何问题,欢迎讨论(如果文章对您有帮助,还望点个小红心支持一下,谢谢)~