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的渲染机制,配合广大的插件机制,只留下一个简单的核心,其他都以插件化的形式进行扩展,微内核广外延的架构还是很值得参考的。

相关文章
|
3天前
|
前端开发 Java 数据库连接
如何顺利完成毕业项目看完这篇文章有你想要的!
如何顺利完成毕业项目看完这篇文章有你想要的!
|
3天前
|
消息中间件 设计模式 架构师
开发同学的“做事情”&“想事情”&“谈事情”
作为一名后端偏业务向的一线开发,作者抛开技术栈和方案经验等这些具体的内容,从做事情、想事情、谈事情三个方面总结了自己的一些感悟。
459 2
|
3天前
|
存储 测试技术 开发工具
软件测试/测试开发|GitHub怎么用,这篇文章告诉你
软件测试/测试开发|GitHub怎么用,这篇文章告诉你
60 0
|
7月前
|
缓存 监控 Java
从零到一构建完整知识体系,阿里最新SpringBoot原理最佳实践真香
Spring Boot不用多说,是咱们Java程序员必须熟练掌握的基本技能。工作上它让配置、代码编写、部署和监控都更简单,面试时互联网企业招聘对于Spring Boot这个系统开发的首选框架也是考察的比较严苛,如果你不是刚入行,只是停留在会用的阶段,那是远远不够的。 虽然Spring Boot易上手,但很多小伙伴也是时不时会跟我反映,Spring Boot技术体系太庞杂了,包含了太多的技术组件,不知道到底该如何高效学习,建立起全面且完整的Spring Boot技术体系和实践技巧,这个时候站在巨人的肩膀上学习就变得非常有必要了,汲取大佬们的学习经验,避免工作面试踩坑,轻松构建Spring Bo
|
10月前
|
Java 应用服务中间件 Nacos
Java后端项目排错经验分享
Java后端项目排错经验分享
180 0
|
10月前
gtiee教程(三板斧)-------好东西我们一起来学习
gtiee教程(三板斧)-------好东西我们一起来学习
|
10月前
|
运维 Java 关系型数据库
spug上线服务踩坑记
spug是一款优秀的自动化运维平台, 这让我们想自动化又向前迈了一步.
353 0
|
存储 NoSQL 中间件
flexiwan项目踩坑实践
flexiManage是以色列一家初创公司flexiWAN开源的基于SD-WAN平台的应用层的框架,包括flexiManage服务端框架,基于此服务端框架进行了一些借鉴和改进
289 0
|
移动开发 资源调度 前端开发
小白如何从项目入手学习前端
前言 已有基础:虽然说是小白,但是本人曾在大一通过freecodecamp平台学习过html5、css中的标签、样式等知识,也曾经用js写过一些简单的算法题,并了解过ES6(不过因为不常用,相当于只记得名字了。
107 0
最近的踩坑分享 | 技术文档和需求拆解
最近的踩坑分享 | 技术文档和需求拆解
最近的踩坑分享 | 技术文档和需求拆解