基座应用加载所有子应用
项目中有个不太规范的写法:通常在Switch里面,只有Route组件,但这里他们添加了Layout组件,这个Layout组件如何加载?
根据路由分析加载路径:
登录 -> login -> success 跳转至/
-> 匹配到<Route exact path="/" render={() => } />
-> 跳到/rdb,rdb不匹配一级路由(和登录平级)中的任何一条规则,所以加载Layout组件,Layout组件主要包括选择项目和租户的树状下拉,快捷跳转,登出,个人中心
在没有错误或鉴权不通过的情况下,由于在跳转到每个子应用时都不会匹配这一层级的任何路由,所以,都会走Layout组件的逻辑
因此把Layout组件挂载完毕作为注册所有子应用的时机
app.tsx
import registerApps from '../config/registerApps';
// ...
export default function App() {
return (
<IntlProvider
locale={_.get(localeMap[language], 'intl', 'zh')}
messages={intlMessages}
>
<ConfigProvider locale={_.get(localeMap[language], 'antd', antdZhCN)}>
<InjectIntlContext>
<Provider value={language}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login} />
<Route path="/auth-callback" component={CallBack} />
<Route path="/change-password" component={ChangePassword} />
<Route path="/register" component={Register} />
<Route path="/403" component={Page403} />
<Route path="/404" component={Page404} />
<Route exact path="/task-output/:taskId/:outputType" component={TaskOutput} />
<Route exact path="/task-output/:taskId/:host/:outputType" component={TaskHostOutput} />
<Route
exact
path="/big-screen/:id"
render={(props: any) => <BigScreen {...props} mode="full-screen" />}
/>
<Route
exact
path="/big-screen/modify/:id"
render={(props: any) => <BigScreen {...props} mode="editable" />}
/>
<Route exact path="/" render={() => <Redirect to="/rdb" />} />
<Layout
language={language}
onLanguageChange={setlanguage}
tenantProjectVisible={tenantProjectVisible}
belongProjects={belongProjects}
selectedTenantProject={selectedTenantProject}
setSelectedTenantProject={setSelectedTenantProjectFunc}
onMount={async () => {
const projsData = await request(`${api.tree}/projs`);
noProjCheck(projsData);
setBelongProjects(projsData);
// 监听子系统修改租户和项目
window.addEventListener('message', (event) => {
const { data } = event;
if (_.isPlainObject(data) && data.type === 'tenantProjectUpdate') {
const tenantProjectByProject = getTenantProjectByProjectId(projsData, data.value);
setSelectedTenantProjectFunc(tenantProjectByProject);
}
}, false);
// 设置租户和项目默认值
if (
(!defaultTenant && !defaultProject)
|| !_.find(projsData, { id: _.get(defaultProject, 'id') })
) {
const defaultTenantProject = getDefaultTenantProject(projsData);
setSelectedTenantProjectFunc(defaultTenantProject);
}
// 注册子系统
registerApps({}, () => {
request(api.permissionPoint).then((res) => {
const permissionPoint: any = {};
_.forEach(res, (_val, key) => {
permissionPoint[key] = true;
});
// 等待 1s 后再发送消息通知
setTimeout(() => {
window.postMessage({
type: 'permissionPoint',
value: permissionPoint,
}, window.location.origin);
}, 1000);
});
window.postMessage({
type: 'tenantProject',
value: {
tenant: _.attempt(JSON.parse.bind(null, localStorage.getItem('icee-global-tenant') as string)),
project: _.attempt(JSON.parse.bind(null, localStorage.getItem('icee-global-project') as string)),
},
}, window.location.origin);
});
distributeResourcePermissionPoint()
}}
>
<div id="ecmc-layout-container" />
</Layout>
<Route render={() => <Redirect to="/404" />} />
</Switch>
</BrowserRouter>
</Provider>
</InjectIntlContext>
</ConfigProvider>
</IntlProvider>
);
}
registerApps
import * as singleSpa from 'single-spa';
const customProps = {
env: {
NODE_ENV: process.env.NODE_ENV,
},
};
function fetchManifest(url, publicPath) {
return fetch(url).then((res) => {
return res.text();
}).then((data) => {
console.log('fetchManifest', data);
if (data) {
const manifest = data.match(/<meta name="manifest" content="([\w|\d|-]+.json)">/);
let result = '';
if (publicPath && manifest) {
result = `${publicPath}${manifest[1]}`;
}
return result;
}
});
}
function prefix(location, ident, matchPath) {
if (matchPath && Object.prototype.toString.call(matchPath) === '[object Function]') {
return matchPath(location);
}
if (location.href === `${location.origin}/${ident}`) {
return true;
}
return location.href.indexOf(`${location.origin}/${ident}/`) !== -1;
}
function getStylesheetLink(ident) {
return document.getElementById(`${ident}-stylesheet`);
}
function createStylesheetLink(ident, path) {
const headEle = document.getElementsByTagName('head')[0];
const linkEle = document.createElement('link');
linkEle.id = `${ident}-stylesheet`;
linkEle.rel = 'stylesheet';
// linkEle.href = systemConf[process.env.NODE_ENV].css;
linkEle.href = path;
headEle.appendChild(linkEle);
}
function removeStylesheetLink(ident) {
const linkEle = getStylesheetLink(ident);
if (linkEle) linkEle.remove();
}
async function getPathBySuffix(systemConf, jsonData, suffix) {
let targetPath = '';
_.forEach(Object.values(jsonData.assetsByChunkName), (assetsArr) => {
if(typeof assetsArr === 'string') {
targetPath = assetsArr
}
if(Array.isArray(assetsArr)) {
targetPath = assetsArr.find((assetStr) => {
return assetStr.indexOf(systemConf.ident) === 0 && _.endsWith(assetStr, suffix);
});
if (targetPath) {
return false;
}
}
});
console.log('targetPath', targetPath);
if (process.env.NODE_ENV === 'development') {
return `${systemConf[process.env.NODE_ENV].publicPath}${targetPath}`;
}
return `${systemConf[process.env.NODE_ENV].publicPath}${targetPath}`;
}
export default function registerApps(props = {}, mountCbk) {
fetch('/static/systemsConfig.json').then((res) => {
return res.json();
}).then((res) => {
res.forEach(async (systemsConfItem) => {
const { ident, matchPath } = systemsConfItem;
const sysUrl = systemsConfItem[process.env.NODE_ENV].index;
console.log('ident, matchPath', ident, matchPath);
singleSpa.registerApplication(ident, async () => {
let manifestUrl = sysUrl;
// html 作为入口文件
if (/.+html$/.test(sysUrl)) {
manifestUrl = await fetchManifest(sysUrl, systemsConfItem[process.env.NODE_ENV].publicPath);
}
console.log('manifestUrl', manifestUrl);
const lifecyclesFile = await fetch(manifestUrl).then((res) => res.json());
let lifecycles = {};
if (lifecyclesFile) {
const jsPath = await getPathBySuffix(systemsConfItem, lifecyclesFile, '.js');
console.log('jsPath', jsPath);
lifecycles = await System.import(jsPath);
} else {
lifecycles = lifecyclesFile;
}
const { mount, unmount } = lifecycles;
mount.unshift(async () => {
if (lifecyclesFile) {
const cssPath = await getPathBySuffix(systemsConfItem, lifecyclesFile, '.css');
console.log('cssPath', cssPath);
createStylesheetLink(ident, cssPath);
}
return Promise.resolve();
});
if (mountCbk) {
mount.unshift(async () => {
mountCbk();
return Promise.resolve();
});
}
unmount.unshift(() => {
removeStylesheetLink(ident);
return Promise.resolve();
});
return lifecycles;
}, location => prefix(location, ident, matchPath), {
...customProps,
...props,
});
});
singleSpa.start();
});
}
systemsConfig.json
[{
"ident": "rdb",
"development": {
"publicPath": "http://localhost:8001/rdb/",
"index": "http://localhost:8001/rdb/manifest.json"
},
"production": {
"publicPath": "/rdb/",
"index": "/rdb/manifest.json"
}
}, {
"ident": "ams",
"development": {
"publicPath": "http://localhost:8002/ams/",
"index": "http://localhost:8002/ams/manifest.json"
},
"production": {
"publicPath": "/ams/",
"index": "/ams/manifest.json"
}
}, {
开发环境和生产环境的差异
1、开发模式下,无论是否以微前端方式启动,都需要提前鉴权(devServer中before的配置),所以开发模式下我们直接就会进入项目的主页,而不需要登录
生产环境中,是经历单点登录 ->
用户身份信息存入rdb ->
Response中在客户端set-cookie ->
进入各个子系统后各个子系统发请求时,客户端会携带cookie ->
请求先到达各应用下的rbac-proxy这一层(前端nginx配置的对应的地址)->
rbac-proxy会携带request中的cookie信息去rdb鉴权 ->
鉴权成功后再将请求转发至对应的业务方服务
2、开发模式下,以微前端方式启动,node服务器需要支持跨域:"Access-Control-Allow-Origin": "*",
生产环境中,不存在服务之间互相访问,而只是访问不同的静态文件,所以没有这个问题
子应用的注册
每个子应用都有两种启动方式:微前端方式启动、独立启动
- 微前端方式启动:fetk run devServer,项目入口是 ****./src/index.tsx
react项目:
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: (_props: any) => {
auth.checkAuthenticate()
return <App />;
},
domElementGetter,
});
export const bootstrap = [
reactLifecycles.bootstrap,
];
export const mount = [
reactLifecycles.mount,
];
export const unmount = [
reactLifecycles.unmount,
];
vue项目(dstor):
// singleSpaVue包装一个vue微前端服务对象
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions,
domElementGetter
})
// console.log('vueLifecycles', vueLifecycles)
// 导出生命周期对象
export const bootstrap = [vueLifecycles.bootstrap] // 启动时
export const mount = [vueLifecycles.mount] // 挂载时
export const unmount = [vueLifecycles.unmount] // 卸载时
- 独立启动
ReactDOM.render(
<App />,
document.getElementById('react-content'),
);
dstor只能通过和基座一起启动来进行开发
由于微前端要求所有js都打包到同一文件中,所以通常都会在webpack中添加如下插件:
new StatsPlugin(manifestName, {
chunkModules: false,
source: true,
chunks: false,
modules: false,
assets: true,
children: false,
exclude: [/node_modules/]
})
权限点
权限点是用来描述页面上按钮、菜单等显示与否的数据结构,用户登录以后系统会默认初始化一个tenant和一个project放到全局(从localStorage里取)
tenant和project可以理解为部门、团队,或者不同层级的组织机构,个人认为tenant和project应该是包含关系
权限点和tenant、project有关,所以切换tenant和project的时候,会获取权限点并通过postMessage派发给各个子应用
layout-web/app.tsx:
useEffect(() => {
window.postMessage({
type: 'tenantProject',
value: selectedTenantProject,
}, window.location.origin);
distributeResourcePermissionPoint()
}, [selectedTenantProject]);
派发权限点时,可能会存在子应用接收不到的情况,所以开了若干定时器,派发了好几次
各个子应用中,通过监听message事件来接收
const distributeResourcePermissionPoint = () => {
if(_.get(selectedTenantProject,"project.id") && window.location.pathname!=='/auth-callback' && window.location.pathname!=='/change-password' && window.location.pathname!=='/register'){
request(api.resourcepermissionPoint+`/${_.get(selectedTenantProject,"project.id")}`).then(res => {
setTimeout(() => {
window.postMessage({
type: 'resourcepermissionPoint',
value: res,
}, window.location.origin);
}, 1000);
setTimeout(() => {
window.postMessage({
type: 'resourcepermissionPoint',
value: res,
}, window.location.origin);
}, 2000);
setTimeout(() => {
window.postMessage({
type: 'resourcepermissionPoint',
value: res,
}, window.location.origin);
}, 4000);
})
}
}
所以有些子应用中,干脆自己在切换tenant和project的时候发请求获取了权限点,而没有依赖基座的派发
部署时的各项准备工作
Linux(centos)下安装node
cd/usr/local/src
wget https://nodejs.org/dist/v8.9.0/node-v8.9.0-linux-x64.tar.xz
## 解压
tar xf node-v8.9.0-linux-x64.tar.xz
cd/usr/local
## 重命名
mv src/node-v8.9.0-linux-x64 node
## 将node添加至path
vi~/.bashrc
export NODE_HOME=/usr/local/node
export PATH=$NODE_HOME/bin:$PATH
source ~/.bashrc
## 安装成功
node -v // 安装成功会显示Node版本
Linux(centos)下安装nginx
sudo yum -y install nginx
Linux(centos)下添加yum源
cd /etc/yum.repos.d/
Linux(centos)下nginx的升级
本地传代码到Linux服务器或Linux服务器传文件到本地:rsync scp nc python
- rsync:
rsync -avz --delete /Users/didi/state-grid-web/mongdb-web/pub/* root@10.96.98.84:/root/ecmc8026/pub/
- scp:
scp -r -p7755 /Users/didi/Downloads/pub-external admin@172.17.238.5:/root/ecmcMicro
- nc的使用:
步骤1,先在B机器上启动一个接收文件的监听,格式如下
意思是把在10086端口接收到的数据都写到file文件里(这里文件名随意取)
nc -l port >file
栗子:nc -l 10086 >zabbix.rpm
步骤2,在A机器上往B机器的10086端口发送数据,把下面rpm包发送过去
nc 192.168.0.2 10086 < zabbix.rpm
B机器接收完毕,它会自动退出监听,文件大小和A机器一样,md5值也一样
- python:
python -m SimpleHTTPServer 9000