尝试给公司脚手架新增个用vite起react项目的功能, 分享一下思路和遇到的问题.
背景提要 因为是公司项目, 总体代码结构会顺着已有的走, 没有参考性, 所以本文只记录碰到的问题, 并贴一些代码片段, 没有贴出整个仓库. 另外, 经过了一段时间的努力, 电脑里的30个左右的项目都可以跑了, 但方法也许并不通用, 并且有许多地方可以优化的.
思路 现有的dev server用的是webpack, 迁移vite的大方向要先看看他们间的区别.
webpack用了自己写的一些函数把module graph连接起来, vite作为bundleless工具的根本是基于esm. 于是非esm的module就成了问题, vite目前解决得也不太好, 需要我们进一步操作.
webpack加载module的时候可以用loader/plugin进行一些操作, vite也有插件系统, 所以我们的工作之一就是要让webpack存在的配置迁移到vite上.
关于vite插件 在vite插件上我算是踩了一些坑, vite(写作时版本2.9.1)对于plugin文档写得比较粗糙. 文档里的意思是vite插件包含rollup插件
, 所以文档的hook分为2部分: universal和vite-specific. 而事实是: 很多rollup插件是不兼容vite的, vite在dev的时候options只有ssr
, 而没有rollup的所有options. 所以vite插件的开发办法是: console.log硬调试. 安东尼大佬的vite-plugin-inspect
在很多场景(并不是所有)上很有用.
实施细节 与webpack的dev-server一样, 先获取一些配置, 入口文件等, 用api起一个dev-server. 报什么错就处理什么的方式来调试这个vite服务.
rollup兼容插件 因为公司脚手架已经写了一套对应着webpack配置的rollup配置, 就直接拿来用了. 这些配置我把他们分成2类.
与vite兼容的插件, 如resolve, alias, image, json, polyfill, babel等.
在vite环境下要禁用的插件. 我们传个参数就可以解决禁用的问题. 而这些插件又分为2类: 第一类是vite out-of-box的功能, 比如sass/less, postcss, ts. 第二类是会报错的不兼容插件, 比如cjs插件用到了isEntry
.
index.html vite的index.html需要有script[type=module]的引入, 秉持让用户不动的原则, 我们要给index.html插入这段js. 幸运的是, vite提供了这个hook, 很方便.
1 2 3 4 5 6 transformIndexHtml ( ) { return [{ tag : 'script' , attrs : { type : 'module' , src : './' + basename (entry) }, }] }
有一个包报了global is not defined
的错误, 也可以用这个hook来解决. 只要加一个script让global指向window或globalThis.
multiple entry webpack的entry如果配置成Array, 那么会依次引入并打到一个bundle里. 而vite/rollup没有这个功能. 我们公司脚手架在项目entry前还引入了antd的css和babel的polyfill, 如果不引入就会失去样式, 或是报async/await失去垫片的错误regeneratorxxx is not defined
.
这个问题我选择了transform的时候, 判断是否是入口文件来添上对应的引入. 而判断入口文件, 只能靠自己, 而不能靠rollup的isEntry.
1 2 3 4 5 transform (code, id ) { if (id.endsWith (entry)) { return entries.map (eachentry => `import '${eachentry} ';` ).join ('' ) + code; } },
代码中的entries是外部传入的配置, 能让这个plugin更灵活, 完整的代码会在后面贴上.
神策埋点 也不是说埋点有问题, 我们项目中是用神策压缩后的js放在项目文件里的. 鉴于dev时本来就不需要埋点, 于是我用正则的方式把神策埋点都去掉了.
1 2 3 4 5 transform (code, id ) { if (id.endsWith (entry)) { return code.replace (/sensorsdata\.init\([^)]*\);/g , '' ).replace (/.*sensorsdata.*\n/g , '' ) } }
umd+external包 项目中有个包是rollup打的umd+external包. 我尝试了很多插件, (也许是基础不牢固的原因), 我没能力把umd转esm.
最后解决方案, degit那个包, 然后修改rollup配置, 获得一个esm包, 直接放脚手架里, 然后替换.
这里要用到2个新的hook, 因为transform的时候找不到这个import. (可能是node_module里的关系, 具体我不懂)
1 2 3 4 5 6 7 8 9 10 11 resolveId (id ) { if (id.includes ('@hanyk/rc-viewer' )) { return virtualImport; } }, load (id ) { if (id.includes (virtualImport)) { const code = readFileSync (resolve (__dirname, 'rc-view-esm.js' )).toString () return { code } } }
插件完整代码 到这里, 插件解决的问题都齐了, 把完整的插件贴上来记录一下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 const inspect = require ('vite-plugin-inspect' )const { basename, resolve } = require ('path' )const { readFileSync } = require ('fs-extra' )function replacercviewer ( ) { const virtualImport = '\0rc-viewer' return { name : 'replacercviewer' , enforce : 'pre' , apply : 'serve' , resolveId (id ) { if (id.includes ('@hanyk/rc-viewer' )) { return virtualImport; } }, load (id ) { if (id.includes (virtualImport)) { const code = readFileSync (resolve (__dirname, 'rc-view-esm.js' )).toString () return { code } } }, } } function entryappendency ({ entries, entry } ) { return { name : 'entryappendency' , enforce : 'pre' , apply : "serve" , transform (code, id ) { if (id.endsWith (entry)) { return entries.map (eachentry => `import '${eachentry} ';` ).join ('' ) + code; } }, transformIndexHtml ( ) { return [{ tag : 'script' , attrs : { type : 'module' , src : './' + basename (entry) }, }, { tag : 'script' , children : 'global = globalThis' }] } }; } function removeSensor ({ entry } ) { return { name : 'removesensor' , enforce : 'pre' , apply : "serve" , transform (code, id ) { if (id.endsWith (entry)) { return code.replace (/sensorsdata\.init\([^)]*\);/g , '' ).replace (/.*sensorsdata.*\n/g , '' ) } } } } function reactProjCompatible (options ) { const { entry, multiEntry, dev } = options; let plugins = [entryappendency ({ entry, entries : multiEntry ?? [] }), removeSensor ({ entry }), replacercviewer ()] if (dev) { plugins.push (inspect ()) } return plugins; } module .exports = reactProjCompatible
调用的时候:
1 2 3 4 5 plugins : [reactProjCompatible ({ dev : true , entry, multiEntry : ['@babel/polyfill' , 'antd/dist/antd.css' ] })
react-virtualized 这个是react常用的table库, 而这个库有奇怪的引用, 提了issue作者也没回复. 所以我们只能通过hack的方式来处理了. 因为这个依赖项属于pre-bundle阶段的代码, 所以插件的hook不能获取到, 等以后提供esbuild插件的hook的时候可以用esbuild插件来处理. 方式有2个, 一个是用patch-package, 一个是修改node_modules. 还是出于”让用户修改最少”的角度, 我选择修改node_modules. 所以写了个脚本, 在每次起dev-server前把node_modules修改了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const fse = require ('fs-extra' )const os = require ('os' )const slash = os.platform () === 'win32' ? '\\' : '/' ;const bogusImportString = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";` const removeBogusImportLine = ( ) => { try { const rvPath = require .resolve ('react-virtualized' , { paths : [process.cwd ()] }) const filePath = rvPath.replace (/([\\\/])commonjs\1index.js/ , `${slash} es${slash} WindowScroller${slash} utils${slash} onScroll.js` ) const filecontent = fse.readFileSync (filePath) if (filecontent.toString ().includes (bogusImportString)) { console .log ('removing bogus import' ) fse.writeFileSync (filePath, filecontent.toString ().replace (bogusImportString, '' )) } } catch (e) { console .log ('react-virtualized not found' ) } } module .exports = removeBogusImportLine
相关issue:
错误的文件拓展名 记得在做这个迁移项目前我就看到过一个issue, webpack可以把js也是用jsx的loader, 但vite不行, evan认为这是错误的, 正确做法是把js改成jsx.
把转换做在vite插件里又难又没有必要, 只需要第一次执行的时候检查所有文件就可以了. 于是我写了段脚本.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 const { extname, join } = require ('path' )const { readdirSync, lstatSync, readFileSync, renameSync, writeFileSync } = require ('fs' );const excludes = ['node_modules' ]const cwd = process.cwd ()const walk = (dir, cb ) => { readdirSync (dir).forEach (d => { const curDir = join (dir, d); const stat = lstatSync (curDir) if (stat.isDirectory () && !excludes.includes (d)) { walk (curDir, cb) } else if (stat.isFile ()) { cb (curDir) } }) } const getUnusedImport = (content ) => { const defaultImportRegex = /import (\w+)\b from/g const multiImportRegex = /import \{(.*)\} from/g const defaultImports = [...content.matchAll (defaultImportRegex)].map (i => i[1 ]) const multiImports = [...content.matchAll (multiImportRegex)].map (i => i[1 ].split (',' )).flat ().map (s => s.trim ()) return { defaultImports, multiImports, } } const isJsx = (content ) => { return /<(\w+)([^>]|\n)+>/ .test (content) } const isTs = (content ) => { return /(\bdeclare\b|\binterface\b|import type|function \w+\b\(\w+\b: \w+\b)/ .test (content) } const transform = (file ) => { const fileExt = extname (file) let newExt = fileExt if (!['.js' , '.ts' , '.jsx' , '.tsx' ].includes (fileExt)) return let content = readFileSync (file).toString () const { defaultImports, multiImports } = getUnusedImport (content) if (defaultImports.length || multiImports.length ) { let changed = false function isUnused (str ) { return content.match (new RegExp (`\\b${str} \\b` , 'g' )).length === 1 } defaultImports.forEach ((item ) => { if (isUnused (item)) { changed = true content = content.replace (new RegExp (`import ${item} from .*\n` ), '' ) } }) multiImports.forEach ((item ) => { if (isUnused (item)) { changed = true content = content.replace (new RegExp (`(?<=import.*)\s?${item} ,?\s?` ), '' ) } }) if (changed) { writeFileSync (file, content) } } if (isJsx (content) && !fileExt.endsWith ('x' )) newExt += 'x' if (isTs (content) && !fileExt.includes ('t' )) newExt.replace ('j' , 't' ) if (fileExt !== newExt) renameSync (file, file.replace (fileExt, newExt)) } const transformfile = ( ) => { if (!readdirSync (cwd).find (dir => ['src' , 'packages' ].includes (dir))) { console .log ('请在项目下运行' ) return } walk (cwd, transform) } module .exports = transformfile
总结
webpack转vite, 非esm的第三方包会是个问题.
webpack转vite, 各种特性会是个问题. (就像不同浏览器对同一个约定进行了不同的实现)
webpack转vite不容易写成共用库, 反而更像是修修补补, 在内部小范围可以快速响应问题的地方使用更合理.
期待vite后续版本解决这系列问题. (如果vite足够流行, 都用vite起项目, 那这也将会不成为问题, 就像抛弃ie)