一、调研背景
- 目前项目使用 Vue-cli 创建,打包方式为 webpack,随着项目不断迭代,后续可能会慢慢演变成一个中大型项目,项目打包时间会变长,构建效率降低
项目作为qiankun子应用运行, qiankun是否支持子应用切换 vite 开发
二、Vite介绍
vite是一种新型前端构建工具, 能够显著提升前端开发体验, 主要由两部分构成:
- 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
- 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源,底层实现上,Vite 是基于 esbuild 预构建依赖的。esbuild使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快10 - 100倍。因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒。
三、Vite优势
vite 与 webpack 启动方式的差异
webpack启动方式
vite启动方式
- webpack先打包,再启动开发服务器,请求服务器时直接给予打包后的结果;
- vite直接启动开发服务器,请求哪个模块再对哪个模块进⾏实时编译;
- 由于现代浏览器本⾝就⽀持ES Modules,会主动发起请求去获取所需⽂件。vite充分利⽤这点,将开发环境下的模块⽂件,就作为浏览器要执⾏的⽂件,⽽不是像webpack先打包,交给浏览器执⾏的⽂件是打包后的;
- 由于vite启动的时候不需要打包,也就⽆需分析模块依赖、编译,所以启动速度⾮常快。当浏览器请求需要的模块时,再对模块进⾏编译,这种按需动态编译的模式,极⼤缩短了编译时间,当项⽬越⼤,⽂件越多时,vite的开发时优势越明显;
- 在HRM⽅⾯,当某个模块内容改变时,让浏览器去重新请求该模块即可,⽽不是像webpack重新将该模块的所有依赖重新编译;
四、webpack转vite准备工作
-
利用vite + ts创建一个项目,并不是在原项目使用
-
将项目相关的package.json、文件直接拷贝使用
-
前置动作准备完毕,我们开始yarn 进行依赖包的安装
五、开始排错yarn dev
- 别名问题
解决方法:通过alias配置好别名,如果你有使用process相关变量,可通过define进行替换
let alias = {
'@': path.resolve(__dirname, './src')
}
let define = {
'process.env.NODE_ENV': command === 'serve' ? '"development"' : '"production"'
}
- 无法识别require问题
- 解决方法
手动修改 js引入方式,require改为import, 图片等静态资源可通过new URL(url, import.meta.url)
export function getAssetsImages (href: string):string {
return new URL(`../assets/${href}`, import.meta.url).href
}
第二种解决方式
通过 vite-plugin-require-transform 插件,在plugin使用
requireTransform({
fileRegex: /.js$|.ts$|.vue$/
})
- 在vue组件使用全局stylus 变量无法生效
但是因为我们有很多全局变量放在一个styl的文件里面,可如下引用
css: {
preprocessorOptions: {
stylus: {
imports: [path.resolve(__dirname, 'src/styles/variable.styl')]
},
scss: {
additionalData: `@import "./src/assets/css/global.scss";`
}
}
}
此时yarn dev启动问题解决了
六、 yarn build处理
- 拆分js和css文件
let rollupOptions = {
output:{
entryFileNames: 'js/[name].[hash].js',
// ⽤于命名代码拆分时创建的共享块的输出命名
chunkFileNames: 'js/[name].[hash].js',
// ⽤于输出静态资源的命名,[ext]表⽰⽂件扩展名
assetFileNames: '[ext]/[name].[hash].[ext]'
}
}
打包会出现如下警告:是因为打包的文件超出了默认的500K
解决方法
- 加大限制的大小将500kb改成1000kb或者更大:
build: {
chunkSizeWarningLimit : 600
}
- 分解块
let rollupOptions = {
output:{
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
// ⽤于从⼊⼝点创建的块的打包输出格式[name]表⽰⽂件名,[hash]表⽰该⽂件内容hash值
entryFileNames: 'js/[name].[hash].js',
// ⽤于命名代码拆分时创建的共享块的输出命名
chunkFileNames: 'js/[name].[hash].js',
// ⽤于输出静态资源的命名,[ext]表⽰⽂件扩展名
assetFileNames: '[ext]/[name].[hash].[ext]'
}
}
打包生成结果
- 使用cdn可通过Vite-html-plugin 插件配置变量,在plugin配置,在html中引入,类似webpack插件htmlWebpackPlugin
createHtmlPlugin({
minify: true, // 压缩
inject: {
data: {
title: '树根Mvp1',
cdn
},
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
<!-- 使用CDN的CSS文件 -->
<% for (var i = 0; i < cdn.css.length; i++) { %>
<link href="<%= cdn.css[i].url %>" rel="preload" as="style">
<% } %>
<!-- 使用CDN的JS文件 -->
<% for (var i = 0; i < cdn.js.length; i++) { %>
<script src="<%= cdn.js[i].url %>" preload></script>
<% } %>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
使用cdn,在build时将不需要打包的包排除,extenal 以及output.glabol配置,配置完成之后打包运行报错,路径解析不正确
查阅资料可通过rollup-plugin-external-globals 插件解决,out.format为umd格式
externalGlobals({
vue: 'Vue',
'element-plus': 'ElementPlus'
})
依旧报错,umd格式下不支持带代码分割
UMD and IIFE output formats are not supported for code-splitting builds.
因为我们的应用中有路由,使用了按需加载。我们将 rollup 的 output.inlineDynamicImports 配置true不进行代码分割
如果配置了output.inlineDynamicImports: true ,则分解快output.manualChunks 要删除,不支持
let rollupOptions = {
external: ['vue', 'element-plus'],
output:{
format: 'umd',
inlineDynamicImports: true,
// globals: {
// 'vue': 'Vue',
// 'element-plus': 'ElementPlus'
// },
// manualChunks(id) {
// if (id.includes('node_modules')) {
// return id.toString().split('node_modules/')[1].split('/')[0].toString();
// }
// },
entryFileNames: 'js/[name].[hash].js',
// ⽤于命名代码拆分时创建的共享块的输出命名
chunkFileNames: 'js/[name].[hash].js',
// ⽤于输出静态资源的命名,[ext]表⽰⽂件扩展名
assetFileNames: '[ext]/[name].[hash].[ext]'
}
}
const plugins = [
legacyPlugin({
targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
}),
vuePlugin(),
createHtmlPlugin({
minify: true, // 压缩
inject: {
data: {
title: '树根Mvp1',
cdn
},
}
})
]
if (command !== 'serve') {
plugins.push(
externalGlobals({
vue: 'Vue',
'element-plus': 'ElementPlus'
})
)
}
上述cdn加载方法不推荐,因为不支持分割代码,所有js打包成一个js文件,会很大
cdn方式我们还可以通过另外一个插件vite-plugin-cdn-import进行配置,来避免上述的操作比较麻烦
import importToCDN from 'vite-plugin-cdn-import'
importToCDN({
modules:[
{
name:'vue',
var:'Vue',
path:'https://unpkg.com/vue@3.2.30/dist/vue.global.js'
},
{
name:'element-plus',
var:'ElementPlus',
path:'https://unpkg.com/element-plus@2.1.4/dist/index.full.js',
css: 'https://unpkg.com/element-plus@2.1.4/dist/index.css'
}
]
})
\
七、如果项目是qiankun子应用,会遇到下面问题
- 开发模式:在开发环境下,如果我们使用 vite 来构建 vue3 子应用,基于vite的构建机制,会在子应的 html 的入口文件的 script 标签上携带 type=module。而我们知道qiankun父应用引入子应用,本质上是将html做为入口文件,并通过import-html-entry这个库去加载子应用所需要的资源列表Js、css,然后通过eval直接执行,而基于vite构建的js中import、export并没有被转码,会导致直接报错(不允许在非 type=module 的 script 里面使用 import)
- 生产模式:生产模式下,因为没有诸如webpack中支持运行时publicPath,也就是__webpack_public_path__,换句话说就是vite不支持运行时publicPath,其主要作用是用来解决微应用动态载入的脚本、样式、图片等地址不正确的问题。
两种解决方案
-
只解决生产模式集成
a. 通过配置vite build 相关api加上@rollup/plugin-html 插件 ( 构建生产模式下 html ****入口) demo地址
i. 局限性:为了拿到子应用导出的 生命周期钩子, 需要将项目打包成 umd 格式, 但vite的code-splitting(代码分割)功能并不支持iife和umd两种格式,这会导致路由无法实现懒加载。 ii. vite不支持运行时publicPath,所以只能在打包时写死Base配置 1iii 图片最终会被打包成 base64b. 开发模式下不支持,不便于本地开发调试
-
解决开发➕生产模式集成, 官方issue上有人提出的解决方案
a. 使用 vite-plugin-qiankun插件解决开发与生产的集成 参考文章
如果子项目使用cdn的话,可能会遇到在主应用报错,cdn 需要是umd格式,因为所有获取资源的请求是被乾坤劫持处理,在window.proxy挂在对应的变量
vite-plugin-qiankun使用会导致子应用在基座中(主应用)访问时, window是主应用的window对象,而不是子应用widow的代理对象(window.proxy)
所以如果使用了cdn, 如ecahrts, 使用方式为qiankunWindow.echarts,但是本地启动时要用原有的window,所以我们可以在全局变量上这样绑定
import * as echarts from 'echarts'
if (process.env.NODE_ENV === 'development') {
app.config.globalProperties.$globalEcharts = echarts
} else {
app.config.globalProperties.$globalEcharts = qiankunWindow.echarts
}
vue.config.js如下
/**
* @description:
* @Author: 赵
* @Date: 2022-05-22 12:37:22
*/
/* eslint-disable */
import fs from 'fs'
import { defineConfig, loadEnv } from 'vite'
import legacyPlugin from '@vitejs/plugin-legacy';
import qiankun from 'vite-plugin-qiankun';
// 配置cdn
import externalGlobals from "rollup-plugin-external-globals"
import importToCDN, { autoComplete } from 'vite-plugin-cdn-import'
import * as path from 'path';
import vuePlugin from '@vitejs/plugin-vue';
import { createHtmlPlugin } from 'vite-plugin-html'
import { getCdnList } from './build/cdn/cdn'
const cdn = getCdnList()
export default ({
command,
mode
}) => {
const ENV = loadEnv(mode, process.cwd())
let rollupOptions = {
external: ['echarts'],
output:{
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
entryFileNames: 'js/[name].[hash].js',
// ⽤于命名代码拆分时创建的共享块的输出命名
chunkFileNames: 'js/[name].[hash].js',
// ⽤于输出静态资源的命名,[ext]表⽰⽂件扩展名
assetFileNames: '[ext]/[name].[hash].[ext]'
},
}
let optimizeDeps = {};
let alias = {
'@': path.resolve(__dirname, './src'),
}
let proxy = {}
// todo 替换为原有变量
let define = {
'process.env.NODE_ENV': command === 'serve' ? '"development"' : '"production"'
}
const plugins = [
legacyPlugin({
targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
}),
vuePlugin(),
createHtmlPlugin({
minify: true, // 压缩
inject: {
data: {
title: '项目title',
cdn
},
}
}),
importToCDN({
modules:[
{
name: 'echarts',
var: 'echarts',
path: 'https://unpkg.com/echarts@5.3.3/dist/echarts.js',
}
]
}),
qiankun(packageName, {
useDevMode: true
})
]
let esbuild = {}
return {
base: ENV.VITE_APP_BASE_URL, // index.html文件所在位置
root: './', // js导入的资源路径
resolve: {
alias,
},
define: define,
server: {
// 代理
proxy,
host: '0.0.0.0'
},
build: {
target: 'es2015',
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
manifest: false, // 是否产出maifest.json
sourcemap: false, // 是否产出soucemap.json
outDir: 'dist', // 产出目录
rollupOptions,
chunkSizeWarningLimit: 900,
},
esbuild,
optimizeDeps,
plugins,
css: {
preprocessorOptions: {
stylus: {
imports: [path.resolve(__dirname, 'src/styles/variable.styl')]
}
},
},
};
};