详解如何实现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 相关文章推荐
用PHP和ACCESS写聊天室(七)
Oct 09 PHP
PHP中防止SQL注入攻击和XSS攻击的两个简单方法
Apr 15 PHP
解析ajax事件的调用顺序
Jun 17 PHP
php从数组中随机选择若干不重复元素的方法
Mar 14 PHP
php开发中的页面跳转方法总结
Apr 26 PHP
PHP使用星号替代用户名手机和邮箱的实现代码
Feb 07 PHP
PHP实现的数组和XML文件相互转换功能示例
Mar 15 PHP
PHP PDOStatement::bindParam讲解
Jan 30 PHP
PHP实现的62进制转10进制,10进制转62进制函数示例
Jun 06 PHP
php判断目录存在的简单方法
Sep 26 PHP
在Laravel 的 Blade 模版中实现定义变量
Oct 14 PHP
ThinkPHP类似AOP思想的参数验证的实现方法
Dec 18 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
解析CodeIgniter自定义配置文件
2013/06/18 PHP
php 实现简单的登录功能示例【基于thinkPHP框架】
2019/12/02 PHP
jquery 框架使用教程 AJAX篇
2009/10/11 Javascript
javascript学习笔记(九)javascript中的原型(prototype)及原型链的继承方式
2011/04/12 Javascript
关于extjs treepanel复选框选中父节点与子节点的问题
2013/04/02 Javascript
javascript使用数组的push方法完成快速排序
2014/09/15 Javascript
兼容主流浏览器的jQuery+CSS 实现遮罩层的简单代码
2014/10/14 Javascript
fckeditor粘贴Word时弹出窗口取消的方法
2014/10/30 Javascript
前端面试知识点锦集(JavaScript篇)
2016/12/28 Javascript
JavaScript字符集编码与解码详谈
2017/02/02 Javascript
Vue.js实例方法之生命周期详解
2017/07/03 Javascript
基于jquery实现五星好评
2017/11/18 jQuery
详解html-webpack-plugin用法全解
2018/01/22 Javascript
在vscode中统一vue编码风格的方法
2018/02/22 Javascript
js实现下拉框二级联动
2018/12/04 Javascript
详解Vue.js v-for不支持IE9的解决方法
2018/12/29 Javascript
微信小程序授权登录解决方案的代码实例(含未通过授权解决方案)
2019/05/10 Javascript
react-native聊天室|RN版聊天App仿微信实例|RN仿微信界面
2019/11/12 Javascript
vue-cli4.x创建企业级项目的方法步骤
2020/06/18 Javascript
Vue Render函数原理及代码实例解析
2020/07/30 Javascript
Vue使用轮询定时发送请求代码
2020/08/10 Javascript
js实现菜单跳转效果
2020/12/11 Javascript
Python读取网页内容的方法
2015/07/30 Python
python根据京东商品url获取产品价格
2015/08/09 Python
利用Python如何生成随机密码
2016/04/20 Python
Python3实现发送QQ邮件功能(附件)
2020/12/23 Python
python编程培训 python培训靠谱吗
2018/01/17 Python
pyqt5 使用label控件实时显示时间的实例
2019/06/14 Python
在django中,关于session的通用设置方法
2019/08/06 Python
Python django框架输入汉字,数字,字符生成二维码实现详解
2019/09/24 Python
浅析pip安装第三方库及pycharm中导入第三方库的问题
2020/03/10 Python
解析HTML5中的新功能本地存储localStorage
2016/03/01 HTML / CSS
当文件系统受到破坏时,如何检查和修复系统?
2012/03/09 面试题
大学生旷课检讨书
2014/01/22 职场文书
2016年社区“我们的节日·中秋节”活动总结
2016/04/05 职场文书
《黑岩★★射手 DAWN FALL》BD发售宣传CM公开
2022/04/04 日漫