微前端qiankun框架接入实战

背景

随着项目的演进,前端的业务架构也会变得更加庞大、复杂,并常常会出现需要模块复用的场景:
1、组件复用,例如统一的导航栏、侧边栏、路由权限处理逻辑等
2、模块级别复用,例如统一的用户管理模块、文档中心等
3、系统级别复用,总的系统由多个系统组合而成,不同的系统可能由不同的开发团队维护、使用不同的技术栈开发。

除了代码层面复用(复制粘贴),也需要更加完善的模块和系统复用方案。引入微前端,将代码根据业务逻辑划分至不同的项目之中进行维护,能够有效的降低维护难度,每个系统既可以独立运行、独立部署,也可以组合起来构成一个完整的系统,能够更快速地响应客户的需求。


之前页面嵌入都使用iframe,简捷易用,两行代码就可以搞定,但在加载速度方面略有些不尽人意。可以看这篇➡Why Not Iframe
听说隔壁项目组都已经用qiankun用的飞起了,我们必不能落后于人~于是在赶鸭子上架下,使用了qiankun进行了一次完整的实践。

实战步骤

什么是qiankun

qiankun官方文档

qiankun是基于single-spa的封装,可以参考:https://single-spa.js.org/ .
single-spa是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架。
我们每个前端项目最终都会被打包成一个单页应用,该应用以index.html为入口,在其中引入打包后的js和css文件。

前端代码改造

qiankun在逻辑上,将前端应用划分为主应用(又称为基座应用)和微应用,主应用拉取微应用打包后的js,并设置一定的规则来控制微应用的生命周期(装载、卸载等),微应用则要暴露出生命周期钩子函数,供主函数调用。

在主应用下安装依赖:

npm i qiankun --save-dev

主应用改造

1、改造入口文件

mainApp/main.js

import { registerMicroApps, start } from "qiankun"
let msg = {
   // 传入子应用的内容
};

// 注册子应用
registerMicroApps(
    [
        {
            name: "turing-permission",
            entry: "//127.0.0.1:9527",
            //  指定子应用的挂载容器
            container: '#subApp',
            activeRule: "#/turing-permission",
            props: msg
        },
        {
            name: "turing-moss",
            entry: "//127.0.0.1:9528",
            //  指定子应用的挂载容器
            container: '#subApp',
            activeRule: "#/turing-moss",
            props: msg
        },
    ],

);

const request = url => { return fetch(url, { referrerPolicy: 'origin-when-cross-origin' }) };
start({ prefetch: true, sandbox: { experimentalStyleIsolation: true }, fetch: request });

说明:
参看qiankunAPI说明文档

  • registerMicroApps方法接收子应用列表。 参数解释:
    1、name - 子应用名称
    2、entry - 主应用使用fetch请求,从该入口获取子应用的js、css等资源,注意该地址需要去掉协议(http/https)。部署到线上时,该地址可填写为部署地址IP + 端口 + /subApp的形式,使用nginx代理,后面说到部署时会给出示例。此处为本地开发时的地址。
    3、container - 子应用挂载的DOM根节点。需要注意在子应用加载时,该DOM节点必须存在,否则会报子应用挂载失败错误
    4、activeRule - 触发子应用挂载的条件。如果子应用使用的路由为hash模式,则需要加#,如果使用的是history 模式,则不需要加#。本次实战使用的路由模式均为hash模式(也是默认的模式)
    该方法可以自定义方法实现
    5、props: 可以定义主应用传入到子应用的值。可以将主应用的store和router都传过去。

  • start方法
    参考API文档进行配置。这里踩了一个坑,如果沙箱隔离配置为 sandbox: { strictStyleIsolation: true },可能会导致element-UI组件样式被影响(下拉框挂到左上角)。

2、通信

应用在鉴权中,用于同步登录状态和传递token(若使用同一个域下的Cookie来鉴权,此处可忽略):

import qiankunActions from '@/store/qiankun'
//   登录成功后,获取到访问令牌
const { permissionList, accessToken } = await store.dispatch('user/getInfo')
qiankunActions.setGlobalState({ token: accessToken });

3、提供子应用挂载的根节点

App.vue

<template>
    <div id="container" class="container">
        <head-top v-if="$route.name != 'Home'" />
        <router-view />
        <div  id="subApp" />
    </div>
</template>

注意:如果使用了<router-view/>,该节点需要与最高层级的<router-view>同级!

子应用改造

子应用无需安装qiankun依赖

1、入口文件改造

subApp/main.js

let instance;
function render(props) {
  let container = props ? props.container : undefined;
  instance = new Vue({
    router,
    store,
    render: h => h(App),
  }).$mount(container ? container.querySelector('#app') : '#app');
}
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
export async function bootstrap() { 
}

export async function mount(props) {
  props.onGlobalStateChange((state, prevState) => {
    store.commit('user/SET_TOKEN', prevState.token)
  }, true);
  render(props);
}
export async function unmount() {
  console.log('[turing-permission] unmounted');
  instance.$destroy();
  instance = null;
}

从代码逻辑易得,在子应用中暴露出的mount钩子方法中执行了Vue的render方法。该方法根据传入的props(会将container传入),找到对应的dom节点,在该dom节点下插入子应用的模板代码,再执行Vue的mount方法。需要区分两个mount:一个是子应用的挂载,一个是Vue应用的挂载。
通过window.__POWERED_BY_QIANKUN__,可以判断是否是被嵌入在主应用中运行。
props.onGlobalStateChange((state, prevState) => { store.commit('user/SET_TOKEN', prevState.token) }, true);第二个参数是必须的,用于从主应用中获取到token。

2、router改造

若使用history模式,mode: 'history',需要增加base: '/sub-app'
router/index.js

const createRouter = () =>{
    // 微应用运用的路由是只读的,需要先进行全量的定义
    let actualRoutes = window.__POWERED_BY_QIANKUN__ ?constantRoutes.concat(asyncRoutes) : constantRoutes;
    let prefix = "/sub-app";
    //  若需要
    if(window.__POWERED_BY_QIANKUN__){
        actualRoutes.forEach(item => {
            item.path = prefix + item.path;
            item.redirect = prefix + item.redirect;
        })
    }

    return new Router({
        mode: 'hash',
        routes: actualRoutes
    });
}
const router = createRouter();
export default router

3、打包地址改造

vue.config.js

const { name } =require(`./package`);
...
 configureWebpack: {
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    },
    output: {
      // 把子应用打包成 umd 库格式
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`,
    }
  },

新增public-path.js,并引入到main.js
src/public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

4、请求改造

1)baseUrl改造(方便之后的代理)

let baseURL = window.__POWERED_BY_QIANKUN__ ? '/turing-moss' + process.env.VUE_APP_BASE_API : process.env.VUE_APP_BASE_API
// 创建axios实例
const service = axios.create({
  baseURL: baseURL, // api的base_url
  timeout: 300000 // 请求超时时间
})

  1. 鉴权请求头改造,适配改造后的鉴权方案
function createHeader(token, isformdata) {
  var contentType = isformdata ? 'multipart/form-data' : 'application/json'
  let headers = {
    'Content-Type': contentType,
    'time': new Date().getTime(),
    'salt': rdNum(6),
  }
  if (window.__POWERED_BY_QIANKUN__) {
    headers.Authorization = store.getters.token
    headers.useToken = true
  }
  return headers;
}

后端代码改造

主要是鉴权改造。
鉴权顺序:
用户在主应用登录➡主应用后端生成令牌传递给前端➡前端微应用共享该令牌,在请求微应用后端时携带➡微应用后端拿到令牌后,请求主应用接口,判断是否合法,并获取用户信息

原先的鉴权方案都是CAS鉴权。

主应用后端

1、生成token,并在请求用户信息接口中返回给前端(使用OAuth2)

  UserDetails userDetails = domainUserDetailsService.createSpringSecurityUser(userInfoDTO);
                Authentication userAuth = new 
PreAuthenticatedAuthenticationToken(userDetails,userDetails.getPassword(),userDetails.getAuthorities());
String token = tokenProvider.createToken(userAuth,true);

2、提供内部鉴权接口

   @GetMapping("/inner/tokenValid")
    AuthResp validToken(String token){
        System.out.println(token);
        if(tokenProvider.validateToken(token)){
            Authentication authentication = tokenProvider.getAuthentication(token);
            String accountName = authentication.getName();
            System.out.println(authentication);
            return AuthResp.builder().accountName(accountName)
                    .isAuth(true).build();
        }else{
            //  权限校验失败
            return AuthResp.builder().accountName("")
                    .isAuth(false).build();
        }

微应用后端

定义优先级高于CasFilter的自定义过滤器进行鉴权

@Order(0)
@Slf4j
@Component
public class TokenAuthorFilter implements Filter {


    @Resource
    UserService userService;
    @Resource
    AuthClient authClient



    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
        String accessToken = ((RequestFacade) servletRequest).getHeader("Authorization");
        HttpSession session = ((RequestFacade) servletRequest).getSession();
        if(((RequestFacade) servletRequest).getHeader("useToken")!=null && ((RequestFacade) servletRequest).getHeader("useToken").equals("true")){
            log.info("进入微前端鉴权逻辑");
            if(StringUtils.isNotBlank(accessToken) && session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) == null) {
                // 能够获取到token
               AuthResp authResp = authClient.authToken(accessToken);
                if (authResp.getIsAuth()) {
                    String accountName = authResp.getAccountName();
                    Assertion assertion = new AssertionImpl(accountName);
                    session.setAttribute(AbstractCasFilter.CONST_CAS_ASSERTION, assertion);
                    // assertion 非空:从assertion中获取数据
                    log.debug("从permission获取当前用户信息,用户名称 = {}", accountName);
                    session.setAttribute(Constants.SESSION_KEY,userService.getUserFullyByAccountName(accountName));
                }else{
                    throw new RuntimeException("qiankun主应用鉴权失败!");
                }

            }
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

}

此处逻辑:识别到携带useToken头的请求(此处头的名称支持自定义),请求主应用的后台,进行token的合法性校验,通过则执行后续逻辑,不通过抛出异常,前端跳转主应用的登录页。

部署

将main.js中微应用的地址改为//${IP}/subApp的形式,使用nginx进行部署。
配置(配置到server 80或 443下(https)),将subAppIp设置为微服务的地址。


nginx配置参考

本地进行联调时,需要将proxyTable代理至微服务地址。
至此,接入完成。

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

推荐阅读更多精彩内容