乾坤子项目为例,webpack转vite

851 阅读8分钟

一、调研背景

  1. 目前项目使用 Vue-cli 创建,打包方式为 webpack,随着项目不断迭代,后续可能会慢慢演变成一个中大型项目,项目打包时间会变长,构建效率降低

项目作为qiankun子应用运行, qiankun是否支持子应用切换 vite 开发

二、Vite介绍

vite是一种新型前端构建工具, 能够显著提升前端开发体验, 主要由两部分构成:

  1. 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)
  2. 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源,底层实现上,Vite 是基于 esbuild 预构建依赖的。esbuild使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快10 - 100倍。因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒。

三、Vite优势

vite 与 webpack 启动方式的差异

webpack启动方式

image.png vite启动方式

image.png

  1. webpack先打包,再启动开发服务器,请求服务器时直接给予打包后的结果;
  2. vite直接启动开发服务器,请求哪个模块再对哪个模块进⾏实时编译;
  3. 由于现代浏览器本⾝就⽀持ES Modules,会主动发起请求去获取所需⽂件。vite充分利⽤这点,将开发环境下的模块⽂件,就作为浏览器要执⾏的⽂件,⽽不是像webpack先打包,交给浏览器执⾏的⽂件是打包后的;
  4. 由于vite启动的时候不需要打包,也就⽆需分析模块依赖、编译,所以启动速度⾮常快。当浏览器请求需要的模块时,再对模块进⾏编译,这种按需动态编译的模式,极⼤缩短了编译时间,当项⽬越⼤,⽂件越多时,vite的开发时优势越明显;
  5. 在HRM⽅⾯,当某个模块内容改变时,让浏览器去重新请求该模块即可,⽽不是像webpack重新将该模块的所有依赖重新编译;

四、webpack转vite准备工作

  1. 利用vite + ts创建一个项目,并不是在原项目使用

  2. 将项目相关的package.json、文件直接拷贝使用

  3. 前置动作准备完毕,我们开始yarn 进行依赖包的安装

五、开始排错yarn dev

  1. 别名问题

解决方法:通过alias配置好别名,如果你有使用process相关变量,可通过define进行替换

let alias = {
   '@': path.resolve(__dirname, './src')
}
let define = {
   'process.env.NODE_ENV': command === 'serve' ? '"development"' : '"production"'
}
  1. 无法识别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$/
})
  1. 在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处理

  1. 拆分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子应用,会遇到下面问题

  1. 开发模式:在开发环境下,如果我们使用 vite 来构建 vue3 子应用,基于vite的构建机制,会在子应的 html 的入口文件的 script 标签上携带 type=module。而我们知道qiankun父应用引入子应用,本质上是将html做为入口文件,并通过import-html-entry这个库去加载子应用所需要的资源列表Js、css,然后通过eval直接执行,而基于vite构建的js中import、export并没有被转码,会导致直接报错(不允许在非 type=module 的 script 里面使用 import)
  2. 生产模式:生产模式下,因为没有诸如webpack中支持运行时publicPath,也就是__webpack_public_path__,换句话说就是vite不支持运行时publicPath,其主要作用是用来解决微应用动态载入的脚本、样式、图片等地址不正确的问题。

两种解决方案

  1. 只解决生产模式集成

    a. 通过配置vite build 相关api加上@rollup/plugin-html 插件 构建生产模式下 html ****入口) demo地址

    i.  局限性:为了拿到子应用导出的 生命周期钩子, 需要将项目打包成 umd 格式, 但vite的code-splitting(代码分割)功能并不支持iife和umd两种格式,这会导致路由无法实现懒加载。
    ii.  vite不支持运行时publicPath,所以只能在打包时写死Base配置
    1iii  图片最终会被打包成 base64
    

    b. 开发模式下不支持,不便于本地开发调试

  2. 解决开发➕生产模式集成, 官方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')]
        }
      },
 
    },
  };
};