什麼是微服務?先看看維基百科的定義:css
微服務(英語:Microservices)是一種軟件架構風格,它是以專一於單一責任與功能的小型功能區塊 (Small Building Blocks) 爲基礎,利用模塊化的方式組合出複雜的大型應用程序,各功能區塊使用與語言無關 (Language-Independent/Language agnostic)的API集相互通訊。
換句話說,就是將一個大型、複雜的應用分解成幾個服務,每一個服務就像是一個組件,組合起來一塊兒構建成整個應用。html
想象一下,一個上百個功能、數十萬行代碼的應用維護起來是個什麼場景?前端
一個應用分解成多個服務,每一個服務獨自服務內部的功能。例如原來的應用有 abcd 四個頁面,如今分解成兩個服務,第一個服務有 ab 兩個頁面,第二個服務有 cd 兩個頁面,組合在一塊兒就和原來的應用同樣。vue
當應用其中一個服務出故障時,其餘服務仍能夠正常訪問。例如第一個服務出故障了, ab 頁面將沒法訪問,但 cd 頁面仍能正常訪問。node
好處:不一樣的服務獨立運行,服務與服務之間解耦。咱們能夠把服務理解成組件,就像本小書第 3 章《前端組件化》中所說的同樣。每一個服務能夠獨自管理,修改一個服務不影響總體應用的運行,隻影響該服務提供的功能。react
另外在開發時也能夠快速的添加、刪除功能。例如電商網站,在不一樣的節假日時推出的活動頁面,活動事後立刻就能夠刪掉。webpack
難點:不容易確認服務的邊界。當一個應用功能太多時,每每多個功能點之間的關聯會比較深。於是就很難肯定這一個功能應該歸屬於哪一個服務。git
PS:微前端就是微服務在前端的應用,也就是前端微服務。github
如今咱們將使用微前端框架 qiankun 來構建一個微前端應用。之因此選用 qiankun 框架,是由於它有如下幾個優勢:web
樣式隔離的原理是:每次切換子應用時,都會加載該子應用對應的 css 文件。同時會把原先的子應用樣式文件移除掉,這樣就達到了樣式隔離的效果。
咱們能夠本身模擬一下這個效果:
<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="index.css"> <body> <div>移除樣式文件後將不會變色</div> </body> </html>
/* index.css */ body { color: red; }
如今咱們加一段 JavaScript 代碼,在加載完樣式文件後再將樣式文件移除掉:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <link rel="stylesheet" href="index.css"> <body> <div>移除樣式文件後將不會變色</div> <script> setTimeout(() => { const link = document.querySelector('link') link.parentNode.removeChild(link) }, 3000) </script> </body> </html>
這時再打開頁面看一下,能夠發現 3 秒後字體樣式就沒有了。
主應用在切換子應用以前會記錄當前的全局狀態,而後在切出子應用以後恢復全局狀態。假設當前的全局狀態以下所示:
const global = { a: 1 }
在進入子應用以後,不管全局狀態如何變化,未來切出子應用時都會恢復到原先的全局狀態:
// global { a: 1 }
官方還提供了一張圖來幫助咱們理解這個機制:
好了,如今咱們來建立一個微前端應用吧。這個微前端應用由三部分組成:
對應的目錄以下:
-main -vue -react
咱們使用 vue-cli 建立主應用(而後執行 npm i qiankun
安裝 qiankun 框架):
vue create main
若是主應用只是起到一個基座的做用,即只用於切換子應用。那能夠不須要安裝 vue-router 和 vuex。
App.vue
文件主應用必須提供一個可以安裝子應用的元素,因此咱們須要將 App.vue
文件改造一下:
<template> <div class="mainapp"> <!-- 標題欄 --> <header class="mainapp-header"> <h1>QianKun</h1> </header> <div class="mainapp-main"> <!-- 側邊欄 --> <ul class="mainapp-sidemenu"> <li @click="push('/vue')">Vue</li> <li @click="push('/react')">React</li> </ul> <!-- 子應用 --> <main class="subapp-container"> <h4 v-if="loading" class="subapp-loading">Loading...</h4> <div id="subapp-viewport"></div> </main> </div> </div> </template> <script> export default { name: 'App', props: { loading: Boolean, }, methods: { push(subapp) { history.pushState(null, subapp, subapp) } } } </script>
能夠看到咱們用於安裝子應用的元素爲 #subapp-viewport
,另外還有切換子應用的功能:
<!-- 側邊欄 --> <ul class="mainapp-sidemenu"> <li @click="push('/vue')">Vue</li> <li @click="push('/react')">React</li> </ul>
main.js
根據 qiankun 文檔說明,須要使用 registerMicroApps()
和 start()
方法註冊子應用及啓動主應用:
import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'react app', // app name registered entry: '//localhost:7100', container: '#yourContainer', activeRule: '/yourActiveRule', }, { name: 'vue app', entry: { scripts: ['//localhost:7100/main.js'] }, container: '#yourContainer2', activeRule: '/yourActiveRule2', }, ]); start();
因此如今須要將 main.js
文件改造一下:
import Vue from 'vue' import App from './App' import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from 'qiankun' let app = null function render({ loading }) { if (!app) { app = new Vue({ el: '#app', data() { return { loading, } }, render(h) { return h(App, { props: { loading: this.loading } }) } }); } else { app.loading = loading } } /** * Step1 初始化應用(可選) */ render({ loading: true }) const loader = (loading) => render({ loading }) /** * Step2 註冊子應用 */ registerMicroApps( [ { name: 'vue', // 子應用名稱 entry: '//localhost:8001', // 子應用入口地址 container: '#subapp-viewport', loader, activeRule: '/vue', // 子應用觸發路由 }, { name: 'react', entry: '//localhost:8002', container: '#subapp-viewport', loader, activeRule: '/react', }, ], // 子應用生命週期事件 { beforeLoad: [ app => { console.log('[LifeCycle] before load %c%s', 'color: green', app.name) }, ], beforeMount: [ app => { console.log('[LifeCycle] before mount %c%s', 'color: green', app.name) }, ], afterUnmount: [ app => { console.log('[LifeCycle] after unmount %c%s', 'color: green', app.name) }, ], }, ) // 定義全局狀態,能夠在主應用、子應用中使用 const { onGlobalStateChange, setGlobalState } = initGlobalState({ user: 'qiankun', }) // 監聽全局狀態變化 onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev)) // 設置全局狀態 setGlobalState({ ignore: 'master', user: { name: 'master', }, }) /** * Step3 設置默認進入的子應用 */ setDefaultMountApp('/vue') /** * Step4 啓動應用 */ start() runAfterFirstMounted(() => { console.log('[MainApp] first app mounted') })
這裏有幾個注意事項要注意一下:
name
必須和子應用下的 package.json
文件中的 name
同樣。loader()
方法,這是爲了應對用戶直接從子應用路由進入頁面的狀況而設的。進入子頁面時判斷一下是否加載了主應用,沒有則加載,有則跳過。loading
配置。vue-cli 的默認訪問端口通常爲 8080,爲了和子應用保持一致,須要將主應用端口改成 8000(子應用分別爲 800一、8002)。建立 vue.config.js
文件,將訪問端口改成 8000:
module.exports = { devServer: { port: 8000, } }
至此,主應用就已經改造完了。
子應用不須要引入 qiankun 依賴,只須要暴露出幾個生命週期函數就能夠:
bootstrap
,子應用首次啓動時觸發。mount
,子應用每次啓動時都會觸發。unmount
,子應用切換/卸載時觸發。如今將子應用的 main.js
文件改造一下:
import Vue from 'vue' import VueRouter from 'vue-router' import App from './App.vue' import routes from './router' import store from './store' Vue.config.productionTip = false let router = null let instance = null function render(props = {}) { const { container } = props router = new VueRouter({ // hash 模式不須要下面兩行 base: window.__POWERED_BY_QIANKUN__ ? '/vue' : '/', mode: 'history', routes, }) instance = new Vue({ router, store, render: h => h(App), }).$mount(container ? container.querySelector('#app') : '#app') } if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } else { render() } function storeTest(props) { props.onGlobalStateChange && props.onGlobalStateChange( (value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true, ) props.setGlobalState && props.setGlobalState({ ignore: props.name, user: { name: props.name, }, }) } export async function bootstrap() { console.log('[vue] vue app bootstraped') } export async function mount(props) { console.log('[vue] props from main framework', props) storeTest(props) render(props) } export async function unmount() { instance.$destroy() instance.$el.innerHTML = '' instance = null router = null }
能夠看到在文件的最後暴露出了 bootstrap
mount
unmount
三個生命週期函數。另外在掛載子應用時還須要注意一下,子應用是在主應用下運行仍是本身獨立運行:container ? container.querySelector('#app') : '#app'
。
根據 qiankun 文檔提示,須要對子應用的打包配置項做以下更改:
const packageName = require('./package.json').name; module.exports = { output: { library: `${packageName}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${packageName}`, }, };
因此如今咱們還須要在子應用目錄下建立 vue.config.js
文件,輸入如下代碼:
// vue.config.js const { name } = require('./package.json') module.exports = { configureWebpack: { output: { // 把子應用打包成 umd 庫格式 library: `${name}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${name}` } }, devServer: { port: 8001, headers: { 'Access-Control-Allow-Origin': '*' } } }
vue.config.js
文件有幾個注意事項:
'Access-Control-Allow-Origin': '*'
。devServer
裏更改端口。另一個子應用 react 的改造方法和 vue 是同樣的,因此在此再也不贅述。
咱們將使用 express 來部署項目,除了須要在子應用設置跨域外,沒什麼須要特別注意的地方。
主應用服務器文件 main-server.js
:
const fs = require('fs') const express = require('express') const app = express() const port = 8000 app.use(express.static('main-static')) app.get('*', (req, res) => { fs.readFile('./main-static/index.html', 'utf-8', (err, html) => { res.send(html) }) }) app.listen(port, () => { console.log(`main app listening at http://localhost:${port}`) })
vue 子應用服務器文件 vue-server.js
:
const fs = require('fs') const express = require('express') const app = express() const cors = require('cors') const port = 8001 // 設置跨域 app.use(cors()) app.use(express.static('vue-static')) app.get('*', (req, res) => { fs.readFile('./vue-static/index.html', 'utf-8', (err, html) => { res.send(html) }) }) app.listen(port, () => { console.log(`vue app listening at http://localhost:${port}`) })
react 子應用服務器文件 react-server.js
:
const fs = require('fs') const express = require('express') const app = express() const cors = require('cors') const port = 8002 // 設置跨域 app.use(cors()) app.use(express.static('react-static')) app.get('*', (req, res) => { fs.readFile('./react-static/index.html', 'utf-8', (err, html) => { res.send(html) }) }) app.listen(port, () => { console.log(`react app listening at http://localhost:${port}`) })
另外須要將這三個應用打包後的文件分別放到 main-static
、vue-static
、react-static
目錄下。而後分別執行命令 node main-server.js
、node vue-server.js
、node react-server.js
便可查看部署後的頁面。如今這個項目目錄以下:
-main -main-static // main 主應用靜態文件目錄 -react -react-static // react 子應用靜態文件目錄 -vue -vue-static // vue 子應用靜態文件目錄 -main-server.js // main 主應用服務器 -vue-server.js // vue 子應用服務器 -react-server.js // react 子應用服務器
我已經將這個微前端應用的代碼上傳到了 github,建議將項目克隆下來配合本章一塊兒閱讀,效果更好。下面放一下 DEMO 的運行效果圖:
對於大型應用的開發和維護,使用微前端能讓咱們變得更加輕鬆。不過若是是小應用,建議仍是單獨建一個項目開發。畢竟微前端也有額外的開發、維護成本。
帶你入門前端工程 全文目錄: