PHP教程系列4-按需加载的代码才有灵魂

有一天产品经理给大家提了个需求,指着淘宝说,做个一样的,这个功能很简单,怎么实现我不管……
说时迟那时快,小明拿起键盘就是干。小明快速的封装了两个类,分别如下:

  1. TestA.php

    1
    2
    3
    4
    5
    6
    <?php 
    class TestA{
    public function getDemo(){
    echo '淘宝首页';
    }
    }
  2. TestB.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <?php 
    class TestB{
    public function getDemo(){
    echo '淘宝商品页';
    }
    }
    /****
    小明拿到需求后干劲十足,于是在代码中添加了一亿行注释
    什么渣渣需求,大爷不干了……
    什么渣渣需求,大爷不干了……
    什么渣渣需求,大爷不干了……
    *****/

    包含注释信息的TestB.php文件有1G,请自行粘贴复制将TestB.php文件弄成1G左右,下面会有惊喜。

然后创建一个index.php文件来调用这两个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("TestA.php");
include("TestB.php");

if(isset($_GET['type'])){
if($_GET['type'] == 1){
$a = new TestA();
}else{
$a = new TestB();
}
$a->getDemo();
}else{
echo "页面不存在,请指访问 http://127.0.0.1:3000/index.php?type=1";
}

在当前目录下打开命令行,然后执行 php -S 127.0.0.1:3000 (启动PHP服务),再访问 http://127.0.0.1:3000/index.php?type=1 等你的页面加载完,你会打心里说一句“这么卡,PHP这语言真**”。

于是我们优化一下index.php的代码,让首页不那么卡,优化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['type'])){
if($_GET['type'] == 1){
include("TestA.php");
$a = new TestA();
}else{
include("TestB.php");
$a = new TestB();
}
$a->getDemo();
}else{
echo "页面不存在,请指访问 http://127.0.0.1:3000/index.php?type=1";
}

这次再访问 http://127.0.0.1:3000/index.php?type=1 发现速度快了很多。

从上面这个简单的例子可以得出下面一些结论:

  1. PHP是解释型语言,每次执行时都需要加载文件、编译文件中的代码、最后执行。虽然注释不是代码,但也会被加载导致速度变慢。
  2. 硬盘的读取速度很慢,45M/秒左右。
  3. 按需加载能优化程序,因为不同请求要加载的类和文件是不一样的。

一. 解释型语言的优缺点

编程语言分为解释型语言和编译型语言,像c,golang就是编译型语言,需要build成可执行文件才能运行。像PHP,Python,JavaScript就是解释型语言,可以理解为需要实时编译(解释)再执行。

作为解释型语言,PHP最大的优点有如下几个:

  1. 不关注变量类型,解释器会自己推算出变量类型。定义变量很轻松,很大程度的提升了编程速度。比如定义了 $a = 1000; 之后执行 $a = 12.11;$a = "abc"; 程序都能正常执行,开发人员不用关心变量是int、float、char。
  2. 热更新,代码修改了之后是实时生效的,不用重启服务器。
  3. 自带很多实用的扩展,常用的时间处理,字符串处理,文件处理,数据库读写等等,真正的开箱即用。
  4. 容易上手,也导致很多人只注重功能实现不注重代码质量。

作为解释型语言,PHP最大的缺点如下:

  1. 有很多历史包袱,年纪太大的锅。
  2. 函数命名不规范,怎么开心怎么来的节奏,今天小驼峰明天匈牙利。
  3. 缺少好用的包管理器和依赖管理方案,安装扩展很麻烦。
  4. 只能用于Web开发,其他领域不实用。也算是优点吧(专一)

二. 硬盘读取速度的局限

任何编程语言,在项目代码多了之后都会遇到硬盘读取速度的瓶颈,很多编程语言设计之初就引入了包管理的概念,比如C++ 的 using namespace std; 比如 golang的 import "fmt" Python的 import re。通过合理的对包做管理,就能解决由于项目文件太大导致用户请求变慢的问题,很少有哪个请求或者方法需要将所有包代码都加载到内存中。

如果一个资源文件的大小是100K,400个用户同时发起请求就是40M。这个时候硬盘的瓶颈就出来了,通常我们会将文件放入到内存中,内存的读取速度在10G/秒左右,100K的文件支持10万用户同时获取。理论上是这样,但网卡不一定承受的住,现在大部分网卡还是千M网卡,扛不住10G/秒的请求。有兴趣的朋友可以看看PHP教程系列3-写PHP程序前必须知道的5点信息

三. 按需加载要解决哪些问题

还是之前的例子,小明封装的文件越来越多,新加一个文件就写一条 include 语句,淘宝网有几万个页面,难道要写几万个include?我们需要再优化一下代码,让程序变的智能一点,别写太多include。优化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
function autoload($className)
{
$fileName = $className. '.php';
include $fileName;
}
spl_autoload_register('autoload');

if(isset($_GET['type'])){
if($_GET['type'] == 1){
$a = new TestA();
}else{
$a = new TestB();
}
$a->getDemo();
}else{
echo "页面不存在,请指访问 http://127.0.0.1:3000/index.php?type=1";
}

我们去掉了之前的include语句,通过 spl_autoload_register() 函数来实现按需加载,这个函数的含义是如果当前找不到类名,就去指定的目录查找。通过 spl_autoload_register() 方法我们把 include 去掉了,这时候再新建TestC.php ,TestD.php 都不需要再通过 include 引入。大大的节省了编程时间,简单的实现了按需加载。

不过上面的方法有个问题,如果我需要引入小芳的代码库,小芳代码库的文件名并不等于类名,例如文件名是TestC.class.php类名是TestC。我们就没法通过类名加载到指定的文件了。如果小芳也有一个TestA.php文件,和小明的冲突了怎么办?

为了解决上面这两个问题,PHP5.3引入namespace命名空间的概念,PHP5.3之前的版本是不支持命名空间的。命名空间的引入,主要就是为了优化包管理,按需加载等问题。官方指定了PSR-0和PSR-4两个自动加载规范,其中PSR-0已经被弃用。现在PSR-4应用最广泛,通过Composer一句命令就实现。PHP官方制定了很多编码规范,主要是防止一些PHP程序员瞎写代码。具体规范地址看这里 https://www.php-fig.org/psr/

  1. PSR-0 早期版本的按需加载规范

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?php

    function autoload($className)
    {
    $className = ltrim($className, '\\');
    $fileName = '';
    $namespace = '';
    if ($lastNsPos = strrpos($className, '\\')) {
    $namespace = substr($className, 0, $lastNsPos);
    $className = substr($className, $lastNsPos + 1);
    $fileName = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
    }
    $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . '.php';

    require $fileName;
    }
    spl_autoload_register('autoload');
  2. PSR-4 最新版本的按需加载规范

    PSR-4可通过composer实现(理论上所有PSR标准都能通过composer命令来自动实现)。composer.json信息如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    "name": "lesliexiong/php-server",
    "description": "server",
    "authors": [
    {
    "name": "layne",
    "email": "layne.xfl@gmail.com"
    }
    ],
    "require": {
    "php": ">=5.4.0"
    },
    "autoload": {
    "psr-4":{
    "Layne\\Taobao\\": "src/"
    }
    }
    }

    然后将 TestA.php 和 TestB.php 放到src目录下,最后执行composer install,所有按需加载的代码会自动生成。之后任何人都可以访问封装好的TestA.php 和 TestB.php。像Yii2,Laravel5这种上万个源代码文件的项目就是使用Composer做管理并共享组件。下一章我将详细说说composer的使用。

四. 写在最后

为了解决按需加载、命名冲突、包管理这些问题,我们引出了命名空间和PSR标准,这些标准都来之不易,每一位PHPer都应该掌握。代码千万行,注释第一行,编码无标准,家人两行泪。