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插件后的打包结果
上述情形会报错,因为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)) return
if (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)