配置一個殼子項目為vue技術(shù)棧的single-spa應(yīng)用

技術(shù)選型:single-spa vue ts systemjs webpack5 umd

完整的代碼結(jié)構(gòu)圖

single-spa落地結(jié)構(gòu)圖.png

殼子項目改造

開啟history模式(試想一下,如果不開啟,殼子應(yīng)用跟子應(yīng)用都是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, // (縮寫) 相當(dāng)于 routes: routes
})

添加single-spa.config配置

declare var System: any
import { Message } from 'element-ui'
import * as singleSpa from 'single-spa' //導(dǎo)入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, '=========初始化項目參數(shù)')
singleSpa.setMountMaxTime(3000)
Object.keys(appMap).forEach((appName) => {
  appMap[appName].entry = getEntry(appName)
  singleSpa.registerApplication(
    //注冊子應(yīng)用
    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 子應(yīng)用位置定義

import appMap from '../single-spa.config'
appNameList = Object.keys(appMap)

index-laout.vue 放置子應(yīng)用盒子

<div
        v-for="appName, index in appNameList"
        :key="index"
        :id="appName"
        class="height100 micro-app-box hide"
      >
      </div>

微應(yīng)用項目改造

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, // 主應(yīng)用中你需要掛載到的地方,子應(yīng)用對外統(tǒng)一暴露應(yīng)用名
  router,
  store,
  render: (h: any) => h(App),
}
const singleSpaNavigate = (window as any).singleSpaNavigate

// 單獨訪問子應(yīng)用的正常掛載
if (!singleSpaNavigate) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}
// single-spa 的 生命周期
const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions,
})
// 子應(yīng)用必須導(dǎo)出 以下生命周期 bootstrap、mount、unmount
// Vue.mixin(globalMixin)
export function bootstrap(props: any) {
  console.log(props.authToken, ' ---from bootstrap') //打印出來主應(yīng)用攜帶的參數(shù)信息
  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請求需要加上這個字段,并設(shè)置為true
 
        add_header 'Access-Control-Allow-Credentials' 'true' ;
 
        #   允許請求的方式
 
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
 
       #表示請求頭的字段動態(tài)獲取                                                                                                                                                           

        #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請求需要加上這個字段,并設(shè)置為true
 
        add_header 'Access-Control-Allow-Credentials' 'true' ;
 
        #   允許請求的方式
 
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
 
       #表示請求頭的字段動態(tài)獲取                                                                                                                                                           
 
        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/*;
}

以上就是一個完整的基座風(fēng)格的基于single-spa搭建的,做好樣式隔離的微前端架構(gòu)方案


部署最常見錯誤(子應(yīng)用掛載失敗)

bafac3ffe663efd5af6f045b1c0e0ccf.png
解決思路(目前這個問題在single-spa官網(wǎng)出現(xiàn)較多,我也是多次折騰之后才意識到問題點所在)

打包之后的腳本段會被splitchunk,會導(dǎo)致子應(yīng)用的single-spa生命周期無法被準(zhǔn)確的打包進main.js,因為時間比較趕,我暫時禁用這個選項。

寫在最后single-spa與qiankun的對比


最大的區(qū)別點在性能。qiankun因為是想打造成開箱即用的微服務(wù)工具,所以集成了很多方便項目快速搭建的功能,但是其卡頓問題也是顯而易見的。目前為止沒有得到正面回答。

因為公司業(yè)務(wù)需要,所以我才從無到有搭建了公司內(nèi)部自己的微服務(wù)架構(gòu)。后續(xù)我也在文中的基礎(chǔ)架構(gòu)中設(shè)置了很多微應(yīng)用平滑過度的視覺交互,微服務(wù)是個新概念,想要玩好玩精還需要不斷學(xué)習(xí)打磨。這次分享就到此結(jié)束!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容