zoukankan      html  css  js  c++  java
  • thinkphp5 自动注册Hook机制钩子扩展

    Hook.php 文件已更新
    1、修复在linux环境下类的 在basename 下无法获取到类名的问题
    2、修复linux 环境下无法使用hook::call 调用失败问题

    请先安装thinkphp5,包里的文件可直接使用,
    application ags.php、applicationindexcontrollerIndex.php 文件都已经把文件名改成tags.php.bak,Index.php.bak

    钩子机制我这里分为两种使用方法,注:二者只能选其一
    1、采用thinkphp5内置的钩子调用方案。 配置参数 jntoo_hook_call 为true 情况下才有效,为了兼容已经写好了钩子机制的使用
    2、采用自有的钩子调用方案,此方法支持钩子分类,钩子权重自动排序

    个人不建议使用think内置钩子作为系统的扩展。

    如何写自动加载文件:
    在模块下新建一个hook目录,并在目录下创建Category.php文件,如下图:


    类名等于钩子类型,方法名等于钩子名称。(注:类型只存在于钩子机制方法2。方法1只有方法名生效)
    如图所示:

    我们创建了 Category 类型的钩子
    里面有两个方法:
    index 和 index_5

    方法1的调用方法: hinkHook::listen('index'); 就可以直接调用到Category 类 index 方法。
    输出结果:


    方法2的调用方法:appcommonHook::call('Category','index'); 就可以调用到 Category 类 index 方法和index_5 方法
    输出结果:


    方法2为什么能 调用Category.index 钩子时会调用到Category.index_5 这个钩子呢?这个我们后面进行一个讲解。


    讲个小故事
    有一天客户说要求实现一个会员的注册模块。
    我这里键盘敲了半天完成了代码会员的注册模块。
    第二天客户说“现在客户注册没问题了,我想能不能在注册完成后给他添加50积分,让他能进行一些消费。”,我说“没问题”
    敲敲敲半天代码我完成了客户的要求。现在注册会员后,有赠送50积分了。
    第三天同事问我你前两天不是在系统里弄了个注册模块和赠送积分?我说是啊。同事说“把你这个提交一下svn库,我这边也有个客户想弄个注册模块和赠送积分,还要添加发送邮件验证”,我:“好的,不过你要自己添加发送邮件验证。”代码提交到svn库,
    同事敲了半天代码,完成了发送邮件。
    第四天我找同事让他提交一下SVN库,让我进行一下代码同步。
    第五天另一个客户问我“你们的注册模块有没有会员注册验证手机”,这个我当然只能说有(实际上是没有短信认证),客户说”能装好环境让我测试一下?“,我只能说:“抱歉先生,我们现在还没装好环境,这个我明天给您装好,您在测试一下”,客户说“好”
    蹭蹭蹭敲了半天代码在会员注册模块上面加了会员注册手机验证。
    第六天客户觉得挺好的。
    故事讲到这里。
    假设以上会员注册模块的形成是这样的一段代码片段:

    1. namespace appindexcontroller;
    2. class member{
    3.        function register($username , $password , $tel , $email){
    4.               // 第五天加的代码:
    5.               if(是否短信验证)
    6.                      VerifTel($tel , 验证码);
    7.               // 第一天写的代码:
    8.               if(table('member')->where('username' , $username)->count())
    9.               {
    10.                      $this->error = '用户名已存在'
    11.                      return false;
    12.               }
    13.               $uid = 写入数据库得到新的用户UID
    14.               ....省略大部分代码
    15.              
    16.               // 第二天加的代码:
    17.               if(赠送积分)
    18.                     sendPoints($uid . 赠送积分); // 发送积分50
    19.               // 第三天
    20.              if(发送邮件)
    21.                    sendEmail($uid , 邮件);
    22.              
    23.              return true;
    24.        }
    25. }
    复制代码

    从以上我们可以看出,当写完一个功能模块的时候,并不知道客户一些需求的情况,我们会在当前模块中加入代码。
    如果一直下去加下去,回头看代码的时候你会发现你的代码实在是太多了。
    这个代码是不是可以变得优雅一些

    如果我们在这个代码中加入钩子机制会是怎么样的呢?
    看如下代码

    1. namespace appindexcontroller;
    2. class member{
    3.        function register($username , $password , $tel , $email){
    4.               
    5.               // 我不知道以后会变成什么样,先加个前置钩子,参数:$username,$password,$tel,$email
    6.               $param = [ $username,$password,$tel,$email ];
    7.               appcommonHook::call('member','registerPre',$param);
    8.                
    9.               // 第一天写的代码:
    10.               if(table('member')->where('username' , $username)->count())
    11.               {
    12.                      $this->error = '用户名已存在'
    13.                      return false;
    14.               }
    15.               $uid = 写入数据库得到新的用户UID
    16.               ....省略大部分代码
    17.              
    18.              // 我不知道以后会变成什么样,先加个注册成功后的钩子,参数 $uid
    19.               $param = [ $uid ];
    20.               appcommonHook::call('member','registerSuccess',$param);
    21.               
    22.              return true;
    23.        }
    24. }
    复制代码

    那么问题来了我们怎么去解决钩子的问题?
    假设我们的类都已经注册进了钩子里:
    我们来完成第二天的事情,如下代码:

    1. namespace appindexhook;
    2. class member{
    3.        // 钩子类型,挂入 member 类型钩子,中的registerSuccess钩子
    4.        function registerSuccess($uid)
    5.        {
    6.               if(赠送积分)
    7.                     sendPoints($uid . 赠送积分); // 发送积分50
    8.        }
    9. }
    复制代码

    第三天的时候同事要用我的代码怎么办?那么我会告诉他:“我这边已经下了两个钩子,一个是注册前置钩子,一个是注册成功后的钩子”并告诉他钩子类型,名称,还有相应的参数,同事的代码将变成这样

    1. namespace app ongshihook;
    2. class member{
    3.        // 钩子类型,挂入 member 类型钩子,中的registerSuccess钩子
    4.        function registerSuccess($uid)
    5.        {
    6.               if(发送邮件)
    7.                    sendEmail($uid , 邮件);
    8.        }
    9. }
    复制代码

    第五天的时候这样的:

    1. namespace appindexhook;
    2. class member{
    3.        // 挂入 member 类型钩子,中的registerSuccess钩子
    4.        function registerSuccess($uid)
    5.        {
    6.               if(赠送积分)
    7.                     sendPoints($uid . 赠送积分); // 发送积分50
    8.        }
    9.        // 挂入member 类型钩子中的registerPre钩子
    10.        function registerPre()
    11.        {
    12.               if(是否短信验证)
    13.                      VerifTel($tel , 验证码);
    14.        }
    15. }
    复制代码

    回到我们上面讲的“为什么能 调用Category.index 钩子时会调用到Category.index_5 这个钩子呢?”

    在钩子机制设计时想到钩子有可能有先后顺序,而设计了一个能写入权重排序的数字,在尾部添加数字,排序从小到大排序,实际的钩子注册会自动删除尾部的“_数字”:



    一:先来讲讲钩子机制
    在项目代码中,你认为要扩展(暂时不扩展)的地方放置一个钩子函数,等需要扩展的时候,把需要实现的类和函数挂载到这个钩子上,就可以实现扩展了。
    思想就是这样听起来比较笼统。
    在二次开发别人写的代码时,如果有钩子机制,作者会告诉你哪些地方给下了钩子,当你扩展时就可以在不改动原代码的情况下进行一个升级扩展。


    本插件扩展配置参数信息:

    1. 1jntoo_hook_cache
    2.      逻辑值,是否开启钩子编译缓存,开启后只需要编译一次,以后都将成为惰性加载,如果安装了新的钩子,需要先调用Hook::clearCache() 清除缓存
    3. 2jntoo_hook_call
    4.      逻辑值,是否使用think钩子系统。值为真使用think钩子后,将无法使用权重排序和钩子自动分类,只有类中的方法将自动注册到think钩子机制中
    5. 3jntoo_hook_path
    6.      某个文件夹下的PHP文件都自动为其注册钩子
    7.      配置实现
    8.      jntoo_hook_path => [
    9.            [
    10.                  'path'=>'你的路径', // 路径尾部必须加斜杠 "/"
    11.                  'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\module\\hook\\([0-9a-zA-Z_]+)/'
    12.            ],
    13.            ....
    14.     ]
    15. 4jntoo_hook_plugin
    16.      多模块目录自动编译,在模块文件夹下加入hook目录,此目录下的php文件会自动注册钩子
    17.      配置实现:
    18.      'jntoo_hook_plugin' => [
    19.             [
    20.                  'path'=>'你的app模块路径',
    21.                  'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\([0-9a-zA-Z_]+)\\hook\\([0-9a-zA-Z_]+)/'
    22.            ],
    23.            ....
    24.      ]
    复制代码


    请在application/tags.php
    'app_init'行为处加上:'\app\common\Hook'
    例如:
    // 应用行为扩展定义文件
    return [
    // 应用初始化
    'app_init' => [
    '\app\common\Hook'
    ],
    // 应用开始
    'app_begin' => [],
    // 模块初始化
    'module_init' => [],
    // 操作开始执行
    'action_begin' => [],
    // 视图内容过滤
    'view_filter' => [],
    // 日志写入
    'log_write' => [],
    // 应用结束
    'app_end' => [],
    ];





    请自行创建文件:

    applicationindexcontrollerIndex.php下钩子文件

    1. namespace appindexcontroller;
    2. use appcommonHook;
    3. use thinkHook AS thinkHook;
    4. class Index
    5. {
    6.     public function index()
    7.     {
    8.         Hook::call('Category' , 'index');
    9.     }
    10. }
    复制代码

    applicationindexhookCategory.php钩子文件

    1. <?php
    2. /**
    3.  * Created by PhpStorm.
    4.  * User: JnToo
    5.  * Date: 2016/11/12
    6.  * Time: 1:11
    7.  */
    8. namespace appindexhook;
    9. class Category
    10. {
    11.     function index()
    12.     {
    13.         echo '我是Category类型钩子中的index方法<br>';
    14.     }
    15.     function index_5()
    16.     {
    17.         echo '我是Category类型钩子中的index方法,我的权重比较低<br>';
    18.     }
    19. }
    复制代码

    applicationcommonhook.php主文件

    1. <?php
    2. /**
    3.  * Created by PhpStorm.
    4.  * User: JnToo
    5.  * Date: 2016/11/11
    6.  * Time: 22:57
    7.  */
    8. namespace appcommon;
    9. use thinkConfig;
    10. use thinkHook as thinkHook;
    11. use thinkCache;
    12. /**
    13.  * 请在application/tags.php
    14.  * 'app_init'行为处加上:'\app\common\Hook'
    15.  * 例如:
    16.  * // 应用行为扩展定义文件
    17. return [
    18. // 应用初始化
    19. 'app_init'     => [
    20. '\app\common\Hook'
    21. ],
    22. // 应用开始
    23. 'app_begin'    => [],
    24. // 模块初始化
    25. 'module_init'  => [],
    26. // 操作开始执行
    27. 'action_begin' => [],
    28. // 视图内容过滤
    29. 'view_filter'  => [],
    30. // 日志写入
    31. 'log_write'    => [],
    32. // 应用结束
    33. 'app_end'      => [],
    34. ];
    35.  * Class Hook
    36.  * @package appcommon
    37.  */
    38. class Hook
    39. {
    40.     /**
    41.      * 编译钩子时使用计数器
    42.      * @var int
    43.      */
    44.     static protected $index = 0;
    45.     /**
    46.      * 添加引用计数
    47.      * @var int
    48.      */
    49.     static protected $indexAdd = 1;
    50.     /**
    51.      * 已编译好的钩子列表
    52.      * @var array
    53.      */
    54.     static protected $hookList = array();
    55.     /**
    56.      * application/config.php 文件中加入如下的配置信息
    57.      * @var array
    58.      */
    59.     static protected $default =[
    60.         // 是否开启钩子编译缓存,开启后只需要编译一次,以后都将成为惰性加载,如果安装了新的钩子,需要先调用Hook::clearCache() 清除缓存
    61.         'jntoo_hook_cache'=>false,
    62.         // 钩子是否使用think钩子系统
    63.         'jntoo_hook_call'=>false ,
    64.         /**
    65.          * 某个文件夹下hook加载,配置文件方法实现
    66.          * jntoo_hook_path => [
    67.          *     [
    68.          *          'path'=>'你的路径', // 路径尾部必须加斜杠 "/"
    69.          *          'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\module\\hook\\([0-9a-zA-Z_]+)/'
    70.          *     ],
    71.          *     ....
    72.          * ]
    73.          */
    74.         'jntoo_hook_plugin'=>[],
    75.         /**
    76.          *  多模块目录下自动搜索,配置文件方法实现
    77.          * 'jntoo_hook_plugin' => [
    78.          *     [,
    79.          *          'path'=>'你的app模块路径'
    80.          *          'pattern'=> '规则,类的匹配规则' 例如:'/plugin\\([0-9a-zA-Z_]+)\\hook\\([0-9a-zA-Z_]+)/'
    81.          *     ],
    82.          *     ....
    83.          * ]
    84.          */
    85.         'jntoo_hook_plugin'=>[],
    86.     ];
    87.     /**
    88.      * 提供行为调用
    89.      */
    90.     public function run()
    91.     {
    92.         self::init();
    93.     }
    94.     /**
    95.      * 注册钩子
    96.      * @param $type 钩子类型
    97.      * @param $name 钩子名称
    98.      * @param $param Closure|array
    99.      */
    100.     static public function add($type , $name , $param , $listorder = 1)
    101.     {
    102.         $key = strtolower($type .'_'.$name);
    103.         isset(self::$hookList[$key]) or self::$hookList[$key] = [];
    104.         self::$hookList[$key][$listorder.'_'.self::$indexAdd++] = $param;
    105.         ksort(self::$hookList[$key]);
    106.         // 兼容
    107.         if(Config::get('jntoo_hook_call'))
    108.         {
    109.             thinkHook::add($name , $param);
    110.         }
    111.         return;
    112.     }
    113.     /**
    114.      * 清除编译钩子的缓存
    115.      */
    116.     static public function clearCache()
    117.     {
    118.         // 清楚编译钩子缓存
    119.         if(Config::get('jntoo_hook_cache')){
    120.             cache('jntoo_hook_cache' , null);
    121.         }
    122.     }
    123.     /**
    124.      * 执行钩子
    125.      * @param $type string
    126.      * @param $name string
    127.      * @param array $array
    128.      * @param mixe
    129.      */
    130.     static public function call($type , $name , &$array = array())
    131.     {
    132.         static $_cls = array();
    133.         $ret = '';
    134.         if(Config::get('jntoo_hook_call')){
    135.             return thinkHook::listen($name , $array);
    136.         }else{
    137.             $key = strtolower($type.'_'.$name);
    138.             // 自有的调用方案
    139.             if(isset(self::$hookList[$key]))
    140.             {
    141.                 foreach(self::$hookList[$key] as $r){
    142.                     // 闭包处理
    143.                     $result = '';
    144.                     if(is_callable($r)){
    145.                         $result = call_user_func_array($r, $array);
    146.                     }elseif(is_object($r)){
    147.                         // 自己定义对象钩子
    148.                         if(method_exists($r , $name)){
    149.                             $result = call_user_func_array(array($r , $name), $array);
    150.                         }
    151.                     }else{
    152.                         // 自动搜索出来的钩子
    153.                         $class = $r['class'];
    154.                         if(class_exists($class , false)){
    155.                             // 如果不存在
    156.                             if($r['filename'])require_once(ROOT_PATH.$r['filename']);
    157.                         }
    158.                         if(class_exists($class , false)){
    159.                             if(!isset($_cls[$class])){
    160.                                 $_cls[$class] = new $class();
    161.                             }
    162.                             $func = $r['func'];
    163.                             $result = call_user_func_array(array($_cls[$class] , $func), $array);
    164.                         }
    165.                     }
    166.                     if($result)$ret.=$result;
    167.                 }
    168.             }
    169.         }
    170.         return $ret;
    171.     }
    172.     /**
    173.      * 初始化钩子
    174.      */
    175.     static protected function init()
    176.     {
    177.         // 取钩子的缓存
    178.         self::$hookList = self::getCache();
    179.         if(!self::$hookList)
    180.         {
    181.             // 保存在当前变量中
    182.             $saveArray = [];
    183.             // 钩子不存在,先搜索app目录下的模块
    184.             //echo APP_PATH;
    185.             //echo ROOT_PATH;
    186.             $result = self::searchDir(APP_PATH);
    187.             // 先编译此模块
    188.             self::compileHook($result , '/app\\([0-9a-zA-Z_]+)\\hook\\([0-9a-zA-Z_]+)/' , $saveArray);
    189.             //print_r($saveArray);
    190.             // 多模块实现搜索加载
    191.             $jntooHook = Config::get('jntoo_hook_plugin');
    192.             if($jntooHook){
    193.                 foreach($jntooHook as $t){
    194.                     $result = self::searchDir($t['path']);
    195.                     self::compileHook($result , $t['pattern'] , $saveArray);
    196.                 }
    197.             }
    198.             // 单个路径的模块搜索
    199.             $jntooHook = Config::get('jntoo_hook_path');
    200.             if($jntooHook){
    201.                 foreach($jntooHook as $t){
    202.                     $result = [];
    203.                     self::searchHook($t['path'] , $result);
    204.                     self::compileHook($result , $saveArray);
    205.                 }
    206.             }
    207.             // 编译完成,现在进行一个权重排序
    208.             foreach($saveArray as $k=>$t){
    209.                 ksort($saveArray[$k]);
    210.             }
    211.             self::setCache($saveArray);
    212.             self::$hookList = $saveArray;
    213.         }
    214.         //print_r(self::$hookList);
    215.         $calltype = Config::get('jntoo_hook_call');
    216.         // 检测他的调用方法,是否需要注册到think中,不建议注册到 think 中,
    217.         // 因为这个系统含有分类的形式,注册进去后将无法使用排序功能
    218.         if($calltype){
    219.             // 注册进think 钩子中
    220.             self::registorThink();
    221.         }else{
    222.             // 注册系统行为钩子
    223.             self::registorCall();
    224.         }
    225.     }
    226.     /**
    227.      * 注册系统行为调用
    228.      */
    229.     static protected function registorCall()
    230.     {
    231.         thinkHook::add('app_init' , function( &$params = null ){
    232.             $arg = [&$params];
    233.             Hook::call('system' , 'app_init' , $arg);
    234.         });
    235.         thinkHook::add('app_begin' , function( &$params = null ){
    236.             $arg = [&$params];
    237.             Hook::call('system' , 'app_begin' , $arg);
    238.         });
    239.         thinkHook::add('module_init' , function( &$params = null ){
    240.             $arg = [&$params];
    241.             Hook::call('system' , 'module_init' , $arg);
    242.         });
    243.         thinkHook::add('action_begin' , function( &$params = null ){
    244.             $arg = [&$params];
    245.             Hook::call('system' , 'action_begin' , $arg);
    246.         });
    247.         thinkHook::add('view_filter' , function( &$params = null ){
    248.             $arg = [&$params];
    249.             Hook::call('system' , 'view_filter' , $arg);
    250.         });
    251.         thinkHook::add('app_end' , function( &$params = null ){
    252.             $arg = [&$params];
    253.             Hook::call('system' , 'app_end' , $arg);
    254.         });
    255.         thinkHook::add('log_write' , function( &$params = null ){
    256.             $arg = [&$params];
    257.             Hook::call('system' , 'log_write' , $arg);
    258.         });
    259.         thinkHook::add('response_end' , function( &$params = null ){
    260.             $arg = [&$params];
    261.             Hook::call('system' , 'response_end' , $arg);
    262.         });
    263.     }
    264.     /**
    265.      * 将钩子注册进thinkHook 钩子中
    266.      */
    267.     static protected function registorThink()
    268.     {
    269.         foreach(self::$hookList as $key=>$list)
    270.         {
    271.             foreach($list as $r){
    272.                 thinkHook::add($r['func'] , $r['class']);
    273.             }
    274.         }
    275.     }
    276.     /**
    277.      * 搜索目录下的钩子文件
    278.      * @param $path string
    279.      * @param $saveArray array 保存的文件路径
    280.      * @return null
    281.      */
    282.     static protected function searchHook( $path , &$saveArray)
    283.     {
    284.         $fp = opendir($path);
    285.         if($fp){
    286.             while($file = readdir($fp))
    287.             {
    288.                 if(substr($file , -4) == '.php')
    289.                 {
    290.                     $saveArray[] = $path.$file;
    291.                 }
    292.             }
    293.         }
    294.     }
    295.     /**
    296.      * 编译钩子,编译后直接保存在静态成员变量 self::$hookList
    297.      * @param $filelist array 文件路径
    298.      * @param $namespace string 命名空间规则
    299.      * @param $saveHook array 保存Hook
    300.      * @return null
    301.      */
    302.     static protected function compileHook($filelist , $namespace , &$saveHook)
    303.     {
    304.         $root_path = strtr(ROOT_PATH,'\' , '/');
    305.         //print_r($filelist);
    306.         // 当前引用计数
    307.         $index = self::$index;
    308.         $indexAdd = self::$indexAdd;
    309.         foreach ($filelist as $file)
    310.         {
    311.             require_once($file);
    312.             // 获取已经加载的类
    313.             $class_list = get_declared_classes();
    314.             // 搜索计数器
    315.             for($len = count($class_list);$index<$len;$index++)
    316.             {
    317.                 $classname = $class_list[$index];
    318.                 if(preg_match($namespace , $classname))
    319.                 {
    320.                     // 这个类满足我们的需求
    321.                     $ec = new ReflectionClass($classname);
    322.                     // 钩子的类型
    323.                     $type = basename(strtr($classname , '\' , '/'));
    324.                     foreach($ec->getMethods() as $r){
    325.                         if($r->name[0] != '_' && $r->class == $classname){
    326.                             // 暂时还不知道怎么实现排序 方法名后面有
    327.                             $name = $r->name;
    328.                             $listorder = 1;
    329.                             if(strpos($name , '_') !== false){
    330.                                 // 存在排序
    331.                                 $temp = explode('_',$name);
    332.                                 $num  = array_pop($temp);
    333.                                 if(is_numeric($num)){
    334.                                     $name = implode('_' , $temp);
    335.                                     $listorder = $num;
    336.                                 }
    337.                             }
    338.                             $typename = strtolower($type.'_'.$name);
    339.                             !isset($saveHook[$typename]) AND $saveHook[$typename] = [];
    340.                             $saveHook[$typename][$listorder.'_'.$indexAdd++] = [
    341.                                 'filename'=>str_replace($root_path,'',$file), // 保存文件路径的好处是方便快速加载,无需在进行路径的查找
    342.                                 'class'=>$classname, // 保存类的名称
    343.                                 'func'=>$r->name,   // 保存方法名
    344.                                 'listorder'=>$listorder // 排序,编译完成后,进行一个权重的排序
    345.                             ];
    346.                         }
    347.                     }
    348.                 }
    349.             }
    350.         }
    351.         self::$index = $index;
    352.         self::$indexAdd = $indexAdd;
    353.     }
    354.     /**
    355.      * @param $path 搜索模块路径
    356.      * @return array
    357.      */
    358.     static protected function searchDir( $path )
    359.     {
    360.         // 目录自动补全
    361.         $path = strtr(realpath($path),'\' , '/');
    362.         $char = substr($path,-1);
    363.         if( $char != '/' || $char != '\' ){
    364.             $path .= '/';
    365.         }
    366.         $path .= '*';
    367.         $dirs = glob($path, GLOB_ONLYDIR );
    368.         $result = array();
    369.         foreach($dirs as $dir){
    370.             if(is_dir($dir .'/hook'))
    371.             {
    372.                 self::searchHook($dir .'/hook/' , $result);
    373.             }
    374.         }
    375.         return $result;
    376.     }
    377.     /**
    378.      * 获取编译好的钩子
    379.      * @return bool|array
    380.      */
    381.     static protected function getCache()
    382.     {
    383.         if(Config::get('jntoo_hook_cache')){
    384.             // 获取缓存
    385.             return cache('jntoo_hook_cache');
    386.         }
    387.         return false;
    388.     }
    389.     /**
    390.      * 保存编译的缓存
    391.      * @param $value array
    392.      * @return bool
    393.      */
    394.     static protected function setCache( $value )
    395.     {
    396.         // 设置为永久缓存
    397.         if(Config::get('jntoo_hook_cache')){
    398.             cache('jntoo_hook_cache' , $value , null);
    399.         }
    400.         return true;
    401.     }
    402. }
  • 相关阅读:
    Docker----mysql数据持久化
    Docker-----容器数据卷
    Docker-----制造自己的镜像
    Docker----常用命令(镜像和容器的常用命令)
    Docker----阿里云镜像加速
    Docker概述
    com.aliyuncs.exceptions.ClientException: InvalidVersion : Specified parameter Version is not valid.
    夜神模拟器的使用踩坑--adb devices无法连接设备
    python利用smtplib模块发送邮件
    appium环境安装说明
  • 原文地址:https://www.cnblogs.com/liliuguang/p/10857501.html
Copyright © 2011-2022 走看看