Laravel-admin 在 Laravels 运行的的一些问题解析——记一次因为莽而付出的代价

初生牛犊不怕虎,直接莽上laravels

前因

peJ1oswJgm.jpg!large

我们公司一直都是采用 think3.2 ,虽然使用起来确实不错,但是哪能有 laravel 舒服

于是我才用 laravel ,但是经理觉得后台太慢了

LfN53diEDf.jpg!large

我做了一部分优化还是慢,我太难了,之前他让我随便选框架

tyio36wkkD.jpg!large 我就想到了大佬的 [laravels](https://github.com/hhxsv5/laravel-s) 心里想laravels+laravel-admin岂不是美哉

结果一使用出大问题

发现问题,问题介绍

前端问题laravel-s已提供Cleaner,请参阅文档使用

  1. 第一个问题就是比较常见的,顶部的刷新无限增多,如图

laravel-admin在Laravels运行的的一些问题解析——记一次因为莽而付出的代价

  1. 第二个问题就是,删除这个却进入上一个进入的删除路由,导致各种删除失败

laravel-admin在Laravels运行的的一些问题解析——记一次因为莽而付出的代价

  1. 导出excel提示exit
  2. Pjax中间件提示exit

解决问题,提供思路

授人以鱼不如授人以渔

我们先排查第一个问题,这个比较常见,其实就是我们的Admin实例并没有被清除,导致无限添加navBar

GPi9iLMekg.png!large

然后在 admin/bootstrap.php中每次都调用一下

于是我想到第一个办法,这个方法比较 憨批 ,因为当时认为这个实例重新创建应该挺麻烦,所以采用局部清理的方式

解决问题之后测试删除功能,又发现新的问题,也就是问题2

laravel-admin在Laravels运行的的一些问题解析——记一次因为莽而付出的代价

laravel-admin在Laravels运行的的一些问题解析——记一次因为莽而付出的代价

这个问题很明显是实例没清理干净,导致上一步生成的js下一步仍然存在,排除在控制器生成的可能,
所以初步判断只有可能存在于\Encore\Admin\Admin实例中,因为这个实例是存在于容器中
所以直接从中寻找,发现原来是\Encore\Admin\Admin静态变量

//因为是表单所以我们先进入 Encore\Admin\Form\Form 寻找Tool 发现renderDelete 
 protected function renderDelete()
 {
 $trans = [
  'delete_confirm' => trans('admin.delete_confirm'),
  'confirm' => trans('admin.confirm'),
  'cancel' => trans('admin.cancel'),
  'delete' => trans('admin.delete'),
 ];
    .
    .
    .

  Admin::script($script);

  return <<<HTML
<div class="btn-group pull-right" style="margin-right: 5px">
 <a href="javascript:void(0);" class="btn btn-sm btn-danger {$class}-delete" title="{$trans['delete']}">
 <i class="fa fa-trash"></i><span class="hidden-xs"> {$trans['delete']}</span>
 </a>\</div>
HTML;
 }

看到 Admin::script 这个原来是在Admin 下的 HasAssets


/**
 * @param string $script
 * @param bool   $deferred
 * @return array|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
 */
 public static function script($script = '', $deferred = false)
{
  if (!empty($script)) {
         if ($deferred) {
              return self::$deferredScript = array_merge(self::$deferredScript, (array) $script);
         }
          return self::$script = array_merge(self::$script, (array) $script);
     }
      $script = array_unique(array_merge(static::$script, static::$deferredScript));

  return view('admin::partials.script', compact('script'));
}

那么解决起来就狠方便了 直接在bootstrap中初始化一下这些静态变量即可,尽量少改源代码

所以自己实现一个clean , laravel-s已提供Cleaner这里仅供参考

        \Encore\Admin\Admin::$script=[];
        \Encore\Admin\Admin::$deferredScript=[];
        \Encore\Admin\Admin::$headerJs = [];
        \Encore\Admin\Admin::$manifestData = [];
        \Encore\Admin\Admin::$extensions = [];
        .
        .
        .
        $app->forgetInstance(\Encore\Admin\Admin::class);
        Facade::clearResolvedInstance(\Encore\Admin\Admin::class);

JvCwNzii7a.jpg!large 由此,`问题一`和`问题二`已经基本解决

下面就是解决exit问题了,采用抛出异常的方式

Pjax中的exit

Export中

     //csv导出操作
     $res = Response::stream(function () {
            $handle = fopen('php://output', 'w');
            $titles = [];
            $this->chunk(function ($records) use ($handle, &$titles) {
                if (empty($titles)) {
                    $titles = $this->getHeaderRowFromRecords($records);
                    // Add CSV headers
                    fputcsv($handle, $titles);
                }

                foreach ($records as $record) {
                    fputcsv($handle, $this->getFormattedRecord($record));
                }
            });
            // Close the output stream
            fclose($handle);
        }, 200, $headers);
        swoole_exit($res);
    //Pjax 修改后
    $next = function () use ($response) {
              return $response;
     };

  swoole_exit((new static())->handle(Request::capture(), $next));

    //swoole_exit实际是封装的抛出异常
    if (!function_exists('swoole_exit')){
      function swoole_exit($response)
         {
             throw new App\Exceptions\SwooleExitException($response);
         }
     }

我们需要修改异常处理Handler

 <?php

namespace App\Exceptions;

use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;

class Handler extends ExceptionHandler\
{
  /**
 * A list of the exception types that are not reported.\ *\ * @var array
 */ 
 protected $dontReport = [
  SwooleExitException::class
  ];


  /**
 * A list of the inputs that are never flashed for validation exceptions.\ *\ * @var array
 */
 protected $dontFlash = [
  'password',
  'password_confirmation',
 ];
  /**
 * Report or log an exception@param \Exception  $exception\
 * @return void\
 */
 public function report(Exception $exception)
 {
     parent::report($exception);
 }
  /**
 * Render an exception into an HTTP response.\ *\ * @param \Illuminate\Http\Request  $request
 * @param \Exception  $exception
 * @return \Illuminate\Http\Response
 */
 public function render($request, Exception $exception)
 {
     //判断是否为我们的自定义异常
     if ($exception instanceof SwooleExitException) {
             //直接调用perpare
          return $exception->getResponse()->prepare($request);
     }
     return parent::render($request, $exception);
 }
}

自定义异常代码SwooleExitException

<?php

namespace App\Exceptions;

use Exception;
use Throwable;

class SwooleExitException extends Exception\
{
  protected $response;
  public function __construct($response,$message = "", $code = 0, Throwable $previous = null)
 {
 $this->response = $response;
  parent::__construct($message, $code, $previous);
 }
//获取响应内容
public function getResponse(){
  return $this->response;
}

kGTCROO7Pe.jpg!large

如果你直接把exit去掉,那么 导出csv 会提示你 oops … ob_end_clean() 这个报错

我们可以在Hhxsv5\LaravelS\Illuminate\Laravel中的 handleDynamic 加一个判断ob_get_length()也可以不加,因为后期解决exit退出问题,也就不会有这种报错

解决思路因为exit其实就是不执行后续的响应 ,所以我们想到异常就是执行到异常抛出之前,于是我们可以定义一种特定的异常,来提前结束,用来代替exit ,至于 swoole/laravels 为什么不能使用这些函数我就不在这赘述

使用异常,但是我们也得返回请求,但是我们的请求不能直接send掉,因为laravels需要使用swoole的方式返回,我们直接执行 send 是不起作用的,那我们就可以通过特定的异常返回我们所需要的响应,

1mCWY5nTni.jpg!large

public static function toResponse($request, $response)\
{
      if ($response instanceof Responsable) {
          $response = $response->toResponse($request);
     }
      if ($response instanceof PsrResponseInterface) {
          $response = (new HttpFoundationFactory)->createResponse($response);
     } elseif ($response instanceof Model && $response->wasRecentlyCreated) {
          $response = new JsonResponse($response, 201);
     } elseif (! $response instanceof SymfonyResponse &&
                      ($response instanceof Arrayable ||
                      $response instanceof Jsonable ||
                      $response instanceof ArrayObject ||
                      $response instanceof JsonSerializable ||
                      is_array($response))) {
                          $response = new JsonResponse($response);
     } elseif (! $response instanceof SymfonyResponse) {
          $response = new Response($response);
     }
     if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {
              $response->setNotModified();
    }
    return $response->prepare($request);
}

Router::toResponse(); $exception->prepare() 两种方式都可以,不过推荐使用后者

出现json格式异常/其他异常,主要是因为 其他扩展可能有替换Handler/自定义Handler没有进行处理,需要额外注意

感谢z-song提供的laravel-admin优质扩展

感谢hhxsv5提供的laravel-s优质扩展

具体分析请等下次更新,我们把laravels大致走一遍,学习一下(🕊)

本作品采用《CC 协议》,转载必须注明作者和本文链接
学习,冲冲冲~
附言 1  ·  4年前

如果使用了dingo,记得修改dingo的handle,否则会出现json的报错

附言 2  ·  4年前

LaravelS 已内置 Laravel Admin Cleaner,打开即可。

附言 3  ·  3年前

execl导出$this->download($this->fileName)->prepare(request())->send();替换为swoole_exit($this->download($this->fileName));

本帖由系统于 4年前 自动加精
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 38

LaravelS v3.6.4 已内置 Laravel Admin Cleaner,打开即可。

4年前 评论
blankqwq

@dreamfish 谢谢大佬围观 :grin:多向你学习

4年前 评论
Epona

很赞,不过我觉得 laravel-admin 有点慢是需要加载 太多东西了。。。

另外 上了 laravels 之后 速度有提升么?

4年前 评论
blankqwq

@Epona 响应基本200ms左右

4年前 评论
宇宙最厉害

哈哈哈,我们这边是后台慢无所谓,接口必须要快。打开慢一般来说是路由太多性能损耗都在路由上面。

4年前 评论
hkzj0571 4年前
宇宙最厉害 (作者) 4年前
blankqwq

@沈益飞 蛤蛤蛤,用了laravels,接口也一样很快,就是可能用其他扩展会容易出问题,不过暂时我还没遇到什么,主要是后台问题比较多,遇到了再看看代码改改

4年前 评论
Epona

@blankqwq 后台慢我觉得和 Laravels 无关,主要是Laravel-admin前端要加载的东西太多了。。。(包括乱七八糟的JS等等

4年前 评论
felo

:+1:楼主太及时了,正在用这个

4年前 评论
blankqwq

@felo 遇到其他问题也可以分析一波,一起进步一起学习

4年前 评论
blankqwq

@Epona emmm大佬说的没错,每次都是动态加载静态资源,导致加载比较耗时,于laravel无关,用上laravels快很多,基本秒开

4年前 评论
Epona

@blankqwq 可以试试把laravel-admin 的 js啥的放到 cdn上,情况应该能好一些

4年前 评论
blankqwq

@Epona 嗯嗯,谢谢大佬思路,我觉得应该需要一个生成blade文件的扩展,这样应该还能再快一些,我有空试一下 :smiley:

4年前 评论

是的,前段时间遇到laravelS和laravel-excel的大部分坑楼主都给出了解决方案,非常感谢楼主! :kissing_heart:

4年前 评论

请教楼主几个问题,之前使用laravel-excel3.1的时候的确会出现和laravelS冲突的情况,就是swoole exit的异常!但是限制不会了,报错是:

    [2019-11-05 20:43:06] [TRACE] Swoole is running, press Ctrl+C to quit.
    PHP Fatal error:  Method Encore\Admin\Grid\Tools\Paginator::__toString() must not throw an exception, caught BadMethodCallException: Method Illuminate\Database\Eloquent\Collection::firstItem does not exist. in /www/wwwroot/sdxz.yunnto.cn/storage/framework/views/de157d8e42a184e1ba9ef4552aea83053705a3f4.php on line 0
    Symfony\Component\Debug\Exception\FatalErrorException  : Method Encore\Admin\Grid\Tools\Paginator::__toString() must not throw an exception, caught BadMethodCallException: Method Illuminate\Database\Eloquent\Collection::firstItem does not exist.at /www/wwwroot/project/storage/framework/views/de157d8e42a184e1ba9ef4552aea83053705a3f4.php:0

    Symfony\Component\Debug\Exception\FatalErrorException  : Method Encore\Admin\Grid\Tools\Paginator::__toString() must not throw an exception, caught BadMethodCallException: Method Illuminate\Database\Eloquent\Collection::firstItem does not exist.

Encore\Admin\Grid\Tools\Paginator继承了AbstractTool,所以使用到tostring的地方是AbstractTool中的:

    public function __toString()
        {
            return $this->render();
    }

找了想了很多办法,比如抛出异常,均未奏效!

付:
"encore/laravel-admin": "^1.7.2",

"hhxsv5/laravel-s": "~3.5.0",

"laravel/framework": "5.8.*",

"maatwebsite/excel": "^3.1",

所以我的问题是,楼主在使用过程中是否遇到过这样的问题?

4年前 评论
blankqwq

@她来听我的演唱会
Method Illuminate\Database\Eloquent\Collection::firstItem建议查询文件康康具体原因,我暂时没法分析问题,稍微有点忙

   Symfony\Component\Debug\Exception\FatalErrorException  : Method Encore\Admin\Grid\Tools\Paginator::__toString() must not throw an exception, caught BadMethodCallException: Method Illuminate\Database\Eloquent\Collection::firstItem does not exist.at 

Method Illuminate\Database\Eloquent\Collection::firstItem

4年前 评论
她来听我的演唱会 4年前

看后,不敢用了。以后会不会有新的开箱即用的方案?

4年前 评论
她来听我的演唱会 4年前
blankqwq (楼主) 4年前

LaravelS v3.6.4 已内置 Laravel Admin Cleaner,打开即可。

4年前 评论
blankqwq

@好好先森V5 哇,那就方便很多了,感谢大佬

4年前 评论

请教个问题: 后台登陆了管理员, 在其它浏览器和无痕模式再打开后台, 直接就是已登录页面, 这个怎么处理??

3年前 评论

网上搜了好多好多导出的处理,因为版本的原因,都不行.还是提问下吧,报错和"她来听我演唱会"的一样:

3年前 评论
afanxingzhou (作者) 3年前
afanxingzhou (作者) 3年前
blankqwq (楼主) 3年前
blankqwq (楼主) 3年前
blankqwq (楼主) 3年前
afanxingzhou (作者) 3年前
blankqwq (楼主) 3年前

laravel-adimin 请求admin 404

3年前 评论
blankqwq (楼主) 3年前

toResponse 这个是放在那个文件的

1年前 评论
blankqwq (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!