配置一个壳子项目为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 = {
  '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 放置子应用盒子

微应用项目改造

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 = {
  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

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因为是想打造成开箱即用的微服务工具,所以集成了很多方便项目快速搭建的功能,但是其卡顿问题也是显而易见的。目前为止没有得到正面回答。

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

你可能感兴趣的:(配置一个壳子项目为vue技术栈的single-spa应用)