在学习之前,存疑的几个问题
- 为什么浏览器发出的请求是localHost开头的
因为前端代码里面,会获取当前访问的host以及端口,连接到请求的端口/静态文件地址的前面,从而请求到了中间代理,进一步替换host以及端口,访问到实际端口/静态文件的地址,从而获取到数据
- 如何检测到本地文件的变化
在本文中是根据node-watch插件监听的本地文件变化,在本地文件发生变化时,通知客户端
- 代理中的html的作用
对于服务器来说,是不同的路由,根据路由不同,请求不同的资源
- 为什么要去掉文件的md5戳,去掉之后,怎么保证请求的是最新代码
服务器只有一份文件,只要保证请求的路由是对的,那代码就是最新部署的代码(可以根据请求文件路由的不同,请求不同的环境)
-
replaceIndex/replaceDomain怎么处理的XV的文件
-
为什么要有一个加md5戳的,一个不加的,请求回来的时候,为什么要用没有md5的
-
dns-prefetch(DNS预获取)是前端网络性能优化的一种措施。它根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到系统缓存中,缩短DNS解析时间,进而提高网站的访问速度。
依赖解析
express: The Express philosophy is to provide small, robust tooling for HTTP servers, making it a great solution for single page applications, websites, hybrids, or public HTTP APIs.
Express does not force you to use any specific ORM or template engine. With support for over 14 template engines via Consolidate.js, you can quickly craft your perfect framework.
建立本地代理服务器,代码逐行分析
const express = require('express'); //node服务
const expressWs = require('express-ws'); //建立websocket连接
const { createProxyMiddleware, responseInterceptor } = require('http-proxy-middleware');
// createProxyMiddleware创建中间代理; responseInterceptor处理返回数据
const path = require('path');
const fs = require('fs'); //文件系统
const config = require('./config'); //配置文件
const MockAuth = require('@gamma/autologin').default; //模拟用户身份
// const open = require('open');
const watch = require('node-watch'); //监听本地文件修改
const fileRegExp = /([a-zA-Z0-9-~_]+)([.|-]([a-f0-9]{4,}))\.(min\.)?(js|css)$/;
// 将请求文件的md5戳去掉
const hostname = config.macOS ? 'localhost:8888' : 'localhost';
const hostPrefix = config.env === 'ceshi112' ? 'crm' : 'www';
const app = express();
const wsInstance = expressWs(app);// webSocket实例,wsInstance.getWss().clients,用来获取链接到此服务的客户端,遍历连接的客户端,给客户端发信息
const projectKeys = [];
Object.keys(config.project).forEach(key => {
if (config.project[key].enable) {
projectKeys.push(key);
}
});
function valid(path) {
return projectKeys.some(project => path.indexOf(project) !== -1);
}
// 根据配置,请求那个是环境的资源
function replaceIndex(response) {
return config.index ? response.replace(/(?<!<|\.)(html\d*)(?!>)/g, config.index) : response;
}
function replaceDomain(response) {
const regexp = new RegExp(`${config.env === 'ceshi112' ? 'ceshi112': 'a9'}.fspage.com`, 'g');
return response.replace(regexp, hostname).replace(/(www|crm).(ceshi112|fxiaoke).com/, hostname);
}
// 请求在客户端建立websocket的文件,用来在客户端发起对/field-watch的连接
function replaceFS(response) {
return response.replace(/(<\/head>)/, `<script src="http://${hostname}/first-override.js"></script>\n` + '$1')
.replace(/(<\/body>)/, `<script src="http://${hostname}/override.js"></script>\n` + '$1');
}
// 模拟用户登陆,获取用户信息
function getUpstreamAccount(account) {
return {
upstreamEa: '',
ea: account.upstreamEa,
username: account.upstreamUsername,
password: account.upstreamPassword
};
}
// 对所有请求的统一处理
app.use(function (req, res, next) {
const basename = path.basename(req.path);
const extname = path.extname(basename);
if ((extname === '.js' || extname === '.css') && valid(req.path)) {
const result = fileRegExp.exec(req.path);
if (result) {
const md5 = result[2];
req.url = req.url.replace(md5, '');
}
}
next();
});
if (config.project['crm-dist'] && config.project['crm-dist'].enable) {
const localPath = config.project['crm-dist'].localPath;
// 临时兼容crm打包路径问题
app.use(`/${config.index}/crm-dist/assets/style/connect`, express.static(path.join(localPath, '/connect')));
app.use(`/${config.index}/crm-dist/assets/style/assets`, express.static(path.join(localPath, '/assets')));
}
projectKeys.forEach(key => {
const project = config.project;
app.use(`/${config.index}${project[key].onlinePath}`, express.static(project[key].localPath));
app.use(`/FSR/frontend/${config.index}${project[key].onlinePath}`, express.static(project[key].localPath));
});
app.use(express.static('public'));
// 登陆后的入口,处理index/home.js
app.use('/XV', createProxyMiddleware({
target: `https://${hostPrefix}.${config.env}.com`,
changeOrigin: true,
selfHandleResponse: true,
onProxyRes: responseInterceptor(async (responseBuffer) => {
return replaceFS(replaceIndex(replaceDomain(responseBuffer.toString('utf8'))));
}),
}));
// 接口转发
app.use(/\/(FHH|FSC|open|h5app|online)/, createProxyMiddleware({
target: `https://${hostPrefix}.${config.env}.com`,
changeOrigin: true,
}));
app.use('/FSR', createProxyMiddleware({
target: `https://a9.fspage.com`,
changeOrigin: true
}));
// 下游接口的处理
if (!config.developDownstream) {
app.use(/\/(H|temp|fs-er-biz)/, createProxyMiddleware({
target: `https://${hostPrefix}.${config.env}.com`,
changeOrigin: true,
}));
}
// 对静态文件的处理
app.use(`/${config.index}`, createProxyMiddleware({
target: 'https://ceshi112.fspage.com',
changeOrigin: true,
}));
// 对图片的处理
app.use('/image', createProxyMiddleware( {
target: `https://img.${config.env}.com`,
changeOrigin: true,
}));
function setCookieAndRedirect(cookieObj, res) {
Object.keys(cookieObj).forEach((key) => {
res.cookie(key, cookieObj[key]);
});
// 业务的处理,请求玩home文件后,请求,index文件
const homeUrl = config.developDownstream ? `/XV/Cross/Portal${config.appId ? '?fs_out_appid=' + config.appId : ''}` : '/XV/Home/Index';
res.redirect(302, homeUrl);
}
app.use('/login', function (req, res) {
// 无租户只能用静态cookie登录
if (config.staticCookie && config.staticCookie.ERInfo) {
setCookieAndRedirect(config.staticCookie, res);
return;
}
const env = config.env === 'ceshi112' ? '112' : 'fxiaoke';
const mockAuth = MockAuth.create({ env, appId: config.appId });
const p = config.developDownstream ? mockAuth.login(config.account) : mockAuth.fsLogin(getUpstreamAccount(config.account));
p.then((r) => {
setCookieAndRedirect(r, res);
}).catch((e) => {
res.json({ e });
});
});
app.listen(config.macOS ? 8888 : 80);
if (config.watchFile) {
projectKeys.forEach(key => {
//node-watch 监听本地文件,发生变化,给客户端发送change
watch(config.project[key].localPath, { recursive: true }, (evt, name) => {
console.log(config.project[key].localPath);
wsInstance.getWss().clients.forEach(client => {
client.send('change');
});
});
});
}
// 服务路由
app.ws('/field-watch', (ws, req) => {
console.log('WebSocket connect');
});
// 增加一个每一个小时的提醒
function handleTimeTip() {
const filePath = path.join(__dirname, 'time.json');
if (fs.existsSync(filePath)) {
const content = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
const { time, count } = content;
let newContent = content;
const lastDay = new Date(time).getDay();
const date = new Date();
const today = date.getDay();
if (today !== lastDay) {
content.time = new Date().getTime();
newContent = {
time: date.getTime(),
count: 0
};
fs.writeFileSync(filePath, JSON.stringify(newContent), 'utf-8');
} else {
const currentCount = Math.floor((date.getTime() - time) / (60 * 60 * 1000));
if (currentCount > count) {
newContent.count = currentCount;
fs.writeFileSync(filePath, JSON.stringify(newContent), 'utf-8');
wsInstance.getWss().clients.forEach(client => {
client.send('timeTip');
});
}
}
} else {
fs.writeFileSync(filePath, JSON.stringify({
time: new Date().getTime(),
count: 0
}), 'utf-8');
}
}
handleTimeTip();
setInterval(() => {
handleTimeTip();
}, 5 * 60 * 1000);
// open(`http://${hostname}/login`);