基于 React 搭建的 安卓,Ios,Web 三端架构(附上 IceE 三端框架)

2 篇文章 0 订阅

前言

本节介绍基于该架构的一个 CMS 项目,如果你没有兴趣了解,可以直接跳到下一节
本栏目的文章基于 IceEmblem 架构设计
Github:https://github.com/IceEmblem
演示地址:http://www.iceemblem.cn/
演示账号/密码:admini/123456 (是admini,不是admin)

IceEmblem 是跨越了三端的 CMS 产品,可视化编辑,通过组件堆积搭建页面,组件数据配置极为灵活,可发布各种类型的文章,如下一些示例图片
如下是通过 CMS 生成的 Web 端和 App 端页面

如果你觉得这个页面有点丑,那么你可以随意编辑这个页面
在这里插入图片描述
当然,你也可以编辑 App 页面
在这里插入图片描述

IceEmblem 的灵活程度超乎你以为的,看到这里有的同学就有问了,这么 perfect 的项目到底在哪里,好吧,项目在这里,各位看官行行好,我已经好多天没吃到 Star 了,有没有人给我个 Star >_<|||
Github:https://github.com/IceEmblem

IceEmblem 的架构图
在这里插入图片描述
好了介绍完毕,如下开始介绍 IceE 框架

三端框架 IceE

github 地址:https://github.com/IceEmblem/IceE
github 上有开始使用教程,这里就不介绍了如果安装了

框架架构(目前阶段,随时更新)

可能 IceEmblem 的架构图比较复杂,反正你们也不看,所以这里还是介绍 IceE框架 架构吧
在这里插入图片描述
如上架构分为 3 个区域,WebApp,RNApp,Common,顾名思义
WebApp:Web 端的代码
RNApp:ReactNative 端的代码
Common:公共代码
这里的区域分别对应项目的三个文件夹
在这里插入图片描述

每个区域下面都有很多模块(如 RNApp 包含 3 个模块 RNStart,RNCommon …),所以我们的代码目录如下:
在这里插入图片描述
注:RNTest 是一个示例模块,我没有再架构图中画出来

区域依赖关系

WebApp 和 RNApp 均依赖于 Common,而 Common 不能有依赖于这两个区域,可以说,删除 WebApp 和 RNApp,Common 也可以正常通过编译

Common 区域下的代码,Web 端,RN 端均可使用

模块系统

上面所提供的模块不同于我们 ES6 中所说的模块,IceEmblem 所说的模块相当于一个 npm 包(C# 的一个项目或者说一个 dll)
IceE 的启动依赖于模块系统,模型系统的设计很简单,如下
在这里插入图片描述
如下,是一个 Test 模块的声明

import {BaseModule, PageProvider, ModuleFactory, Page} from 'ice-common'
import CoreModule from 'Core/Module';
import Test from './Test'

export default class Module extends BaseModule
{
    initialize(){
        PageProvider.register(new Page("Test", "/Test", Test));
    }
}

// 向模块工厂组成本模块,本模块依赖于 CoreModule 模块
new ModuleFactory().register(Module, [
    CoreModule
]);

Test 模块的目录
在这里插入图片描述
如何声明一个模块
要声明一个模块,你需要再区域下面新建一个文件夹,再其下并新建一个 package.json 包声明,并运行 yarn install 安装该包,然后新建一个 Module.js 文件,Module.js 的内容参考 Test 模块,如此,便完成一个模块的声明,IceE 会自动运行该模块

模块的运行顺序
每个模块都有三个方法

class BaseModule
{
    constructor(){
    }

    preInitialize(){
    }

    initialize(){
    }

    postInitialize(){
    }
}

export default BaseModule;

分别对应 初始化前 preInitialize,初始化 initialize,初始化后 postInitialize
以 Test 模块示例
Test 模块依赖于 Core 模块,所以执行顺序为,依此类推其他模块

  1. Core.preInitialize
  2. Test.preInitialize
  3. Core.initialize
  4. Test.initialize
  5. Core.postInitialize
  6. Test.postInitialize

异步初始化:preInitialize,initialize,postInitialize 允许返回一个 Promise,如果返回 Promise,则会等待该 Promise 执行完成后才进入下一步

关于模块依赖关系
Core模块是整个项目的核心,所有模块都直接或间接依赖于该模块

IceE 启动介绍

IceE 的启动很简单,如下代码

import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import { Provider } from 'react-redux'

// 导入当前模块
import './Module'

import {ModuleFactory} from 'ice-common'
import {IEStore} from 'ice-common'
import {PageProvider} from 'ice-common'

// 部件
import Error from './Parts/Error';
import Loading from './Parts/Loading'

import 'css/bootstrap.min.css'
import 'css/open-iconic-bootstrap.min.css';
import 'css/common.css';

import './index.css';

import {Spin} from 'antd'

let moduleFactory = new ModuleFactory();
// 运行所有模块
moduleFactory.init().then(()=>{
    // 运行完成后

    // 获取 redux store
    let store = IEStore.ieStore;

    // 向页面渲染
    ReactDOM.render(
        <Provider store={store}>
            <BrowserRouter>
                <Suspense fallback={<Spin className="w-100 h-100" size="large"></Spin>}>
                    <Switch>
                        {PageProvider.pages.map(item => (<Route key={item.url} path={item.url} component={item.component} />))}
                    </Switch>
                </Suspense>
                <Error />
                <Loading />
            </BrowserRouter>
        </Provider>,
        document.getElementById('root'));
        
});

你可能会区域内的模块为什么会自动被 import 到系统中,那么,请你网下看

导入模块

上面代码中有一段代码如下

// 导入当前模块
import './Module'

Module.js 代码

import {BaseModule} from 'ice-common'
import {ModuleFactory} from 'ice-common'
import CoreModule from 'Core/Module';
import ModuleList from '../../ModuleList';
import {MiddlewareFactory} from 'ice-common'
import ReduxErrorMiddleware from './Middlewares/ReduxErrorMiddleware'

export default class Module extends BaseModule
{
    initialize(){
        MiddlewareFactory.register(ReduxErrorMiddleware);
    }
}

// ModuleList 为当前区域的所有模块,ModuleList 在 js 编译阶段生成
new ModuleFactory().register(Module, [...ModuleList, CoreModule]);

原因就是:Start 模块自动导入了当前区域的所有模块

ice-common 包提供的其他功能

ice-common 包位于 Common 区域下,它是构建整个框架的核心,

AntIcons
AntIcons 是一个图标的封装,如下:

class AntIcons {
    icons : Map<string, React.Component>;
    getIcon : (name: IconNameType, style: any) => React.Component;
}

icons 是 AntIcons 的所有图标
getIcon 获取图标

如果要扩展图标,你需要扩展 IconNameType 类型,然后修改 RNApp 区域的 RNCommon 的 AntIcons,然后在修改 WebApp 区域的 Common 的 AntIcons,修改很简单,看源码即可

IocContainer
IocContainer 并不能算做依赖注入容器,更像是一个键值对注册工具,其使用如下:

import React from 'react'
import {IocContainer} from 'ice-common';

// 要注册的类型
export class ICommonStyleConfigComponent extends BaseConfig{
}
// iocKey 是必须的
ICommonStyleConfigComponent.iocKey = Symbol();

export default class CommonStyleConfigComponent extends ICommonStyleConfigComponent {
    render() {
        return <></>
    }
}

// 向 IocContainer 注册 ICommonStyleConfigComponent 单例实例,其实例为 CommonStyleConfigComponent
IocContainer.registerSingleIntances(ICommonStyleConfigComponent, CommonStyleConfigComponent);

// 获取 ICommonStyleConfigComponent 类型的实例
IocContainer.getService(ICommonStyleConfigComponent);

PageProvider
页面提供者提供了页面的注册,如下,我们一般在模块初始化时向其注册页面

import {BaseModule, PageProvider, ModuleFactory, Page} from 'ice-common'
import CoreModule from 'Core/Module';
import Test from './Test'

export default class Module extends BaseModule
{
    initialize(){
        // 注册一个页面名为 Test,其 url 为 /Test,其使用的组件为 Test
        // 当访问 /Test 时,会显示 Test 组件
        PageProvider.register(new Page("Test", "/Test", Test));
    }
}

// 向模块工厂组成本模块,本模块依赖于 CoreModule 模块
new ModuleFactory().register(Module, [
    CoreModule
]);

Theme 主题
IceE 提供了动态修改主题,并提供了当前主题的 10 中颜色,默认主题为 明青色,如果想修改默认主题,修改 Theme 即可

主题特定平台的代码在 WebApp 区域的 Common 模块,RNApp 区域的 RNCommon 模块

IETool
IETool 是一个工具类,其封装了一些方法

BaseIERedux
BaseIERedux 是对 Redux 的一个封装,如下是 BaseSetting 模块的示例:

BaseSetting 的 IERedux.js

import {BaseIERedux} from 'ice-common'

class Redux extends BaseIERedux
{
    getStateType(): string {
        return "Setting";
    }
}

export default new Redux()

BaseSetting 的 Module.js

import {BaseModule} from 'ice-common'
import {ModuleFactory} from 'ice-common'
import {IEStore} from 'ice-common'
import {getSiteSettingsFetch} from './IEReduxs/Actions'
import Redux from './IEReduxs/IERedux'
import {reducer} from './IEReduxs/Reducer'
import RootRedux from 'Core/IEReduxs/RootRedux'
import CoreModule from 'Core/Module'

export default class Module extends BaseModule
{
    initialize(){
        // 设置当前模块的 Redux 所使用的 reducer
        Redux.setReducer(reducer);
        // 将当前 Redux 注册到 RootRedux 下
        RootRedux.register(Redux);
    }

    postInitialize(){
        IEStore.ieStore.dispatch(getSiteSettingsFetch());
    }
}

new ModuleFactory().register(Module, [
    CoreModule
]);

将当前 Redux 注册到 RootRedux 下,实际会生成这样一个 state 结构

state = {
    // 这里是根 state
    Setting: {
        // 这里是 BaseSetting 模块的 state

        // 这里的 siteSettings 只是一个假设数据
        siteSettings: {...}
        ...
    }
}

使用 Redux

import React from 'react';
import IERedux from 'BaseSetting/IEReduxs/IERedux';

import './index.css'

class SiteSetting extends React.Component {
    render() {
        return (<></>);
    }
}

const mapStateToProps = (state, ownProps) => { // ownProps为当前组件的props
    return {
        siteSettings: state.siteSettings,
    }
}

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
    }
}

// 使用 BaseSetting 的 IERedux
const Container = IERedux.connect(
    mapStateToProps, // 关于state
    mapDispatchToProps
)(SiteSetting)

export default Container;

封装的好处就是我们不用从根 state 下面获取,而直接从当前模块获取,还有一个看不见的好处,我们不用关心其他模块的 state

IEStore

class IEStore  {
    ieStore = undefined;

    createIEStore(reducer) {
        ...
    }
}

export default new IEStore();

IEStore 是对 redux store 的封装,我们不用关心其 createIEStore 方法,框架初始化时会调用,我们可以通过 IEStore.ieStore 获取当前的 store

MiddlewareFactory
MiddlewareFactory 是对 Redux 中间件的封装,在模块 initialize 方法中,我们可以向 MiddlewareFactory 注册中间件

Core 模块提供的功能

ApiScopeAuthorityManager
ApiScopeAuthorityManager(Api域权限管理器),这是针对 Api 访问的权限检查,其设计如下:
在这里插入图片描述
UserScopeAccessAuthority 是当前用户可以访问的 Api 域
AccessScope 是 Api 域

其源码很简单

import AccessScope, { ApiScopeNodeType } from "./AccessScope";
import UserScopeAccessAuthority from "./UserScopeAccessAuthority";

export default class ApiScopeAuthorityManager
{
    userScopeAccessAuthorities: Array<UserScopeAccessAuthority>;

    // 实例化 ApiScopeAuthorityManager
    // userScopeAccessAuthorities 是用户可以访问的 Api 域集合
    constructor(userScopeAccessAuthorities:Array<UserScopeAccessAuthority>) {
        this.userScopeAccessAuthorities = userScopeAccessAuthorities;
    }

    // 检查当前用户是否可以访问 accessScope 这个 Api域
    isAllowAccessScope(accessScope: AccessScope)
    {
        for(let item in this.userScopeAccessAuthorities)
        {
            if(accessScope.isAllowAccess(this.userScopeAccessAuthorities[item])){
                return true;
            }
        }

        return false;
    }
}

IEToken
IEToken 封装了 IEToken 的存取,在 Web 会存在 Cookie 中,RN 则会存在 缓存中

Core 模块 > IEReduxs > Actions > createIEThunkAction
createIEThunkAction 是对 redux ThunkAction 的封装,由于每个系统的后端返回的数据格式均不一致,所以这里不进行封装,在使用前 createIEThunkAction,你需要对其进行修改,其源码并不难

如果不使用 createIEThunkAction ,你将无法使用框架提供的错误处理方式

// 生成 ieThunkAcion,如果请求成功
// url: 请求的 url
// postData: 发送的数据
// actionType: 数据接收成功后要分发的 action 类型
// method: fetch 方法类型,默认 post
// isPackage: 结果是否有包装有,一般后端都会包装 post 请求结果(如后端返回{ success: true, message: "", result: "正真的结果数据" })
export function createIEThunkAction(url:string, postData:any, actionType:string, method: string = 'post', isPackage: boolean = true) {
  return async function (dispatch:any) {
    let curFecthSign = fecthSign++;
    let requestAction = request(postData);
    requestAction.fecthSign = curFecthSign;
    dispatch(requestAction);

    // 生成 fetch 请求数据结果
    let token = await IEToken.getToken();
    let headers : any = {
      'Content-Type': 'application/json'
    }

    if(token && token != ""){
      headers.Authorization = "Bearer " + token;
    }

    return await fetch(Weburl.handleWeburl(url), {
      method: method,
      headers: headers,
      body: postData && JSON.stringify(postData)
    }).then(
      response => {
        // 再这里处理 html 异步请求结果,如 404 等问题
        if (response.status >= 200 && response.status < 300) {
          return response.json();
        }

        if(response.status == 400 || response.status == 401 || response.status == 403 || response.status == 500){
          return response.json();
        }

        const error = new Error(response.statusText);

        throw error;
      }
    ).then(
      data => {
        // 在这里处理后端的结果,如 后端返回 { success: false, message: "文章发表以达到上限", result: null }
        // 如果后端处理通过,你应该将数据返回,如 return data.result;
        // 否则,你应该抛出异常 throw new Error(data.message);
        // 后续的处理流程由框架处理
        if(!isPackage){
          return data;
        }

        if (data.success == true){
          return data.result;
        }

        if(data && data.error.validationErrors && data.error.validationErrors.length > 0){
          throw new Error(data.error.validationErrors[0].message);
        }
        
        throw new Error(data.error.message);
      }
    ).catch(
      errorData => {
        let errorAction = error(errorData.message);
        errorAction.fecthSign = curFecthSign;
        dispatch(errorAction);
        return Promise.reject(errorData.message);
      }
    ).then(
      value => {
        let receiveAction = receivePack(actionType, value);
        receiveAction.fecthSign = curFecthSign;
        dispatch(receiveAction);
        return value;
      }
    )
  }
}

IEReduxFetch
IEReduxFetch 封装了 fetch,其每次请求时会触发 redux 的动作,使用 IEReduxFetch 之前,你需要更改 Core 模块 > IEReduxs > Actions > createIEThunkAction 方法

RNInfrastructure 模块提供的功能

RNInfrastructure 的目的是为了封装对设备的调用,模块提供了几个封装
Camera:摄像头调用
IEWebView:WebView 高度自适应封装
Device:当前设备的信息

好吧,这篇文章就到这里,其他模块没什么好介绍的,后续如果有更新请留意如下教程地址,bye star

本系列教程文章地址https://blog.csdn.net/dabusidede/category_10348509.html

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:数字20 设计师:CSDN官方博客 返回首页

打赏作者

dabusidede

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值