一、背景
在前前后端分离项目日常开发中,前端总是避免不了开发过程中需要切换服务端地址的需求,如果你的项目开发环境构建工具使用的是 Vite,那这篇文章就当学习下,万一以后有这种需求呢🤣,如果您的项目使用的是 webpack 启动的,如果我们的项目很大,则修改一次代理配置地址再重新启动一次真的很浪费时间,像笔者所在公司的项目,每重启一次至少需要两分钟时间(而且还是笔者优化之后),每次启着前端项目排查问题,如果连接的后端有问题或者后端当前环境没数据,想要切换环境都要重启一下前端项目,很浪费时间。
二、解决方案
笔者所在公司内部前端项目启动工具是公司研发中心研发的内部 CLI(类似 Vue/CLI 的东西),内部实现依赖 webpack4,项目中的代理配置和 webpack-dev-server 配置大致相似,笔者按照目前项目中的代理配置迁移到一个 webpack5 的示例项目中,webpack.config.js 配置如下:
// Generated using webpack-cli https://github.com/webpack/webpack-cli
const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const isProduction = process.env.NODE_ENV == 'production';
const stylesHandler = isProduction
? MiniCssExtractPlugin.loader
: 'style-loader';
let backendServer = ''; // 记录当前后端服务地址
let backendServerVersions = {}; // 记录当前后端各个服务的版本
// 加载配置
function loadConfig () {
let jsCode = fs.readFileSync('conf.js'); // 直接读取 conf.js 内容
let mockModule = {
exports: {}
};
try { // 防止 执行报错,导致服务停止
Function('module', jsCode)(mockModule); // 动态创建一个方法,将内容导出
const { schemeAndHost, serverVersions } = mockModule.exports; // 读取导出配置内容
backendServer = schemeAndHost; // 修改后端服务地址
backendServerVersions = serverVersions; // 修改后端各个服务版本
} catch(e) {
console.error(e);
}
}
loadConfig();
/**
* @type { import('webpack').Configuration }
*/
const config = {
output: {
path: path.resolve(__dirname, 'dist'),
},
devServer: {
open: false,
host: 'localhost',
setupMiddlewares (middlewares, devServer) {
// if (!devServer) {
// throw new Error('webpack-dev-server is not defined');
// }
// 每次请求 /config 路径的时候重新加载配置
middlewares.push({
name: 'configuration-middlware',
path: '/config',
middleware (req, res) {
loadConfig(); // 重新加载配置
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS'
}
);
res.write([
`<h3>服务端地址:${backendServer}</h3>`,
`<h3>服务端版本:${Object.entries(backendServerVersions).map(entry => entry.join(' => ')).join('、')}</h3>`,
].join(''));
res.end();
}
});
return middlewares;
},
// proxy 里面的每个配置都和 http-proxy-middleware 官方配置想对应,具体有疑问可参考官方文档说明,写得很详细!!!
proxy: {
'/api': {
target: backendServer,
router () { // 如果有 router 方法,则代理时目标之后使用 router 返回值,target 此时虽然不起作用,但是必须配置!!!
return backendServer;
},
pathRewrite (path, req) { // pathRewrite 直接使用方法配置,根据请求 path 动态修改
/**
* 下方配置意思是:
* 如果 配置了 user 服务对应的微服务版本是 v1,则将前端请求 /api/user/v/foo 地址重写为 /api/user/v1/foo,
* 此时访问 /api/user/v/foo 则相当于访问了 /api/user/v1/foo。
*/
const reg = /(\/api\/([\w_-]+))\/([\w_-]+)\/(.*)/g;
let matchResult = reg.exec(path);
if (!matchResult) {
return path;
}
let serverName = matchResult[2];
let customVersion = backendServerVersions[serverName];
if (!customVersion) {
return path;
}
const realPath = path.replace(reg, `$1/${customVersion}/$4`);
console.log('realPath: ', realPath); // 打印当前真实访问地址,笔者公司项目中是将此字符串加入响应头,方便在 devtool 中查看。
return realPath;
},
changeOrigin: true,
secure: false
}
}
},
plugins: [
new HtmlWebpackPlugin({
template: 'index.html',
}),
// Add your plugins here
// Learn more about plugins from https://webpack.js.org/configuration/plugins/
],
module: {
rules: [
{
test: /\.(js|jsx)$/i,
loader: 'babel-loader',
},
{
test: /\.less$/i,
use: [stylesHandler, 'css-loader', 'postcss-loader', 'less-loader'],
},
{
test: /\.css$/i,
use: [stylesHandler, 'css-loader', 'postcss-loader'],
},
{
test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i,
type: 'asset',
},
// Add your rules for custom modules here
// Learn more about loaders from https://webpack.js.org/loaders/
],
},
cache: {
type: 'filesystem'
}
};
module.exports = () => {
if (isProduction) {
config.mode = 'production';
config.plugins.push(new MiniCssExtractPlugin());
config.plugins.push(new WorkboxWebpackPlugin.GenerateSW());
} else {
config.mode = 'development';
}
return config;
};
conf.js 配置如下:
module.exports = {
schemeAndHost: [ // 后端服务地址
'http://127.0.0.1:3000',
'http://127.0.0.1:3001'
][1],
serverVersions: { // 后端不同微服务对应的版本
user: 'v2'
}
}
三、实际测试
./server/server1.js(测试服务端1)
const express = require('express');
const app = express();
const port = 3000;
app.get('/api/user/v1/own', (req, res) => {
res.json({
name: '张三',
gender: '男'
});
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
});
./server/server2.js(测试服务端2)
const express = require('express');
const app = express();
const port = 3001;
app.get('/api/user/v2/own', (req, res) => {
res.json({
name: '李四',
gender: '男'
});
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
项目中本身就安装了 webpack-dev-server,无需再安装 express,因为 webpack-dev-server 依赖 express。
./src/index.js 测试代码
async function fetchUserInfo () {
const resp = await fetch('/api/user/v/own');
const user = await resp.json();
console.log(user);
document.querySelector('.content').innerHTML = JSON.stringify(user);
}
fetchUserInfo();
效果展示
四、总结
如果读者希望每次修改配置文件之后不要在浏览器重新手动刷新特定地址页面才生效,可以在项目启动的时候动态监听配置 conf.js 文件变化并重新加载配置达到此目的,希望笔者思路能起到抛砖引玉的作用,帮助大家提升开发体验,减少开发过程中不必要的时间浪费。