Swoft| Swoft 框架组件化改造

date: 2018-3-21 13:22:16
title: Swoft| Swoft 框架组件化改造
description: Swoft 框架从单体应用到组件化改造的架构升级之路

经过一年多的开发, Swoft 框架功能越来越完善, 也越来越复杂. 初创时期的 单体应用, 已经无法支撑项目的快速发展, 于是开发组在年前为 1.0-beta 版制定了 组件化改造 的重构方案.

内容速览:

  • 组件化原理: PHP 包管理基础知识
  • 组件化方案: 来自 laravel/symfony 等成熟框架的组件化实现方案
  • Swoft 框架组件化实现

组件化原理

编程始终要解决的一个问题: 代码复用. 好的代码, 基本要求是 正确, 能拿到预期的结果, 少 bug. 语言层的代码复用解决方案, 通常称之为 包管理(或者 依赖管理). 流行的编程语言, 都提供了很好的工具链对包管理的支持:

  • 一个命令行工具, 用来 获取/管理 包, 比如 php 的 composer, python 的 pip, js 的 npm, java 的 maven, go 的 go get
  • 一个包管理的配置文件, 用来说明需要用到(依赖)的包, 比如 PHP 中 composer 使用 composer.josn, js 的 npm 使用 package.json
  • 一个浏览包的网站, 用来查看包的信息, 比如 php 的 packgist, python 的 pypi 等

这样, 当我们需要不同功能的时候, 就可以去查看是否有包已经提供了类似功能, 不用重复造轮子, 站在巨人的肩膀上.

回到 PHP 中, PHP中的包管理是如何实现的呢?

  • 命名空间

首先需要知道的一个基础概念, 是 命名空间. 引入命名空间是为了 解决同名冲突 -- 2 个包中有名字相同的类, 同时使用时就会出现类重复定义的提示. 使用命名空间后, 因为不同的包有不同的命名空间, 就不会出现冲突.

// 如果需要在同一个文件中使用相同名字的类, 使用别名
use A\Far;
use B\Far as BFar;
  • 自动加载 & PSR4

第二个需要知道的基础概念, 是 自动加载. PHP 中最基础(或者说最原始)的复用代码的方法: include/include_one/require/require_once. 不过得益于 PHP 的 SPL库 中的 spl_autoload_register() 方法, 现在有了更优雅的方式来复用代码 -- 自动加载. 自动加载的规范也经历了一段时间的升级与打磨, 最新的是 PSR4标准.

关于自动加载, 有一个很好的教程: 5-1 SPL使用spl_autoload_register函数装载类 (10:03)

composer 中的包管理

了解了基础知识后, 就可以来掌握工具怎么用了. composer 中的包管理 根据 composer.json 文件中的 autoload / require / require-dev 配置项来管理.

autoload 定义自动加载, 项目自身的代码, 也应该按照包管理的规范, 进行组织, 比如 Swoft 的 composer.json 配置文件:

...
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        },
        "files": [
            "app/Swoft.php"
        ]
    },
...

composer 支持多种方式的自动加载方式, 这里面有一定的历史原因, 因为需要兼容一些 陈旧 的代码. 现在比较常用的 2 种方式:

  • psr-4: PSR4 标准, 优先推荐的方式
  • files: 直接加载文件, 通常用来加载 帮助函数, 类似于 PHP 的 require 语法来代码复用

require 标识需要依赖的包, 格式是 包名 - 版本限制 的键值对:

...
    "require": {
        "php": ">=7.0",
        "swoft/framework": "^1.0",
        "swoft/rpc": "^1.0",
        "swoft/rpc-server": "^1.0",
        "swoft/rpc-client": "^1.0",
        "swoft/http-server": "^1.0",
        "swoft/http-client": "^1.0",
        "swoft/task": "^1.0",
        "swoft/http-message": "^1.0",
        "swoft/view": "^1.0",
        "swoft/db": "^1.0",
        "swoft/cache": "^1.0",
        "swoft/redis": "^1.0",
        "swoft/console": "^1.0",
        "swoft/devtool": "^1.0",
        "swoft/session": "^1.0",
        "swoft/i18n": "^1.0",
        "swoft/process": "^1.0",
        "swoft/memory": "^1.0",
        "swoft/service-governance": "^1.0"
    },
...

关于 版本控制 的知识, 以及 >= ^ ~ 等特殊字符, alpha beta dev dev-master 等标识, 只是约定俗成的一些定义, 了解清楚即可.

require-dev 标识开发环境需要依赖的包, 即正式环境不需要使用到的包, 比如单元测试等:

...
    "require-dev": {
        "eaglewu/swoole-ide-helper": "dev-master",
        "phpunit/phpunit": "^5.7"
    },
...

类似的, 还有 autoload-dev, 表示测试环境下使用到自动加载.

组件化方案: laravel 与 symfony 使用的方案

参考 symfony 中的 composer.json 配置文件laravel 中的 composer.json 配置文件, 会发现里面有一个配置项: replace.

replace 这个配置项, 在普通项目中很难看到, 却是组件化改造中的重要配置, 它的定义如下:

使用项目中已有的包, 替换需要依赖的包

比如 symfony 中的 composer.json 配置文件:

...
    "replace": {
        "symfony/asset": "self.version",
        "symfony/browser-kit": "self.version",
        "symfony/cache": "self.version",
        "symfony/config": "self.version",
        "symfony/console": "self.version",
        "symfony/css-selector": "self.version",
        "symfony/dependency-injection": "self.version",
        ...

其中 "symfony/asset" 包, 有一个单独的github 仓库 symfony/asset, symfony 项目 本身也包含 "symfony/asset" 包, 使用 replace, symfony 就可以使用自身包含的包, 不用去单独获取.

这样带来的好处:

  • 主包包含所有的子包, 使用时使用 replace 配置, 所有的修改和提交都在主包中进行
  • 其他项目依旧可以使用 require, 单独使用子包; 子包只接受来自主包分发来的代码, 不接受在子包上的更改

Swoft 框架组件化实现

Swoft 在 1.0-beta版中的依赖, Swoft 项目:

...
    "require": {
        "php": ">=7.0",
        "swoft/framework": "^1.0",
        "swoft/rpc": "^1.0",
        "swoft/rpc-server": "^1.0",
        "swoft/rpc-client": "^1.0",
        "swoft/http-server": "^1.0",
        "swoft/http-client": "^1.0",
        "swoft/task": "^1.0",
        "swoft/http-message": "^1.0",
        "swoft/view": "^1.0",
        "swoft/db": "^1.0",
        "swoft/cache": "^1.0",
        "swoft/redis": "^1.0",
        "swoft/console": "^1.0",
        "swoft/devtool": "^1.0",
        "swoft/session": "^1.0",
        "swoft/i18n": "^1.0",
        "swoft/process": "^1.0",
        "swoft/memory": "^1.0",
        "swoft/service-governance": "^1.0"
    },
...

改造后, Swoft 项目, 主项目只用依赖 "swoft/framework":

...
    "require": {
        "php": ">=7.0",
        "swoft/framework": "^1.0"
    },
...

"swoft/framework" 项目, 包含其他子包:

...
    "replace": {
        "swoft/rpc": "self.version",
        "swoft/rpc-server": "self.version",
        "swoft/rpc-client": "self.version",
        "swoft/http-server": "self.version",
        "swoft/http-client": "self.version",
        "swoft/task": "self.version",
        "swoft/http-message": "self.version",
        "swoft/view": "self.version",
        "swoft/db": "self.version",
        "swoft/cache": "self.version",
        "swoft/redis": "self.version",
        "swoft/console": "self.version",
        "swoft/devtool": "self.version",
        "swoft/session": "self.version",
        "swoft/i18n": "self.version",
        "swoft/process": "self.version",
        "swoft/memory": "self.version",
        "swoft/service-governance": "self.version"
    }
...

其中子项目声明到主项目提交修改:

整个开发流程如下:

  • daydaygo/swoft-framework 项目 新建 component2 分支开发此次组件化改造
  • 修改 Swoft 项目的 composer.json 文件, 快速获取所有 Swoft 组件的 master 分支代码:
    "require": {
        "php": ">=7.0",
        "swoft/framework": "dev-master",
        "swoft/rpc": "dev-master",
        "swoft/rpc-server": "dev-master",
        "swoft/rpc-client": "dev-master",
        "swoft/http-server": "dev-master",
        "swoft/http-client": "dev-master",
        "swoft/task": "dev-master",
        "swoft/http-message": "dev-master",
        "swoft/view": "dev-master",
        "swoft/db": "dev-master",
        "swoft/cache": "dev-master",
        "swoft/redis": "dev-master",
        "swoft/console": "dev-master",
        "swoft/devtool": "dev-master",
        "swoft/session": "dev-master",
        "swoft/i18n": "dev-master",
        "swoft/process": "dev-master",
        "swoft/memory": "dev-master",
        "swoft/service-governance": "dev-master"
    },
  • 复制各个组件的代码到 swoft-framework 项目中, 修改 composer.json 的中的 autoload / replace 配置(具体修改点击链接查看)

Swoft 各组件依赖关系图: http://naotu.baidu.com/file/7...

  • 提交 swoft-framework 代码.

下面以推送 swoft-view 组件到对应仓库中为例:

出于 github 网速的原因, 测试过程使用 gitee 来加速

推送子项目到相应的 github 仓库中, 参考:

# 建立 gitee.com:daydaygo/swoft-framework 仓库
git remote add gitee git@gitee.com:daydaygo/swoft-framework.git
git push gitee component2

# 拆分
git subsplit init git@gitee.com:daydaygo/swoft-framework.git
# 更多项目, 一次填写即可
git subsplit publish --heads="component2" --no-tags view:git@gitee.com:daydaygo/swoft-view.git
# 清除生成的临时文件
rm .subsplit

这个拆分过程耗时较长, 拆分后的效果: gitee.com/daydaygo/swoft-view, gitee.com/daydaygo/swoft-framework

可以通过添加 github webhook 来做自动化, 具体请参考: dflydev/dflydev-git-subsplit-github-webhook

最后, 测试拆分后的代码:

  • 修改 swoft 项目的 composer.json 文件, 使用新版的 swoft-framework 项目
...
  // 现在只需要依赖 swoft/framework, 版本号要制定分支, composer 会默认给分支名带上 dev- 前缀
  "require": {
    "php": ">=7.0",
    "swoft/framework": "dev-component2"
  },
....
  "repositories": {
    "packagist": {
      "type": "composer",
      "url": "https://packagist.phpcomposer.com"
    },
    // 制定包的地址, 这里指向我的 giee 仓库地址
    "0": {
      "type": "vcs",
      "url": "https://gitee.com/daydaygo/swoft-framework"
    }
  }
...
# 删除以前的依赖
rm -rf composer.lock vendor
# 更新
composer install --no-dev

至此, 大功告成.

写在最后

对项目进行组件化拆分, 推送子包到不同 github 仓库 这样的需求, 也许只有写一个大型框架才会遇到. 但这也是正是动手写一个框架的乐趣所在. PHP 中的包管理的基础知识一直感觉 游刃有余, 直到遇到新的问题, 提出新的挑战, 才发现还有更多的天地. 愿你也能感受到这分技术的乐趣.

阅读 4.6k

推荐阅读
Swoft
用户专栏

!本专栏已停止更新! !本专栏已停止更新! !本专栏已停止更新! !本专栏已停止更新! !本专栏已停...

568 人关注
14 篇文章
专栏主页