在前后台分離開發模式大行其道的今天,前端也形成了自己的一套工程體系,隨着業務的不同,前端也誕生了很多相應的解決方案,那么我們在開發初期因該如何選擇呢,我們來回顧常用應用有哪些。(本文只是自己得理解,
有理解錯得地方希望老鳥幫忙指點一二)
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": "*" }
以上就是微前端的基本知識點,之后會不停更新。