微前端解决方案初探 08 基于模块联邦的微前实现方案(管理登录状态、Dashboard 应用初始化和路由保护)

该博客介绍了如何在微前端架构下管理登录状态并实现路由保护。首先,通过在容器应用中创建登录状态和设置方法,并传递给微应用。然后,在微应用中接收这些方法,实现登录状态的改变和路由监听。接着,创建了一个Dashboard应用,并在用户登录后跳转至。最后,为Dashboard应用添加了路由保护,只有登录后才能访问。整个过程涉及React、Vue和Webpack等技术。
摘要由CSDN通过智能技术生成

存储和设置登录状态

由于每个微应用都有可能用到登录状态以及设置登录状态的方法,所以存储和设置登录状态的方法需要放在容器应用中,哪个微应用需要,就传递给它。

创建登录状态和设置方法,并传递给微应用组件:

// container\src\App.js
import React, { lazy, Suspense, useState, useEffect } from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import Header from './components/Header'

// 懒加载微应用
const MarketingApp = lazy(() => import('./components/MarketingApp'))
const AuthApp = lazy(() => import('./components/AuthApp'))

const history = createBrowserHistory()

function App() {
  // 创建登录状态和设置方法
  const [status, setStatus] = useState(false)

  useEffect(() => {
    console.log(status)
  }, [status])

  return (
    <Router history={history}>
      <Header />
      <Suspense fallback={<div>正在加载组件</div>}>
        <Switch>
          <Route path="/auth/signin">
            <AuthApp setStatus={setStatus} />
          </Route>
          <Route path="/">
            <MarketingApp />
          </Route>
        </Switch>
      </Suspense>
    </Router>
  )
}

export default App

// container\src\components\AuthApp.js
import React, { useEffect, useRef } from 'react'
import { mount } from 'auth/AuthApp'
import { useHistory } from 'react-router-dom'

export default function AuthApp({ setStatus }) {
  const ref = useRef()
  const history = useHistory()

  useEffect(() => {
    const { onParentNavigate } = mount(ref.current, {
      onNavigate({ pathname: nextPathname }) {
        const pathname = history.location.pathname

        // 路由不同时更新路由
        if (pathname !== nextPathname) history.push(nextPathname)
      },

      // 传递当前路由作为微应用的默认路由
      initialPath: history.location.pathname,

      // 设置登录状态的方法
      setStatus
    })

    // 容器应用路由变化通知微应用
    if (onParentNavigate) history.listen(onParentNavigate)
  }, [])

  return <div ref={ref}></div>
}

// auth\src\bootstrap.js
import React from 'react'
import ReactDOM from 'react-dom'
import { createMemoryHistory, createBrowserHistory } from 'history'
import App from './App'

function mount(el, { onNavigate, defaultHistory, initialPath, setStatus } = {}) {
  const history = defaultHistory || createMemoryHistory({
    // 指定默认匹配地址
    initialEntries: [initialPath]
  })

  if (onNavigate) history.listen(onNavigate)

  ReactDOM.render(<App history={history} setStatus={setStatus} />, el)

  return {
    onParentNavigate({ pathname: nextPathname }) {
      const pathname = history.location.pathname

      if (pathname !== nextPathname) {
        history.push(nextPathname)
      }
    }
  }
}

if (process.env.NODE_ENV === 'development') {
  const el = document.querySelector('#dev-auth')
  if (el) {
    mount(el, {
      defaultHistory: createBrowserHistory()
    })
  }
}

export { mount }

// auth\src\App.js
import React from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import Signin from './components/Signin'

function App({ history, setStatus }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/auth/signin">
          <Signin setStatus={setStatus} />
        </Route>
      </Switch>
    </Router>
  )
}

export default App

// auth\src\components\Signin.js
import React from 'react'

export default function SignIn({ setStatus }) {
  const styles = {
    width: '400px',
    margin: '20px auto'
  }
  return (
    <div style={styles}>
      <h2>登录</h2>
      <form onSubmit={e => e.preventDefault()} noValidate>
        <input type="email" name="email" placeholder="邮件地址" autoFocus />
        <br />
        <input type="password" name="password" placeholder="密码" />
        <br />
        <input type="checkbox" /> 记住我
        <br />
        <button type="submit" onClick={() => setStatus(true)}>
          登录
        </button>
      </form>
    </div>
  )
}

登录状态应用

实现三个事情:

  1. 根据登录状态显示登录/退出链接
  2. 点击退出链接取消登录状态
  3. 如果已登录,跳转到 Dashboard 应用

向 Header 组件传递登录状态和设置方法:

// container\src\App.js
<Header status={status} setStatus={setStatus} />

Header 组件使用登录状态:

// container\src\components\Header.js
import React from 'react'
import { Link } from 'react-router-dom'

export default function Header({ status, setStatus }) {
  const styles = {
    backgroundColor: '#ddd',
    padding: '10px',
    display: 'flex',
    justifyContent: 'space-between'
  }
  return (
    <div style={styles}>
      <Link to="/">首页</Link>
      {status ? (
        <Link to="/" onClick={() => setStatus(false)}>
          退出
        </Link>
      ) : (
        <Link to="/auth/signin">登录</Link>
      )}
    </div>
  )
}

修改监听 status 的地方:

// container\src\App.js
useEffect(() => {
  // 如果已登录,跳转到 Dashboard
  if (status) {
    history.push('/dashboard')
  }
  console.log(status)
}, [status])

Dashboard - 应用初始化

Dashboard 应用使用 vue3 开发。

# 创建应用文件夹
mkdir dashboard
cd dashboard

# 初始化 package.json
npm init -y

# 安装生产依赖(共享模块)
npm install vue
# 安装开发依赖
npm install -D @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader style-loader vue-loader vue-style-loader webpack webpack-cli webpack-dev-server webpack-merge html-webpack-plugin @vue/compiler-sfc

本例依赖版本如下:

"dependencies": {
  "vue": "^3.2.31"
},
"devDependencies": {
  "@babel/core": "^7.17.2",
  "@babel/plugin-transform-runtime": "^7.17.0",
  "@babel/preset-env": "^7.16.11",
  "@vue/compiler-sfc": "^3.2.31",
  "babel-loader": "^8.2.3",
  "css-loader": "^6.6.0",
  "html-webpack-plugin": "^5.5.0",
  "style-loader": "^3.3.1",
  "vue-loader": "^17.0.0",
  "vue-style-loader": "^4.1.3",
  "webpack": "^5.68.0",
  "webpack-cli": "^4.9.2",
  "webpack-dev-server": "^4.7.4",
  "webpack-merge": "^5.8.0"
}

修改启动脚本:

"scripts": {
  "start": "webpack serve"
},

添加模板文件:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
</head>
<body>
  <div id="dev-dashboard"></div>
</body>
</html>

添加入口文件:

// dashboard\src\index.js
import('./bootstrap')

添加 bootstrap.js:

// dashboard\src\bootstrap.js
import { createApp } from 'vue'
import Dashboard from './components/Dashboard'

function mount(el) {
  const app = createApp(Dashboard)
  app.mount(el)
}

if (process.env.NODE_ENV === 'development') {
  const el = document.querySelector('#dev-dashboard')
  if (el) {
    mount(el)
  }
}

export { mount }

添加 Dashboard 组件:

<!-- dashboard\src\components\Dashboard.vue -->
<template>
  <div>Dashboard</div>
</template>

<script>
export default {
  name: 'Dashboard',
  data() {
    return {

    }
  }

}
</script>

<style scoped></style>

添加 webpack 配置文件:

// dashboard\webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
const packageJson = require('./package.json')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    publicPath: 'http://localhost:8083/'
  },
  resolve: {
    extensions: ['.js', '.vue']
  },
  devServer: {
    port: 8083,
    historyApiFallback: true
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: 'vue-loader'
      },
      {
        test: /\.css$/,
        use: ['vue-style-loader', 'style-loader', 'css-loader']
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new VueLoaderPlugin(),
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js',
      exposes: {
        './DashboardApp': './src/bootstrap'
      },
      shared: packageJson.dependencies
    })
  ]
}

npm start 启用应用,访问 http://localhost:8083

Container - 加载 Dashboard 应用

添加微应用配置:

// container\webpack.config.js
remotes: {
  marketing: 'marketing@http://localhost:8081/remoteEntry.js',
  auth: 'auth@http://localhost:8082/remoteEntry.js',
  dashboard: 'dashboard@http://localhost:8083/remoteEntry.js'
},

添加加载微应用的组件:

// container\src\components\DashboardApp.js
import { useEffect, useRef } from 'react'
import { mount } from 'dashboard/DashboardApp'

export default function DashboardApp() {
  const ref = useRef()

  useEffect(() => {
    mount(ref.current)
  }, [])

  return <div ref={ref}></div>
}

添加懒加载路由配置:

// container\src\App.js
import React, { lazy, Suspense, useState, useEffect } from 'react'
import { Router, Route, Switch } from 'react-router-dom'
import { createBrowserHistory } from 'history'
import Header from './components/Header'

// 懒加载微应用
const MarketingApp = lazy(() => import('./components/MarketingApp'))
const AuthApp = lazy(() => import('./components/AuthApp'))
const DashboardApp = lazy(() => import('./components/DashboardApp'))

const history = createBrowserHistory()

function App() {
  // 创建登录状态和设置方法
  const [status, setStatus] = useState(false)

  useEffect(() => {
    // 如果已登录,跳转到 Dashboard
    if (status) {
      history.push('/dashboard')
    }
    console.log(status)
  }, [status])

  return (
    <Router history={history}>
      <Header status={status} setStatus={setStatus} />
      <Suspense fallback={<div>正在加载组件</div>}>
        <Switch>
          <Route path="/auth/signin">
            <AuthApp setStatus={setStatus} />
          </Route>
          <Route path="/dashboard">
            <DashboardApp />
          </Route>
          <Route path="/">
            <MarketingApp />
          </Route>
        </Switch>
      </Suspense>
    </Router>
  )
}

export default App

重启容器应用。

DashboardApp 路由保护

现在 Dashboard 页面可以随意访问,下面添加登录访问权限,只有登录后才访问。

// container\src\App.js
import { Router, Route, Switch, Redirect } from 'react-router-dom'

<Route path="/dashboard">
  {/* 如果未登录,重定向到首页 */}
  {!status && <Redirect to="/" />}
  <DashboardApp />
</Route>

至此案例完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值