image

项目背景

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


维李设论
1.1k 声望4k 粉丝

专注大前端领域发展