微前端應用解決方案


在前后台分離開發模式大行其道的今天,前端也形成了自己的一套工程體系,隨着業務的不同,前端也誕生了很多相應的解決方案,那么我們在開發初期因該如何選擇呢,我們來回顧常用應用有哪些。(本文只是自己得理解,

有理解錯得地方希望老鳥幫忙指點一二)

SPA,單頁面應用

單頁面應用做為各種解決方案得基礎,不得不說得力於webpack大行其道,webpack通過入口將所有彼此依賴得文件打成一個網,最終輸出到磁盤中,index.html只關心最終輸出文件,當然這里涉及到更核心得概念就是模塊化編程,

比如amd,cmd,commonjs,es module等等這里就做闡述了。作為一個前端,我們很容易可以創建一個單頁面應用。然而隨着一個項目需求變得越來越多,項目體積變得越來越大得時候,單頁面應用得弊端也漸漸得暴漏出來,

最大直觀問題就是文件加載過大導致頁面性能下降,到這里你會說,我可以做按需加載,可以uglify,可以tree shaking,可以提取公共文件等等,當然這些都是解決方案,那么如何可以更好得解決這個問題,是不是可以從業務上

進行拆分呢,各個模塊單獨使用各自得html呢,於是有了MPA(多頁面應用)

 

MPA,多頁面應用

通過webpack控制入口文件,打包出來多個最終文件同時提供多個html,可以實現模塊之間項目獨立從而達到解耦得目的,達到了我們得目的,但是也隨之帶來了一些弊端,MPA路由基於文檔跳轉,每一次跳轉帶來得負擔就是需要重新加載

公共資源文件,性能上對比SPA大大降低,切合到實際開發中當項目太大多個部門共同開發時,所有人共同開發一個獨立工程,一旦一個模塊代碼出現問題會影響到整個前端工程,線上發布同樣會遇到同樣得問題,一個模塊會影響整個工程。

如何避免呢,答案就是微前端解決方案,那么什么是微前端設計方案呢

 

MicroFrontend,微前端

個人對於微前端的理解是基於對微服務的理解

微服務將單體服務拆分成多個服務如圖

 

 

 多個服務相互獨立,通過聚合層對外暴露公共端口,每個服務實現獨立部署,那么前端是不是也可以這么做呢,於是微前端就誕生了

微前端架構解決了哪些SPA與MPA解決不了的問題呢?

1)對前端拆分解決了MPA的資源重新加載的問題

2)解決了SPA體積過大的問題

3)解決開發過程中各個模塊相互影響的問題,達到了模塊獨立開發。

整體結構如圖

 

 

 

 

 

 那么如何創建一個微前端的應用呢

我們用兩種方式實現,(核心思想都是single-spa)什么是single-spa自己查吧

1)html嵌套

核心:single-spa,htmlEntry

注冊中心

import * as singleSpa from "single-spa";

import GlobalInstance from "./globalInstance";

import config from "./conf";

import { importEntry } from "import-html-entry";

var globalInstance = new GlobalInstance();

var registeredModule = [];

async function register(name, storeUrl, moduleUrl, path) {
  if (registeredModule.includes(name)) return;

  registeredModule.push(name);

  let storeModule = {},
    customProps = { globalInstance: globalInstance };
  // storeModule = await SystemJS.import(storeUrl);

  if (storeModule && globalInstance) {
    customProps.store = storeModule;
    // globalInstance.registerStore(storeModule);
  }

  singleSpa.registerApplication(
    name,
    () => {
      // return SystemJS.import(moduleUrl);
      return loadApp(moduleUrl);
    },
    () => {
      return location.pathname === path;
    },
    customProps
  );
}
async function loadApp(htmlPath) {
  const { template, execScripts, assetPublicPath } = await importEntry(
    htmlPath
  );

  const global = window;

  const appContent = template;

  let element = createElement(appContent);

  const execScriptsRes = await execScripts(global);

  var root = document.getElementById("root");
  root.appendChild(element);

  var appInstanceId = "test" + new Date().getTime();

  return {
    name: appInstanceId,
    bootstrap: execScriptsRes.bootstrap,
    mount: execScriptsRes.mount,
    unmount: execScriptsRes.unmount
  };
}

function createElement(htmlElement) {
  var container = document.createElement("div");
  container.innerHTML = htmlElement;
  return container;
}

config.forEach(c => {
  register(c.name, c.storeUrl, c.moduleUrl, c.path);
});

singleSpa.start();

這里加載應用利用的是html嵌套

子應用需要暴露三個鈎子函數

bootstrap,mount,unmount
import singleSpaReact from 'single-spa-react';
import RootComponent from './component/root.component';

const reactLifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent: RootComponent,
    domElementGetter: () => document.getElementById('blog-root')
})


export const bootstrap = [
    reactLifecycles.bootstrap,
]

export const mount = [
    reactLifecycles.mount,
]

export const unmount = [
    reactLifecycles.unmount,
]

打包時候,針對出口配置如下

output: {
        path: path.resolve(__dirname, "./dist/"),
        filename: '[name]-[chunkhash].js',
        libraryTarget: "umd",
        library: "blog",
    },

這里要注意打包輸出采用umd形式以保證importEntry可以正確加載到

2)js動態加載

核心single-spa,systemjs

import * as singleSpa from "single-spa";

// import appJson from "./appConf/importmap.json";
import confs from "./appConf/importConf.js";

function loadApp(url) {
  return System.import(url)
    .then(module => {
      console.log(module);
      return module.default;
    })
    .then(manifest => {
      const { entrypoints, publicPath } = manifest;
      const assets = entrypoints["app"].assets;
      return System.import(publicPath + assets[0])
    });
}

confs.forEach(conf => {
  register(conf);
});

function register(target) {
  singleSpa.registerApplication(
    target.name,
    () => {
      return loadApp(target.url);
    },
    () => {
      return location.pathname === target.path;
    }
  );
}

singleSpa.start();

子應用同樣必須暴漏三個鈎子函數

bootstrap,mount,unmount
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import RootComponent from './root.component'

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: RootComponent
  // domElementGetter: () => document.getElementById('common-root')
})


export const bootstrap = [
  reactLifecycles.bootstrap,
]

export const mount = [
  reactLifecycles.mount,
]

export const unmount = [
  reactLifecycles.unmount,
]

該種方式利用system進行加載目標應用

整個工程核心思想就這些,但是在實現過程中,我們如何正確加載到子應用

路由匹配子應用時候如何解決跨域問題

方案1

跳過跨域問題,由server解決路由問題

const express = require('express');
const path = require('path');
const { createProxyMiddleware } = require('http-proxy-middleware');

const port = process.env.PORT || 3001;
const app = express();

app.use(express.static(__dirname))


app.get('/blog', function (request, response) {
    response.sendFile(path.resolve(__dirname, 'index.html'))
})

app.get('/login', function (request, response) {
    response.sendFile(path.resolve(__dirname, 'index.html'))
})
var currentModule = '';

const getTargetServer = function (req) {
    var conf;
    switch (req.path) {
        case '/common_module':
            currentModule = 'common_module';
            conf = {
                protocol: 'http',
                host: 'localhost',
                port: 3002
            };
            break;
        case '/blog_module':
            currentModule = 'blog_module';
            conf = {
                protocol: 'http',
                host: 'localhost',
                port: 3003
            };
            break;case '/login_module':
            currentModule = 'login_module';
            conf = {
                protocol: 'http',
                host: 'localhost',
                port: 3005
            };
            break;default:
            switch (currentModule) {
                case 'common_module':
                    conf = {
                        protocol: 'http',
                        host: 'localhost',
                        port: 3002
                    };
                    break;
                case 'blog_module':
                    conf = {
                        protocol: 'http',
                        host: 'localhost',
                        port: 3003
                    };
                    break;case 'login_module':
                    conf = {
                        protocol: 'http',
                        host: 'localhost',
                        port: 3005
                    };
                    break;
                case 'vedio_module':
            }
            break;
    }
    return conf;
}

const options = {
    target: 'http://localhost:3002',
    changeOrigin: true,
    pathRewrite: {
        '/common_module': '/',
        '/blog_module': '/','/login_module': '/',
    },
    router: function (req) {
        return getTargetServer(req);
    }
}
const filter = function (pathname, req) {

    var result;
    result = (pathname.match('/common_module') ||
        pathname.match('/blog_module') ||
        pathname.match('/login_module') ||
        pathname.match('/*.css') ||
        pathname.match('/*.js')) && req.method === 'GET';
    return result;
}
app.use(createProxyMiddleware(filter, options));


app.listen(port, function () {
    console.log("server started on port " + port)
})

方案2

前台通過cors解決跨域問題

headers: {
  "Access-Control-Allow-Origin": "*" }

以上就是微前端的基本知識點,之后會不停更新。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM