愿景

希望通过本文,能给读者提供一个存/增量项目接入Vite的点子,起抛砖引玉的作用,减少这方面能力的建设成本

在阐述过程中同时也会逐渐完善webpack-vite-serve这个工具

读者可直接fork这个工具仓库,针对个人/公司项目场景进行定制化的二次开发

背景

在当下的业务开发中处处可见webpack的身影,大部分的业务项目采用的构建工具也都是它。

随着时间的推移,存量老项目体积越来越大,开发启动(dev)/构建(build) 需要的时间越来越长。针对webpack的优化手段越来越有限。

于是乎某些场景出现了用其它语言写的工具,帮助构建/开发提效。如SWC(Rust),esbuild(Go)

当然上述工具并不是一个完整的构建工具,不能取代webpack直接使用,只是通过plugin,为webpack工作提效

当下另一种火热的方案是bundleless,利用浏览器原生支持ES Module的特性,让浏览器接管"打包"工作,工具只负责对浏览器请求的资源进行相应的转换,从而极大的减少服务的启动时间,提升开发体验与开发幸福感

比较出名的两个产品就是snowpackVite

本文的主角就是Vite下一代前端开发与构建工具

由于Vite的周边还处于建设期,要完全替代webpack,还需要一定时日,为了保证存量线上项目的稳定性,Vite作为一个开发时可选的能力接入是比较推荐的一个做法。

# webpack devServer
npm run dev # Vite devServer
npm run vite

目标

为webpack项目开发环境提供最简单的Vite接入方案

待接入项目只需要做极小的变动就能享受到Vite带来的开发乐趣

方案

  1. 做一个CLI工具,封装Vite启动项目的能力
  2. 将Vite相关的配置全部收敛于插件内,自动将webpack配置转化为Vite配置
  3. 对外提供一些可选参数,用于手动指定配置文件的位置

demo效果

Vue SPA

React SPA

在最简单的Demo工程中,Vite的启动/HMR速度也是明显比webpack快不少的

其它常见项目类型的demo也会逐渐的完善到源码仓库中

实现

1. 初始化工程

完整的工程结构移步仓库

注册一个启动方法start

src/bin.ts

#!/usr/bin/env node
import { Command } from 'commander';
import { startCommand } from './command';
program.command('start')
.alias('s')
.action(startCommand); program.parse(process.argv);
export default function startCommand() {
console.log('hello vite');
}

package.json中添加指令

  • 其中wvs为自定义的指令
  • npm run dev:利用typescript依赖提供的指令,监听文件变动,自动将其转换js文件
{
"bin": {
"wvs": "./dist/bin.js"
},
"scripts": {
"dev": "tsc -w -p .",
"build": "rimraf dist && tsc -p ."
},
}

项目根目录执行npm link,注册指令

npm link

测试

wvs start

紧接着我们用Vue-CLICreate React App分别创建两个webpack的SPA应用进行接下来的实验

vue create vue-spa

npx create-react-app react-spa

2. 收敛Vite启动

Vite的启动比较简单,只需要执行vite这个指令就行s

在我们的CLI工具里使用spawn创建子进程启动Vite

  • 其中cwd用于指定子进程的工作目录
  • stdio:子进程的标准输入输出配置
import { spawn } from 'child_process';

export default function startCommand() {
const viteService = spawn('vite', ['--host', '0.0.0.0'], {
cwd: process.cwd(),
stdio: 'inherit',
}); viteService.on('close', (code) => {
process.exit(code);
});
}

这里为了方便调试,咱们全局安装一下Vite

npm i -g vite

在启动模板public/index.html里添加一个<h1>Hello Vite</h1>

在demo项目里运行wvs start

打开对应地址

# vue
http://localhost:3000/
# react
http://localhost:3001/

得到了如下的结果,提示找不到页面(意料之中)

通过文档得知,Vite会默认寻找index.html作为项目的入口文件

这就带来了第一个要处理的问题,多页应用下可能有多个模板文件

如何根据访问路由动态的指定这个x.html的入口

在解决问题之前,咱们再简单完善一下启动指令,为其指定一个vite.config.js 配置文件

通过vite --help,可以看到通过--config参数指定配置文件位置

export default function startCommand() {
const configPath = require.resolve('./../config/vite.js');
const viteService = spawn('vite', ['--host', '0.0.0.0', '--config', configPath], {
cwd: process.cwd(),
stdio: 'inherit',
});
}

这里指向配置文件的绝对路径

config/vite.ts

import { defineConfig } from 'vite';

module.exports = defineConfig({
plugins: [],
optimizeDeps: {},
});

3. html模板处理

拓展Vite的能力就是定制各种的插件,根据插件文档

编写一个简单的plugin,利用configServer钩子,读取浏览器发起的资源请求

import type { PluginOption } from 'vite';

export default function HtmlTemplatePlugin(): PluginOption {
return {
name: 'wvs-html-tpl',
apply: 'serve',
configureServer(server) {
const { middlewares: app } = server;
app.use(async (req, res, next) => {
const { url } = req;
console.log(url);
next();
});
},
};
}

在上述的配置文件中引入

import { htmlTemplatePlugin } from '../plugins/index';
module.exports = defineConfig({
plugins: [
htmlTemplatePlugin(),
]
});

再次启动服务观察

  • 访问http://localhost:3000,终端中输出/
  • 访问http://localhost:3000/path1/path2,终端中输出/path1/path2
  • 访问http://localhost:3000/path1/path2?param1=123,终端中输出/path1/path2?param1=123

在 devTools面板内容中可以看到,第一个资源请求头上的Accept字段中带有text/html,application/xhtml+xml等内容,咱们就以这个字段表明请求的是html文档

再次修改一下处理资源请求的代码

import { readFileSync } from 'fs';
import path from 'path';
import { URL } from 'url'; function loadHtmlContent(reqPath) {
// 单页默认 public/index.html
const tplPath = 'public/index.html';
// 可以根据请求的path:reqPath 作进一步的判断
return readFileSync(path.resolve(process.cwd(), tplPath));
} // 省略了前面出现过的代码
app.use(async (req, res, next) => {
const { pathname } = new URL(req.url, `http://${req.headers.host}`);
const htmlAccepts = ['text/html', 'application/xhtml+xml'];
const isHtml = !!htmlAccepts.find((a) => req.headers.accept.includes(a));
if (isHtml) {
const html = loadHtmlContent(pathname);
res.end(html);
return;
}
next();
});

再次在demo中启动服务,访问就能正确看到Hello Vite

在终端中会发现一个报错

UnhandledPromiseRejectionWarning: URIError: URI malformed

打开模板可以发现是由于有一些其它的内容,里面包含一些变量,这部分在webpack中是由 html-webpack-plugin插件处理

<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>

这里编写一个简单的方法对模板先做一些简单处理(这个方法只处理了当前遇到的这种情况)

/**
* 初始化模板内容(替换 <%= varName %> 一些内容)
*/
function initTpl(tplStr:string, data = {}, ops?:{
backup?:string
matches?:RegExp[]
}) {
const { backup = '', matches = [] } = ops || {};
// match %Name% <%Name%>
return [/<?%=?(.*)%>?/g].concat(matches).reduce((tpl, r) => tpl.replace(r, (_, $1) => {
const keys = $1.trim().split('.');
const v = keys.reduce((pre, k) => (pre instanceof Object ? pre[k] : pre), data);
return (v === null || v === undefined) ? backup : v;
}), tplStr);
}

如果模板中还有复杂的ejs语法可以使用 ejs 库做进一步处理

import ejs from 'ejs';

/**
* ejs渲染
*/
function transformEjsTpl(html:string, data = {}) {
return ejs.render(html, data);
}

当然如果还有其它未考虑到的case,可根据特定情况,再对模板做进一步的处理

下面将上述编写的方法集成到插件中

export default function HtmlTemplatePlugin(): PluginOption {
return {
configureServer(server) {
const { middlewares: app } = server;
app.use(async (req, res, next) => {
// 省略代码
if (isHtml) {
const originHtml = loadHtmlContent(pathname);
// 调用插件中的transformIndexHtml 钩子对模板做进一步处理
const html = await server.transformIndexHtml(req.url, originHtml, req.originalUrl);
res.end(html);
return;
}
next();
});
},
transformIndexHtml(html) {
// data可以传入模板中包含的一些变量
// 可以再此处获取webpack配置,做自动转换
return initTpl(html, {
PUBLIC_URL: '.',
BASE_URL: './',
htmlWebpackPlugin: {
options: {
title: 'App',
},
},
});
},
};
}

到此再次在demo中运行,页面跑起来了,终端中也无报错,页面的模板到此算是处理完毕

有了初始的模板,就意味着我们已经为Vite提供了页面的入口,但其中还没有处理的js/ts的依赖即 entry

下面将介绍往模板中插入entry

4. 指定entry入口

入口文件名(entryName)通常为(main|index).js|ts|jsx|tsx

  • 单页应用(SPA)中entryBase通常为:src
  • 多页应用(MPA)中entryBase通常为:src/pages/${pageName}

利用transformIndexHtml钩子往模板中插入<script type="module" src="entryFile"></script>

export default function pageEntryPlugin(): PluginOption {
return {
name: 'wvs-page-entry',
apply: 'serve',
transformIndexHtml(html, ctx) {
return html.replace('</body>', `<script type="module" src="${getPageEntry(ctx.originalUrl)}"></script>
</body>
`);
},
};
}

这里以SPA为例

function getPageEntry(reqUrl) {
// SPA
const SPABase = 'src';
return getEntryFullPath(SPABase);
}

getEntryFullPath 实现如下

  • 先判断目录是否存在
  • 读取目录,遍历文件利用正则/(index|main)\.[jt]sx?$/判断文件是否为目标文件
const resolved = (...p) => path.resolve(getCWD(), ...p);
const getEntryFullPath = (dirPath) => {
if (!existsSync(resolved(dirPath))) {
return false;
}
// main|index.js|ts|jsx|tsx
const entryName = /(index|main)\.[jt]sx?$/;
const entryNames = readdirSync(resolved(dirPath), { withFileTypes: true })
.filter((v) => {
entryName.lastIndex = 0;
return v.isFile() && entryName.test(v.name);
});
return entryNames.length > 0 ? path.join(dirPath, entryNames[0].name) : false;
};

将这个插件加入到配置里

import { pageEntryPlugin } from '../plugins/index';
module.exports = defineConfig({
plugins: [
pageEntryPlugin(),
]
});

启动demo查看效果,抛出了一堆错误

wvs start

下面是针对框架特定的处理

React

  1. React: the content contains invalid JS syntax

React中将带有jsx语法的js文件后缀改为jsx,关于直接在js中使用jsx语法的处理方案,见文章:解决Vite-React项目中.js使用jsx语法报错的问题

  1. Uncaught ReferenceError: React is not defined

在 react组件顶部引入React,或引入@vitejs/plugin-react插件,同下3处理方案

import React from 'react';
  1. HMR支持

引入@vitejs/plugin-react插件

import react from '@vitejs/plugin-react'

module.exports = defineConfig({
plugins: [
react(),
]
});

Vue

需要添加插件处理.vue文件

引入@vitejs/plugin-vue插件

import vue from '@vitejs/plugin-vue'

module.exports = defineConfig({
plugins: [
vue(),
]
});

同时 @vitejs/plugin-vue 需要 vue (>=3.2.13)

由于前面采用的是npm link创建软连接进行的调试,配置文件中会在开发目录下去查找Vue依赖,不会在指令运行目录下查找,会不断的抛出上述问题

这里在demo项目里本地安装我们的依赖,然后在package.json添加相关指令

yarn add file:webpack-vite-service-workspace-path
{
"scripts": {
"vite": "wvs start -f vue"
},
}

Vue项目中并没有React相关依赖,所以在Vue项目中不能引入@vitejs/plugin-react插件

可以在指令入口添加框架相关参数判断处理一下,只引入对应框架的插件

// src/bin.ts
program.command('start')
.option('-f, --framework <type>', 'set project type [vue/react]')
.action(startCommand); // src/command/start.ts
export default function startCommand(options:{[key:string]:string}) {
const { framework = '' } = options;
process.env.framework = framework.toUpperCase();
} // src/config/vite.ts
import react from '@vitejs/plugin-react';
import vue from '@vitejs/plugin-vue'; const extraPlugins: any[] = [
process.env.framework === 'REACT' ? [react()] : [],
process.env.framework === 'VUE' ? [vue()] : [],
];
module.exports = defineConfig({
plugins: [
htmlTemplatePlugin(),
pageEntryPlugin(),
...extraPlugins,
],
});

到此最关键的两个步骤就算完成了

5. 其它工程能力

目前针对webpack常见的能力,社区已经有了许多插件和方案,下面只做简单介绍

这些插件当然也有些场景可能处理不了,还是期望广大开发者,勇于实验,然后向插件作者提交PR/issues

  • Sass/Less:在依赖中安装Sass/Less即可
  • 组件库按需引入:vite-plugin-style-import
  • process.env:vite-plugin-env-compatible
  • window.xx/xx undefined:使用transformIndexHtml钩子开发插件,在模板中提前引入这个方法的polyfill或者兜底处理
  • ...

总结

企业:大部分是拥有自己的研发框架,在研发框架中只需要加入一个Vite启动的CLI指令,这样对接入方的影响与使用成本是最小的

个人:喜欢折腾/不想改动原来的代码,可以按上述流程自己接一下,新项目可以直接使用Vite官方模板开发

总之:开发中使用Vite还是很香的

由于篇幅与时间都有限,文中部分地方只介绍了实现思路,并没粘贴完整代码,完整代码可在源码仓库中查看,也可fork直接进行二次开发

webpackvite配置的转换这部分的内容将放在下期做介绍

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

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

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

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

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

  3. 原有vue项目接入typescript

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

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

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

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

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

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

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

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

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

  8. antd+react项目迁移vite的解决方案

    antd+react+webpack往往是以react技术栈为主的前端项目的标准组合,三者都有成熟的生态和稳定的表现,但随着前端圈的技术不断革新,号称下一代构建平台vite2的发布,webpack似乎 ...

  9. 《转》iOS 平台 Cocos2d-x 项目接入新浪微博 SDK 的坑

    最近在做一个 iOS 的 cocos2d-x 项目接入新浪微博 SDK 的时候被“坑”了,最后终于顺利的解决了.发现网上也有不少人遇到一样的问题,但是能找到的数量有限的解决办法写得都不详细,很难让人理 ...

随机推荐

  1. 腾讯的表妹告诉我怎么学Python,今天就教我搭建Python环境和基本语法,我【码上开始】

    本文首发公众号:码上开始 环境准备 Pycharm Python3 window10/win7 安装 Python 打开Python官网地址 下载 executable installer,x86 表 ...

  2. 深入浅出WPF-05.控件与布局

    控件与布局 突出特点:1.专门的UI设计语言XAML,无需像MFC那样使用编程语言设计UI.2.前几代在UI和数据交互方面是由消息Message到控件事件,始终是把UI控件放在主导位置而把数据放在了次 ...

  3. MFC修改窗口图标

    Visual Studio写MFC应用程序,默认的程序左上角图标是自带的(如下图),想要自己个性化定制一个新的图标则需要以下几个步骤. 一.准备工作(icon图标) 首先准备一个自己个性化定制的图片, ...

  4. Python setattr() 函数 ,Python super() 函数: Python 内置函数 Python 内置函数

    描述 setattr 函数对应函数 getatt(),用于设置属性值,该属性必须存在. 语法 setattr 语法: setattr(object, name, value) 参数 object -- ...

  5. 1-Java继承中多态情况特性下变量,方法,静态方法的访问

    在Java继承下,多态特性下类成员访问情况 /* 在继承中,变量时静态的绑定的,非静态方法是动态的绑定的,静态方法是静态绑定的 */ class Parent{ int number = 11; pu ...

  6. 洛谷4366——最短路(dijkstra,思维,异或)

    题目大意 给定一个n个点,m条边的图,每条边有边权,而每个点\(i\)也可以直接到达\(j\),代价是\(i\ xor\ j\),给定一个S和T,求S到T的最小代价 其中\(n\le100000,m\ ...

  7. 【数据结构与算法Python版学习笔记】图——强连通分支

    互联网 我们关注一下互联网相关的非常巨大图: 由主机通过网线(或无线)连接而形成的图: 以及由网页通过超链接连接而形成的图. 网页形成的图 以网页(URI作为id)为顶点,网页内包含的超链接作为边,可 ...

  8. XSS_Labs靶场通关

    XSS-labs靶场(1-20) 开始通关!   0x01 (直接漏洞注入) 反射型xss注入 1.遇到?name=text,尝试参数注入 注入语句: <script>alert('xss ...

  9. Java:volatile笔记

    Java:volatile笔记 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 1. volatile 和 JMM 内存模型的可见性 JUC 下的三个包 java ...

  10. Java:修饰符小记

    Java:修饰符小记 对 Java 中的 修饰符,做一个微不足道的小小小小记 Java 语言提供了很多修饰符,大概分为两类: 访问权限修饰符 非访问权限修饰符 访问权限修饰符 修饰符 说明 publi ...