基于single-spa的微前端配置
基于已有项目改造成微前端
Vue子项目改造
Vue版本:2.6.10
- 下载single-spa-vue包
- 修改main.js或者main.ts文件,使项目作为一个单一的spa应用程序工作
const options = {
// vue的配置参数
el: '#vue',
render: h => h(App),
router,
store
}
const vueLifeCycles = singleSpaVue({
Vue,
appOptions: options
})
//判断是否是微前端模式
if (window.singleSpaNavigate) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = 'http://localhost:8888/vue'
}
if (!window.singleSpaNavigate) {
delete options.el
new Vue(options).$mount('#app')
}
// props 是主系统给子系统传递的参数
export function bootstrap(props){
return vueLifeCycles.bootstrap(props)
}
// export const mount = vueLifeCycles.mount
export function mount(props) {
// do something with the common authToken in app1
return vueLifeCycles.mount(props);
}
export function unmount(props){
return vueLifeCycles.unmount(props)
}
- 修改vue.config.js配置(打包配置)
PS: library需要和主系统加载文件时return的名字
output: {
library: 'singleVue',
libraryTarget: 'umd'
}
错误提示
- single-spa.min.js:2 Uncaught Error: application 'app3' died in status LOADING_SOURCE_CODE: {"isTrusted":true} 在因为加载子系统失败,singleSpa.registerApplication中访问的app.js无法访问的原因,可能是因为子系统未启动,也可能是访问路径写错了
主子系统之间传参
singleSpa.registerApplication(
'app3',
async()=>{
await runScript(process.env.REACT_APP_CSPJ_JS)
return window.cspj;
},
location => { return location.pathname.startsWith('/react') },
{
domElement: domElementGetter(),
authToken: getStore('token')
};
)
通过对象直接传递的authToken并不是登录之后获取到的最新Token,而是上一次登录存储在Store中的Token,将改字段修改成一个方法并在方法中return该Token,这样获取到的Token就是最新的Token
singleSpa.registerApplication(
'app3',
async()=>{
await runScript(process.env.REACT_APP_CSPJ_JS)
return window.cspj;
},
location => { return location.pathname.startsWith('/react') },
(name, location) => {
return {
domElement: domElementGetter(),
authToken: getStore('token')
};
},
)
主系统(javascript)改造
由于主系统只有登陆功能,在登陆后会根据用户权限自动跳转至子系统中,所以主系统没有使用任何的前端框架,而是使用了javascript进行改造
- 下载single-spa包
- 改造main.js文件
// 在mian.js文件中配置
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App';
import * as singleSpa from 'single-spa';
import { getStore } from './utils/store';
async function runScript(url){
return new Promise((resolve,reject)=>{
const script = document.createElement("script");
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script)
})
}
// 记载React
const domElementGetter = () => {
let el = document.getElementById('root');
if (!el) {
el = document.createElement('div');
el.id = 'root';
document.body.appendChild(el);
}
return el;
}
singleSpa.registerApplication('app2',
// 要返回三个方法
async()=>{
await runScript('http://localhost:8888/vue/static/js/chunk-vendors.js')
await runScript('http://localhost:8888/vue/static/js/app.js')
return window.singleVue; //bootstrap mount unmount
},
location => {return location.pathname.startsWith('/vue')},
{ authToken: 'ddd' }
)
singleSpa.registerApplication('app3',
async()=>{
await runScript('http://localhost:3000/js/app.js')
return window.singleReact;
},
location => { return location.pathname.startsWith('/react') },
(name, location) => {
return {
domElement: domElementGetter(),
token: ()=>{ return getStore('token')},
};
},
)
singleSpa.start();
ReactDOM.render(<App />, document.getElementById('app'))
- 主系统跳转至子系统使用 navigateToUrl 进行跳转
例如: singleSpa.navigateToUrl('/react#/list?')
主系统(Vue)改造
- 下载single-spa相关包
- single-spa-vue
- 修改main.js配置
import Vue from 'vue'
import App from './App'
import Print from 'vue-print-nb'
import singleSpaVue from 'single-spa-vue'
Vue.use(Print)
Vue.use(VueCookies)
Vue.config.productionTip = false
const options = {
// vue的配置参数
el: '#vue',
render: h => h(App),
router,
store
}
const vueLifeCycles = singleSpaVue({
Vue,
appOptions: options
})
//判断是否是微前端模式
if (window.singleSpaNavigate) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = 'http://localhost:8888/vue/'
}
if (!window.singleSpaNavigate) {
delete options.el
new Vue(options).$mount('#app')
}
// props 是主系统给子系统传递的参数
export function bootstrap(props){
return vueLifeCycles.bootstrap(props)
}
// export const mount = vueLifeCycles.mount
export function mount(props) {
// do something with the common authToken in app1
return vueLifeCycles.mount(props);
}
export function unmount(props){
return vueLifeCycles.unmount(props)
}
React子系统改造
react子应用是使用create-react-app脚手架安装的
- 下载single-spa相关包
- single-spa-react
- single-spa-css
- 如果暴露出webpack.config.js文件则直接修改webpack.congig.js文件
config/webpack.confi.js文件
// 修改output字段
output: {
path: paths.appBuild,
pathinfo: isEnvDevelopment,
filename: 'js/app.js', //主系统加载react时需要加载的文件
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
assetModuleFilename: 'static/media/[name].[hash][ext]',
publicPath: 'http://localhost:3000/',
library: 'app3', // 主系统加载文件时的return值
libraryTarget: 'umd',
}
//修改plugins文件
plugins:[
//将Css文件提取并暴露起文件名
isEnvProduction &&
new MiniCssExtractPlugin({
filename: '[name].css',
}),
isEnvProduction &&
new ExposeRuntimeCssAssetsPlugin({
// filename 必须与 MiniCssExtractPlugin 中的 filename 一一对应
filename: "[name].css",
}),
]
- 修改index.js文件
import singleSpaReact from 'single-spa-react'
import singleSpaCss from 'single-spa-css';
// 子应用独立运行
if (!window.singleSpaNavigate) {
ReactDOM.render(rootComponent(), document.getElementById('root'))
}
//加载CSS文件
const cssLifecycles = process.env.NODE_ENV === 'development'
? ''
: singleSpaCss({
// 需要加载的 css 列表
cssUrls: ['http://localhost:3000/main.css'],
// 是否是 webpack 导出的 css,如果是要做额外处理(webpack 导出的文件名通常会有 hash)
webpackExtractedCss: true,
// 当子应用 unmount 的时候,css 是否需要一并删除
shouldUnmount: true,
createLink(url) {
const linkEl = document.createElement('link');
linkEl.rel = 'stylesheet';
linkEl.href = url;
return linkEl;
},
});
const domElementGetter = () => {
let el = document.getElementById("app");
if (!el) {
el = document.createElement('div');
el.id = 'app';
document.body.appendChild(el);
}
return el;
}
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent,
errorBoundary(err, info, props) {
return <div>
This renders when a catastrophic error occurs
</div>
},
domElementGetter
})
// 这里和vue不一样,props必须向下传递
export const bootstrap = async props => {
//开发环境下cssLifecycles.bootstrap(props)不需要导出,否则开发环境下会报错
return process.env.NODE_ENV === 'development' ? reactLifecycles.bootstrap(props) : [cssLifecycles.bootstrap(props),reactLifecycles.bootstrap(props)];
}
export const mount = async props => {
return process.env.NODE_ENV === 'development' ? reactLifecycles.mount(props): [cssLifecycles.mount(props),reactLifecycles.mount(props)];
}
export const unmount = async props => {
return process.env.NODE_ENV === 'development' ? reactLifecycles.unmount(props) : [reactLifecycles.unmount(props),cssLifecycles.unmount(props)];
}
// 根组件
function rootComponent() {
return <React.StrictMode>
<App />
</React.StrictMode>
}
Nginx配置
- 根文件配置
#user nobody;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
include single-spa-vue1.conf;
include single-spa-vue2.conf;
include single-spa-react.conf;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
listen 8080;
server_name localhost;
root /Users/xxx/Desktop/data;
index index.html;
location / {
root /Users/xxx/Desktop/data/frontend;
index index.html index.htm;
try_files $uri $uri/ @router;
}
location @router {
rewrite ^.*$ /index.html last;
}
}
include servers/*;
}
- 子文件配置
server {
listen 8888;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
index index.html;
try_files $uri /index.html;
root /Users/xxx/Desktop/data/vue1;
location / {
root /Users/xxx/Desktop/data/vue1;
index index.html;
try_files $uri /index.html;
}
location /labopslims {
root /Users/xxx/Desktop/data/vue1;
index index.html index.htm;
try_files $uri $uri/ @router;
}
}
SPA能正常访问的关键:在 location @router { rewrite ^.*$ /index.html break; }这部分配置。在SPA单页面访问时,实际上访问的是单页面的路由,例如/goods/list,对应的其实是单页面路由中配置的路由组件。但是对于nginx服务器来讲,被认为会寻找根目录下goods文件夹下的list文件夹,当然是找不到的。单页面实际上只有一个页面index.html,因此将所有的页面都rewirte到index页面,即可完成配置
网友评论