背景

我们有个用umi搭建的项目一直在迭代增加功能,项目引入的第三方库也越来越多,本地开发webpack启动dev-server越来越慢,就拿我自己用的Macbook pro 14寸来说,搭载了M1 Pro芯片都要一分钟左右,热更新要3秒左右,其他前端小伙伴用的机子启动要3分钟,热更新等10多秒,这是非常影响开发体验和效率的,目前构建工具除了webpack之外还有esbuild以及swc,而目前基于esbuild的vite是非常不错的选择,所以有了迁移vite的动力

vite VS webpack

相信都2022年了大家应该都或多或少了解过vite,简介我就不再赘述了,简单列一下vite与webpack的优缺点:

vite Webpack
启动开发服务器 利用浏览器的原生ESM加载源码模块,启动时处理一次依赖预构建即可,二次启动秒开,访问时浏览器原生解析依赖 递归依赖分析、转换代码、编译、打包输出主流浏览器可直接执行的js文件,浏览器访问直接渲染
构建打包器 基于ESBuild使用Go编写,构建速度比nodejs快10-100倍 webpack由javascript编写运行在nodejs环境,运行效率较差
热更新 精准更新已修改的ESM模块 修改文件后需要重新构建文件,且随着应用体积增大而花费更长时间
生态 vite生态只能说勉强够用,某些功能可能需要妥协或者自己实现 Loader和Plugin生态丰富,方案齐全
生产环境 Esbuild本身存在一些限制,所以生产环境采用的rollup 依托丰富的生态,稳定可靠

vite就像苹果刚出M1芯片的那种新世代的感觉,第一感觉是快,上手后是真香,但生态是个小遗憾。

总结一下优缺点:

在启动开发服务器、热更新方面vite掌握了绝对优势,而在生态与生产环境构建的场景下rollup对比webpack优势不明显,rollup比较适合于类库的项目打包,所以对于生产环境构建和生态考虑我依然会选择webpack

所以从对比来看vite和webpack 各自的优点都非常绝妙的满足了对方的缺点,这难道就是命中注定的另一半么,那么为何不取长补短,将双方的优势合为一体呢

vite + webpack

我们只需要在开发环境用vite,生产环境用webpack就可以形成互补,即能提高开发效率,还能保证生产稳定,值得注意的是这两套构建工具要在同一套代码里正常执行。

vite对于文件结构有特殊要求(模块化的样式文件名必须要有.module),与webpack的配置形式也大相径庭,所以还需要一个适配器(Adapter),只使用一种配置形式实现两套构建工具正常执行。

这个适配器,就是来自阿里飞冰团队的ice.js`,ice.js从2.0开始同时支持Webpack@5 以及 Vite@2 两种模式,只用一套特定的配置形式,只要通过环境变量就可以实现开发环境用vite,生产环境用webpack了。

实现vite + webpack

我们可以快速初始化一个ice.js项目:

1
2
3
$ npm init ice <projectName>
# or
$ yarn create ice <projectName>

根据交互提示选择想要的模板,创建完就可以进入项目里安装依赖启动:

1
2
3
4
5
$ cd <projectName>
# 安装依赖
$ npm install
# 启动项目
$ npm start

执行上述命令后,会自动打开浏览器窗口访问 http://localhost:3333,这时应该看到默认的页面。

然后在package.json里配置启动命令区分开发环境使用vite模式,生产环境使用webpack模式即可:

1
2
3
4
5
"scripts": {
"start": "icejs start --mode vite",
"build": "icejs build --mode webpack",
....
},

个性化定制

由于ice.js内置了很多约定式的第三方能力(路由、状态管理、请求库、权限,封装过的entry等),而我从旧项目迁移必定会有不兼容写法的情况,所以想放弃内置功能,不要约定式的文件结构,自己写entry,自己接入路由、请求库、状态管理等功能,ice.js也是支持这么做的,需要在ice的配置中关闭这些功能:

1
2
3
4
store: false, // 关闭自带的状态管理
auth: false, // 关闭自带的权限控制
request: false, // 关闭自带的request
disableRuntime: false, // 关闭所有运行时能力

然后根据自己需要配置src/app.tsx的entry代码就可以了,我这里有一个已经引入了react-router-dom、dva的项目可以参考:lipten/ice-vitepack-project: icejs2.0 + antd + dva.js 定制entry和工程配置的初始项目 (github.com)

常见问题

第三方库不兼容ESModule:

vite只能加载ESModule规范的模块,对于其他模块规范文件需要特殊处理,vite的依赖预构建默认会找package.json里的依赖包自动处理兼容的写法,所以第三方库我们可以放到src路径下,通过yarn add ./src/xxx 添加本地库

添加后就会在package.json自动有下面这行依赖

1
"xxx": "./src/libs/xxx",

require语句换成Import xxx from ‘xxx’

一些静态资源可能用了require语句,需要换成ESM的写法

动态路径加载文件地址

当vite需要实现require(@/assets/images/${imgPath})这种动态路径加载时,换成以下写法:

1
2
// 仅支持相对路径
new URL(`../../../src/${path}`, import.meta.url).href

可以做一下兼容webpack的处理

1
2
3
export const importAssetsPath = (path: string) => {
return window.IS_VITE ? new URL(`../../../src/assets/${path}`, import.meta.url).href : require(`../assets/${path}`);
};

动态加载模块import().then()

vite不支持动态加载模块import(‘xxxx’).then()

Image
Image

vite自带的rollup支持这种用法,所以可以增加 / @vite-ignore / 可以支持import(‘xxxx’).then()

Image
Image

引入@ant-design/compatible报错

Image
Image

因为@ant-design/compatible依赖了draft-js里面用了global这个变量在浏览器会报错,只要在入口js赋值一下global即可

1
window.global = window

require()使用动态路径时必须要有src/xxx作为静态路径

webpack可能在分析require路径时需要确定src下以及路径作为静态字符串目录

下面的代码是无法正常解析的:

1
2
3
4
const path = 'assets/images/xxx.png'
require(`@/${path}`)
require(`../${path}`)
require(`~/src/${path}`)

应该改为下面的路径写法:

1
2
3
4
const path = 'images/xxx.png'
require(`@/assets/${path}`)
require(`../assets/${path}`)
require(`~/src/assets/${path}`)