Laravel 从 $request 到 $response 的过程解析二(必读)

2020-02-19 23:12:44

参考地址 Laravel 从 $request 到 $response 的过程解析

laravel 的请求会组装成 $request 对象,然后会依次经过中间件(前置部分),最终到达 url 指向的控制器方法,然后把返回数据组装为 $response 对象返回,再依次经过中间件 (后置部分),最终返回。


其实有两大部分:


1.laravel 如何管理中间件

2.laravel 如何通过 url 匹配到路由,找到目标方法

第一部分,laravel 通过管道来管理中间件

laravel 的中间件的管理都是通过管道来实现的,把注册的中间件数组传递到管道中,管道类会按照我们的顺序执行这些中间件。


实例化 App\Http\Kernel::class

我们知道,框架执行是通过 Kernell->hand () 方法开始的。看看 kernel 的实例化


public function __construct(Application $app, Router $router)

    {

        $this->app = $app;

        $this->router = $router;


        $router->middlewarePriority = $this->middlewarePriority;//优先级中间件,用于中间件排序

        foreach ($this->middlewareGroups as $key => $middleware) {//中间件组

            $router->middlewareGroup($key, $middleware);

        }

        foreach ($this->routeMiddleware as $key => $middleware) {//路由中间件

            $router->aliasMiddleware($key, $middleware);

        }

    }

实例化 http 核心类,就是把 middlewarePriority,middlewareGroups,aliasMiddleware 注册到路由类的属性中。所以这些中间件的执行都是要再路由解析后执行的。


管道通过 Illuminate\Pipeline\Pipeline 类来实现。

框架执行是通过 Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter () 来实现控制器访问


return (new Pipeline($this->app)) //传入$app实例化管道类

                    ->send($request)

                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)

                    ->then($this->dispatchToRouter());

这个就是启动后,传入 $request,通过管道的调度,执行各种中间件后返回 $response 实例。


1.send()

public function send($passable)

    {

        $this->passable = $passable; //就是把$request对象挂到属性passable中

        return $this;

    }

2.through()

public function through($pipes)

    {

        $this->pipes = is_array($pipes) ? $pipes : func_get_args();//这个就是中间件数组

        return $this;

    }

我们看看这时候的中间件有哪些?$this->middleware,这个就是全局中间件,定义在 Illuminate\Foundation\Http\Kernel 类的 middleware 属性中。


3.then (), 这个才是执行的关键

 public function then(Closure $destination)

    {

        $pipeline = array_reduce(

            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)

        );

        return $pipeline($this->passable);

    }

array_reduce (参数 1,参数 2,参数 3) 是 php 的自带的函数,参数 1 是一个数组,参数 2 是一个函数,参数 3 选填。需要去查看手册了解清楚这个函数的原理。


4.carry () 就是把中间件封装成闭包

protected function carry()

    {

        return function ($stack, $pipe) {

            return function ($passable) use ($stack, $pipe) {

                if (is_callable($pipe)) {

                    return $pipe($passable, $stack);

                } elseif (! is_object($pipe)) {

                    [$name, $parameters] = $this->parsePipeString($pipe);


                    $pipe = $this->getContainer()->make($name);


                    $parameters = array_merge([$passable, $stack], $parameters);

                } else {                  

                    $parameters = [$passable, $stack];

                }


                $response = method_exists($pipe, $this->method)

                                ? $pipe->{$this->method}(...$parameters)

                                : $pipe(...$parameters);

                return $response instanceof Responsable

                            ? $response->toResponse($this->getContainer()->make(Request::class))

                            : $response;

            };

        };

    }

array_reduce 就是把中间件的闭包嵌套起来。可以参考一下这一篇 https://learnku.com/articles/38189#reply127271


简单来说:


array_reduce( [a,b], 'carry', fun);


比如有中间件数组为 [a,b] 两个个中间件实例,参数 3 为闭包 fun, carry () 方法会得到三个闭包函数 funA,funB。fun 会在 funA 肚子里面,funA 会在 funB 肚子里面。这就是函数嵌套的关系。array_reduce 返回的是 funB。执行 funB 的时候,运行到 $next (),就是调用 funA。所以 fun 是在这个嵌套的最底层。


嵌套中最底层的函数,就是 then 的参数

$this->prepareDestination($destination),//这个就是我们路径要访问的控制器的闭包执行,


protected function prepareDestination(Closure $destination)

    {

        return function ($passable) use ($destination) {

            return $destination($passable); //这个就是执行控制器方法,$passable就是$request

        };

    }

1. 先看看这个 $destination,就是执行我们目标控制器方法,返回 $response

App\Http\Kernel 类的 dispatchToRouter 方法


protected function dispatchToRouter()

    {

        return function ($request) {

            $this->app->instance('request', $request);

            return $this->router->dispatch($request);//路由类调用dispatch,就是执行我们的目标方法

        };

    }

第二部分,路由类通过 request 匹配到路由,执行对应的控制器方法

先解释三个类


Illuminate\Routing\Router 路由类,就是门面Route::调用的那个类,负责路由实现的对外接口

Illuminate\Routing\Route  路由规则类,我们用Route::get(),就会生成一个路由规则对象,相当于一个url路径,就会有一个路由规则实例。路由匹配的时候,就是要找到对应的路由规则类。

Illuminate\Support\Collection 集合类,其实是laravel的一个工具类,可以把数组转为集合,然后使用集合类封装的方法处理各个元素。

路由类 Illuminate\Routing\Router

 public function dispatch(Request $request)

    {

        $this->currentRequest = $request;//把request赋值给属性currentRequest

        return $this->dispatchToRoute($request);

    }


 public function dispatchToRoute(Request $request)

    {

        return $this->runRoute($request, $this->findRoute($request));//通过request,找到匹配的路由规则对象

    }

1. 通过 request, 找到匹配的路由规则对象

protected function findRoute($request)

    {

        $this->current = $route = $this->routes->match($request);

        $this->container->instance(Route::class, $route);

        return $route;

    }

//$this->routes就是Illuminate\Routing\RouteCollection类,在路由实例化的时候注入进来的

所以说真正执行 match 操作的是 Illuminate\Routing\RouteCollection 类,看一下 match 方法


public function match(Request $request)

    {

        $routes = $this->get($request->getMethod()); //通过请求方法,得到所有的路由规则,比如get

        $route = $this->matchAgainstRoutes($routes, $request);//然后进行匹配

        if (! is_null($route)) {

            return $route->bind($request);

        }

        $others = $this->checkForAlternateVerbs($request);


        if (count($others) > 0) {

            return $this->getRouteForMethods($request, $others);

        }


        throw new NotFoundHttpException;

    }

//$routes就是get/post下的所有路由规则对象组成的数组

protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)

    {

        //把$routes规则数组转为集合

        [$fallbacks, $routes] = collect($routes)->partition(function ($route) {

            return $route->isFallback;

        });

        // first()方法返回集合中通过指定条件的第一个元素:

        return $routes->merge($fallbacks)->first(function ($value) use ($request, $includingMethod) {

            return $value->matches($request, $includingMethod);

        });

    }

最终匹配是根据路由规则类的 matches 方法来做的,如果匹配上就返回 true


路由规则类其实是 Illuminate\Routing\Route, 也就是说 $routes 数组的元素是 Illuminate\Routing\Route 类的实例,一条路由规则就是一个实例。


2. 路由规则类的介绍 Illuminate\Routing\Route

我们知道,laravel 的路由规则都需要我们在 routes 目录下定义,比如 routes\web.php


    Route::group(['prefix'=>'article'], function(){

        Route::get('index', 'ArticleController@index');

        Route::post('create', 'ArticleController@create');

        Route::get('edit/{article}', 'ArticleController@edit');

        Route::get('show/{article}', 'ArticleController@show');

    });

这时候就会生成 4 个路由规则对象,保存在 Illuminate\Routing\Router 的属性中,比如上面讲的 $routes 路由规则数组,因为我是通过 GET 访问,打印出来就是是这样的


Collection {#306 ▼

  #items: array:14 [▼

    "_debugbar/open" => Route {#129 ▶}

    "_debugbar/clockwork/{id}" => Route {#130 ▶}

    "api/user" => Route {#180 ▶}

    "article/index" => Route {#182 ▶}

    "article/edit/{article}" => Route {#184 ▶}

    "article/show/{article}" => Route {#185 ▶}

  ]

}

当然因为我安装了 debugbar 包,所以还有一些其他的路由规则注册进来了,但是还是可以看到有三个 article 的路由规则对象。每个路由规则对象都包含了对应的 uri,method,controller,路由参数等等。具体如何生成路由规则对象,并且注册到路由属性中,可以看 Route::get () 方法。


我们可以看一下一个路由规则对象有哪些属性

比如 Route::get ('index', 'ArticleController@index') 这个语句生成的路由规则对象


Route {#182 ▼

  +uri: "article/index"

  +methods: array:2 [▶]

  +action: array:6 [▶]

  +isFallback: false

  +controller: null

  +defaults: []

  +wheres: []

  +parameters: []

  +parameterNames: []

  #originalParameters: []

  +computedMiddleware: null

  +compiled: CompiledRoute {#324 ▶}

  #router: Router {#26 ▶}

  #container: Application {#2 ▶}

}

3. 循环所有的路由规则对象,用路由规则对象的 matches 来判断是否匹配上

Illuminate\Routing\Route 路由规则对象的 matches 方法


 public function matches(Request $request, $includingMethod = true)

    {

        $this->compileRoute();//路由规则的正则编译

        foreach ($this->getValidators() as $validator) {

            if (! $includingMethod && $validator instanceof MethodValidator) {

                continue;

            }

            if (! $validator->matches($this, $request)) {

                return false;

            }

        }

        return true;

    }


//通过RouteCompiler类编译路由规则实例

 protected function compileRoute()

    {

        if (! $this->compiled) {//一个路由规则实例只编译一次,编译完成会标识

            $this->compiled = (new RouteCompiler($this))->compile(); //编译成功后返回正则编译对象

        }

        return $this->compiled;

    }

3.1 路由规则的正则编译是通过 Symfony 框架来实现,最终得到一个正则编译对象

还是比较复杂的,原理就是通过正则表达式来判断路由规则实例是否匹配上,这里就不展开细节了,可以看一下这个博客 https://learnku.com/articles/5426/laravel-http-routing-uri-regular-compilation


不过可以看看一下这个正则编译后返回的对象 $this->compiled,路由规则是 Route::get ('index', 'ArticleController@index')


CompiledRoute {#309 ▼

  -variables: []

  -tokens: array:1 [▶]

  -staticPrefix: "/_debugbar/open"

  -regex: "#^/_debugbar/open$#sDu"

  -pathVariables: []

  -hostVariables: []

  -hostRegex: null

  -hostTokens: []

}

返回一个 Symfony\Component\Routing\CompiledRoute 对象。


3.2 四个验证器验证路由规则是否匹配

public static function getValidators()

    {

        if (isset(static::$validators)) {

            return static::$validators;

        }

        return static::$validators = [

            new UriValidator, new MethodValidator,

            new SchemeValidator, new HostValidator,

        ];

    }

这四个路由验证器类在 Illuminate\Routing\Matching\ 目录下,他们将分别使用 matches 来验证路由是否匹配上,只要有一个验证不通过,就表示不匹配。


//UriValidator验证器

public function matches(Route $route, Request $request)

    {

        $path = $request->path() === '/' ? '/' : '/'.$request->path();

        return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));

    }

//MethodValidator验证器

public function matches(Route $route, Request $request)

    {

        return in_array($request->getMethod(), $route->methods());

    }

//SchemeValidator验证器

public function matches(Route $route, Request $request)

    {

        if ($route->httpOnly()) {

            return ! $request->secure();

        } elseif ($route->secure()) {

            return $request->secure();

        }

        return true;

    }

//HostValidator验证器

public function matches(Route $route, Request $request)

    {

        if (is_null($route->getCompiled()->getHostRegex())) {

            return true;

        }

        return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());

    }

其中 UriValidator 验证,HostValidator 验证都需要正则编译对象来实现。


4. 得到匹配的路由规则对象,执行路由类的 runRoute 方法

Illuminate\Routing\Router


$this->runRoute($request, $this->findRoute($request));//$this->findRoute($request)就是返回匹配上的路由规则对象

protected function runRoute(Request $request, Route $route)

    {

        $request->setRouteResolver(function () use ($route) {//向request绑定路由规则对象

            return $route;

        });

        $this->events->dispatch(new Events\RouteMatched($route, $request));//监听RouteMatched事件

        return $this->prepareResponse($request,

            $this->runRouteWithinStack($route, $request)

        );

    }

先看看如何运行方法


 protected function runRouteWithinStack(Route $route, Request $request)

    {

        $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&

                                $this->container->make('middleware.disable') === true;

        $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

        return (new Pipeline($this->container))

                        ->send($request)

                        ->through($middleware)

                        ->then(function ($request) use ($route) {

                            return $this->prepareResponse(

                                $request, $route->run()

                            );

                        });

    }

我们开头说过,只有全局中间件,才是在路由解析前放入到管道中的,而我们的路由中间件,中间件组,只有执行到这里时才会加入到管道中的。


5. 如何得到路由解析后的中间件

Kernell 实例化的时候,已经把所有的路由中间件,中间件组注册到路由类的属性中,我们只要匹配需要执行的中间件即可。


Illuminate\Routing\Router


 public function gatherRouteMiddleware(Route $route)

    {

        $middleware = collect($route->gatherMiddleware())->map(function ($name) {

            return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);

        })->flatten();


        return $this->sortMiddleware($middleware);//把得到的中间件实例排序

    }

首先去对应的路由规则类获取中间件信息(比如这个路由绑定的中间件别名,中间件组的 key)

Illuminate\Routing\Route


public function gatherMiddleware()

    {

        if (! is_null($this->computedMiddleware)) {

            return $this->computedMiddleware;

        }

        $this->computedMiddleware = [];

        return $this->computedMiddleware = array_unique(array_merge(//数据有两个来源

            $this->middleware(), $this->controllerMiddleware()

        ), SORT_REGULAR);

    }

路由规则中间件信息源头一 $this->middleware ()

Illuminate\Routing\Route


 public function middleware($middleware = null)

    {

        if (is_null($middleware)) {//每有传参数时

            return (array) ($this->action['middleware'] ?? []);

        }

        if (is_string($middleware)) {//把参数转为数组

            $middleware = func_get_args();

        }

        $this->action['middleware'] = array_merge(//有传参数时

            (array) ($this->action['middleware'] ?? []), $middleware

        );

        return $this;

    }

这个路由规则的 middleware ($middleware) 的方法有两个作用:


没传参数时,返回 $this->action ['middleware'] 属性的值


有参数传入时,会把参数整合到 $this->action ['middleware'] 属性中


我们知道,每一条路由都会生成一个路由规则对象,路由规则对象生成的时候,如果是在 web.php 的路由,会向这个路由规则传入‘web’,如果路由定义在 api.php,这里就会传参数 'api'。


当我们定义路由规则 middleware (‘test’),例如


Route::get('/user', 'Home\UserController@user')->middleware('test');

就会向这个路由规则传入 'test'


路由规则中间件信息来源二 $this->controllerMiddleware ()

Illuminate\Routing\Route


public function controllerMiddleware()

    {

        if (! $this->isControllerAction()) {

            return [];

        }

        return $this->controllerDispatcher()->getMiddleware(

            $this->getController(), $this->getControllerMethod()

        );

    }

综合上述两个来源,如果访问 web.php 中的路由 Route::get ('/user', 'Home\UserController@user')->middleware ('test'),


$route->gatherMiddleware () 会返回 ['web','test'] 数组。通过 MiddlewareNameResolver::resolve 就得到了对应的中间件实例了。


6. 再次通过管道把中间件封装成闭包嵌套起来。

Illuminate\Routing\Router


 return (new Pipeline($this->container))

                        ->send($request)

                        ->through($middleware)

                        ->then(function ($request) use ($route) {

                            return $this->prepareResponse(

                                $request, $route->run()

                            );

                        });

我们看到,嵌套最底层的就是我们控制器的方法,$route->run (),终于找到你了,就是路由规则对象的 run 方法


通过路由规则对象的 run 方法执行

public function run()

    {

        $this->container = $this->container ?: new Container;

        try {

            if ($this->isControllerAction()) {

                return $this->runController();//路由指向的是控制器

            }

            return $this->runCallable();//路由指向的闭包

        } catch (HttpResponseException $e) {

            return $e->getResponse();

        }

    }

//执行controller

 protected function runController()

    {

        return $this->controllerDispatcher()->dispatch(

            $this, $this->getController(), $this->getControllerMethod()

        );

    }

laravel 执行 controller 也是通过 controllerDispatcher 类来执行的,先看看需要什么参数


7.1 通过路由规则对象,从容器获取目标控制器对象

Illuminate\Routing\Router


 public function getController()

    {

        if (! $this->controller) {

            $class = $this->parseControllerCallback()[0];

            $this->controller = $this->container->make(ltrim($class, '\\'));

        }

        return $this->controller;

    }

7.1 通过路由规则对象,得到目标方法名

protected function getControllerMethod()

    {

        return $this->parseControllerCallback()[1];

    }

7.3 获取控制器分发器

public function controllerDispatcher()

{

    if ($this->container->bound(ControllerDispatcherContract::class)) {

        return $this->container->make(ControllerDispatcherContract::class);

    }

    return new ControllerDispatcher($this->container);

}

8 通过控制器分发器执行目标

Illuminate\Routing\ControllerDispatcher


 public function dispatch(Route $route, $controller, $method)

    {

        $parameters = $this->resolveClassMethodDependencies(//通过反射获取参数

            $route->parametersWithoutNulls(), $controller, $method

        );

        if (method_exists($controller, 'callAction')) {

            return $controller->callAction($method, $parameters);

        }

        return $controller->{$method}(...array_values($parameters));//这里返回的是方法的返回值

    }

泪奔了,终于看到控制器调用方法了。不过还有一个问题,我们的目标方法的参数如果是对象,我们还要解析出来。


8.1 通过反射准备目标方法的参数

 protected function resolveClassMethodDependencies(array $parameters, $instance, $method)

    {

        if (! method_exists($instance, $method)) {

            return $parameters;

        }

        return $this->resolveMethodDependencies(

            $parameters, new ReflectionMethod($instance, $method)

        );

    }

8.2 把控制器 return 的内容封装为 response 对象

Illuminate\Routing\Router,我们再看看这个方法,$route->run (), 返回值是控制器 的 return 内容,还需要 prepareResponse 进行处理。


 return (new Pipeline($this->container))

                        ->send($request)

                        ->through($middleware)

                        ->then(function ($request) use ($route) {

                            return $this->prepareResponse(

                                $request, $route->run()

                            );

                        });

public function prepareResponse($request, $response)

    {

        return static::toResponse($request, $response);

    }


//根据方法return内容的数据类型,组装response对象

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);//数组,json等等

        } elseif (! $response instanceof SymfonyResponse) {

            $response = new Response($response);//字符串

        }


        if ($response->getStatusCode() === Response::HTTP_NOT_MODIFIED) {

            $response->setNotModified();

        }


        return $response->prepare($request);

    }

我们简单分析一下,如果我们的方法返回字符串,数组,模型对象,response 对象有什么区别


1. 控制器返回字符串


$response = new Response($response);//参数$response是字符串


Response {#404 ▼

  +headers: ResponseHeaderBag {#366 ▶}

  #content: "aaaa" //字符串内容

  #version: "1.0"

  #statusCode: 200

  #statusText: "OK"

  #charset: null

  +original: "aaaa"

  +exception: null

}

2. 如果是数组或者对象


 public function setData($data = [])

    {

        try {

            $data = json_encode($data, $this->encodingOptions);//会把data进行json_encode

        } catch (\Exception $e) {

            if ('Exception' === \get_class($e) && 0 === strpos($e->getMessage(), 'Failed calling ')) {

                throw $e->getPrevious() ?: $e;

            }

            throw $e;

        }

        if (JSON_ERROR_NONE !== json_last_error()) {

            throw new \InvalidArgumentException(json_last_error_msg());

        }

        return $this->setJson($data);//json_encode后挂到属性data中

    }


 public function setContent($content)

    {

        if (null !== $content && !\is_string($content) && !is_numeric($content) && !\is_callable([$content, '__toString'])) {

            throw new \UnexpectedValueException(sprintf('The Response content must be a string or object implementing __toString(), "%s" given.', \gettype($content)));

        }

        $this->content = (string) $content; //把属性data的值写入到属性content

        return $this;

    }

JsonResponse {#404 ▼

  #data: "["aa",["bb"]]" //对数组,对象镜像json_encode

  #callback: null

  #encodingOptions: 0

  +headers: ResponseHeaderBag {#366 ▶}

  #content: "["aa",["bb"]]" //对数组,对象镜像json_encode

  #version: "1.0"

  #statusCode: 200

  #statusText: "OK"

  #charset: null

  +original: array:2 [▶]

  +exception: null

}

所以说最后 $response 保存内容都在 content 属性中,如果是数组,或者对象,会进行 json_encod 处理。


  • 2019-12-04 10:46:26

    nuxt.js项目中全局捕获异常并生成错误日志全过程

     需求:客户在使用过程中页面报错时,可以生成错误记录传回服务器,以便改进。   步骤:     一.全局捕获异常,     二.发送到服务端,     三.生成错误日志。   一.全局捕获异常 如图,vue提供了errorHandle这个方法来处理全局异常,更多详细内容参见官网。

  • 2019-12-04 10:47:59

    nuxt.js项目中全局捕获异常并生成错误日志全过程

     需求:客户在使用过程中页面报错时,可以生成错误记录传回服务器,以便改进。   步骤:     一.全局捕获异常,     二.发送到服务端,     三.生成错误日志。   一.全局捕获异常 如图,vue提供了errorHandle这个方法来处理全局异常,更多详细内容参见官网。

  • 2019-12-04 10:48:18

    vue 项目资源文件 static 和 assets 不说区别直接使用?

    assets中资源会webpack构建压缩到你代码中,而static文件直接引用。 static 中长存放类包、插件等第三方的文件,assets里放属资源文件比如自己资源图片、css文件、js文件。 引入资源的方式static文件夹可以使用~/static/方式引入, assets文件夹可以使用 ~@/assets 方式引入

  • 2019-12-05 17:01:36

    Vue 结合 Axios 接口超时统一处理

    当网路慢的时候。又或者公司服务器不在内地的时候,接口数据请求不回来超时报错的情况相信大家肯定遇到过的,这里我把我公司项目请求超时的处理方法分享下,希望看过后有帮助。

  • 2019-12-05 17:13:40

    JS模板工具lodash.template的简单用法

    lodash是从underscore分支的一个项目,之前我写了一篇JS模板工具underscore.template的简单用法,lodash跟underscore很相似,这也简单介绍一下lodash的template方法。 先把underscore的文章中用过的代码贴过来,把underscore的js文件换成lodash的js,其他一字不改,然后我们试试:

  • 2019-12-06 10:47:29

    date-fns日期工具的使用方法详解

    isToday() 判断传入日期是否为今天 isYesterday() 判断传入日期是否为昨天 isTomorrow() 判断传入日期是否为 format() 日期格式化 addDays() 获得当前日期之后的日期 addHours() 获得当前时间n小时之后的时间点 addMinutes() 获得当前时间n分钟之后的时间 addMonths() 获得当前月之后n个月的月份 subDays() 获得当前时间之前n天的时间 subHours() 获得当前时间之前n小时的时间 subMinutes() 获得当前时间之前n分钟的时间 subMonths() 获得当前时间之前n个月的时间 differenceInYears() 获得两个时间相差的年份 differenceInWeeks() 获得两个时间相差的周数 differenceInDays() 获得两个时间相差的天数 differenceInHours() 获得两个时间相差的小时数 differenceInMinutes() 获得两个时间相差的分钟数

  • 2019-12-06 10:49:39

    npm 查看源 换源

    npm,cnpm,查看源,切换源,npm config set registry https://registry.npmjs.org

  • 2019-12-06 11:01:31

    npm发布包流程详解 有demo

    npm发布包步骤,以及踩过的坑(见红颜色标准): 1.注册npm账号,并完成Email认证(否则最后一步提交会报Email错误) 2.npm添加用户或登陆:npm adduser 或 npm login