配置一个壳子项目为vue技术栈的single-spa应用

技术选型:single-spa vue ts systemjs webpack5 umd

完整的代码结构图

single-spa落地结构图.png

壳子项目改造

开启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搭建的,做好样式隔离的微前端架构方案


部署最常见错误(子应用挂载失败)

bafac3ffe663efd5af6f045b1c0e0ccf.png
解决思路(目前这个问题在single-spa官网出现较多,我也是多次折腾之后才意识到问题点所在)

打包之后的脚本段会被splitchunk,会导致子应用的single-spa生命周期无法被准确的打包进main.js,因为时间比较赶,我暂时禁用这个选项。

写在最后single-spa与qiankun的对比


最大的区别点在性能。qiankun因为是想打造成开箱即用的微服务工具,所以集成了很多方便项目快速搭建的功能,但是其卡顿问题也是显而易见的。目前为止没有得到正面回答。

因为公司业务需要,所以我才从无到有搭建了公司内部自己的微服务架构。后续我也在文中的基础架构中设置了很多微应用平滑过度的视觉交互,微服务是个新概念,想要玩好玩精还需要不断学习打磨。这次分享就到此结束!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容