如果您是一名移动应用程序开发人员,也许您曾梦想过在线开发的灵活性,以便在飞行中进行布局和逻辑更改,以及在几秒钟内运行假设测试并更快地处理结果的能力?

移动开发人员被教导要相信,应用程序的发布和更新速度直接关系到它们如何快速到达用户手中。App Store的审核时间可能长得令人沮丧。构建一个软件开发工具包(SDK)会更慢,因为你必须把你的需求融入别人的产品开发和发布周期。这可能是一个很好的借口,根本不需要测试假设。

在这篇博文中,我们将对这一主题进行介绍,并带领大家了解Backend-Driven Development的步骤,描述它如何用于解决具体问题,以及它给我们带来了哪些好处。这篇文章的材料来自于关于MovilePay的例子的< data-mce-href="https://medium.com/movile-tech/backend-driven-development-ios-d1c726f2913b" href="https://medium.com/movile-tech/backend-driven-development-ios-d1c726f2913b" target="_blank" rel="noopener">来源。原文作者为Rodrigo Maximo。

什么是后台驱动开发?

后台驱动开发(或称后台驱动开发,或称后台驱动UI,或称服务器驱动UI)是基于服务器响应开发前端应用程序的概念。屏幕和流程根据服务器的响应而改变。

创建移动应用程序的大部分工作通常与构建用户界面有关:将用户与之互动的元素放在屏幕上,以便他能够快速执行一个或另一个动作。其中一些组件被填充了API有效载荷:通常是具有原始类型(整数、布尔、字符串)的JSON或应用程序的业务流程。

实现屏幕的传统方法有一定的缺点,因为应用程序方面可能有很多业务逻辑,代码变得更加难以维护:

  • 需要在许多平台(Android、iOS、Web等)使用相同的代码。这种方法使得维持跨平台的兼容性更加困难,这增加了出现错误和不兼容的机会;
  • 移动应用程序的每一次更新或修改都意味着需要改变代码,这导致应用程序在App Store中的发布时间延长;
  • 在数字方面,A/B测试更加困难。测试概念和收集用户数据以了解重要的产品信息更具挑战性;
  • 由于大部分业务逻辑都在应用程序一侧,因此代码的维护和保养更具挑战性。

面向后台的开发已经到来,可以解决这些问题。

请考虑以下场景(该场景基于示例的 iOS 应用程序,但可轻松转换为任何其他前端项目):

后台驱动开发场景

{
    "pageTitle": "示范性标题",
    "box": []。
        {
            "type":"bigBlue",
            "标题":"Nice box",
            "副标题":"副标题of box"
        },
        { 
            "类型"。"smallRed",
            "标题":"伟大的盒子"
        },"title":"Great box
        {
            "类型"。"rectangleGreen",
            "标题":"难以置信的盒子",
            "副标题":"副标题of box",
            "数字":10
        }
    ]
}

所有用于显示字段以及显示文本和视觉信息的业务逻辑在服务器端被整合和抽象化,从而消除了处理该API的多个接口的需要。然后服务器应用业务逻辑,并使用其结果生成JSON格式的API响应。

在这个例子中,为了在屏幕上显示块("bigBlue"、"smallRed "和 "rectangleGreen"),来自每个字段的额外信息被使用。有趣的是,"box "变量允许后端处理服务器给出的多少个块。

你可能已经猜到在这种情况下如何进行A/B测试?服务器可以选择特定的用户只查看 "bigBlue "区块,而其他人可以看到所有三种类型的区块。此外,你还可以进行A/B测试,改变屏幕上块的显示顺序。

服务器驱动开发的另一个用例是对一个应用程序的界面进行修改。例如,如果我们需要改变应用程序中的标题,只需简单地改变服务器的响应即可。字幕和块的显示顺序也很容易改变。在这种情况下,您不需要在AppStore中发布新的应用程序版本。

在后台驱动开发中对界面进行更改

{
    "pageTitle": "示范性标题",
    "box": []。
        {
            "类型"。"rectangleGreen",
            "标题": "另一个标题",
            "副标题": "另一个副标题",
            "数字"100
        },
        {
            "类型"。"smallRed",
            "标题":"不同的标题"
        }
        {"不同的标题" }
            "类型"。"bigBlue",
            "标题":"Backend Driven",
            "副标题":"开发"
        }"副标题":"开发
    ]
}

三思而后行

在使用后台驱动开发方法时,有几件事需要记住。首先,你需要多大程度的灵活性?

根据经验和研究,我们认为中等程度的灵活性是最佳的。

你不应该从一个极端冲向另一个。大量的自由度会对你的开发回报率产生负面影响。你不可能预见一切,尤其是设备规格或屏幕尺寸是你无法控制的。最后,你最终会出现非常混乱和超负荷的应用端表现逻辑。另外,你也无法预见你的设计团队在未来可能要做的一切。因此,您仍然必须对应用代码进行修改,而且在测试过程中,会发现许多复杂的路线来实现目标。

因此,总之,在绝大多数情况下,您不会需要HTML所具有的灵活性。因此,在开发使用 Backend-Driven Development 理念的屏幕和服务器解决方案之前,我们建议你想清楚所有可能的选择。

一个超级应用程序的创造,Movile 技术案例

你可能对超级应用程序的概念很熟悉,并且听说过微信这样的应用程序。微信是一个在中国开发的移动信息平台和社交网络,并已在全球范围内极受欢迎。

这样一个应用程序旨在将几个经常性的服务收集在一个地方,并为用户提供一个单一的访问点,以解决他们日常生活中的大多数在线查询。它是软件开发的圣杯:把许多功能放在一起,使其看起来像一个,为用户提供复杂问题的简单答案和大问题的解决方案。

过去,MovilePay参与了一个超级应用程序的开发,本文介绍了其改进版本的开发过程。它应该是一个测试和验证中心,团队可以测试新的服务,并在使用中找到共同点。

MovilePay有一个问题,即创建一个可以快速对代码进行修改的应用程序。它需要测试许多服务和服务,并根据显示的信息对用户行为进行研究,测试假设。MovilePay希望能够快速打开和关闭功能。在这样的条件下,不可能使用传统的方法,每一个变化都要发布。考虑到所有这些要点和一个复杂的场景,我们决定采用Backend-Driven Development。

MovilePay创建了一个基于类别的主屏幕来解决这个问题。每个部分都有自己的项目集,称为小工具。各个部分以任何顺序显示任何已经实施的服务的入口,并突出显示个别小工具。

有时只显示一个服务。其他时候有三个,这取决于当前正在测试的内容。MovilePay将选择权留给了服务器,而不是在应用程序中硬编码任何规则。因此,应用程序只知道服务的入口点的定义以及如何呈现每个特定的风格。他们的服务器告诉哪些服务应该被生成,以什么顺序生成。下面是MovilePay应用程序可以显示的一些小部件。

MovilePay应用程序的小部件

MovilePay应用的小部件

MovilePay应用程序的小部件

MovilePay应用程序中的小部件

因此,为了创建一个高度适应的主屏幕,我们必须从后台获得一个包含部分列表的响应,每个列表包含一个小部件列表。下面是一个JSON响应的例子,MovilePay应用程序应该解析并创建一个像下面这样的主屏幕。

解析和创建主屏幕

【/span>
    {
        "title": "Section 1",
        "widgets": []。
            { 
                "identifier": "COLLECTION_WIDGET"
                "content": []。
                    { 
                        "title": "Title A", 
                        "image": "A",
                        "color":"yellow"
                    },
                    { 
                        "title": "Title B",
                        "image": "B",
                        "color": "blue" 
                    },
                    { 
                        "title": "Title C",
                        "image": "C",
                        "color": "red"
                    },
                    { 
                        "title": "Title D",
                        "image": "D",
                        "color": "紫色"
                    },
                    { 
                        "title": "Title E",
                        "image": "E",
                        "color": "green" 
                    }
                ]
            },
            { 
                "identifier": "IMAGES_WIDGET",
                "content": []。
                    { 
                        "image": "Image", 
                        "color": "green" 
                    },
                    { 
                        "image": "Image",
                        "color": "blue"/span>
                    },
                    { 
                        "image": "Image",
                        "color": "orange"/span>
                    }
                ]
            },
            { 
                "identifier": "COLLECTION_WIDGET",
                "content": []。
                    { 
                        "title": "Title E", 
                        "image": "E",
                        "color": "green" 
                    },
                    { 
                        "title": "Title F",
                        "image": "F",
                        "color": "紫色"
                    },
                    { 
                        "title": "Title G",
                        "image": "G",
                        "color": "red"
                    },
                    { 
                        "title": "Title H",
                        "image": "H",
                        "color": "blue" 
                    },
                    { 
                        "title": "Title H",
                        "image": "H",
                        "color": "yellow"
                    }
                ]
            } }
        ]
    },
    { 
        "title": "Section 2",
        "widgets": []。
            { 
                "identifier": "CELLS_WIDGET",
                "content": []。
                    { 
                        "title": "cell 1",
                        "color": "red"
                    },
                    { 
                        "title": "cell 2",
                        "color": "紫色"
                    },
                    { 
                        "title": "cell 3",
                        "color": "yellow"
                    },
                    { 
                        "title": "cell 4",
                        "color": "blue"
                    },
                    { 
                        "title": "cell 5",
                        "color": "dark green"
                    }
                ]
            } }
        ]
    } }
]

我们还将通过一些可能使用后台驱动开发构建的屏幕。

后台驱动开发的主屏幕

灵活的导航

MovilePay开发的超级APP已经实现了其预期的创新目标。MovilePay从假设测试中学到了很多东西,但他们特别擅长的一点是支付处理或各种服务和产品的支付过程。他们有一个支付应用程序,可以为他们提供的任何服务处理付款。管理支付交易的能力是一个重要的优势,因为MovilePay可以通过处理多个交易来降低价格,并获得对消费者行为的宝贵见解。

MovilePay决定开发一个基于Google Pay、Apple Pay或VisaCheckout模式的支付SDK,可以与任何其他应用程序集成。

然而,由于在SDK上工作意味着使用典型开发模式测试的能力非常小,自动化是必要的。

由于MovilePay处理支付,流量的转换是关键。MovilePay不能在任何转换漏斗阶段失去其用户。因此,优化整个业务流程是必要的--从用户注册到添加卡片和支付。

用BDD优化业务流程

MovilePay的应用程序屏幕都不知道下一个屏幕是什么。在前一个屏幕完成其任务后,服务器负责返回下一个应该显示的屏幕。

路由是应用程序可以识别的动作,并通过一个被称为路由器的结构来响应。登录机制包括两个独立的动作分支:一个用于页面标题,另一个用于动作树中遇到的其他元素(如用户名变更或新密码)。路由器通过将它们发送到服务器来处理它们,然后服务器解释它们的动作,并决定哪些解释应该出现在下一个屏幕上。

这种策略上的适度转变允许精简。MovilePay 尝试了许多不同的方式来显示注册表。它可以测试在添加卡片之前或之后显示结账屏幕是否更合适。例如,这是MovilePay能够比其他支付方式提高30%转换率的原因之一。

另一个例子是MovilePay如何使用Backend-Driven Development来解决其问题。我们认为你想知道如何在实践中应用这种方法。让我告诉你这有多简单。

有许多替代方法可以实现相同的目标。我们将向你展示MovilePay在iOS上是如何做到的。如果需要,这个概念可以应用于任何其他的前端平台。

当 MovilePay 实施它时,他们考虑了诸如易于添加额外部件、代码可读性和单一责任等要求。为了使事情变得更简单和更原生,MovilePay决定使用Codable API进行序列化。

Widgets (iOS)

在灵活性方面,MovilePay认为最好的解决方案是将需要用协议进行解析的widgets通用化。MovilePay还设置了一个枚举,以确定解析数据的小部件结构。

protocol Widget: Decodable {}

enum WidgetIdentifier: String, Decodable {
    case banner = "BANNER"/span>
    case collection = " COLLECTION"
    case list = "LIST"/span>

    var metatype: Widget.Type {>
        switchself {
        case .banner:
            return BannerWidget.self
        case .collection: 
            return CollectionWidget.self
        case .list:
            returnListWidget.self
        }
    } }
}

他们正在通过Widget协议实现的Decodable协议来利用Codable API。下面是一个定义Widget结构的例子。

struct BannerWidget: Widget {>
    private let imageURLString: String
}

结构 CollectionWidget: Widget {

    结构 Item: Decodable {
        let imageURLString: String
        let title: String
        let subtitle: String
    }

    let sectionTitle: String
    let list: [/span>Item]
}

结构 ListWidget: Widget {

    结构 Item: Decodable {
        let imageURLString: String
        let text: String
    }

    let sectionTitle: String
    let list: [/span>Item]
}

最后,它被定义为一个类型擦除,当人们需要改变Decodable给出的自定义初始化时,它可以解释任何具有所需初始化方法的小部件。

final class AnyWidget:Decodable {>

    private enum CodingKeys: CodingKey{
        case标识符
    }

    let widget: Widget? 

    要求init(from decoder:Decoder)抛出{>
        do {
            let容器= try解码器container(keyedByCodingKeys.self)
            let type = try container.decodeWidgetIdentifierself,forKey: .identifier)。
            self.widget = try type.metatypeinit(from:解码器)
        } catch {
            self.widget = nil
        }
    }
}

这个擦除类型被用来解密Widget的标识符和它的 "meta-type "属性,以确定哪个Widget结构应该被用来解析其余被解析的Widget的数据。

所有这些导致下面的结构能够解析一个包含所有关于Widget信息的响应。它有一个独特的功能:一个Widget协议类型的数组,并且可以使用上面定义的类型擦除法来解密每个Widget。

struct HomeResponse: Decodable{>

    private enum CodingKeys: CodingKey{
        case widgets
    }

    let widgets: [/span>Widget]

    init(from decoder: Decoder)抛出 {
        let容器= try解码器container(keyedByCodingKeys.self)
        self.widgets = try containerdecode([AnyWidget]self,forKey: .widgets)compactMap {$0.widget }}

    initwidgets [Widget] {
        self.widgets = widgets
    }
}

MovilePay可以选择其他选项,例如不使用协议并依靠后端返回每个支持的部件的数组进行解析。然而,我们发现,我们的协议选择是维护和可读性的最佳选择。这种方法足以创建一个新的结构,并在每次需要创建新的小部件时向枚举添加案例。在类似的情况下使用不同的系统,HomeResponse的设计将必须改变。

下面是该模型将解析的可能的JSON API响应。

{
    "widgets": [
        { 
            "标识符": "BANNER",
            "imageURLString": "url_image_to_be_downloaded"
        },
        { 
            "identifier": "COLLECTION",
            "sectionTitle": "section Title",
            "list": []。
                { 
                    "imageURLString": "url_image_to_be_downloaded"
                    "title": "Title item 1",/span>
                    "subtitle": "subtitle item 1"
                },
                { 
                    "imageURLString": "url_image_to_be_downloaded",
                    "title": "Title item 2",/span>
                    "subtitle": "subtitle item 2"
                },
                { 
                    "imageURLString": "url_image_to_be_downloaded",
                    "title": "Title item 3",
                    "subtitle": "subtitle item 3"
                }
            ]
        },
        { 
            "identifier": "LIST",
            "sectionTitle": "section title",
            "list": []。
                { 
                    "imageURLString": "url_image_to_be_downloaded"
                    "text": "Text item 1"
                },
                { 
                    "imageURLString": "url_image_to_be_downloaded",
                    "text": "Text item 2"
                },
                { 
                    "imageURLString": "url_image_to_be_downloaded",
                    "text": "Text item 3"
                }
            ]
        },
        { 
            "identifier": "BANNER",
            "imageURLString": "url_image_to_be_downloaded"
        }
    ]
}

这种方法非常接近于超级应用程序的开发,它允许MovilePay向不同的用户展示不同的服务,测试许多显示选项,制定假设,并确定哪种小部件将用于哪种服务。屏幕排序和服务分组的变化与MovilePay之前所做的很接近。

导航(iOS)

在解决了小部件的问题后,MovilePay也试图类似地改善导航。他们创建了 Action 协议,这与 Widget 协议相同。

Action 是一些 MovilePay API 的 JSON 响应中返回的结构化对象,带有一个 ID 和任何应该在其代表的场景中显示的参数。因此,Action协议负责帮助解构结构化对象。

协议Action: Decodable {>
    func scene() ->  UIViewController
}

enum ActionIdentifier: String,  Decodable {
    case home = "HOME" 
    case screenOne = "SCREEN_ONE" 
    case screenTwo = "SCREEN_TWO" 

    var metatype: Action.Type {>
        switch self {
        case .home:
            return HomeAction.self
        case .screenOne:
            return ScreenOneAction.self
        case .screenTwo: 
            return ScreenTwoAction.self
        }
    } }
}
view raw

Action协议和Widget协议的唯一区别是,我们提供了一个方法,为Action定义中的每个Action返回相应的场景。作为一个例子,看看这些动作是如何实现的。

struct HomeAction: Action{
    func scene() ->  UIViewController {
        return HomeCoordinator.scene
    }
}

结构 ScreenOneAction: Action {
    let title: String

    func scene() ->  UIViewController {
        return ScreenOneCoordinatorscene(titleself.title)
    }
}

结构 ScreenTwoAction: Action {
    let title: String
    let subtitle: String

    func scene() ->/span> UIViewController {
        return ScreenTwoCoordinator.scene(title: self标题,副标题:自我.副标题}
}

上面的例子表明,在创建时,Actions必须包含所有必要的属性来初始化其场景,并调用Coordinators方法来实例化UIViewController。Coordinators是提供UIViewController的结构。这里有一点要提到。MovilePay在Coordinators结构中使用了一种设计模式,由一个静态的scene()方法代表,该方法的任务是为每个阶段生成一个UIViewController实例。

final class HomeCoordinator: Coordinator {>
    static func scene() -> UIViewController {{/span>}。
        //span>创建ViewController和这个场景的所有架构组件...
        return createdViewController
    }
}

final class ScreenOneCoordinator: Coordinator {>
    static func scene() - > UIViewController {{/span>}。
        //span>创建ViewController和这个场景的所有架构组件...
        return createdViewController
    }
}

final class ScreenTwoCoordinator: Coordinator {>
    static func scene() - > UIViewController {{/span>}。
        //span>创建ViewController和这个场景的所有架构组件...
        return createdViewController
    }
}

还需要注意的是,尽管选择了架构设计模式(MVC、MVP、MVVM-C、VIPER-C、VIP或任何其他使用协调器来创建场景并从一个场景移动到另一个场景的架构),使用动作和后台驱动开发来实现是非常合适的。

对于行动MovilePay上下文,我们使用了与小工具相同的擦除类型,并稍作调整。

final class AnyAction: decodable {>

    private enum CodingKeys: CodingKey{
        case标识符
    }

    let action: Action

    要求init(from decoder:Decoder)抛出{>
        do {
            let容器= try解码器container(keyedByCodingKeys.self)
            let type = try container.decode(ActionIdentifierself,forKey: .identifier)。
            self.action= try type.metatypeinit(from:解码器)
        } catch {
            self.action = nil
        }
    }
}

这里有一个相关的说明,MovilePay可以使用通用设计模式来避免重复的类型清除代码。然而,他们选择不这样做。下面是一个包含Action的结构的例子。

struct ResponseModelForActions:可解码的{}。
    private enum CodingKeys: CodingKey{
        case action, text
    }

    let action: Action? 
    let text: String

    init(from decoder: Decoder) throws {
        let容器= try解码器container(keyedByCodingKeys.self)
        self.text = try container.decodeString.self,forKey: .text)let anyAction = try containerdecodeAnyActionself,forKey: .action)。
        self.action = anyAction? .action
    }
}

它可以是由API提供的JSON,例如,在屏幕上创建一个UIButton。用户点击这个按钮后处理的动作对象可以执行所提供的动作,使应用程序显示主屏幕。

{}。
    "text": "Demonstrative text", 
    "action": { >。
        "identifier": "HOME_SCREEN" 
    }
}

通过扩展Coordinator协议,让所有协调者通过Actions对象获得一个新的场景,这很容易实现。

它允许协调者采取行动,即为该行动生成下一个UIViewController实例,然后显示它。

extension Coordinator {
    func scene(using action:Action) -> UIViewController {
        return action.scene
    }
}

关于服务器实现的提示

你可能想知道这一切在服务器端是什么样子。如何不把你的服务和核心能力与外部信息混淆?软件开发成功的秘诀是分层工作。

因此,除了所有复杂的核心服务,MovilePay在服务器上增加了另一层,通过抽象所有其他服务和应用所有应用逻辑,将数据转化为前端所期望的响应。这一层被称为BFF,或者说Backend For Frontend是一个在系统的两个独立和不相关的末端之间提供通信的层。它是配置字符串、图像、流和样式变化的地方,并在将其发送给应用程序之前应用于底层数据。

结论

使用后端驱动的方法有几个优点,我们在整个文章中试图澄清这些优点。然而,这只是另一个解决方案模板。它不是应用开发的灵丹妙药。此外,必须考虑应用程序的使用环境。你需要为多个平台创建应用程序吗?你想做哪些类型的测试?你需要对所有屏幕进行完全控制吗?你的有效载荷会有多大?您是否有足够的资源来执行这个项目,包括一个能够处理这些任务的开发团队?

最重要的是,您应该始终谨慎地考虑您需要何种程度的灵活性。创建极为通用的 UI 组件或使用非标准的字体和页边距会使代码库变得异常复杂,从而导致更差的用户体验。

大多数项目不需要这种灵活性。在选择 Backend-Driven 方法时,必须考虑您是否有资金和开发团队的资源来开发和维护这样一个项目,以及您是否能够正确计算您的应用程序的有效载荷。