详解如何实现Laravel的服务容器的方法示例


Posted in PHP onApril 15, 2019

1. 容器的本质

  • 服务容器本身就是一个数组,键名就是服务名,值就是服务。
  • 服务可以是一个原始值,也可以是一个对象,可以说是任意数据。
  • 服务名可以是自定义名,也可以是对象的类名,也可以是接口名。
// 服务容器
$container = [
  // 原始值
  'text' => '这是一个字符串',
  // 自定义服务名
  'customName' => new StdClass(),
  // 使用类名作为服务名
  'StdClass' => new StdClass(),
  // 使用接口名作为服务名
  'Namespace\\StdClassInterface' => new StdClass(),
];

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

// 绑定服务到容器
$container['standard'] = new StdClass();
// 获取服务
$standard = $container['standard'];
var_dump($standard);

2. 封装成类

为了方便维护,我们把上面的数组封装到类里面。

$instances还是上面的容器数组。我们增加两个方法,instance用来绑定服务,get用来从容器中获取服务。

class BaseContainer
{

  // 已绑定的服务
  protected $instances = [];

  // 绑定服务
  public function instance($name, $instance)
  {
    $this->instances[$name] = $instance;
  }

  // 获取服务
  public function get($name)
  {
    return isset($this->instances[$name]) ? $this->instances[$name] : null;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new BaseContainer();
// 绑定服务
$container->instance('StdClass', new StdClass());
// 获取服务
$stdClass = $container->get('StdClass');
var_dump($stdClass);

3. 按需实例化

现在我们在绑定一个对象服务的时候,就必须要先把类实例化,如果绑定的服务没有被用到,那么类就会白白实例化,造成性能浪费。

为了解决这个问题,我们增加一个bind函数,它支持绑定一个回调函数,在回调函数中实例化类。这样一来,我们只有在使用服务时,才回调这个函数,这样就实现了按需实例化。

这时候,我们获取服务时,就不只是从数组中拿到服务并返回了,还需要判断如果是回调函数,就要执行回调函数。所以我们把get方法的名字改成make。意思就是生产一个服务,这个服务可以是已绑定的服务,也可以是已绑定的回调函数,也可以是一个类名,如果是类名,我们就直接实例化该类并返回。

然后,我们增加一个新数组$bindings,用来存储绑定的回调函数。然后我们把bind方法改一下,判断下$instance如果是一个回调函数,就放到$bindings数组,否则就用make方法实例化类。

class DeferContainer extend BaseContainer
{
  // 已绑定的回调函数
  protected $bindings = [];

  // 绑定服务
  public function bind($name, $instance)
  {
    if ($instance instanceof Closure) {
      // 如果$instance是一个回调函数,就绑定到bindings。
      $this->bindings[$name] = $instance;
    } else {
      // 调用make方法,创建实例
      $this->instances[$name] = $this->make($name);
    }
  }

  // 获取服务
  public function make($name)
  {
    if (isset($this->instances[$name])) {
      return $this->instances[$name];
    }

    if (isset($this->bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this->bindings[$name]);
    } else {
      // 还没有绑定到容器中,直接new.
      $instance = new $name();
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new DeferContainer();
// 绑定服务
$container->bind('StdClass', function () {
  echo "我被执行了\n";
  return new StdClass();
});
// 获取服务
$stdClass = $container->make('StdClass');
var_dump($stdClass);

StdClass这个服务绑定的是一个回调函数,在回调函数中才会真正的实例化类。如果没有用到这个服务,那回调函数就不会被执行,类也不会被实例化。

4. 单例

从上面的代码中可以看出,每次调用make方法时,都会执行一次回调函数,并返回一个新的类实例。但是在某些情况下,我们希望这个实例是一个单例,无论make多少次,只实例化一次。

这时候,我们给bind方法增加第三个参数$shared,用来标记是否是单例,默认不是单例。然后把回调函数和这个标记都存到$bindings数组里。

为了方便绑定单例服务,再增加一个新的方法singleton,它直接调用bind,并且$shared参数强制为true。

对于make方法,我们也要做修改。在执行$bindings里的回调函数以后,做一个判断,如果之前绑定时标记的shared是true,就把回调函数返回的结果存储到$instances里。由于我们是先从$instances里找服务,所以这样下次再make的时候就会直接返回,而不会再次执行回调函数。这样就实现了单例的绑定。

class SingletonContainer extends DeferContainer
{
  // 绑定服务
  public function bind($name, $instance, $shared = false)
  {
    if ($instance instanceof Closure) {
      // 如果$instance是一个回调函数,就绑定到bindings。
      $this->bindings[$name] = [
        'callback' => $instance,
        // 标记是否单例
        'shared' => $shared
      ];
    } else {
      // 调用make方法,创建实例
      $this->instances[$name] = $this->make($name);
    }
  }

  // 绑定一个单例
  public function singleton($name, $instance)
  {
    $this->bind($name, $instance, true);
  }

  // 获取服务
  public function make($name)
  {
    if (isset($this->instances[$name])) {
      return $this->instances[$name];
    }

    if (isset($this->bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this->bindings[$name]['callback']);

      if ($this->bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this->instances[$name] = $instance;
      }
    } else {
      // 还没有绑定到容器中,直接new.
      $instance = new $name();
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new SingletonContainer();
// 绑定服务
$container->singleton('anonymous', function () {
  return new class
  {
    public function __construct()
    {
      echo "我被实例化了\n";
    }
  };
});
// 无论make多少次,只会实例化一次
$container->make('anonymous');
$container->make('anonymous');
// 获取服务
$anonymous = $container->make('anonymous');
var_dump($anonymous)

上面的代码用singleton绑定了一个名为anonymous的服务,回调函数里返回了一个匿名类的实例。这个匿名类在被实例化时会输出一段文字。无论我们make多少次anonymous,这个回调函数只会被执行一次,匿名类也只会被实例化一次。

5. 自动注入

自动注入是Ioc容器的核心,没有自动注入就无法做到控制反转。

自动注入就是指,在实例化一个类时,用反射类来获取__construct所需要的参数,然后根据参数的类型,从容器中找到已绑定的服务。我们只要有了__construct方法所需的所有参数,就能自动实例化该类,实现自动注入。

现在,我们增加一个build方法,它只接收一个参数,就是类名。build方法会用反射类来获取__construct方法所需要的参数,然后返回实例化结果。

另外一点就是,我们之前在调用make方法时,如果传的是一个未绑定的类,我们直接new了这个类。现在我们把未绑定的类交给build方法来构建,因为它支持自动注入。

class InjectionContainer extends SingletonContainer
{

  // 获取服务
  public function make($name)
  {
    if (isset($this->instances[$name])) {
      return $this->instances[$name];
    }
    if (isset($this->bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this->bindings[$name]['callback']);

      if ($this->bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this->instances[$name] = $instance;
      }
    } else {
      // 使用build方法构建此类
      $instance = $this->build($name);
    }

    return $instance;
  }

  // 构建一个类,并自动注入服务
  public function build($class)
  {

    $reflector = new ReflectionClass($class);

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor->getParameters() as $dependency) {
      if (is_null($dependency->getClass())) {
        // 参数类型不是类时,无法从容器中获取依赖
        if ($dependency->isDefaultValueAvailable()) {
          // 查找参数的默认值,如果有就使用默认值
          $dependencies[] = $dependency->getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency->getName());
        }
      } else {
        // 参数类型是类时,就用make方法构建该类
        $dependencies[] = $this->make($dependency->getClass()->name);
      }
    }

    return $reflector->newInstanceArgs($dependencies);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
}

class Cache
{
  protected $redis;

  // 构造函数中依赖Redis服务
  public function __construct(Redis $redis)
  {
    $this->redis = $redis;
  }
}

$container = new InjectionContainer();
// 绑定Redis服务
$container->singleton(Redis::class, function () {
  return new Redis();
});
// 构建Cache类
$cache = $container->make(Cache::class);
var_dump($cache);

6. 自定义依赖参数

现在有个问题,如果类依赖的参数不是类或接口,只是一个普通变量,这时候就无法从容器中获取依赖参数了,也就无法实例化类了。

那么接下来我们就支持一个新功能,在调用make方法时,支持传第二个参数$parameters,这是一个数组,无法从容器中获取的依赖,就从这个数组中找。

当然,make方法是用不到这个参数的,因为它不负责实例化类,它直接传给build方法。在build方法寻找依赖的参数时,就先从$parameters中找。这样就实现了自定义依赖参数。

需要注意的一点是,build方法是按照参数的名字来找依赖的,所以parameters中的键名也必须跟__construct中参数名一致。

class ParametersContainer extends InjectionContainer
{
  // 获取服务
  public function make($name, array $parameters = [])
  {
    if (isset($this->instances[$name])) {
      return $this->instances[$name];
    }
    if (isset($this->bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this->bindings[$name]['callback']);

      if ($this->bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this->instances[$name] = $instance;
      }
    } else {
      // 使用build方法构建此类
      $instance = $this->build($name, $parameters);
    }

    return $instance;
  }

  // 构建一个类,并自动注入服务
  public function build($class, array $parameters = [])
  {
    $reflector = new ReflectionClass($class);

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor->getParameters() as $dependency) {

      if (isset($parameters[$dependency->getName()])) {
        // 先从自定义参数中查找
        $dependencies[] = $parameters[$dependency->getName()];
        continue;
      }

      if (is_null($dependency->getClass())) {
        // 参数类型不是类或接口时,无法从容器中获取依赖
        if ($dependency->isDefaultValueAvailable()) {
          // 查找默认值,如果有就使用默认值
          $dependencies[] = $dependency->getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency->getName());
        }
      } else {
        // 参数类型是类时,就用make方法构建该类
        $dependencies[] = $this->make($dependency->getClass()->name);
      }
    }

    return $reflector->newInstanceArgs($dependencies);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
}

class Cache
{
  protected $redis;

  protected $name;

  protected $default;

  // 构造函数中依赖Redis服务和name参数,name的类型不是类,无法从容器中查找
  public function __construct(Redis $redis, $name, $default = '默认值')
  {
    $this->redis = $redis;
    $this->name = $name;
    $this->default = $default;
  }
}

$container = new ParametersContainer();
// 绑定Redis服务
$container->singleton(Redis::class, function () {
  return new Redis();
});
// 构建Cache类
$cache = $container->make(Cache::class, ['name' => 'test']);
var_dump($cache);

提示:实际上,Laravel容器的build方法并没有第二个参数$parameters,它是用类属性来维护自定义参数。原理都是一样的,只是实现方式不一样。这里为了方便理解,不引入过多概念。

7. 服务别名

别名可以理解成小名、外号。服务别名就是给已绑定的服务设置一些外号,使我们通过外号也能找到该服务。

这个就比较简单了,我们增加一个新的数组$aliases,用来存储别名。再增加一个方法alias,用来让外部注册别名。

唯一需要我们修改的地方,就是在make时,要先从$aliases中找到真实的服务名。

class AliasContainer extends ParametersContainer
{
  // 服务别名
  protected $aliases = [];

  // 给服务绑定一个别名
  public function alias($alias, $name)
  {
    $this->aliases[$alias] = $name;
  }

  // 获取服务
  public function make($name, array $parameters = [])
  {
    // 先用别名查找真实服务名
    $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name;

    return parent::make($name, $parameters);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

$container = new AliasContainer();

// 绑定服务
$container->instance('text', '这是一个字符串');
// 给服务注册别名
$container->alias('string', 'text');
$container->alias('content', 'text');

var_dump($container->make('string'));
var_dump($container->make('content'));

8. 扩展绑定

有时候我们需要给已绑定的服务做一个包装,这时候就用到扩展绑定了。我们先看一个实际的用法,理解它的作用后,才看它是如何实现的。

// 绑定日志服务
$container->singleton('log', new Log());

// 对已绑定的服务再次包装
$container->extend('log', function(Log $log){
  // 返回了一个新服务
  return new RedisLog($log);
});

现在我们看它是如何实现的。增加一个$extenders数组,用来存放扩展器。再增加一个extend方法,用来注册扩展器。

然后在make方法返回$instance之前,按顺序依次调用之前注册的扩展器。

class ExtendContainer extends AliasContainer
{
  // 存放扩展器的数组
  protected $extenders = [];

  // 给服务绑定扩展器
  public function extend($name, $extender)
  {
    if (isset($this->instances[$name])) {
      // 已经实例化的服务,直接调用扩展器
      $this->instances[$name] = $extender($this->instances[$name]);
    } else {
      $this->extenders[$name][] = $extender;
    }
  }

  // 获取服务
  public function make($name, array $parameters = [])
  {
    $instance = parent::make($name, $parameters);

    if (isset($this->extenders[$name])) {
      // 调用扩展器
      foreach ($this->extenders[$name] as $extender) {
        $instance = $extender($instance);
      }
    }

    return $instance;
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Redis
{
  public $name;

  public function __construct($name = 'default')
  {
    $this->name = $name;
  }

  public function setName($name)
  {
    $this->name = $name;
  }
}

$container = new ExtendContainer();

// 绑定Redis服务
$container->singleton(Redis::class, function () {
  return new Redis();
});

// 给Redis服务绑定一个扩展器
$container->extend(Redis::class, function (Redis $redis) {
  $redis->setName('扩展器');
  return $redis;
});
$redis = $container->make(Redis::class);
var_dump($redis->name);

9. 上下文绑定

有时侯我们可能有两个类使用同一个接口,但希望在每个类中注入不同的实现,例如两个控制器,分别为它们注入不同的Log服务。

class ApiController
{
  public function __construct(Log $log)
  {
  }
}

class WebController
{
  public function __construct(Log $log)
  {
  }
}

最终我们要用以下方式实现:

// 当ApiController依赖Log时,给它一个RedisLog
$container->addContextualBinding('ApiController','Log',new RedisLog());

// 当WebController依赖Log时,给它一个FileLog
$container->addContextualBinding('WebController','Log',new FileLog());

为了更直观更方便更语义化的使用,我们把这个过程改成链式操作:

$container->when('ApiController')
    ->needs('Log')
    ->give(new RedisLog());

我们增加一个$context数组,用来存储上下文。同时增加一个addContextualBinding方法,用来注册上下文绑定。以ApiController为例,$context的真实模样是:

$context['ApiController']['Log'] = new RedisLog();

然后build方法实例化类时,先从上下文中查找依赖参数,就实现了上下文绑定。

接下来,看看链式操作是如何实现的。

首先定义一个类Context,这个类有两个方法,needs和give。

然后在容器中,增加一个when方法,它返回一个Context对象。在Context对象的give方法中,我们已经具备了注册上下文所需要的所有参数,所以就可以在give方法中调用addContextualBinding来注册上下文了。

class ContextContainer extends ExtendContainer
{
  // 依赖上下文
  protected $context = [];

  // 构建一个类,并自动注入服务
  public function build($class, array $parameters = [])
  {
    $reflector = new ReflectionClass($class);

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor->getParameters() as $dependency) {

      if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) {
        // 先从上下文中查找
        $dependencies[] = $this->context[$class][$dependency->getName()];
        continue;
      }

      if (isset($parameters[$dependency->getName()])) {
        // 从自定义参数中查找
        $dependencies[] = $parameters[$dependency->getName()];
        continue;
      }

      if (is_null($dependency->getClass())) {
        // 参数类型不是类或接口时,无法从容器中获取依赖
        if ($dependency->isDefaultValueAvailable()) {
          // 查找默认值,如果有就使用默认值
          $dependencies[] = $dependency->getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency->getName());
        }
      } else {
        // 参数类型是一个类时,就用make方法构建该类
        $dependencies[] = $this->make($dependency->getClass()->name);
      }
    }

    return $reflector->newInstanceArgs($dependencies);
  }

  // 绑定上下文
  public function addContextualBinding($when, $needs, $give)
  {
    $this->context[$when][$needs] = $give;
  }

  // 支持链式方式绑定上下文
  public function when($when)
  {
    return new Context($when, $this);
  }
}

class Context
{
  protected $when;

  protected $needs;

  protected $container;

  public function __construct($when, ContextContainer $container)
  {
    $this->when = $when;
    $this->container = $container;
  }

  public function needs($needs)
  {
    $this->needs = $needs;

    return $this;
  }

  public function give($give)
  {
    // 调用容器绑定依赖上下文
    $this->container->addContextualBinding($this->when, $this->needs, $give);
  }
}

// ----------- ↓↓↓↓示例代码↓↓↓↓ ----------- //

class Dog
{
  public $name;

  public function __construct($name)
  {
    $this->name = $name;
  }
}

class Cat
{
  public $name;

  public function __construct($name)
  {
    $this->name = $name;
  }
}

$container = new ContextContainer();

// 给Dog类设置上下文绑定
$container->when(Dog::class)
  ->needs('name')
  ->give('小狗');
// 给Cat类设置上下文绑定
$container->when(Cat::class)
  ->needs('name')
  ->give('小猫');

$dog = $container->make(Dog::class);
$cat = $container->make(Cat::class);
var_dump('Dog:' . $dog->name);
var_dump('Cat:' . $cat->name);

10. 完整代码

class Container
{
  // 已绑定的服务
  protected $instances = [];
  // 已绑定的回调函数
  protected $bindings = [];
  // 服务别名
  protected $aliases = [];
  // 存放扩展器的数组
  protected $extenders = [];
  // 依赖上下文
  protected $context = [];

  // 绑定服务实例
  public function instance($name, $instance)
  {
    $this->instances[$name] = $instance;
  }

  // 绑定服务
  public function bind($name, $instance, $shared = false)
  {
    if ($instance instanceof Closure) {
      // 如果$instance是一个回调函数,就绑定到bindings。
      $this->bindings[$name] = [
        'callback' => $instance,
        // 标记是否单例
        'shared' => $shared
      ];
    } else {
      // 调用make方法,创建实例
      $this->instances[$name] = $this->make($name);
    }
  }

  // 绑定一个单例
  public function singleton($name, $instance)
  {
    $this->bind($name, $instance, true);
  }

  // 给服务绑定一个别名
  public function alias($alias, $name)
  {
    $this->aliases[$alias] = $name;
  }

  // 给服务绑定扩展器
  public function extend($name, $extender)
  {
    if (isset($this->instances[$name])) {
      // 已经实例化的服务,直接调用扩展器
      $this->instances[$name] = $extender($this->instances[$name]);
    } else {
      $this->extenders[$name][] = $extender;
    }
  }

  // 获取服务
  public function make($name, array $parameters = [])
  {
    // 先用别名查找真实服务名
    $name = isset($this->aliases[$name]) ? $this->aliases[$name] : $name;

    if (isset($this->instances[$name])) {
      return $this->instances[$name];
    }

    if (isset($this->bindings[$name])) {
      // 执行回调函数并返回
      $instance = call_user_func($this->bindings[$name]['callback']);

      if ($this->bindings[$name]['shared']) {
        // 标记为单例时,存储到服务中
        $this->instances[$name] = $instance;
      }
    } else {
      // 使用build方法构建此类
      $instance = $this->build($name, $parameters);
    }

    if (isset($this->extenders[$name])) {
      // 调用扩展器
      foreach ($this->extenders[$name] as $extender) {
        $instance = $extender($instance);
      }
    }

    return $instance;
  }

  // 构建一个类,并自动注入服务
  public function build($class, array $parameters = [])
  {
    $reflector = new ReflectionClass($class);

    $constructor = $reflector->getConstructor();

    if (is_null($constructor)) {
      // 没有构造函数,直接new
      return new $class();
    }

    $dependencies = [];

    // 获取构造函数所需的参数
    foreach ($constructor->getParameters() as $dependency) {

      if (isset($this->context[$class]) && isset($this->context[$class][$dependency->getName()])) {
        // 先从上下文中查找
        $dependencies[] = $this->context[$class][$dependency->getName()];
        continue;
      }

      if (isset($parameters[$dependency->getName()])) {
        // 从自定义参数中查找
        $dependencies[] = $parameters[$dependency->getName()];
        continue;
      }

      if (is_null($dependency->getClass())) {
        // 参数类型不是类或接口时,无法从容器中获取依赖
        if ($dependency->isDefaultValueAvailable()) {
          // 查找默认值,如果有就使用默认值
          $dependencies[] = $dependency->getDefaultValue();
        } else {
          // 无法提供类所依赖的参数
          throw new Exception('找不到依赖参数:' . $dependency->getName());
        }
      } else {
        // 参数类型是一个类时,就用make方法构建该类
        $dependencies[] = $this->make($dependency->getClass()->name);
      }
    }

    return $reflector->newInstanceArgs($dependencies);
  }

  // 绑定上下文
  public function addContextualBinding($when, $needs, $give)
  {
    $this->context[$when][$needs] = $give;
  }

  // 支持链式方式绑定上下文
  public function when($when)
  {
    return new Context($when, $this);
  }
}

class Context
{
  protected $when;

  protected $needs;

  protected $container;

  public function __construct($when, Container $container)
  {
    $this->when = $when;
    $this->container = $container;
  }

  public function needs($needs)
  {
    $this->needs = $needs;

    return $this;
  }

  public function give($give)
  {
    // 调用容器绑定依赖上下文
    $this->container->addContextualBinding($this->when, $this->needs, $give);
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
超级简单的发送邮件程序
Oct 09 PHP
PHP 数据库树的遍历方法
Feb 06 PHP
PHP下编码转换函数mb_convert_encoding与iconv的使用说明
Dec 16 PHP
PHP 5.5 创建和验证哈希最简单的方法详解
Nov 07 PHP
PHP小教程之实现双向链表
Jun 12 PHP
Laravel 5框架学习之Laravel入门和新建项目
Apr 07 PHP
用PHP代码在网页上生成图片
Jul 01 PHP
使用图灵api创建微信聊天机器人
Jul 23 PHP
实例详解PHP中html word 互转的方法
Jan 28 PHP
使用PHP+MySql实现微信投票功能实例代码
Sep 29 PHP
阿里云的WindowsServer2016上部署php+apache
Jul 17 PHP
thinkPHP5框架分页样式类完整示例
Sep 01 PHP
php xhprof使用实例详解
Apr 15 #PHP
PHP+swoole+linux实现系统监控和性能优化操作示例
Apr 15 #PHP
vmware linux系统安装最新的php7图解
Apr 14 #PHP
php7新特性的理解和比较总结
Apr 14 #PHP
PHP7新功能总结
Apr 14 #PHP
PHP7内核CGI与FastCGI详解
Apr 14 #PHP
Codeigniter里的无刷新上传的实现代码
Apr 14 #PHP
You might like
让ThinkPHP支持大小写url地址访问的方法
2014/10/31 PHP
用PHP的反射实现委托模式的讲解
2019/03/22 PHP
js 火狐下取本地路径实现思路
2013/04/02 Javascript
浅析JQuery获取和设置Select选项的常用方法总结
2013/07/04 Javascript
js渐变显示渐变消失示例代码
2013/08/01 Javascript
JavaScript运行机制之事件循环(Event Loop)详解
2014/10/10 Javascript
js实现禁止中文输入的方法
2015/01/14 Javascript
jQuery常用数据处理方法小结
2015/02/20 Javascript
js实现绿白相间竖向网页百叶窗动画切换效果
2015/03/02 Javascript
JS模仿编辑器实时改变文本框宽度和高度大小的方法
2015/08/17 Javascript
jquery实现转盘抽奖功能
2017/01/06 Javascript
js放大镜放大购物图片效果
2017/01/18 Javascript
利用jQuery实现滑动开关按钮效果(附demo源码下载)
2017/02/07 Javascript
jQuery动画_动力节点节点Java学院整理
2017/07/04 jQuery
详解在WebStorm中添加Vue.js单文件组件的高亮及语法支持
2017/10/21 Javascript
js实现动态改变radio状态的方法
2018/02/28 Javascript
JS模拟实现哈希表及应用详解
2018/05/04 Javascript
angular2 ng2-file-upload上传示例代码
2018/08/23 Javascript
node.JS二进制操作模块buffer对象使用方法详解
2020/02/06 Javascript
编写一个javascript元循环求值器的方法
2020/04/14 Javascript
线程和进程的区别及Python代码实例
2015/02/04 Python
详解Python 数据库的Connection、Cursor两大对象
2018/06/25 Python
用Python实现读写锁的示例代码
2018/11/05 Python
python 用户交互输入input的4种用法详解
2019/09/24 Python
Pytorch 实现focal_loss 多类别和二分类示例
2020/01/14 Python
Python3读写Excel文件(使用xlrd,xlsxwriter,openpyxl3种方式读写实例与优劣)
2020/02/13 Python
pycharm 激活码及使用方式的详细教程
2020/05/12 Python
解决Python Matplotlib绘图数据点位置错乱问题
2020/05/16 Python
详解pycharm配置python解释器的问题
2020/10/15 Python
python中pyplot基础图标函数整理
2020/11/10 Python
美国快时尚彩妆品牌:Winky Lux(透明花瓣润唇膏)
2018/11/06 全球购物
美国排名第一的葡萄酒俱乐部:Firstleaf Wine Club
2020/01/02 全球购物
Silk Therapeutics官网:清洁、抗衰老护肤品
2020/08/12 全球购物
GWT (Google Web Toolkit)有哪些主要的原件组成?
2015/06/08 面试题
会计电算一体化个人简历的自我评价
2013/10/15 职场文书
高效笔记技巧分享:学会这些让你不再困扰
2019/09/04 职场文书