技术选型:single-spa vue ts systemjs webpack5 umd
完整的代码结构图
壳子项目改造
开启history模式(试想一下,如果不开启,壳子应用跟子应用都是hash~)
webpack配置
historyApiFallback: true
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[fullhash].js',
publicPath: '/' //如果不是history模式不用开启
},
routes.ts修改
const router = new VueRouter({
mode: 'history',
routes, // (缩写) 相当于 routes: routes
})
添加single-spa.config配置
declare var System: any
import { Message } from 'element-ui'
import * as singleSpa from 'single-spa' //导入single-spa
import 'systemjs'
import { IENV } from './global-type/index'
const ENV = process.env.ENV as IENV
interface IAppInfo {
port: number
entry: string
name: string
}
const appMap: Record<string, IAppInfo> = {
'vue-app': { port: 9003, entry: 'vue-app', name: '商品模块' },
}
function getEntry(appName: string) {
let result = 'undefined'
switch (ENV) {
case IENV.DEV:
result = '//localhost:' + appMap[appName].port
break
case IENV.PROD:
'//localhost:' + appMap[appName].port + `/${appName}`
break
default:
break
}
return result + '/main.js'
}
console.log(appMap, '=========初始化项目参数')
singleSpa.setMountMaxTime(3000)
Object.keys(appMap).forEach((appName) => {
appMap[appName].entry = getEntry(appName)
singleSpa.registerApplication(
//注册子应用
appName,
() => System.import(appMap[appName].entry),
(location) => location.pathname.includes('/' + appName + '/'),
(appName, location) => {
return {
authToken: 'xc67f6as87f7s9d',
}
}
)
})
singleSpa.addErrorHandler((err) => {
console.error(err)
if (singleSpa.getAppStatus(err.appOrParcelName) === singleSpa.LOAD_ERROR) {
Message({
showClose: true,
message: appMap[err.appOrParcelName].name + '启动失败',
type: 'error',
duration: 800,
onClose() {},
})
}
})
singleSpa.start()
export default appMap
global-type/index.ts
export enum IENV {
DEV = 'development',
PROD = 'production',
}
layout.ts 子应用位置定义
import appMap from '../single-spa.config'
appNameList = Object.keys(appMap)
index-laout.vue 放置子应用盒子
<div
v-for="appName, index in appNameList"
:key="index"
:id="appName"
class="height100 micro-app-box hide"
>
</div>
微应用项目改造
webpack配置
const name = require("./package.json").name
new webpack.DefinePlugin({
'process.env.ENV': JSON.stringify({ env: ENV, name }),
}),
output: {
path: path.resolve(__dirname, 'dist'),
library: name,
libraryTarget: 'umd',
filename: 'main.js', //一定要是main.js
// publicPath: '/' //如果不是history模式不用开启
},
main.ts
import singleSpaVue from 'single-spa-vue'
import { IProcessEnv } from './global-type'
const PROCESS_ENV = process.env.ENV as unknown as IProcessEnv
const appOptions: Record<string, any> = {
el: '#' + PROCESS_ENV.name, // 主应用中你需要挂载到的地方,子应用对外统一暴露应用名
router,
store,
render: (h: any) => h(App),
}
const singleSpaNavigate = (window as any).singleSpaNavigate
// 单独访问子应用的正常挂载
if (!singleSpaNavigate) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}
// single-spa 的 生命周期
const vueLifeCycle = singleSpaVue({
Vue,
appOptions,
})
// 子应用必须导出 以下生命周期 bootstrap、mount、unmount
// Vue.mixin(globalMixin)
export function bootstrap(props: any) {
console.log(props.authToken, ' ---from bootstrap') //打印出来主应用携带的参数信息
return vueLifeCycle.bootstrap(props)
}
export function mount(props: any) {
console.log(props.authToken, ' --from mount')
return vueLifeCycle.mount(props)
}
export const unmount = vueLifeCycle.unmount
global-type/index.ts
export interface IProcessEnv {
env: string
name: string
}
app.ts
import { IProcessEnv } from './global-type'
const PROCESS_ENV = process.env.ENV as unknown as IProcessEnv
cssNameSpace = PROCESS_ENV.name
app.vue
<div class="height100" :id="cssNameSpace">
<router-view></router-view>
</div>
postcss.config.js
const selectorNamespace = require('postcss-selector-namespace')
const name = require('./package.json').name
module.exports = {
plugins: [
require('autoprefixer'),
selectorNamespace({
namespace (css) {
if (css.includes("normalize")) return "" //排除初始化样式
if (css.includes("nprogress")) return "" //排除progress样式
return "#" + name
}
}),
]
}
部署注意点
本地模拟上线niginx配置(多站点)
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 8081;
server_name localhost;
add_header 'Access-Control-Allow-Origin' '*';
# 带cookie请求需要加上这个字段,并设置为true
add_header 'Access-Control-Allow-Credentials' 'true' ;
# 允许请求的方式
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#表示请求头的字段动态获取
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html/main;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# location ^~ /js {
# rewrite ^ /js;
# }
# location ~* /*/*\.js {
# root html/main;
# index index.html index.htm;
# try_files $uri $uri/;
# }
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
server {
listen 9003;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
add_header 'Access-Control-Allow-Origin' '*';
# 带cookie请求需要加上这个字段,并设置为true
add_header 'Access-Control-Allow-Credentials' 'true' ;
# 允许请求的方式
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#表示请求头的字段动态获取
location app-vue/ {
root html/app-vue;
try_files $uri $uri/ /index.html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
include servers/*;
}
以上就是一个完整的基座风格的基于single-spa搭建的,做好样式隔离的微前端架构方案
部署最常见错误(子应用挂载失败)
解决思路(目前这个问题在single-spa官网出现较多,我也是多次折腾之后才意识到问题点所在)
打包之后的脚本段会被splitchunk,会导致子应用的single-spa生命周期无法被准确的打包进main.js,因为时间比较赶,我暂时禁用这个选项。
写在最后single-spa与qiankun的对比
最大的区别点在性能。qiankun因为是想打造成开箱即用的微服务工具,所以集成了很多方便项目快速搭建的功能,但是其卡顿问题也是显而易见的。目前为止没有得到正面回答。
因为公司业务需要,所以我才从无到有搭建了公司内部自己的微服务架构。后续我也在文中的基础架构中设置了很多微应用平滑过度的视觉交互,微服务是个新概念,想要玩好玩精还需要不断学习打磨。这次分享就到此结束!