single-spa.js
什么是 single-spa.js
single-spa 是一個(gè)可以把多種 JavaScript 框架所開(kāi)發(fā)的應(yīng)用聚合在一個(gè)應(yīng)用的前端框架.
它有如下特點(diǎn):
- 多個(gè)SPA的切換無(wú)需刷新
- 每個(gè)應(yīng)用獨(dú)立部署
- 兼容多元框架
- 懶加載
這是一個(gè) single-spa 應(yīng)用的在線例子
從架構(gòu)上來(lái)講, single-spa 應(yīng)用由兩個(gè)部分組成:
- applications
- single-spa-config
Applications
single-spa apps 會(huì)包含眾多的 SPA 應(yīng)用, 并且每一個(gè)應(yīng)用都是一個(gè)完整的應(yīng)用, 都可以從 DOM 中裝載和卸載自身. 與傳統(tǒng)的 SPA 相比, single-spa apps 最大的不同是它可以與其他的應(yīng)用共存, 共享同一個(gè) html page.
如果你的網(wǎng)絡(luò)還不錯(cuò), 一定已經(jīng)打開(kāi)了在線例子. 在元素檢視面板可以看到很多 div.
這些 div 就像插槽一樣, 在啟動(dòng)特定的應(yīng)用時(shí), 在對(duì)應(yīng)的插槽裝載元素. 而當(dāng)應(yīng)用不在是激活狀態(tài)時(shí), 又會(huì)從 DOM 中卸載掉該應(yīng)用的元素. 并且, 所有的應(yīng)用都共享著同一個(gè) html 頁(yè).
single-spa-config
single-spa-config 用于在 single-spa 中注冊(cè)應(yīng)用, 每個(gè)應(yīng)用的注冊(cè)需要如下 3 樣?xùn)|西:
- application name
- application load function
- application active status switch function
從零開(kāi)始
本教程的目標(biāo)是在結(jié)束時(shí), 完成零開(kāi)始到集成完畢. 它共需要 6 步:
- 初始化項(xiàng)目
- 新建 html 文件
- Registering
- Create the home application
- Create the navBar application
- Create the angularJs application
初始化項(xiàng)目
隨意新建一個(gè)文件夾, 例如 single-spa 作為本此嘗試所使用的目錄:
0: 初始化目錄
mkdir single-spa && cd single-spa
yarn init # or npm init
yarn add single-spa # or npm install --save single-spa
mkdir src
1. 安裝配置 Babel
yarn add --dev @babel/core @babel/preset-env @babel/preset-react @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-object-rest-spread
2. 安裝配置 Webpack
single-spa 現(xiàn)階段不得不使用 Webpack, 執(zhí)行如下命令安裝 Webpack, Webpack plugins, loaders.
# Webpack core
yarn add webpack webpack-dev-server webpack-cli --dev
# Webpack plugins
yarn add clean-webpack-plugin --dev
# Webpack loaders
yarn add style-loader css-loader html-loader babel-loader --dev
安裝結(jié)束之后, 在根目錄創(chuàng)建一個(gè) webpack.config.js
文件, 貼入如下代碼:
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
// Set the single-spa config as the project entry point
'single-spa.config': './single-spa.config.js',
},
output: {
publicPath: '/dist/',
filename: '[name].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// Webpack style loader added so we can use materialize
test: /\.css$/,
use: ['style-loader', 'css-loader']
}, {
test: /\.js$/,
exclude: [path.resolve(__dirname, 'node_modules')],
loader: 'babel-loader',
}, {
// This plugin will allow us to use AngularJS HTML templates
test: /\.html$/,
exclude: /node_modules/,
loader: 'html-loader',
},
],
},
node: {
fs: 'empty'
},
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
},
plugins: [
// A webpack plugin to remove/clean the output folder before building
new CleanWebpackPlugin(),
],
devtool: 'source-map',
externals: [],
devServer: {
historyApiFallback: true
}
};
3. 配置 npm run scripts
打開(kāi)根目錄的 package.json
文件, 添加如下腳本:
"scripts": {
"start": "webpack-dev-server --open",
"build": "webpack --config webpack.config.js -p"
},
新建 index.html 文件
在根目錄新建一個(gè) index.html
文件, 內(nèi)容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>single-spa</title>
</head>
<body>
<div id="navBar"></div>
<div id="home"></div>
<div id="angularJS"></div>
<script src="/dist/single-spa.config.js"></script>
</body>
</html>
如果有一些公共的樣式什么的, 可以在此時(shí)一并引入到 index.html
中來(lái).
由于 Webpack 隨后會(huì)將 single-spa.config.js
輸出到 /dist
目錄, 所以single-spa 的配置文件的路徑會(huì)是指向 /dist
的.
注冊(cè)應(yīng)用
通過(guò)配置 single-spa.config.js
注冊(cè)應(yīng)用, 可以告訴 single-spa 如何去引導(dǎo)牡辽、裝載、卸載我們的應(yīng)用.
在根目錄創(chuàng)建一個(gè) single-spa.config.js
文件, 內(nèi)容如下:
import { registerApplication, start } from 'single-spa';
registerApplication(
// 注冊(cè)的應(yīng)用的名稱
'home',
// 加載函數(shù)
() => { },
// 活動(dòng)函數(shù)
location => location.pathname === '' ||
location.pathname === '/' ||
location.pathname.startsWith('/home')
)
start();
該配置注冊(cè)了一個(gè) home app, 并分別指明了其名稱棒坏、加載函數(shù)妨猩、活動(dòng)函數(shù).
加載函數(shù)
loadingFunction
必須是一個(gè) async 函數(shù)或者其他返回一個(gè) 已決議 Promise
的函數(shù), 意思也是一樣的.
當(dāng) loading 一個(gè) app 時(shí), 會(huì)首先調(diào)用本函數(shù), 從這個(gè)角度來(lái)看, 定位有些像鉤子函數(shù).
活動(dòng)函數(shù)
activityFunction
必須是一個(gè)返回 boolean 值或其他可判斷真假的值的純函數(shù), 當(dāng)返回結(jié)果為真時(shí), 本應(yīng)用認(rèn)為是活動(dòng)狀態(tài).
Home App
初始化 home app
在 src/
目錄下新建 home/
文件夾, 并且在 Home/
目錄下新建兩個(gè) js 文件:
- home.app.js
- root.component.js
安裝 react 依賴:
yarn add react react-dom single-spa-react react-router-dom react-transition-group
定義 home app 生命周期
注冊(cè)應(yīng)用以后, single-spa 就已經(jīng)開(kāi)始監(jiān)聽(tīng)?wèi)?yīng)用的引導(dǎo)與狀態(tài)了, 屆時(shí)對(duì)應(yīng)的 app 會(huì)對(duì)此做出響應(yīng).
single-spa-react
提供了將 react 注冊(cè)為 singleSpaReact
所需的通用生命周期鉤子, 可以很方便的注冊(cè).
singleSpaReact
需要 4 個(gè)參數(shù):
- React 實(shí)例
- ReactDOM 實(shí)例
- root 組件
- domElementGetter 函數(shù)
打開(kāi) src/home/home.app.js
文件, 內(nèi)容如下:
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Home from './root.component';
// 獲取自己的槽
function domElementGetter () {
return document.querySelector('#home');
}
// 對(duì)應(yīng)三個(gè)鉤子函數(shù) bootstrap, mount, unmount
const reactLifecycles = singleSpaReact({
React, ReactDOM, rootComponent: Home, domElementGetter
});
export const bootstrap = [reactLifecycles.bootstrap];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
構(gòu)建 React app
打開(kāi) src/home/root.component.js
文件, 內(nèi)容如下:
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import {
BrowserRouter as Router,
Switch,
Route,
Link,
Redirect
} from "react-router-dom";
/* you'll need this CSS somewhere
.fade-enter {
opacity: 0;
z-index: 1;
}
.fade-enter.fade-enter-active {
opacity: 1;
transition: opacity 250ms ease-in;
}
*/
const AnimationExample = () => (
<Router basename="/home">
<Route
render={({ location }) => (
<div style={{position: 'relative', height: '100%'}}>
<Route
exact
path="/"
render={() => <Redirect to="/hsl/10/90/50" />}
/>
<ul style={styles.nav}>
<NavLink to="/hsl/10/90/50">Red</NavLink>
<NavLink to="/hsl/120/100/40">Green</NavLink>
<NavLink to="/rgb/33/150/243">Blue</NavLink>
<NavLink to="/rgb/240/98/146">Pink</NavLink>
</ul>
<div style={styles.content}>
<TransitionGroup>
{/* no different than other usage of
CSSTransition, just make sure to pass
`location` to `Switch` so it can match
the old location as it animates out
*/}
<CSSTransition key={location.key} classNames="fade" timeout={300}>
<Switch location={location}>
<Route exact path="/hsl/:h/:s/:l" component={HSL} />
<Route exact path="/rgb/:r/:g/:b" component={RGB} />
{/* Without this `Route`, we would get errors during
the initial transition from `/` to `/hsl/10/90/50`
*/}
<Route render={() => <div>Not Found</div>} />
</Switch>
</CSSTransition>
</TransitionGroup>
</div>
</div>
)}
/>
</Router>
);
const NavLink = props => (
<li style={styles.navItem}>
<Link {...props} style={{ color: "inherit" }} />
</li>
);
const HSL = ({ match: { params } }) => (
<div
style={{
...styles.fill,
...styles.hsl,
background: `hsl(${params.h}, ${params.s}%, ${params.l}%)`
}}
>
hsl({params.h}, {params.s}%, {params.l}%)
</div>
);
const RGB = ({ match: { params } }) => (
<div
style={{
...styles.fill,
...styles.rgb,
background: `rgb(${params.r}, ${params.g}, ${params.b})`
}}
>
rgb({params.r}, {params.g}, {params.b})
</div>
);
const styles = {};
styles.fill = {
position: "absolute",
left: 0,
right: 0,
top: 0,
bottom: 0
};
styles.content = {
...styles.fill,
top: "40px",
textAlign: "center"
};
styles.nav = {
padding: 0,
margin: 0,
position: "absolute",
top: 0,
height: "40px",
width: "100%",
display: "flex"
};
styles.navItem = {
textAlign: "center",
flex: 1,
listStyleType: "none",
padding: "10px"
};
styles.hsl = {
...styles.fill,
color: "white",
paddingTop: "20px",
fontSize: "30px"
};
styles.rgb = {
...styles.fill,
color: "white",
paddingTop: "20px",
fontSize: "30px"
};
export default AnimationExample;
定義 loading function
打開(kāi)根目錄下的 single-spa.config.js
文件, 修改 loading function:
import { registerApplication, start } from 'single-spa';
registerApplication(
// 注冊(cè)的應(yīng)用的名稱
'home',
// 加載函數(shù)
() => import('./src/home/home.app'), // <- here
// 激活函數(shù)
location => location.pathname === '' ||
location.pathname === '/' ||
location.pathname.startsWith('/home')
)
start();
這時(shí)候可以嘗試啟動(dòng)一下.
Run yarn start
, 如果一切正常的話, 將會(huì)成功啟動(dòng), 并且看到如下頁(yè)面:
NavBar App
創(chuàng)建和注冊(cè) NavBar app 的過(guò)程與 Home app 非常的相似. 不同點(diǎn)在于 NavBar 會(huì)導(dǎo)出一個(gè)帶有生命周期的對(duì)象并且通過(guò)懶加載的方式獲取各個(gè)應(yīng)用的對(duì)象.
注冊(cè) navBar
在 single-spa.config.js
添加如下的代碼:
registerApplication(
'navBar',
() => import('./src/navBar/navBar.app.js').then(module => module.navBar),
() => true
)
navBar.app.js
目前還沒(méi)有, 不過(guò)隨后就會(huì)創(chuàng)建.
由于 navBar 是需要始終顯示的, 因此, activityFunction 這里固定返回一個(gè) true.
初始化 navBar app
在 src 目錄下新建 navBar 目錄, 并在其中分別創(chuàng)建 navBar.app.js
與 root.component.js
文件.
可以通過(guò)在根路徑執(zhí)行以下命令創(chuàng)建:
mkdir src/navBar
touch src/navBar/navBar.app.js src/navBar/root.component.js
定義 navBar app 生命周期
打開(kāi) navBar.app.js
文件, 貼入下面的代碼:
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import NavBar from './root.component.js';
function domElementGetter() {
return document.getElementById("navBar")
}
export const navBar = singleSpaReact({
React,
ReactDOM,
rootComponent: NavBar,
domElementGetter,
})
編寫 navBar app 頁(yè)面
import React from 'react'
import {navigateToUrl} from 'single-spa'
const NavBar = () => (
<nav>
<div className="nav-wrapper">
<a href="/" onClick={navigateToUrl} className="brand-logo">single-spa</a>
<ul id="nav-mobile" className="right hide-on-med-and-down">
<li><a href="/" onClick={navigateToUrl}>Home</a></li>
<li><a href="/angularJS" onClick={navigateToUrl}>AngularJS</a></li>
</ul>
</div>
</nav>
)
export default NavBar
AngularJs App
初始化 AngularJs app
執(zhí)行如下命令:
mkdir src/angularJS
cd src/angularJS
touch angularJS.app.js root.component.js root.template.html routes.js app.module.js gifs.component.js gifs.template.html
為了演示子應(yīng)用內(nèi)部路由效果, 這里需要添加一些包:
yarn add angular angular-ui-router single-spa-angularjs
注冊(cè) AngularJs app
打開(kāi) single-spa.config.js
文件, 添加如下的代碼:
function pathPrefix(prefix) {
return function(location) {
return location.pathname.startsWith(prefix);
}
}
registerApplication(
'angularJS',
() => import ('./src/angularJS/angularJS.app.js'),
pathPrefix('/angularJS')
)
定義 AngularJs app 生命周期
在 angularJs.app.js
文件中貼入如下代碼:
import singleSpaAngularJS from 'single-spa-angularjs';
import angular from 'angular';
import './app.module.js';
import './routes.js';
const domElementGetter = () => document.querySelector('#angularJS');
const angularLifecycles = singleSpaAngularJS({
angular,
domElementGetter,
mainAngularModule: 'angularJS-app',
uiRouter: true,
preserveGlobal: false,
});
export const bootstrap = [
angularLifecycles.bootstrap,
];
export const mount = [
angularLifecycles.mount,
];
export const unmount = [
angularLifecycles.unmount,
];
配置 angular 應(yīng)用
app.module.js
import angular from 'angular';
import 'angular-ui-router';
angular
.module('angularJS-app', ['ui.router']);
root.component.js
import angular from 'angular';
import template from './root.template.html';
angular
.module('angularJS-app')
.component('root', {
template,
});
root.template.html
<div ng-style='vm.styles'>
<div class="container">
<div class="row">
<h4 class="light">
Angular 1 example
</h4>
<p class="caption">
This is a sample application written with Angular 1.5 and angular-ui-router.
</p>
</div>
<div>
<!-- These Routes will be set up in the routes.js file -->
<a class="waves-effect waves-light btn-large" href="/angularJS/gifs" style="margin-right: 10px">
Show me cat gifs
</a>
<a class="waves-effect waves-light btn-large" href="/angularJS" style="margin-right: 10px">
Take me home
</a>
</div>
<div class="row">
<ui-view />
</div>
</div>
</div>
gifs.component.js
import angular from 'angular';
import template from './gifs.template.html';
angular
.module('angularJS-app')
.component('gifs', {
template,
controllerAs: 'vm',
controller ($http) {
const vm = this;
$http
.get('https://api.giphy.com/v1/gifs/search?q=cat&api_key=dc6zaTOxFJmzC')
.then(response => {
vm.gifs = response.data.data;
})
.catch(err => {
setTimeout(() => {
throw err;
}, 0);
});
},
});
gif.template.html
<div style="padding-top: 20px">
<h4 class="light">
Cat Gifs gifs
</h4>
<p>
</p>
<div ng-repeat="gif in vm.gifs" style="margin: 5px;">
<img ng-src="{{gif.images.downsized_medium.url}}" class="col l3">
</div>
</div>
設(shè)置 AngularJs app 內(nèi)部路由
routes.js
import angular from 'angular';
import './gifs.component.js';
import './root.component.js';
angular
.module('angularJS-app')
.config(($stateProvider, $locationProvider) => {
$locationProvider.html5Mode({
enabled: true,
requireBase: false,
});
$stateProvider
.state('root', {
url: '/angularJS',
template: '<root />',
})
.state('root.gifs', {
url: '/gifs',
template: '<gifs />',
});
});
完成
雖然過(guò)程真的好繁瑣, 但是不可否認(rèn)以這種簡(jiǎn)單的例子來(lái)說(shuō)確實(shí)成功了.
執(zhí)行 yarn start
可以查看效果.