老旧React项目迁移vite改造

·  阅读 1109

vite特性

冷启动时会分析html script标签的入口文件,预构建优化所有的模块。后续依次import的时候如果没预构建还会再次触发。

为什么需要预构建?

  • 浏览器不支持bare import,因此把所有的bare import转换成 @/modules/...的形式,这样vite开发服务器就会去nodemoudles下面去查找模块。
  • 虽然http2多路复用可以支持大量并发请求,但是一次性发几百个请求还是会造成额外的网络往返消耗,所以把类似于loadsh这种有几百个模块的库打成几个模块来合并请求。
  • 把commonjs或umd模块打包成esm

vite开发环境采用bundless模式,启动服务器来对不同的文件加载做拦截按需实时转换文件,粒度拆分的足够细可以对每一个模块做缓存。因此,不像webpack那样每次更新需要把一个chunk所有文件一起打包,而是按需更新某一个模块即可,所以更新效率可以做到O(1)。

关于css注释

vite不支持在cssmodule中写类似于 //comment 的注释,插件批量删除即可。

代码参考vite.config.js FormatPlugin

关于热更新

由于vite-react-plugin 采用react-fresh来进行热更新,而react-fresh是不支持类组件热更新的,所以得把类组件改成函数式组件导出,加个包裹即可。本文采取了babel插件的形式转译,有一个小问题就是当tsx文件内没有jsx写法的时候不会注入_jsxDev需要手动注入,不过目前我的项目中几乎没有所以暂时不管。

代码参考vite.config.js HmrPlugin

关于svg

用法如下

import Svg from 'icon.svg'

<div><Svg /></div>
复制代码

使用了svgr插件后的打包结果 截屏2022-04-21 上午10.12.36.png 上述情形会报错,因为vite导入svg默认是url形式,如果要用component形式导入需要@svgr-rollup插件,同时由于vite内置插件的影响,svgr会把component的导出处理成具名导出,默认导出还是url,因此写了个插件transform一下默认导出即可。如果某些情况需要默认导出url,路径后添加"?url"后缀。

代码参考vite.config.js SvgPlugin

关于css后缀名

项目中大量使用了style.css作为postcss模块解析,但是由于vite只支持style.module.css这个后缀解析css module。 解决方法

  • 修改nodemodules源码,使用patch-package打补丁。成功。

  • 使用vite插件,一行代码。但是bug很多,最终放弃。 因此去修改源码,vite版本号为2.8.6,首先下载补丁工具,然后在nodemodules/vite/dist/node/chunks/dep-9c1538.js 修改isModule即可

npm i patch-package --save-dev

const cssModuleRE = new RegExp(cssLangs);

async function compileCSS(id, code, config, urlReplacer, atImportResolvers, server) {
    var _a;
    const { modules: modulesOptions, preprocessorOptions } = config.css || {};
    // const isModule = modulesOptions !== false && cssModuleRE.test(id);
    const nodemodulesRE = /node_modules/
    const isSelf = !nodemodulesRE.test(id)
    const isModule = isSelf ? /.css/.test(id) : modulesOptions !== false && cssModuleRE.test(id);    
    ...
}

npx patch-package vite
复制代码

最后修改下package.json的启动脚本,即可。

"scripts": {
    "dev:vite": "npm run postinstall && vite",
    "postinstall": "patch-package"
},
复制代码

以下只涉及开发模式,暂不支持生产环境

初始化

依赖

yarn add -D vite && yarn add -D @vitejs/plugin-react && yarn add -D @vitejs/plugin-react vite-plugin-inspect && yarn add -D vite-plugin-style-modules && yarn add -D @svgr/rollup && yarn add hoist-non-react-statics
复制代码

index.html

vite以html文件为入口,直接加载入口文件。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/browser/index.js"></script>
  </body>
</html>复制代码

vite.config.js

创建 vite.config.ts配置文件 对runApp、routes/sync、constants/env 做了拦截、添加了css文件格式化插件。对于一些特殊的文件配个alias魔改一下即可。 把同步路由改造成异步路由,因为routes/sync 使用的是require会报错,懒得一个个改了、constants/env同理。

import react from '@vitejs/plugin-react'
import path from 'path'
import Inspect from 'vite-plugin-inspect'
import { GATEWAY_API_URL } from './vite/env'
import svgr from '@svgr/rollup'

require('dotenv').config()

var HmrPlugin = ({ template, traverse }) => {
    const isReactComponent = classPath => {
    const superClassPath = classPath.get('superClass')
    if (superClassPath) {
        const classCode = superClassPath.toString()
        const reactComponentRe = /(React\.)?(PureComponent|Component)/
        return reactComponentRe.test(classCode)
    }
}

    return {
        visitor: {
            ClassDeclaration: path => {
                if (path.node.isReplaced) return
                const idPath = path.get('id')
                if (isReactComponent(path)) {
                        const idName = idPath.toString()
                        idPath.replaceWithSourceString(`_${idName}`)
                        const fnNode = exp => template.ast(`${exp}const ${idName} = props => _jsxDEV(_${idName}, {...props})`)
                        const parentPath = path.parentPath
                        const pType = parentPath.node.type
                        if (pType === 'ExportNamedDeclaration') {
                                path.node.isReplaced = true
                                parentPath.replaceWith(path.node)
                                parentPath.insertAfter(fnNode('export '))
                                parentPath.stop()
                        } else if (pType === 'ExportDefaultDeclaration') {
                                path.node.isReplaced = true
                                parentPath.replaceWith(path.node)
                                parentPath.insertAfter(template.ast(`export default ${idName}`))
                                parentPath.insertAfter(fnNode(''))
                                parentPath.stop()
                        } else {
                                path.insertAfter(fnNode(''))
                        }
                }
            },
        },
    }
}

var FormatPlugin = () => {
    const cssRe = /\.css/
    const commentsRe = /\/\/(.*);/g
    return {
      name: 'format-plugin',
      transform(code, id) {
        if (cssRe.test(id)) {
          if (commentsRe.test(code)) {
            return code.replace(commentsRe, '')
          }
        }
    },
  }
}

var SvgPlugin = () => {
    const svgRe = /\.svg/
    return {
      name: 'svg-plugin',
      transform(code, id) {
        if (svgRe.test(id)) {
          return code.replace(/export default(.*)/, '').replace(/var .* = function/, 'export default function')
        }
      },
    }
  }

export default defineConfig({
    publicDir: path.resolve(__dirname, '..'),
    server: {
            port: 3009,
            proxy: {
              '/api': {
                target: GATEWAY_API_URL,
                secure: false,
                changeOrigin: true,
              },
            },
    },
    resolve: {
      alias: {
        './helpers/runApp': path.resolve(__dirname, './vite/runApp.tsx'),
        'routes/sync': path.resolve(__dirname, 'shared/routes/async'),
        'constants/env': path.resolve(__dirname, './vite/env.js'), // 按顺序解析,出现在上面的先匹配
        'crypto-js/sha256': path.resolve(__dirname, './vite/crypto.js'), // 由于只有log文件使用,所以拦截后随便处理
        routes: path.resolve(__dirname, 'shared/routes'),
        containers: path.resolve(__dirname, 'shared/containers'),
      },
    },
    define: {
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
       global: {},
    },
    css: {
      preprocessorOptions: {
        less: {
          javascriptEnabled: true,
        },
      },
      modules: {
        generateScopedName: '[name]__[local]___[hash:base64:5]', // 防止hash篡改
      },
    },
    plugins: [
        Inspect(),
        HmrPlugin(), // 类组件热更新插件
        { ...FormatPlugin(), enforce: 'pre' }, // 正则替换css中的注释,影响性能,除了调试别开启,碰见报错手动删除注释
        react({
          babel: {
            babelrc: false,
            plugins: [['@babel/plugin-proposal-decorators', { legacy: true }]],
          },
        }),
        { ...SvgPlugin(), enforce: 'post' },
        svgr(),
    ],
})
复制代码

suffixFix.js

思考如下代码,经过vite服务器转译后的代码会是什么样子?

import { wordShort } from 'utils'
复制代码

在js中会被转换成成如下代码,我们的resolve alias配置并没有生效。在js文件中bare import 会去nodemodules文件下面寻找,不会走resolve.alias配置。解决办法js改成ts文件就会先解析成'src/utils',不是bareimport自然就去项目里面查找模块

import { wordShort } from '@modules/utils'
复制代码

在 ts | tsx 中会被解析成我们的项目路径,所以对 components和containers目录修改后缀名为tsx,其余的遇见手动修改。

import { wordShort } from 'shared/utils'
复制代码

批量修改后缀脚本

const fs = require('fs')
const path = require('path')
​
const suffixFix = (dir, filename = '', relativeDir = '') => {
  const nextDir = path.join(dir, filename) // frontendserv/shared
  const nextRelativeDir = path.join(relativeDir, filename)
​
  // 遍历目录
  const excludes = ['store', 'types']
  if (excludes.includes(filename)) returnif (fs.statSync(nextDir).isDirectory()) {
    const subPageFiles = fs.readdirSync(nextDir)
    subPageFiles.forEach(filename => {
      suffixFix(nextDir, filename, nextRelativeDir)
    })
  } else {
    // 处理文件
    // const includes = ['components']
    console.log('当前目录', nextDir)
    console.log('文件名:', filename)
    // 每碰见一个特例就加进来,没有div标签的文件尽量别用tsx
    if (
      nextDir.indexOf('components') !== -1 ||
      nextDir.indexOf('containers') !== -1 ||
      nextDir.indexOf('utils/test.js') !== -1
    ) {
      console.log(nextDir)
      if (/.js$/.test(filename)) {
        fs.renameSync(nextDir, nextDir.replace(/js$/, 'tsx')) // tsx 才可以解析alias
      }
      if (/.jsx$/.test(filename)) {
        fs.renameSync(nextDir, nextDir.replace(/jsx$/, 'tsx'))
      }
    }
  }
}
​
const root = path.resolve(__dirname)
suffixFix(__dirname, 'src')
console.log(root, path.basename(root))
​
复制代码

注意,当有一些导入是 from '[path].js' 的格式会导致报错,此时用webpack全量打包一下就可以识别哪些文件导不进来修改掉即可。

所有准备工作做好后 运行 yarn dev:vite 即可。

vite报错处理

[vite] warning: This case clause will never be evaluated because it duplicates an earlier case clause

解决方法:枚举类型重复key 删除即可

Failed to resolve entry for package "crypto". The package may have incorrect main/module/exports specified in its package.json: Failed to resolve entry for package "crypto". The package may have incorrect main/module/exports specified in its package.json.

解决方法:crypto 依赖包除了package.json 和 readme 没有其他文件,应该是不再维护使用了,改用js-md5插件后问题解决。

js不能被解析成jsx

尤雨溪原话:改个后缀名很难?js用jsx格式解析会损耗性能。

解决方法:写了个suffixFix脚本批量修改后缀名,全部转换成tsx |ts不然alias会有点问题。

点评:个人认为一个成熟的项目还是需要考虑一下历史包袱的。

[vite] Internal server error: [path]/style.css:58:7: Unknown word

解决方法:去掉css中的注释、写个插件批量转换,代码参考vite.config.js

code.replace(/\/.*;/g, "")
复制代码

require is not defined

解决方法:vite不支持require导入的形式,所以项目中一律用import,看到不规范的地方顺手改一下。

ant design inline script

参考文章

第三方库global is not defined

1.修改vite.config.js 奏效

define: {
  global: {}
}
复制代码

2.入口处加window.global = window 没生效

关于一些nodemodules三方包的问题

如果三方包有什么不规范的写法可以去源码中修改,改完后打个patch-package补丁就行。(尽量别采用这种方式)

总结

总之,源码中有不符合vite规范的就上插件转译,能用正则用正则,用不了正则就上AST魔改(babel | esbuild)

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改