WeaverDI 1.0.7

WeaverDI 1.0.7

Theophane Rupin 维护。



WeaverDI 1.0.7

  • Theophane Rupin

logo

Swift (iOS/macOS/Linux) 的声明式、易于使用且安全的依赖注入框架

Build Status codecov Carthage compatible CocoaPods Version Gitter

demo

特性

  • 通过注解声明依赖(无需配置文件)
  • 自动生成 DI 容器
  • 依赖图编译时验证
  • 支持 ObjC
  • 非可选依赖解析
  • 类型安全
  • 带参数的注入
  • 注册作用域
  • DI 容器层次结构
  • 线程安全

依赖注入

依赖注入基本上意味着“给对象其实例变量” ¹。它似乎并不是什么大事,但随着项目的规模变大,它就会变得复杂。构造函数变得过于复杂,通过多个层级传递依赖变得耗时,甚至仅仅找到依赖的来源都足够容易让人放弃,最终使用单例。

然而,依赖注入是软件开发架构的基本方面,没有理由不正确实现它。这就是 Weaver 可以帮助的地方。

什么是 Weaver?

Weaver 是一个为 Swift 提供的声明式、易于使用且安全的依赖注入框架。

  • 声明式 因为它允许开发者在 Swift 代码中直接通过注解 声明依赖
  • 易于使用 因为它 生成必要的样板代码 以将依赖注入到 Swift 类型中。
  • 安全 因为它 在编译时验证依赖图,并在有问题时输出美丽的 Xcode 错误。

如何使用Weaver?

尽管Weaver能够使依赖注入自行工作,了解它在底层做了什么也是很重要的。需要关注的有两个阶段:编译时间和运行时间。

在编译时

                                                    |-> link() -> dependency graph -> validate() -> valid/invalid 
swift files -> scan() -> [Token] -> parse() -> AST -| 
                                                    |-> generate() -> source code 

Weaver的命令行工具扫描项目的Swift源代码,寻找注解,并生成一个AST(抽象语法树)。它使用SourceKitten,这是由苹果的SourceKit支持的,这使得这一步非常可靠。

然后使用这个AST生成一个依赖图,在这个图上执行一系列的安全检查,以确保代码在运行时不会崩溃。它会检查无法解决的依赖和不可解决的循环依赖。如果发现任何问题,则不会生成代码,这意味着项目将无法编译。

相同的AST还用于生成模板代码。它为每个具有可注入依赖的类/结构体生成一个依赖容器。它还生成一堆扩展和协议,以便将依赖注入对开发者几乎透明。

在运行时

Weaver实现了一个轻量级DI(依赖注入)容器对象,该对象能够根据范围、协议或具体类型、名称和参数来注册和解决依赖。每个容器可以有一个父容器,允许在容器层次结构中解决依赖。

当对象注册一个依赖关系时,其关联的DI容器存储一个构建器(和有时是一个实例)。当另一个对象声明对该同一依赖关系的引用时,其相关DI容器声明一个访问器,该访问器尝试解决依赖。解决依赖基本上意味着在回溯容器层次结构的过程中寻找构建器/实例。如果没有找到依赖关系,或者这个过程陷入无限递归,它将在运行时崩溃,这就是为什么在编译时检查依赖图非常重要的原因。

安装

Weaver 包含 3 个部分

  1. Swift 框架,可包含到您的项目中
  2. 命令行工具,用于在您的机器上安装
  3. 构建阶段,可添加到您的项目中

(1) - Weaver 框架安装

Weaver 的 Swift 框架可以通过 CocoaPodsCarthageSwiftPM 使用。

CocoaPods

Podfile 中添加 pod 'WeaverDI', '~> 0.9.11'

Carthage

Cartfile 中添加 github "scribd/Weaver" ~> 0.9.11

SwiftPM

Package.swift 文件的依赖部分添加 .package(url: "https://github.com/scribd/Weaver.git", from: "0.9.11")

(2) - Weaver 命令行工具安装

可以使用 Homebrew 或手动方式安装 Weaver 命令行工具。

二进制形式

发布标签页下载预构建的二进制文件。将存档解压到目标位置,并运行bin/weaver

Homebrew

在其添加到主Homebrew存储库之前,可按如下方式安装Weaver

$ brew tap trupin/homebrew-core
$ brew install weaver

从源码构建

发布标签页下载最新发布的源代码或克隆存储库。

在项目目录中,运行brew update && brew bundle && make install以构建和安装命令行工具。

检查安装

运行以下命令以检查Weaver是否已正确安装。

$ weaver --help

Usage:

    $ weaver <input_paths>

Arguments:

    input_paths - Swift files to parse.

Options:
    --output_path [default: .] - Where the swift files will be generated.
    --template_path - Custom template path.
    --unsafe [default: false]

(3) - Weaver构建阶段

在Xcode中,将以下命令添加到命令行构建阶段

weaver --output_path ${SOURCE_ROOT}/output/path `find ${SOURCE_ROOT} -name '*.swift' | xargs -0`

注意 - 将此构建阶段移动到编译源代码阶段之上,这样Weaver就可以在编译之前生成样板代码。

警告 - 不推荐使用--unsafe。这将禁用图验证,这意味着生成的代码如果依赖图无效可能会崩溃。只有在图验证阻止项目编译,尽管它不应该时,才将其设置为false。如果您遇到这种情况,请随时提交一个错误报告。

基本用法

要查看更完整的用法示例,请查看示例项目

让我们实现一个显示电影列表的非常基础的程序。它将由三个明显的对象组成

  • AppDelegate在这里注册依赖。
  • MovieManager提供电影。
  • MoviesViewController在屏幕上显示电影列表。

让我们来看看代码。

AppDelegate:

import UIKit
import Weaver

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    private let dependencies = AppDelegateDependencyContainer()
    
    // weaver: movieManager = MovieManager <- MovieManaging
    // weaver: movieManager.scope = .container
    
    // weaver: moviesViewController = MoviesViewController <- UIViewController
    // weaver: moviesViewController.scope = .container
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        
        window = UIWindow()

        let rootViewController = dependencies.moviesViewController
        window?.rootViewController = UINavigationController(rootViewController: rootViewController)
        window?.makeKeyAndVisible()
        
        return true
    }
}

AppDelegate注册了两个依赖

  • // weaver: movieManager = MovieManager <- MovieManaging
  • // weaver: moviesViewController = MoviesViewController <- UIViewController

这些依赖因为它们的范围设置为container,所以可以被来自AppDelegate的任何对象访问

  • // weaver: movieManager.scope = .container
  • // weaver: moviesViewController.scope = .container

依赖注册会自动在AppDelegateDependencyContainer中生成注册代码和一个访问器,这就是为什么可以构建rootViewController

  • let rootViewController = dependencies.moviesViewController.

MovieManager:

protocol MovieManaging {
    
    func getMovies(_ completion: @escaping (Result<Page<Movie>, MovieManagerError>) -> Void)
}

final class MovieManager: MovieManaging {

    func getMovies(_ completion: @escaping (Result<Page<Movie>, MovieManagerError>) -> Void) {
        // fetches movies from the server...
        completion(.success(movies))        
    }
}

MoviesViewController:

final class MoviesViewController: UIViewController {
    
    private let dependencies: MoviesViewControllerDependencyResolver
    
    private var movies = [Movie]()
    
    // weaver: movieManager <- MovieManaging
    
    required init(injecting dependencies: MoviesViewControllerDependencyResolver) {
        self.dependencies = dependencies
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Setups the tableview... 
        
        // Fetches the movies
        dependencies.movieManager.getMovies { result in
            switch result {
            case .success(let page):
                self.movies = page.results
                self.tableView.reloadData()
                
            case .failure(let error):
                self.showError(error)
            }
        }
    }

    // ... 
}

MoviesViewController声明一个依赖引用

  • // weaver: movieManager <- MovieManaging

这个注解在MoviesViewControllerDependencyResolver中生成一个访问器,但没有注册,这意味着MovieManager不会存储在MoviesViewControllerDependencyContainer中,而是在其父级中,即构建它的容器。在这种情况下是AppDelegateDependencyContainer

MoviesViewController还需要声明一个特定的初始化器

  • required init(injecting dependencies: MoviesViewControllerDependencyResolver)

这个初始化器用于注入DI容器。请注意,MoviesViewControllerDependencyResolver是一个协议,这意味着可以在测试时注入假的DI容器版本。

API

代码注解

Wheaver 允许您通过使用类似这样的注释 // weaver: ... 来声明依赖。

目前它支持以下注解。

- 依赖注册注解

  • 将依赖构建器添加到容器中。
  • 将依赖的访问器添加到容器的解析器协议中。

示例

// weaver: dependencyName = DependencyConcreteType <- DependencyProtocol

// weaver: dependencyName = DependencyConcreteType
  • dependencyName:依赖项的名称。用于在其他对象和/或注解中引用依赖项。
  • DependencyConcreteType:依赖项的实现类型。可以是 structclass
  • DependencyProtocol:如果有,依赖项的 protocol。可选,您可以仅使用具体类型注册依赖项。

- 作用域注解

设置依赖项的作用域。默认作用域为 graph。仅与注册注解一起使用。

scope 定义了依赖项的访问级别和缓存策略。有以下四个作用域:

  • transient:每次解析时始终创建一个新实例。不能从子对象访问。
  • graph:第一次解析时创建一个新实例,然后在整个容器生命周期中存在。不能从子对象访问。
  • weak:第一次解析时创建一个新实例,然后在其强引用存在的时间内存在。可以从子对象访问。
  • container:类似于 graph,但可以从子对象访问。

示例

// weaver: dependencyName.scope = .scopeValue

scopeValue:作用域的值。可以是上述描述中的一种值。

- 依赖项引用注解

向容器的协议中添加对依赖项的访问器。

示例

// weaver: dependencyName <- DependencyType

DependencyType:依赖项的具体系列或抽象系列。这还定义了依赖项访问器返回的类型。

- 自定义引用注解

将方法 dependencyNameCustomRef(_ dependencyContainer:) 添加到容器的解析 protocol 中。默认值是 false。Weaver 会保留此方法未实现,这意味着您需要自行实现它并手动解析/构建依赖项。

与注册和引用注解一起使用。

警告 - 确保您不要在此方法中执行任何不安全操作,因为 dependencyContainer 参数不会被依赖项图验证器捕获。

示例

// weaver: dependencyName.customRef = aBoolean

aBoolean:布尔值,定义依赖项是否应具有自定义引用。可以取 truefalse 的值。

- 参数注释

向容器解析协议添加一个参数。这意味着生成的容器需要在初始化时获取这些参数。这也意味着所有相关的依赖访问器都需要获取这个参数。

示例

// weaver: parameterName <= ParameterType

- 配置注释

为相关的对象设置配置属性。

示例

// weaver: self.attributeName = aValue
配置属性
  • isIsolated: Bool (默认值: false): 任何将此设置为true的对象都被Weaver视为项目未使用的对象。标记为孤立的对象只能有孤立的后代。此属性对开发不包含项目中所有依赖的功能很有用。

阅读更多...

致谢

Weaver的DI容器功能受到了Swinject的启发。

参与贡献

  1. 克隆项目
  2. 创建您的功能分支(git checkout -b my-new-feature
  3. 提交您的更改(git commit -am 'Add some feature'
  4. 推送到分支(git push origin my-new-feature
  5. 创建一个新的拉取请求

许可证

MIT许可证。有关详细信息,请参阅LICENSE文件