愿景

希望通过此系列文章,能给读者提供一个存/增量项目接入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}.html
  • src/pages/${entryName}/index.html
  • public/${entryName}.html
  • public/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对应的entryJsentryHtml
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进行临时的存储
  • 构建入口模板路径htmlEntryPathentryJs的目录加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,
},
},
};
}
}

构建入口内容生成

其中resolveIdload钩子一起完成入口文件的处理

  • 其中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钩子,在构建完成后,服务关闭前进行文件调整

  • 遍历entrydist/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.jsindex.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配置文件进行自行的拓展

后续规划

  1. 目前wp2vite在配置转换这一块,还不能太满足使用要求,准备提PR增强一下
  2. 将内部能力抽成一个个单独的vite插件

webpack 项目接入Vite的通用方案介绍(下)的更多相关文章

  1. webpack 项目接入Vite的通用方案介绍(上)

    愿景 希望通过本文,能给读者提供一个存/增量项目接入Vite的点子,起抛砖引玉的作用,减少这方面能力的建设成本 在阐述过程中同时也会逐渐完善webpack-vite-serve这个工具 读者可直接fo ...

  2. 传统Java Web(非Spring Boot)、非Java语言项目接入Spring Cloud方案

    技术架构在向spring Cloud转型时,一定会有一些年代较久远的项目,代码已变成天书,这时就希望能在不大规模重构的前提下将这些传统应用接入到Spring Cloud架构体系中作为一个服务以供其它项 ...

  3. 传统Java Web(非Spring Boot)、非Java语言项目接入Spring Cloud方案--temp

    技术架构在向spring Cloud转型时,一定会有一些年代较久远的项目,代码已变成天书,这时就希望能在不大规模重构的前提下将这些传统应用接入到Spring Cloud架构体系中作为一个服务以供其它项 ...

  4. 原有vue项目接入typescript

    原有vue项目接入typescript 为什么要接入typescript javascript由于自身的弱类型,使用起来非常灵活. 这也就为大型项目.多人协作开发埋下了很多隐患.如果是自己的私有业务倒 ...

  5. 基于Vue/React项目的移动端适配方案

    本文的目标是通过下文介绍的适配方案,使用vue或react开发移动端及H5的时候,不需要再关心移动设备的大小,只需要按照固定设计稿的px值布局,提升开发效率. 下文给出了本人分别使用create-re ...

  6. 京东分布式MySQL集群方案介绍

    背景 数据库作为一个非常基础的系统,任何一家互联网公司都会使用,数据库产品也很多,有Oracle.SQL Server .MySQL.PostgeSQL.MariaDB等,像SQLServer/Ora ...

  7. 在找一份相对完整的Webpack项目配置指南么?这里有

    Webpack已经出来很久了,相关的文章也有很多,然而比较完整的例子却不是很多,让很多新手不知如何下脚,下脚了又遍地坑 说实话,官方文档是蛮乱的,而且有些还是错的错的..很多配置问题只有爬过坑才知道 ...

  8. PC、h5项目接入第三方支付宝扫码登录、扫码付款

    首先介绍一下pc项目接入支付宝扫码支付. 1.pc.移动接入支付宝扫码支付. 其实这个逻辑很简单,前端所需要处理的不是很多,后台会给一个连接,前端只需要将要支付的订单id拼接在这个连接上,然后打开跳转 ...

  9. webpack项目如何正确打包引入的自定义字体?

    一. 如何在Vue或React项目中使用自定义字体 在开发前端项目时,经常会遇到UI同事希望在项目中使用一个炫酷字体的需求.那么怎么在项目中使用自定义字体呢? 其实实现起来并不复杂,可以借用CSS3 ...

  10. webpack项目如何正确打包引入的自定义字体

    webpack项目如何正确打包引入的自定义字体 一. 如何在Vue或React项目中使用自定义字体 在开发前端项目时,经常会遇到UI同事希望在项目中使用一个炫酷字体的需求.那么怎么在项目中使用自定义字 ...

随机推荐

  1. Python中os.walk函数说明

    这个函数对于文件方面的遍历等其他方面的操作来说功能很强大,比如批量修改文件名.批量移动文件.将所有不在一个文件夹下的文件移动到同一个文件夹下等等. 这个其实很简单的,用一个示例就能明白这个函数的具体用 ...

  2. Wamp MySQL 报错 Got a packet bigger than 'max_allowed_packet' bytes

    点击电脑右下角wamp图标,然后进入mysql 下面的 my.ini 转移数据发现报这个错,字面意思允许的不够大.网上很多说法不起作用,解决方法如下: [mysqld] port=3306 expli ...

  3. Delphi批量替换工具Cnpack

    操,delphi官方 没有 批量替换工具,需要用到cnpack才可以,

  4. NC51097 Parity game

    题目链接 题目 题目描述 Now and then you play the following game with your friend. Your friend writes down a se ...

  5. Ubuntu下通过Wine安装LTSpice 17.1.8

    LTSpice LTSpice 是常用的电路模拟软件, 但是只有 Windows 版本和 Mac 版本, 在 Linux 下需要用 Wine 运行. 以下说明如何在 Ubuntu 下安装最新的 LTS ...

  6. IPFS Gateway Selector IPFS下载网关选择工具

    简介 用IPFS作文件分享可以覆盖很多场景, 现在IPFS网关也相当多了, 但是因为国内网络的状况, 不同网关在不同网络运营商的表现差别很大, 导致你提供的下载链接在对方那里可能速度很慢, 甚至无法访 ...

  7. useEffect与useLayoutEffect

    useEffect与useLayoutEffect useEffect与useLayoutEffect可以统称为Effect Hook,Effect Hook可以在函数组件中执行副作用操作,副作用是指 ...

  8. 【Android 逆向】【攻防世界】android2.0

    这是一道纯算法还原题 1. apk安装到手机,提示输入flag,看来输入就是flag 2. jadx 打开apk查看 this.button.setOnClickListener(new View.O ...

  9. Docker实践之07-数据管理

    目录 一.数据卷概述 二.创建数据卷 三.查看数据卷 四.挂载数据卷 五.删除数据卷 六.挂载主机目录或文件 七.挂载数据卷与主机目录/文件的比较 一.数据卷概述 数据卷是一个可供一个或多个容器使用的 ...

  10. django学习第十三天--自定义中间件

    jquery操作cookie 下载地址 http://plugins.jquery.com/cookie/ 引入 <script type="text/javascript" ...