前端微服务化解决方案5 - 路由分发

路由分发式微前端

从应用分发路由到路由分发应用

用这句话来解释,微前端的路由,再合适不过来.

路由分发式微前端,即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决。
就当前而言,通过路由分发式的微前端架构应该是采用最多、最易采用的 “微前端” 方案。但是这种方式看上去更像是多个前端应用的聚合,即我们只是将这些不同的前端应用拼凑到一起,使他们看起来像是一个完整的整体。但是它们并不是,每次用户从 A 应用到 B 应用的时候,往往需要刷新一下页面。 -- 引用自phodal 微前端的那些事儿

模块加载器那一章的示例代码,已经非常充分了展示了路由分发应用的步骤.

在单页面前端的路由,目前有两种形式,
一种是所有主流浏览器都兼容多hash路由,
基本原理为url的hash值的改变,触发了浏览器onhashchange事件,来触发组件的更新

还有一种是高级浏览器才支持的 History API,
window.history.pushState(null, null, "/profile/");的时候触发组件的更新

// hash 模式,项目路由用的是hash模式会用到该函数
export function hashPrefix(app) {
    return function (location) {
        let isShow = false
        //如果该应用 有多个需要匹配的路劲
        if(isArray(app.path)){
            app.path.forEach(path => {
                if(location.hash.startsWith(`#${path}`)){
                    isShow = true
                }
            });
        }
        // 普通情况
        else if(location.hash.startsWith(`#${app.path || app.url}`)){
            isShow = true
        }
        return isShow;
    }
}

// pushState 模式
export function pathPrefix(app) {
    return function (location) {
        let isShow = false
        //如果该模块 有多个需要匹配的路径
        if(isArray(app.path)){
            app.path.forEach(path => {
                if(location.pathname.indexOf(`${path}`) === 0){
                    isShow = true
                }
            });
        }
        // 普通情况
        else if(location.pathname.indexOf(`${app.path || app.url}`) === 0){
            isShow = true
        }
        return isShow;
    }
}

// 应用注册
export async function registerApp(params) {
    // 第三个参数为,该模块是否显示
    singleSpa.registerApplication(params.name,  // 模块名字
                                  () => SystemJS.import(params.main), // 模块渲染的入口文件
                                  params.base ? (() => true) : pathPrefix(params) // 模块显示的条件
                                  );

}

路由分发应用

当url前缀,与配置中的url前缀保持一致的时候,
singleSpa会激活对应的模块,然后把模块内容渲染出来.

应用分发路由

在模块被激活的时候,模块会读取url,再渲染到对的页面.

这就是微前端路由的路由工作流程

微前端路由的挑战

Hash路由

在目前所有支持spa的前端框架中,都支持了Hash路由.
Hash路由都工作大致原理就是: url的Hash值的改变,触发了浏览器onhashchange事件,进而来触发组件的更新.
所有的前端的框架,都是基于onhashchange来更新我们的页面的.
当我们的架构使用微前端的话,如果选择hash路由,便可以保证所有的前端技术框架的更新事件都是一致的.
所以使用Hash路由也是最省心的.如果不介意Hash路由中url的 # 字符,在微前端中使用Hash也是推荐的.

HTML5 History 路由

大家都知道,HTML5中History对象上新增了两个API (pushState与replaceState).
在这两个新API的作用下,我们也是可以做到页面无刷新,并且更新页面的.并且url上不需要出现#号.
保持了最高的美观度(对于一些人来讲).
当然现在几乎所有的主流SPA技术框架都支持这一特性.
但是问题是,这两个API在触发的时候,是没有一个全局的事件触发的.
多种技术框架对History路由的实现都不一样,就算是技术栈都是 React,他的路由都有好几个版本.

那我们如何保证一个项目下,多个技术框架模块的路由做到协同呢?

只有一个history

前提: 假设我们所有的项目用的都是React,我们的路由都在使用着同一个版本.

思路: 我们是可以这样做的,在我们的base前端模块(因为他总是第一个加载,也是永远都不会被销毁的模块)中的Store.js,
实例化一个React router的核心库history,通过消息总线,把这个实例传入到所有的模块中.
在每个模块的路由初始化的时候,是可以自定义自己的history的.把模块的history重新指定到传入的history.
这样就可以做到,所有模块的路由之间的协同了.
因为当页面切换的时候,history触发更新页面的事件,当所有模块的history都是一个的时候,所有的模块都会更新到正确的页面.
这样就保证了所有模块与路由都协同.

如果你看不懂我在讲什么,直接贴代码吧:

//Base前端模块的 Store.js
import { createStore, combineReducers } from 'redux'

// react router 的核心库 history
import createHistory from 'history/createBrowserHistory'

const history = createHistory()

// 传出去
export const storeInstance = createStore(combineReducers({ namespace: () => 'base' ,history }))

// 应用注册
export async function registerApp(params) {
    ...

    // history 直接引入进来,用systemjs直接导入实例
    try {
        storeModule = params.store ? await SystemJS.import(params.store) : { storeInstance: null };
    } catch (e) {
        ...
    }
    ...

    // 跟派发器一起放进 customProps 中
    customProps = { store: storeModule, globalEventDistributor: ... };


    // 在注册的时候传入 customProps
    singleSpa.registerApplication(params.name, 
                                () => SystemJS.import(params.main), 
                                params.base ? (() => true) : pathPrefix(params), 
                                customProps // 应用注册的时候,history会包含在 customProps 中,直接注入到模块中
                                );
}
// React main.js
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: (spa) => {
    // 在这里,把history传入到组件
    return <RootComponent  history={spa.customProps.history}/>
  },
  domElementGetter: () => document.getElementById('root')
})

...

// RootComponent
import React from 'react'
import { Provider } from 'react-redux' 
export default class RootComponent extends React.Component {
    render() {
        return <Provider store={this.state.store}>
            // 在这里重新指定Router的history
          <Router history={this.props.history}>
            <Switch>
                ...
            </Switch>
          </Router>
        </Provider>
    }
}

以上就是让所有模块的路由协同,保证只有一个history的用法

多技术栈模块路由协同

问题: 用上面的方式是可行的,但是遗憾的是,他的应用场景比较小,只能在单一技术栈,单一路由版本的情况下使用.
微前端最大的优势之一就是自由选择技术栈.
在一个项目中,使用多个适合不同模块的技术栈.

思路: 我们其实是可以通过每一个模块对外输出一个路由跳转到接口,基于消息总线的派发,让每一个模块渲染到正确的页面.
比如 模块A要跳转到 /a/b/c ,模块a先更新到/a/b/c路由的页面,然后通过消息总线,告诉所有模块,现在要跳转到 /a/b/c了.
然后其他模块,有/a/b/c这个路由都,就直接跳转,没有的就什么都不做.

我们可以这样做:

// Store.js
import { createStore, combineReducers } from 'redux'
import createHistory from 'history/createBrowserHistory'
const history = createHistory()

// 对外输出一个to的接口,当一个模块需要跳转界面的时候,会向所有的模块调用这个接口,
// 然后对应的模块会直接渲染到正确的页面
function to(state, action) {
  if (action.type !== 'to' ) return { ...state, path: action.path }
  history.replace(action.path)
  return { ...state, path: action.path }
}

export const storeInstance = createStore(combineReducers({ namespace: () => 'base', to }))

export { history }

这是路由跟消息总线的一种完美结合的使用方式,消息总线的潜力还有很多,后续会慢慢说明.
未完待续 ...

相关文章

前端微服务化解决方案1 - 思考

前端微服务化解决方案2 - Single-SPA

前端微服务化解决方案3 - 模块加载器

前端微服务化解决方案4 - 消息总线

前端微服务化解决方案5 - 路由分发

前端微服务化解决方案6 - 构建与部署

前端微服务化解决方案7 - 静态数据共享

前端微服务化解决方案8 - 二次构建

Demo

前端微服务化 Micro Frontend Demo

微前端模块加载器

微前端Base App示例源码

微前端子项目示例源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容