webpack 项目接入Vite的通用方案介绍(下)
愿景
希望通过此系列文章,能给读者提供一个存/增量项目接入Vite的点子,起抛砖引玉的作用,减少这方面能力的建设成本
在阐述过程中同时也会逐渐完善webpack-vite-serve这个工具
读者可直接fork这个工具仓库,针对个人/公司项目场景进行定制化的二次开发
前言
在上一篇的文章中,大概介绍了webpack项目接入Vite的处理思路,大体就是以下步骤:
这些内容的处理都是可以通过vite插件实现
webpack-vite-serve介绍
这段时间就在不断完善这个库的功能,下面先简单介绍一下其使用,再阐述一些插件的实现原理
目标:为webpack项目提供一键接入Vite的能力
安装依赖
npm install webpack-vite-serve -D
# or
yarn add webpack-vite-serve -D
# or
pnpm add webpack-vite-serve -D
添加启动指令
# devServer
wvs start [options]
# build
wvs build [options]
可选参数
-f,--framework <type>:指定使用的业务框架 (vue,react),自动引入业务框架相关的基础插件-s,--spa:按照单页应用目录结构处理src/${entryJs}-m,--mpa:按照多页应用目录结构处理src/pages/${entryName}/${entryJs}-d,--debug [feat]:打印debug信息-w,--wp2vite:使用 wp2vite 自动转换webpack文件
其它说明
项目遵循常规的 单页/多页应用 项目的目录结构即可
vite配置通过官方的vite.config.[tj]s配置文件拓展即可
效果
在线体验demo地址:已创建stackblitz
如由于网络原因无法访问,可clone仓库访问其中demo体验
MPA支持
Dev-页面模板
首先是devServer环境的页面模板处理
根据请求路径获取entryName
- 使用
/拆分请求路径得到paths - 遍历寻找第一个
src/pages/${path}存在的path,此path即为entryName 
function getEntryName(reqUrl:string, cfg?:any) {
  const { pathname } = new URL(reqUrl, 'http://localhost');
  const paths = pathname.split('/').filter((v) => !!v);
  const entryName = paths.find((p) => existsSync(path.join(getCWD(), 'src/pages', p)));
  if (!entryName) {
    console.log(pathname, 'not match any entry');
  }
  return entryName || '';
}
寻找模板文件,按照如下顺序探寻
src/pages/${entryName}/${entryName}.htmlsrc/pages/${entryName}/index.htmlpublic/${entryName}.htmlpublic/index.html
function loadHtmlContent(reqPath:string) {
  // 兜底页面
  const pages = [path.resolve(__dirname, '../../public/index.html')];
  // 单页/多页默认 public/index.html
  pages.unshift(resolved('public/index.html'));
  // 多页应用可以根据请求的 路径 作进一步的判断
  if (isMPA()) {
    const entryName = getEntryName(reqPath);
    if (entryName) {
      pages.unshift(resolved(`public/${entryName}.html`));
      pages.unshift(resolved(`src/pages/${entryName}/index.html`));
      pages.unshift(resolved(`src/pages/${entryName}/${entryName}.html`));
    }
  }
  const page = pages.find((v) => existsSync(v));
  return readFileSync(page, { encoding: 'utf-8' });
}
Dev-entryJs
多页应用的entryJs就按约定读取src/pages/${entryName}/${main|index}文件
function getPageEntry(reqUrl) {
  if (isMPA()) {
    const entryName = getEntryName(reqUrl);
    return !!entryName && getEntryFullPath(`src/pages/${entryName}`);
  }
  // 默认SPA
  const SPABase = 'src';
  return getEntryFullPath(SPABase);
}
Build
vite构建的入口是html模板,可以通过build.rollup.input属性设置
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        index: 'src/pages/index/index.html',
        second: 'src/pages/second/second.html',
      },
    },
  },
});
按照如上配置,构建产物中的html目录将会如下
* dist
  * src/pages/index/index.html
  * src/pages/second/second.html
  * assets
不太符合通常的习惯,常规格式如下
* dist
  * index.html
  * second.html
  * assets
所以需要通过插件处理构建入口文件和调整构建后的产物位置
插件结构
export default function BuildPlugin(): PluginOption {
  let userConfig:ResolvedConfig = null;
  return {
    name: 'wvs-build',
    // 只在构建阶段生效
    apply: 'build',
    // 获取最终配置
    configResolved(cfg) {
      userConfig = cfg;
    },
    // 插件配置处理
    config() {
    },
    resolveId(id) {
    },
    load(id) {
    },
    // 构建完成后
    closeBundle() {
    },
  };
}
通过configResolved钩子获取最终配置,配置提供给其它钩子使用
获取entry
首先获取src/pages下所有的entry
const entry = [];
if (isMPA()) {
  entry.push(...getMpaEntry());
} else {
  // 单页应用
  entry.push({
    entryName: 'index',
    entryHtml: 'public/index.html',
    entryJs: getEntryFullPath('src'),
  });
}
entry的定义为
interface Entry{
  entryHtml:string
  entryName:string
  entryJs:string
}
获取逻辑如下
- 先获取所有的
EntryName - 在遍历获取每个entry对应的
entryJs与entryHtml 
export function getMpaEntry(baseDir = 'src/pages') {
  const entryNameList = readdirSync(resolved(baseDir), { withFileTypes: true })
    .filter((v) => v.isDirectory())
    .map((v) => v.name);
  return entryNameList
    .map((entryName) => ({ entryName, entryHtml: '', entryJs: getEntryFullPath(path.join(baseDir, entryName)) }))
    .filter((v) => !!v.entryJs)
    .map((v) => {
      const { entryName } = v;
      const entryHtml = [
        resolved(`src/pages/${entryName}/${entryName}.html`),
        resolved(`src/pages/${entryName}/index.html`),
        resolved(`public/${entryName}.html`),
        resolved('public/index.html'),
        path.resolve(__dirname, '../../public/index.html'),
      ].find((html) => existsSync(html));
      return {
        ...v,
        entryHtml,
      };
    });
}
生成构建配置
根据得到的entry生成 build.rollup.input
- 获取每个
entryHtml的内容,然后使用map进行临时的存储 - 构建入口模板路径
htmlEntryPath取entryJs的目录加index.html 
实际上htmlEntryPath这个路径并不存在任何文件
所以需要通过其它钩子,利用htmlContentMap存储的内容进行进一步的处理
const htmlContentMap = new Map();
// 省略其它无关代码
{
  config() {
    const input = entry.reduce((pre, v) => {
      const { entryName, entryHtml, entryJs } = v;
      const html = getEntryHtml(resolved(entryHtml), path.join('/', entryJs));
      const htmlEntryPath = resolved(path.parse(entryJs).dir, tempHtmlName);
      // 存储内容
      htmlContentMap.set(htmlEntryPath, html);
      pre[entryName] = htmlEntryPath;
      return pre;
    }, {});
    return {
      build: {
        rollupOptions: {
          input,
        },
      },
    };
  }
}
构建入口内容生成
其中resolveId与load钩子一起完成入口文件的处理
- 其中
id即为资源请求的路径 - 接着直接从
htmlContentMap去除模板的内容即可 
{
  load(id) {
    if (id.endsWith('.html')) {
      return htmlContentMap.get(id);
    }
    return null;
  },
  resolveId(id) {
    if (id.endsWith('.html')) {
      return id;
    }
    return null;
  },
}
产物目录调整
使用closeBundle钩子,在构建完成后,服务关闭前进行文件调整
- 遍历
entry将dist/src/pages/entryName/index.html移动到dist下 - 移除
dist/src下的内容 
closeBundle() {
  const { outDir } = userConfig.build;
  // 目录调整
  entry.forEach((e) => {
    const { entryName, entryJs } = e;
    const outputHtmlPath = resolved(outDir, path.parse(entryJs).dir, tempHtmlName);
    writeFileSync(resolved(outDir, `${entryName}.html`), readFileSync(outputHtmlPath));
  });
  // 移除临时资源
  rmdirSync(resolved(outDir, 'src'), { recursive: true });
}
webpack配置转换
目前社区有一个CLI工具:wp2vite支持了这个功能,所以笔者不打算从0-1再建设一个
由于是cli工具,没有提供一些直接调用的方法去获取转换前后的配置,所以接入插件中的使用体验还不是很好,后续准备提PR改造一下这个工具
接入wp2vite的插件实现如下
import wp2vite from 'wp2vite';
// 省略不重要的 import
export default function wp2vitePlugin(): PluginOption {
  return {
    name: 'wvs-wp2vite',
    enforce: 'pre',
    async config(_, env) {
      const cfgFile = resolved('vite.config.js');
      const tplFile = resolved('index.html');
      const contentMap = new Map([[cfgFile, ''], [tplFile, '']]);
      const files = [cfgFile, tplFile];
      console.time('wp2vite');
      // 判断是否存在vite.config.js 、index.html
      // 避免 wp2vite 覆盖
      files.forEach((f) => {
        if (existsSync(f)) {
          contentMap.set(f, readFileSync(f, { encoding: 'utf-8' }));
        }
      });
      // 转换出配置文件vite.config.js
      await wp2vite.start(getCWD(), {
        force: false,
        // 统一开启debug
        debug: !!process.env.DEBUG,
      });
      // TODO:提PR优化
      // 转换耗时计算
      console.timeEnd('wp2vite');
      // 获取wp2vite转换出的配置
      const cfg = await getUserConfig(env, 'js');
      contentMap.forEach((v, k) => {
        if (v) {
          // 如果修改了内容,还原内容
          writeFileSync(k, v);
        } else {
          // 移除创建的文件
          unlinkSync(k);
        }
      });
      if (cfg.config) {
        const { config } = cfg || {};
        // 留下需要的配置
        return {
          resolve: config?.resolve,
          server: config?.server,
          css: config?.css,
        };
      }
      return null;
    },
  };
}
wp2vite,对外暴露了一个start方法调用
调用后会根据项目的webpack配置生成2个新文件(vite.config.js,index.html),并修改package.json添加指令与依赖
所以在生成前如果项目中存在这些文件则需要先将这些内容存储起来
其中获取用户配置的getUserConfig实现如下
import { loadConfigFromFile, ConfigEnv } from 'vite';
export function getUserConfig(configEnv:ConfigEnv, suffix = '') {
  const configName = 'vite.config';
  const _suffix = ['ts', 'js', 'mjs', 'cjs'];
  if (suffix) {
    _suffix.unshift(suffix);
  }
  const configFile = _suffix.map((s) => `${configName}.${s}`).find((s) => existsSync(s));
  return loadConfigFromFile(configEnv, configFile);
}
vite提供了loadConfigFromFile方法,只需要在此方法中做一层简单的封装即可直接使用,方法内部使用esbuild自动对ts与es语法进行了转换
总结
到目前为止,建设的能力已基本能够满足常规项目的开发
能力未及之处用户亦可直接在工程中添加vite配置文件进行自行的拓展
后续规划
- 目前
wp2vite在配置转换这一块,还不能太满足使用要求,准备提PR增强一下 - 将内部能力抽成一个个单独的vite插件
 
webpack 项目接入Vite的通用方案介绍(下)的更多相关文章
- webpack 项目接入Vite的通用方案介绍(上)
		
愿景 希望通过本文,能给读者提供一个存/增量项目接入Vite的点子,起抛砖引玉的作用,减少这方面能力的建设成本 在阐述过程中同时也会逐渐完善webpack-vite-serve这个工具 读者可直接fo ...
 - 传统Java Web(非Spring Boot)、非Java语言项目接入Spring Cloud方案
		
技术架构在向spring Cloud转型时,一定会有一些年代较久远的项目,代码已变成天书,这时就希望能在不大规模重构的前提下将这些传统应用接入到Spring Cloud架构体系中作为一个服务以供其它项 ...
 - 传统Java Web(非Spring Boot)、非Java语言项目接入Spring Cloud方案--temp
		
技术架构在向spring Cloud转型时,一定会有一些年代较久远的项目,代码已变成天书,这时就希望能在不大规模重构的前提下将这些传统应用接入到Spring Cloud架构体系中作为一个服务以供其它项 ...
 - 原有vue项目接入typescript
		
原有vue项目接入typescript 为什么要接入typescript javascript由于自身的弱类型,使用起来非常灵活. 这也就为大型项目.多人协作开发埋下了很多隐患.如果是自己的私有业务倒 ...
 - 基于Vue/React项目的移动端适配方案
		
本文的目标是通过下文介绍的适配方案,使用vue或react开发移动端及H5的时候,不需要再关心移动设备的大小,只需要按照固定设计稿的px值布局,提升开发效率. 下文给出了本人分别使用create-re ...
 - 京东分布式MySQL集群方案介绍
		
背景 数据库作为一个非常基础的系统,任何一家互联网公司都会使用,数据库产品也很多,有Oracle.SQL Server .MySQL.PostgeSQL.MariaDB等,像SQLServer/Ora ...
 - 在找一份相对完整的Webpack项目配置指南么?这里有
		
Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑 说实话,官方文档是蛮乱的,而且有些还是错的错的..很多配置问题只有爬过坑才知道 ...
 - PC、h5项目接入第三方支付宝扫码登录、扫码付款
		
首先介绍一下pc项目接入支付宝扫码支付. 1.pc.移动接入支付宝扫码支付. 其实这个逻辑很简单,前端所需要处理的不是很多,后台会给一个连接,前端只需要将要支付的订单id拼接在这个连接上,然后打开跳转 ...
 - webpack项目如何正确打包引入的自定义字体?
		
一. 如何在Vue或React项目中使用自定义字体 在开发前端项目时,经常会遇到UI同事希望在项目中使用一个炫酷字体的需求.那么怎么在项目中使用自定义字体呢? 其实实现起来并不复杂,可以借用CSS3 ...
 - webpack项目如何正确打包引入的自定义字体
		
webpack项目如何正确打包引入的自定义字体 一. 如何在Vue或React项目中使用自定义字体 在开发前端项目时,经常会遇到UI同事希望在项目中使用一个炫酷字体的需求.那么怎么在项目中使用自定义字 ...
 
随机推荐
- CTAS建表时报错ORA-65114
			
环境: Oracle 19.16 多租户架构 1.问题现象: SQL> create table T1 as select * from v$active_session_history * E ...
 - 【Flink入门修炼】1-3 Flink WordCount 入门实现
			
本篇文章将带大家运行 Flink 最简单的程序 WordCount.先实践后理论,对其基本输入输出.编程代码有初步了解,后续篇章再对 Flink 的各种概念和架构进行介绍. 下面将从创建项目开始,介绍 ...
 - 开启未来创新之门:.NET Conf China 2023 精彩回顾及资料下载
			
2023年12月16日-17日,一年一度的 .NET Conf China 2023 中国 .NET 开发者大会在北京盛大举办!大会以第一天主会场 + AI..NET 8.云原生.IoT.前端& ...
 - P9801 [NERC2018] King Kog’s Reception
			
题目传送门 前置知识 线段树 解法 第一眼感觉和 luogu P1083 [NOIP2012 提高组] 借教室 很像.本题同样采用线段树维护,\(sum_{l,r}(1 \le l \le r \le ...
 - es6 快速入门 系列 —— 模块
			
其他章节请看: es6 快速入门 系列 模块 es6 以前,每个 javascript 都共享这一个全局作用域,随着代码量的增加,容易引发一些问题,比如命名冲突. 其他语言有包这样的概念来定义作用域, ...
 - tensorflow中交叉熵损失函数详解
			
1 前言 tensorflow中定义了3个交叉熵损失函数: softmax_cross_entropy_with_logits(logits, labels) softmax_cross_entrop ...
 - java处理json类型数据--阿里巴巴fastjson api常用方法实战
			
fastjson介绍 最近工作上经常需要解析json类型数据以及java对象到json类型的互转,特地研究了下阿里巴巴的fastjson,这个是国内用的 比较多的json转换api,还有其他的入jac ...
 - 全栈式测试平台RunnerGo核心功能模块-接口管理
			
全栈式测试平台RunnerGo相对于市面上其他性能测试产品来说更简单,它不用其他相关配件,天然支持分布式,有单独的机器做分布式的负载均衡,自有一套智能算法算压力机的配置从而平均分配,并从场景链路的流 ...
 - [Revit二次开发] 使用过滤器时,可能存在的坑:FilteredElementCollector.MoveNext()报错
			
1.问题描述 在使用FilteredElementCollector时,如果涉及到需要对collector进行多次过滤处理,可能会出现MoveNext的报错. 问题代码如下: 1 var collec ...
 - PrettyTable模块
			
# 用来生成美观的ASCII格式的表格 pip install prettytable # 导入 from prettytable import PrettyTable # 使用 tb = pt.Pr ...