flexiwan项目踩坑实践

简介: flexiManage是以色列一家初创公司flexiWAN开源的基于SD-WAN平台的应用层的框架,包括flexiManage服务端框架以及硬件侧的flexiAgent框架,然而其并没有开源前端框架,为了验证其SD-WAN方案的可行性,需要快速搭建一个前端应用

前端 | flexiwan项目踩坑实践.png

项目背景

flexiManage是以色列一家初创公司flexiWAN开源的基于SD-WAN平台的应用层的框架,包括flexiManage服务端框架以及硬件侧的flexiAgent框架,然而其并没有开源前端框架,为了验证其SD-WAN方案的可行性,需要快速搭建一个前端应用

项目选型

由于探索性质,项目要求能快速搭建,因而放弃了Ant Design Pro以及Vue Element Admin,而是选用了阿里飞冰(ice)的Fusion Design Lite来进行前端页面的搭建

目录结构

  • src

    • components

      • CustomIcon
      • WrapperPage
    • layouts

      • BasicLayout

        • components
        • index.jsx
        • menuConfig.js
    • pages

      • Home
      • Inventory

        • Devices

          • DeviceInfo
        • Tunnels
      • NotFound
    • utils
  • build.json (webpack相关工程配置在这里编写)

踩坑案例

createContext传递上下文

[bug描述] 在DeviceInfo中设立了四个切换的tab,需要在切换时将子组件的数据传递给父组件,由于函数式编程this指向了undefined,其上下文信息需要通过自己创造的上下文进行传递,使用createContext创建唯一一个上下文device对象用于进行更新的接口发送

[bug分析] this上下文信息缺失,需要自定义Context

[解决方案] 利用useState处理父级数据,利用其中的类似setState的函数传递给子组件,使子组件向父组件传递数据

jsx运行时和编译时问题

[bug描述] 父组件异步请求获取数据后传递给子组件时,子组件数据获取后在config处编译时拿到数据,在react的dom渲染运行时无法监听到数据

[bug分析] jsx的编译时与运行时对应的react dom渲染的时点不同,在react dom之前是拿不到动态数据的

[解决方案] 在子组件中再请求一遍接口或通过效应器useEffect将渲染运行时切到一致,类似vue的$nextTick

源码解析

阿里飞冰源码解析

阿里飞冰是淘系的一套主面向后端或其他开发人员的前端全链路的一套全流程自动化构建前端页面框架,其包含了脚手架(不太好用)、low code界面化操作、vscode插件化操作、微前端全流程低配置化服务,可简化前端工程操作

miniapp-render

function miniappRenderer(
  { appConfig, createBaseApp, createHistory, staticConfig, pageProps, emitLifeCycles, ErrorBoundary },
  { mount, unmount, createElement, Component }
) {
  const history = createHistory({ routes: staticConfig.routes });

  const { runtime } = createBaseApp(appConfig);
  const AppProvider = runtime?.composeAppProvider?.();

  const { app = {} } = appConfig;
  const { rootId, ErrorBoundaryFallback, onErrorBoundaryHander, errorBoundary } = app;

  emitLifeCycles();
  class App extends Component {
    public render() {
      const { Page, ...otherProps } = this.props;
      const PageComponent = createElement(Page, {
        ...otherProps
      });

      let appInstance = PageComponent;

      if (AppProvider) {
        appInstance = createElement(AppProvider, null, appInstance);
      }
      if (errorBoundary) {
        appInstance = createElement(ErrorBoundary, {
          Fallback: ErrorBoundaryFallback,
          onError: onErrorBoundaryHander
        }, appInstance);
      }
      return appInstance;
    }
  }

  (window as any).__pagesRenderInfo = staticConfig.routes.map(({ source, component }: any) => {
    return {
      path: source,
      render() {
        const PageComponent = component()();
        const rootEl = document.createElement('div');
        rootEl.setAttribute('id', rootId);
        document.body.appendChild(rootEl);
        const appInstance = mount(createElement(App, {
          history,
          location: history.location,
          ...pageProps,
          source,
          Page: PageComponent
        }), rootEl);

        (document as any).__unmount = unmount(appInstance, rootEl);
      },
      setDocument(value) {
        // eslint-disable-next-line no-global-assign
        document = value;
      }
    };
  });
};

export default miniappRenderer;

本质是一个函数,在window上挂载了一个根APP应用,应用中引入了对应的运行时、路由、属性等信息

react-app-renderer

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import * as ReactDOMServer from 'react-dom/server';
import { createNavigation } from 'create-app-container';
import { createUseRouter } from 'create-use-router';
import * as queryString from 'query-string';

const { createElement, useEffect, useState, Fragment, useLayoutEffect } = React;

const useRouter = createUseRouter({ useState, useLayoutEffect });
const AppNavigation = createNavigation({ createElement, useEffect, useState, Fragment });

// ssr方式
export function reactAppRendererWithSSR(context, options) {
  ...
}

let __initialData__;

// 设置初始值
export function setInitialData(initialData) {
  ...
}

// 获取初始值
export function getInitialData() {
  ...
}

// react的渲染

// 1. 返回的渲染函数
function _renderApp(context, options) {
  const { appConfig, staticConfig = {}, buildConfig = {}, createBaseApp, emitLifeCycles } = options;
  const { runtime, history, appConfig: modifiedAppConfig } = createBaseApp(appConfig, buildConfig, context);

  options.appConfig = modifiedAppConfig;

  // Emit app launch cycle
  emitLifeCycles();

  const isMobile = Object.keys(staticConfig).length;
  if (isMobile) {
    return _renderMobile({ runtime, history }, options);
  } else {
    return _render({ runtime }, options);
  }
}

// 调用react渲染
export async function reactAppRenderer(options) {
  const { appConfig, setAppConfig, loadStaticModules } = options || {};

  setAppConfig(appConfig);

  loadStaticModules(appConfig);

  if (process.env.__IS_SERVER__) return;

  let initialData = {};
  let pageInitialProps = {};

  const { href, origin, pathname, search } = window.location;
  const path = href.replace(origin, '');
  const query = queryString.parse(search);
  const ssrError = (window as any).__ICE_SSR_ERROR__;
  const initialContext = {
    pathname,
    path,
    query,
    ssrError
  };

  // ssr enabled and the server has returned data
  if ((window as any).__ICE_APP_DATA__) {
    initialData = (window as any).__ICE_APP_DATA__;
    pageInitialProps = (window as any).__ICE_PAGE_PROPS__;
  } else {
    // ssr not enabled, or SSR is enabled but the server does not return data
    // eslint-disable-next-line
    if (appConfig.app && appConfig.app.getInitialData) {
      initialData = await appConfig.app.getInitialData(initialContext);
    }
  }

  // set InitialData, can get the return value through getInitialData method
  setInitialData(initialData);

  const context = { initialData, pageInitialProps, initialContext };
  _renderApp(context, options);
}


// 渲染函数
function _render({ runtime }, options) {
  const { ErrorBoundary, appConfig = {} } = options;
  const { ErrorBoundaryFallback, onErrorBoundaryHander, errorBoundary } = appConfig.app;
  const AppProvider = runtime?.composeAppProvider?.();
  const AppRouter = runtime?.getAppRouter?.();
  const { rootId, mountNode } = appConfig.app;

  function App() {
    const appRouter = <AppRouter />;
    const rootApp = AppProvider ? <AppProvider>{appRouter}</AppProvider> : appRouter;
    if (errorBoundary) {
      return (
        <ErrorBoundary Fallback={ErrorBoundaryFallback} onError={onErrorBoundaryHander}>
          {rootApp}
        </ErrorBoundary>
      );
    }
    return rootApp;
  }

  if (process.env.__IS_SERVER__) {
    return ReactDOMServer.renderToString(<App />);
  }

  const appMountNode = _getAppMountNode(mountNode, rootId);
  if (runtime?.modifyDOMRender) {
    return runtime?.modifyDOMRender?.({ App, appMountNode });
  }

  return ReactDOM[(window as any).__ICE_SSR_ENABLED__ ? 'hydrate' : 'render'](<App />, appMountNode);
}

// 渲染手机端
function _renderMobile({ runtime, history }, options) {
  ...
}

// 匹配初始组件
function _matchInitialComponent(fullpath, routes) {
  ...
}

// 挂载节点
function _getAppMountNode(mountNode, rootId) {
  ...
}

调用react的渲染机制,将ice嵌入react中

create-use-router

import * as pathToRegexpModule from 'path-to-regexp';

const cache = {};

let _initialized = false;
let _routerConfig = null;
const router = {
  history: null,
  handles: [],
  errorHandler() { },
  addHandle(handle) {
    return router.handles.push(handle);
  },
  removeHandle(handleId) {
    router.handles[handleId - 1] = null;
  },
  triggerHandles(component) {
    router.handles.forEach((handle) => {
      if (handle) {
        handle(component);
      }
    });
  },
  match(fullpath) {
    if (fullpath == null) return;

    (router as any).fullpath = fullpath;
    const parent = (router as any).root;
    // @ts-ignore
    const matched = matchRoute(
      parent,
      parent.path,
      fullpath
    );

    // eslint-disable-next-line
    function next(parent) {
      const current = matched.next();

      if (current.done) {
        const error = new Error(`No match for ${fullpath}`);
        // @ts-ignore
        return router.errorHandler(error, router.history.location);
      }

      let component = current.$.route.component;
      if (typeof component === 'function') {
        component = component(current.$.params, router.history.location);
      }

      if (component instanceof Promise) {
        // Lazy loading component by import('./Foo')
        // eslint-disable-next-line
        return component.then((component) => {
          // Check current fullpath avoid router has changed before lazy loading complete
          // @ts-ignore
          if (fullpath === router.fullpath) {
            router.triggerHandles(component);
          }
        });
      } else if (component != null) {
        router.triggerHandles(component);
        return component;
      } else {
        return next(parent);
      }
    }

    return next(parent);
  }
};

// 参数解析
function decodeParam(val) {
  try {
    return decodeURIComponent(val);
  } catch (err) {
    return val;
  }
}

// 匹配地址
function matchLocation({ pathname }) {
  router.match(pathname);
}

// 匹配路径
function matchPath(route, pathname, parentParams) {
  ... 正则
}

// 匹配路由 generator函数
function matchRoute(route, baseUrl, pathname, parentParams) {
  let matched;
  let childMatches;
  let childIndex = 0;

  return {
    next() {
      if (!matched) {
        matched = matchPath(route, pathname, parentParams);

        if (matched) {
          return {
            done: false,
            $: {
              route,
              baseUrl,
              path: matched.path,
              params: matched.params,
            },
          };
        }
      }

      if (matched && route.routes) {
        while (childIndex < route.routes.length) {
          if (!childMatches) {
            const childRoute = route.routes[childIndex];
            childRoute.parent = route;

            childMatches = matchRoute(
              childRoute,
              baseUrl + matched.path,
              pathname.substr(matched.path.length),
              matched.params,
            );
          }

          const childMatch = childMatches.next();
          if (!childMatch.done) {
            return {
              done: false,
              $: childMatch.$,
            };
          }

          childMatches = null;
          childIndex++;
        }
      }

      return { done: true };
    },
  };
}

// 获取组件内容
function getInitialComponent(routerConfig) {
  ...
}

// 创建使用路由
export function createUseRouter(api) {
  const { useState, useLayoutEffect } = api;

  function useRouter(routerConfig) {
    const [component, setComponent] = useState(getInitialComponent(routerConfig));

    useLayoutEffect(() => {
      if (_initialized) throw new Error('Error: useRouter can only be called once.');
      _initialized = true;
      const history = _routerConfig.history;
      const routes = _routerConfig.routes;

      // @ts-ignore
      router.root = Array.isArray(routes) ? { routes } : routes;

      // eslint-disable-next-line
      const handleId = router.addHandle((component) => {
        setComponent(component);
      });

      // Init path match
      if (!_routerConfig.InitialComponent) {
        matchLocation(history.location);
      }

      const unlisten = history.listen((location) => {
        matchLocation(location);
      });

      return () => {
        router.removeHandle(handleId);
        unlisten();
      };
    }, []);

    return { component };
  }

  return useRouter;
}

// 创建包裹路由
export function createWithRouter(api) {
  const { createElement } = api;

  function withRouter(Component) {
    function Wrapper(props) {
      const history = router.history;
      return createElement(Component, { ...props, history, location: history.location });
    };

    Wrapper.displayName = `withRouter(${  Component.displayName || Component.name  })`;
    Wrapper.WrappedComponent = Component;
    return Wrapper;
  }

  return withRouter;
}

飞冰路由机制,可获取对应参数等,主要是generator函数实现,正则匹配

总结

飞冰脚手架的构建方式主要是通过一个核心的miniRender来返回调用react的渲染机制,配合广大的插件机制,只留下一个简单的核心,其他都以插件化的形式进行扩展,微内核广外延的架构还是很值得参考的。

相关文章
|
2月前
|
前端开发 Java
表白墙/留言墙 —— 初级SpringBoot项目,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
文章通过一个表白墙/留言墙的初级SpringBoot项目实例,详细讲解了如何进行前后端开发,包括定义前后端交互接口、创建SpringBoot项目、编写前端页面、后端代码逻辑及实体类封装的全过程。
80 3
表白墙/留言墙 —— 初级SpringBoot项目,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
58 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
6月前
|
JSON Java 程序员
马程序员2024最新SpringCloud微服务开发与实战 个人学习心得、踩坑、与bug记录Day1最快 最全(2)
马程序员2024最新SpringCloud微服务开发与实战 个人学习心得、踩坑、与bug记录Day1最快 最全(2)
59 3
|
6月前
|
SQL Java 程序员
马程序员2024最新SpringCloud微服务开发与实战 个人学习心得、踩坑、与bug记录Day1最快 最全(1)
马程序员2024最新SpringCloud微服务开发与实战 个人学习心得、踩坑、与bug记录Day1最快 最全(1)
224 1
|
7月前
|
消息中间件 设计模式 架构师
开发同学的“做事情”&“想事情”&“谈事情”
作为一名后端偏业务向的一线开发,作者抛开技术栈和方案经验等这些具体的内容,从做事情、想事情、谈事情三个方面总结了自己的一些感悟。
|
缓存 监控 Java
从零到一构建完整知识体系,阿里最新SpringBoot原理最佳实践真香
Spring Boot不用多说,是咱们Java程序员必须熟练掌握的基本技能。工作上它让配置、代码编写、部署和监控都更简单,面试时互联网企业招聘对于Spring Boot这个系统开发的首选框架也是考察的比较严苛,如果你不是刚入行,只是停留在会用的阶段,那是远远不够的。 虽然Spring Boot易上手,但很多小伙伴也是时不时会跟我反映,Spring Boot技术体系太庞杂了,包含了太多的技术组件,不知道到底该如何高效学习,建立起全面且完整的Spring Boot技术体系和实践技巧,这个时候站在巨人的肩膀上学习就变得非常有必要了,汲取大佬们的学习经验,避免工作面试踩坑,轻松构建Spring Bo
|
Java 应用服务中间件 Nacos
Java后端项目排错经验分享
Java后端项目排错经验分享
248 0
|
监控 Java 测试技术
JAVA项目开发从0到1的心路历程
JAVA项目开发是一个复杂而有挑战性的过程,它需要经历从项目规划到需求分析、设计、编码、测试、部署和维护等多个阶段。下面我将分享一下从0到1的心路历程。
449 0
|
运维 Java 关系型数据库
spug上线服务踩坑记
spug是一款优秀的自动化运维平台, 这让我们想自动化又向前迈了一步.
560 0
|
存储 NoSQL 中间件
flexiwan项目踩坑实践
flexiManage是以色列一家初创公司flexiWAN开源的基于SD-WAN平台的应用层的框架,包括flexiManage服务端框架,基于此服务端框架进行了一些借鉴和改进
357 0