Project background
FlexiManage is an open source flexiWAN application layer framework based on SD-WAN platform, including flexiManage server side framework and flexiAgent hardware side framework. However, flexiManage does not have an open source front end framework. In order to verify the feasibility of its SD-WAN solution, Need to build a front-end application quickly
Project selection
Due to the nature of exploration, the project required rapid construction, so Ant Design Pro and Vue Element Admin were abandoned, and Ali Ice’s Fusion Design Lite was selected to build the front page
The directory structure
- src
- components
- CustomIcon
- WrapperPage
- layouts
- BasicLayout
- components
- index.jsx
- menuConfig.js
- BasicLayout
- pages
- Home
- Inventory
- Devices
- DeviceInfo
- Tunnels
- Devices
- NotFound
- utils
- components
- Build. json (WebPack related project configuration is written here)
On the pit case
CreateContext Passes context
[Bug Description] Four switch tabs are set up in DeviceInfo, which need to pass the data of the child component to the parent component during the switch. Because the function programming this points to undefined, its context information needs to be passed through the context created by itself. Use createContext to create a unique context device object to send to the interface for updates
[Bug Analysis] This Context information is missing, you need to customize the Context
[Solution] Use useState to process the parent data, and use the setState-like function in it to pass to the child component, so that the child component passes data to the parent component
JSX runtime and compile-time issues
[Bug Description] When the parent component asynchronously requests for data and sends it to the child component, the child component gets the data from the config file when compiling the data. When the REACT DOM rendering runs, the child component cannot listen for the data
JSX is compiled at a different point in time from the react DOM. Dynamic data is not available before the React DOM
Solution: Request the interface again in a child component or use the effector effect to cut the render runtime to a consistent, similar to VUE’s $nextTick
The source code parsing
Ali fly ice source code analysis
Ali Feibing is Amoy department of a main front end or other developers of the front end of the full link of a full process of automation to build the front end page framework, which includes scaffolding (not very easy to use), low code interface operation, VScode plug-in operation, micro front end of the whole process of low configuration services, can simplify front-end engineering operations
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;Copy the code
It is essentially a function that mounts a root APP on the window, importing the corresponding runtime, routing, properties, and other information in the application
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 }); Apprendererwithssr (context, options) {// Apprendererwithset (context, options) {... } let __initialData__; Export function setInitialData(initialData) {... } export function getInitialData() {... 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); } // set apprentices to export async function reactderer (options) {const {apprenconfig, 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); } // render 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) {... }Copy the code
Use the React rendering mechanism to embed ICE in 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 decoam (val) {try {return decodeURIComponent(val); } catch (err) { return val; Function matchLocation({pathname}) {router.match(pathname); Function matchPath(route, pathName, parentParams) {function matchPath(route, pathName, parentParams) {... 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; }Copy the code
Fly ice routing mechanism, can obtain the corresponding parameters, mainly generator function implementation, regular matching
conclusion
The construction method of flying ice scaffolding is mainly through a core miniRender to return to call react rendering mechanism, with a wide range of plug-in mechanisms, leaving only a simple core, the rest are extended in the form of plug-in, microkernel extensive extension architecture is still worth reference.