摘要:服務提供者啟動原理之前我們有學習深度挖掘生命周期和深入剖析服務容器,今天我們將學習服務提供者。的所有核心服務都是通過服務提供者進行引導啟動的,所以想深入了解那么研究服務提供者的原理是個繞不開的話題。
本文首發于 深入剖析 Laravel 服務提供者實現原理,轉載請注明出處。
今天我們將學習 Laravel 框架另外一個核心內容「服務提供者(Service Provider)」。服務提供者的功能是完成 Laravel 應用的引導啟動,或者說是將 Laravel 中的各種服務「注冊」到「Laravel 服務容器」,這樣才能在后續處理 HTTP 請求時使用這些服務。
服務提供者基本概念我們知道 「服務提供者」是配置應用的中心,它的主要工作是使用「服務容器」實現服務容器綁定、事件監聽器、中間件,甚至是路由的注冊。
除核心服務外,幾乎所有的服務提供者都定義在配置文件 config/app.php 文件中的 providers 節點中。
服務提供者的典型處理流程是,當接 Laravel 應用接收到 HTTP 請求時會去執行「服務提供者的 register(注冊)」方法,將各個服務「綁定」到容器內;之后,到了實際處理請求階段,依據使用情況按需加載所需服務。這樣的優勢很明顯能夠提升應用的性能。
細心的朋友可能發現這里用了一個詞「幾乎」,沒錯還有一些屬于核心服務提供者,這些并沒有定義在 providers 配置節點中而是直接由 IlluminateFoundationApplication 服務容器直接在實例化階段就完成了注冊服務。
registerBaseServiceProviders(); ... } /** * Register all of the base service providers. 注冊應用基礎服務提供者 * * @return void */ protected function registerBaseServiceProviders() { $this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this)); }
對服務容器不是很熟悉的老鐵可以閱讀 深入剖析 Laravel 服務容器,并且在文中「注冊基礎服務提供者」一節也有詳細分析服務容器是如何注冊服務提供者的。
另外一個,我們還需要了解的是所有的服務提供者都繼承自 IlluminateSupportServiceProvider 類。不過對于我們來說目前還無需研究基類,所以我們將焦點放到如何實現一個自定義的服務提供者,然后還有兩個需要掌握方法。
服務提供者入門 創建自定義服務提供者要創建自定義的「服務提供者」,可以直接使用 Laravel 內置的 artisan 命令完成。
php artisan make:provider RiskServiceProvider
這個命令會在 app/Providers 目錄下創建 RiskServiceProvider.php 文件,打開文件內容如下:
register 方法在 register 方法中,我們無需處理業務邏輯,在這個方法中你只需去處理「綁定」服務到服務容器中即可。
從文檔中我們知道:
在 register 方法中,你只需要將類綁定到 服務容器 中。而不需要嘗試在 register 方法中注冊任何事件監聽器、路由或者任何其他功能。否則,你可能會意外使用到尚未加載的服務提供器提供的服務。如何理解這句話的含義呢?
如果你有了解過服務容器運行原理,就會知道在「綁定」操作僅僅是建立起接口和實現的對應關系,此時并不會創建具體的實例,即不會存在真實的依賴關系。直到某個服務真的被用到時才會從「服務容器」中解析出來,而解析的過程發生在所有服務「注冊」完成之后。
一旦我們嘗試在 register 注冊階段使用某些未被加載的服務依賴,即這個服務目前還沒有被注冊所以不可用。
這樣就需要在「注冊」綁定時,同時需要關注服務的注冊順序,但這一點 Laravel 并不作出任何保證。
理解了這個道理,我們就可以隨便進入一個「服務提供者」來看看其中的 register 方法的邏輯,現在我們挑選的是 IlluminateCacheCacheServiceProvider 服務作為講解:
app->singleton("cache", function ($app) { return new CacheManager($app); }); $this->app->singleton("cache.store", function ($app) { return $app["cache"]->driver(); }); $this->app->singleton("memcached.connector", function () { return new MemcachedConnector; }); } /** * Get the services provided by the provider. * * @return array */ public function provides() { return [ "cache", "cache.store", "memcached.connector", ]; } }沒錯,如你所預料的一樣,它的 register 方法執行了三個單例綁定操作,僅此而已。
簡單注冊服務對于處理復雜綁定邏輯,可以自定義「服務提供者」。但是如果是比較簡單的注冊服務,有沒有比較方便的綁定方法呢?畢竟,并不是每個服務都會有復雜的依賴處理。
我們可以從 文檔 中得到解答:
如果你的服務提供商注冊許多簡單的綁定,你可能想使用 bindings 和 singletons 屬性而不是手動注冊每個容器綁定。DigitalOceanServerProvider::class, ]; /** * 設定單例模式的容器綁定對應關系 * * @var array */ public $singletons = [ DowntimeNotifier::class => PingdomDowntimeNotifier::class, ]; }此時,通過 bingdings 或 singletons 成員變量來設置簡單的綁定,就可以避免大量的「服務提供者」類的生成了。
boot 方法聊完了 register 方法,接下來進入另一個主題,來研究一下服務提供者的 boot 方法。
通過前面的學習,我們知道在 register 方法中 Laravel 并不能保證所有其他服務已被加載。所以當需要處理具有依賴關系的業務邏輯時,應該將這些邏輯處理放置到 boot 方法內。在 boot 方法中我們可以去完成:注冊事件監聽器、引入路由文件、注冊過濾器等任何你可以想象得到的業務處理。
在 config/app.php 配置中我們可以看到如下幾個服務提供者:
/* * Application Service Providers... */ AppProvidersAppServiceProvider::class, AppProvidersAuthServiceProvider::class, // AppProvidersBroadcastServiceProvider::class, AppProvidersEventServiceProvider::class, AppProvidersRouteServiceProvider::class,選擇其中的 AppProvidersRouteServiceProvider::class 服務提供者它繼承自 IlluminateFoundationSupportProvidersRouteServiceProvider 基類來看下:
// 實現類 class RouteServiceProvider extends ServiceProvider { /** * This namespace is applied to your controller routes. In addition, it is set as the URL generator"s root namespace. */ protected $namespace = "AppHttpControllers"; /** * Define your route model bindings, pattern filters, etc. */ public function boot() { parent::boot(); } /** * Define the routes for the application. 定義應用路由 */ public function map() { $this->mapApiRoutes(); $this->mapWebRoutes(); } /** * Define the "web" routes for the application. These routes all receive session state, CSRF protection, etc. * 定義 web 路由。web 路由支持會話狀態和 CSRF 防御中間件等。 */ protected function mapWebRoutes() { Route::middleware("web") ->namespace($this->namespace) ->group(base_path("routes/web.php")); } /** * Define the "api" routes for the application. These routes are typically stateless. * 定義 api 路由。api 接口路由支持典型的 HTTP 無狀態協議。 */ protected function mapApiRoutes() { Route::prefix("api") ->middleware("api") ->namespace($this->namespace) ->group(base_path("routes/api.php")); } }基類 IlluminateFoundationSupportProvidersRouteServiceProvider:
// 基類 namespace IlluminateFoundationSupportProviders; /** * @mixin IlluminateRoutingRouter */ class RouteServiceProvider extends ServiceProvider { /** * The controller namespace for the application. */ protected $namespace; /** * Bootstrap any application services. 引導啟動服務 */ public function boot() { $this->setRootControllerNamespace(); // 如果已緩存路由,從緩存文件中載入路由 if ($this->app->routesAreCached()) { $this->loadCachedRoutes(); } else { //還沒有路由緩存,加載路由 $this->loadRoutes(); $this->app->booted(function () { $this->app["router"]->getRoutes()->refreshNameLookups(); $this->app["router"]->getRoutes()->refreshActionLookups(); }); } } /** * Load the application routes. 加載應用路由,調用實例的 map 方法,該方法定義在 AppProvidersRouteServiceProvider::class 中。 */ protected function loadRoutes() { if (method_exists($this, "map")) { $this->app->call([$this, "map"]); } } }對于 RouteServiceProvider 來講,它的 boot 方法在處理一個路由載入的問題:
判斷是否已有路由緩存;
有路由緩存,則直接載入路由緩存;
無路由緩存,執行 map 方法載入路由。
感興趣的朋友可以自行了解下 Application Service Providers 配置節點的相關服務提供者,這邊不再贅述。
配置服務提供者了解完「服務提供者」兩個重要方法后,我們還需要知道 Laravel 是如何查找到所有的服務提供者的。這個超找的過程就是去讀取 config/app.php 文件中的 providers 節點內所有的「服務提供器」。
具體的讀取過程我們也會在「服務提供者啟動原理」一節中講解。
延遲綁定服務提供者對于一個項目來說,除了要讓它跑起來,往往我們還需要關注它的性能問題。
當我們打開 config/app.php 配置文件時,你會發現有配置很多服務提供者,難道所有的都需要去執行它的 register 和 boot 方法么?
對于不會每次使用的服務提供者很明顯,無需每次注冊和啟動,直到需要用到它的時候。
為了解決這個問題 Laravel 內置支持 延遲服務提供者 功能,啟用時延遲功能后,當它真正需要注冊綁定時才會執行 register 方法,這樣就可以提升我們服務的性能了。
啟用「延遲服務提供者」功能,需要完成兩個操作配置:
在對應服務提供者中將 defer 屬性設置為 true;
并定義 provides 方法,方法返回在提供者 register 方法內需要注冊的服務接口名稱。
我們拿 config/app.php 配置中的 BroadcastServiceProvider 作為演示說明:
app->singleton(BroadcastManager::class, function ($app) { return new BroadcastManager($app); }); $this->app->singleton(BroadcasterContract::class, function ($app) { return $app->make(BroadcastManager::class)->connection(); }); $this->app->alias( BroadcastManager::class, BroadcastingFactory::class ); } /** * Get the services provided by the provider. 獲取提供者所提供的服務接口名稱。 */ public function provides() { return [ BroadcastManager::class, BroadcastingFactory::class, BroadcasterContract::class, ]; } }小結在「服務提供者入門」這個小節我們學習了服務提供者的基本使用和性能優化相關知識,包括:
如何創建自定義的服務提供者;
創建 register 方法注冊服務到 Laravel 服務容器;
創建 boot 方法啟動服務提供者的引導程序;
配置我們的服務提供者到 config/app.php 文件,這樣才能在容器中加載相應服務;
通過延遲綁定技術,提升 Laravel 服務性能。
下一小節,我們將焦點轉移到「服務提供者」的實現原理中,深入到 Laravel 內核中去探索「服務提供者」如何被注冊和啟動,又是如何能夠通過延遲技術提升 Laravel 應用的性能的。
服務提供者啟動原理之前我們有學習 深度挖掘 Laravel 生命周期 和 深入剖析 Laravel 服務容器,今天我們將學習「服務提供者」。
Laravel 的所有核心服務都是通過服務提供者進行引導啟動的,所以想深入了解 Laravel 那么研究「服務提供者」的原理是個繞不開的話題。
引導程序的啟動流程服務提供者 注冊 和 引導啟動 直到處理 HTTP 請求階段才開始。所以我們直接進入到 AppConsoleKernel::class 類,同時這個類繼承于 IlluminateFoundationHttpKernel 類。
從 IlluminateFoundationHttpKernel 類中我們可以看到如下內容:
class Kernel implements KernelContract { ... /** * The bootstrap classes for the application. 應用引導類 */ protected $bootstrappers = [ ... IlluminateFoundationBootstrapRegisterProviders::class, // 用于注冊(register)「服務提供者」的引導類 IlluminateFoundationBootstrapBootProviders::class, // 用于啟動(boot)「服務提供者」的引導類 ]; /** * Handle an incoming HTTP request. 處理 HTTP 請求 */ public function handle($request) { try { $request->enableHttpMethodParameterOverride(); $response = $this->sendRequestThroughRouter($request); } catch (Exception $e) { ... } catch (Throwable $e) { ... } ... } /** * Send the given request through the middleware / router. 對 HTTP 請求執行中間件處理后再發送到指定路由。 */ protected function sendRequestThroughRouter($request) { ... // 1. 引導類引導啟動。 $this->bootstrap(); // 2. 中間件及請求處理,生成響應并返回響應。 return (new Pipeline($this->app)) ->send($request) ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) ->then($this->dispatchToRouter()); } /** * Bootstrap the application for HTTP requests. 接收 HTTP 請求時啟動應用引導程序。 */ public function bootstrap() { // 引導類啟動由 Application 容器引導啟動。 if (! $this->app->hasBeenBootstrapped()) { $this->app->bootstrapWith($this->bootstrappers()); } } }在 IlluminateFoundationHttpKernel 我們的內核處理 HTTP 請求時會經過一下兩個主要步驟:
啟動引導程序通過 $this->bootstrap() 方法完成,其中包括所有服務提供者的注冊和引導處理;
處理 HTTP 請求(這個問題涉及到中間件、路由及相應處理,本文將不做深入探討)。
進入 IlluminateFoundationApplication 容器中的 bootstrapWith() 方法,來看看容器是如何將引導類引導啟動的:
/** * Run the given array of bootstrap classes. 執行給定引導程序 */ public function bootstrapWith(array $bootstrappers) { $this->hasBeenBootstrapped = true; foreach ($bootstrappers as $bootstrapper) { $this["events"]->fire("bootstrapping: ".$bootstrapper, [$this]); // 從容器中解析出實例,然后調用實例的 bootstrap() 方法引導啟動。 $this->make($bootstrapper)->bootstrap($this); $this["events"]->fire("bootstrapped: ".$bootstrapper, [$this]); } }通過服務容器的 bootstrap() 方法引導啟動時,將定義的在 IlluminateFoundationHttpKerne 類中的應用引導類($bootstrappers)交由 Application 服務容器引導啟動。其中與「服務提供者」有關的引導類為:
當 IlluminateFoundationHttpKerne HTTP 內核通過 bootstrap() 方法引導啟動時,實際由服務容器(Application)去完成引導啟動的工作,并依據定義在 HTTP 內核中的引導類屬性配置順序依次引導啟動,最終「服務提供者」的啟動順序是:
執行「服務提供者」register 方法的引導類:IlluminateFoundationBootstrapRegisterProviders::class,將完成所有定義在 config/app.php 配置中的服務提供者的注冊(register)處理;
執行「服務提供者」boot 方法的引導類:IlluminateFoundationBootstrapBootProviders::class,將完成所有定義在 config/app.php 配置中的服務提供者的啟動(boot)處理。
Laravel 執行服務提供者注冊(register)處理前面說過「服務提供者」的注冊由 IlluminateFoundationBootstrapRegisterProviders::class 引導類啟動方法(botstrap())完成。
1. RegisterProviders 引導注冊
registerConfiguredProviders(); } }在其通過調用服務容器的 registerConfiguredProviders() 方法完成引導啟動,所以我們需要到容器中一探究竟。
2. 由服務容器執行配置文件中的所有服務提供者服務完成注冊。
/** * Register all of the configured providers. 執行所有配置服務提供者完成注冊處理。 * * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php */ public function registerConfiguredProviders() { $providers = Collection::make($this->config["app.providers"]) ->partition(function ($provider) { return Str::startsWith($provider, "Illuminate"); }); $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]); // 通過服務提供者倉庫(ProviderRepository)加載所有的提供者。 (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) ->load($providers->collapse()->toArray()); }3. 最后由服務提供者倉庫(ProviderRepository)執行服務提供者的注冊處理。
loadManifest(); // 首先從服務提供者的緩存清單文件中載入服務提供者集合。其中包含「延遲加載」的服務提供者。 if ($this->shouldRecompile($manifest, $providers)) { $manifest = $this->compileManifest($providers); } // Next, we will register events to load the providers for each of the events // that it has requested. This allows the service provider to defer itself // while still getting automatically loaded when a certain event occurs. foreach ($manifest["when"] as $provider => $events) { $this->registerLoadEvents($provider, $events); } // 到這里,先執行應用必要(貪婪)的服務提供者完成服務注冊。 foreach ($manifest["eager"] as $provider) { $this->app->register($provider); } // 最后將所有「延遲加載服務提供者」加入到容器中。 $this->app->addDeferredServices($manifest["deferred"]); } /** * Compile the application service manifest file. 將服務提供者編譯到清單文件中緩存起來。 */ protected function compileManifest($providers) { // The service manifest should contain a list of all of the providers for // the application so we can compare it on each request to the service // and determine if the manifest should be recompiled or is current. $manifest = $this->freshManifest($providers); foreach ($providers as $provider) { // 解析出 $provider 對應的實例 $instance = $this->createProvider($provider); // 判斷當前服務提供者是否為「延遲加載」類行的,是則將其加入到緩存文件的「延遲加載(deferred)」集合中。 if ($instance->isDeferred()) { foreach ($instance->provides() as $service) { $manifest["deferred"][$service] = $provider; } $manifest["when"][$provider] = $instance->when(); } // 如果不是「延遲加載」類型的服務提供者,則為貪婪加載必須立即去執行注冊方法。 else { $manifest["eager"][] = $provider; } } // 將歸類后的服務提供者寫入清單文件。 return $this->writeManifest($manifest); }在 服務提供者倉庫(ProviderRepository) 處理程序中依次執行如下處理:
如果存在服務提供者緩存清單,則直接讀取「服務提供者」集合;
否則,將從 config/app.php 配置中的服務提供者編譯到緩存清單中;編譯由 compileManifest() 方法完成; 編譯緩存清單時將處理貪婪加載(eager)和延遲加載(deferred)的服務提供者;
對于貪婪加載的提供者直接執行服務容器的 register 方法完成服務注冊;
將延遲加載提供者加入到服務容器中,按需注冊和引導啟動。
最后通過 IlluminateFoundationApplication 容器完成注冊處理:
/** * Register a service provider with the application. 在應用服務容器中注冊一個服務提供者。 */ public function register($provider, $options = [], $force = false) { if (($registered = $this->getProvider($provider)) && ! $force) { return $registered; } // 如果給定的服務提供者是接口名稱,解析出它的實例。 if (is_string($provider)) { $provider = $this->resolveProvider($provider); } // 服務提供者提供注冊方法時,執行注冊服務處理 if (method_exists($provider, "register")) { $provider->register(); } $this->markAsRegistered($provider); // 判斷 Laravel 應用是否已啟動。已啟動的話需要去執行啟動處理。 if ($this->booted) { $this->bootProvider($provider); } return $provider; }為什么需要判斷是否已經啟動過呢?
因為對于延遲加載的服務提供者只有在使用時才會被調用,所以這里需要這樣判斷,然后再去啟動它。
以上,便是
Laravel 執行服務提供者啟動(boot)處理「服務提供者」的啟動流程和注冊流程大致相同,有興趣的朋友可以深入源碼了解一下。
1. BootProviders 引導啟動
boot(); } }2. 由服務容器執行配置文件中的所有服務提供者服務完成啟動。
/** * Boot the application"s service providers. 引導啟動應用所有服務提供者 * * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php */ public function boot() { if ($this->booted) { return; } // Once the application has booted we will also fire some "booted" callbacks // for any listeners that need to do work after this initial booting gets // finished. This is useful when ordering the boot-up processes we run. $this->fireAppCallbacks($this->bootingCallbacks); // 遍歷并執行服務提供者的 boot 方法。 array_walk($this->serviceProviders, function ($p) { $this->bootProvider($p); }); $this->booted = true; $this->fireAppCallbacks($this->bootedCallbacks); } /** * Boot the given service provider. 啟動給定服務提供者 */ protected function bootProvider(ServiceProvider $provider) { if (method_exists($provider, "boot")) { return $this->call([$provider, "boot"]); } }以上便是服務提供者執行 注冊綁定服務 和 引導啟動 的相關實現。
但是稍等一下,我們是不是忘記了還有「延遲加載」類型的服務提供者,它們還沒有被注冊和引導啟動呢!
Laravel 如何完成延遲加載類型的服務提供者對于延遲加載類型的服務提供者,我們要到使用時才會去執行它們內部的 register 和 boot 方法。這里我們所說的使用即使需要 解析 它,我們知道解析處理由服務容器完成。
所以我們需要進入到 IlluminateFoundationApplication 容器中探索 make 解析的一些細節。
/** * Resolve the given type from the container. 從容器中解析出給定服務 * * @see https://github.com/laravel/framework/blob/5.6/src/Illuminate/Foundation/Application.php */ public function make($abstract, array $parameters = []) { $abstract = $this->getAlias($abstract); // 判斷這個接口是否為延遲類型的并且沒有被解析過,是則去將它加載到容器中。 if (isset($this->deferredServices[$abstract]) && ! isset($this->instances[$abstract])) { $this->loadDeferredProvider($abstract); } return parent::make($abstract, $parameters); } /** * Load the provider for a deferred service. 加載給定延遲加載服務提供者 */ public function loadDeferredProvider($service) { if (! isset($this->deferredServices[$service])) { return; } $provider = $this->deferredServices[$service]; // 如果服務為注冊則去注冊并從延遲服務提供者集合中刪除它。 if (! isset($this->loadedProviders[$provider])) { $this->registerDeferredProvider($provider, $service); } } /** * Register a deferred provider and service. 去執行服務提供者的注冊方法。 */ public function registerDeferredProvider($provider, $service = null) { // Once the provider that provides the deferred service has been registered we // will remove it from our local list of the deferred services with related // providers so that this container does not try to resolve it out again. if ($service) { unset($this->deferredServices[$service]); } // 執行服務提供者注冊服務。 $this->register($instance = new $provider($this)); // 執行服務提供者啟動服務。 if (! $this->booted) { $this->booting(function () use ($instance) { $this->bootProvider($instance); }); } }總結今天我們深入研究了 Laravel 服務提供者的注冊和啟動的實現原理,希望對大家有所幫助。
如果對如何自定義服務提供者不甚了解的朋友可以去閱讀 Laravel 服務提供者指南 這篇文章。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/28788.html
摘要:劃下重點,服務容器是用于管理類的依賴和執行依賴注入的工具。類的實例化及其依賴的注入,完全由服務容器自動的去完成。 本文首發于 深入剖析 Laravel 服務容器,轉載請注明出處。喜歡的朋友不要吝嗇你們的贊同,謝謝。 之前在 深度挖掘 Laravel 生命周期 一文中,我們有去探究 Laravel 究竟是如何接收 HTTP 請求,又是如何生成響應并最終呈現給用戶的工作原理。 本章將帶領大...
摘要:外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用。將使用者與子系統從直接耦合,轉變成由外觀類提供統一的接口給使用者使用,以降低客戶端與子系統之間的耦合度。接下來將深入分析外觀服務的加載過程。引導程序將在處理請求是完成引導啟動。 本文首發于 深入淺出 Laravel 的 Facade 外觀系統,轉載請注明出處。 今天我們將學習 Laravel 核心架構中的另一個主題「Fac...
摘要:作者鏈接來源簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。同時順手整理個人對源碼的相關理解,希望能夠稍微填補學習領域的空白。系列文章只會節選關鍵代碼輔以思路講解,請自行配合源碼閱讀。 作者:bromine鏈接:https://www.jianshu.com/p/2f6...來源:簡書著作權歸作者所有,本文已獲得作者授權轉載,并對原文進行了重新的排版。Swoft...
摘要:阻塞,非阻塞首先,阻塞這個詞來自操作系統的線程進程的狀態模型網絡爬蟲基本原理一后端掘金網絡爬蟲是捜索引擎抓取系統的重要組成部分。每門主要編程語言現未來已到后端掘金使用和在相同環境各加載多張小圖片,性能相差一倍。 2016 年度小結(服務器端方向)| 掘金技術征文 - 后端 - 掘金今年年初我花了三個月的業余時間用 Laravel 開發了一個項目,在此之前,除了去年換工作準備面試時,我并...
摘要:阻塞,非阻塞首先,阻塞這個詞來自操作系統的線程進程的狀態模型網絡爬蟲基本原理一后端掘金網絡爬蟲是捜索引擎抓取系統的重要組成部分。每門主要編程語言現未來已到后端掘金使用和在相同環境各加載多張小圖片,性能相差一倍。 2016 年度小結(服務器端方向)| 掘金技術征文 - 后端 - 掘金今年年初我花了三個月的業余時間用 Laravel 開發了一個項目,在此之前,除了去年換工作準備面試時,我并...
閱讀 1456·2021-09-02 13:57
閱讀 1870·2019-08-30 15:55
閱讀 2407·2019-08-30 15:54
閱讀 2241·2019-08-30 15:44
閱讀 2733·2019-08-30 13:18
閱讀 480·2019-08-30 13:02
閱讀 628·2019-08-29 18:46
閱讀 1665·2019-08-29 11:25