在 Laravel 8 中参考 igaster / laravel-theme 示例实现

1、在本地使用 Composer 来创建一个新的 Laravel 项目:laravel-theme-demo。

PS E:\wwwroot> composer create-project laravel/laravel laravel-theme-demo
Creating a "laravel/laravel" project at "./laravel-theme-demo"
Installing laravel/laravel (v8.6.10)
  - Installing laravel/laravel (v8.6.10): Extracting archive
Created project in E:\wwwroot\laravel-theme-demo
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
Lock file operations: 110 installs, 0 updates, 0 removals

2、igaster / laravel-theme 是一个 Laravel 包,增加了对管理主题的基本支持。它允许您在单独的文件夹中构建视图和资产,并支持主题扩展。执行命令:composer require “igaster/laravel-theme”,以安装 igaster/laravel-theme。

PS E:\wwwroot\laravel-theme-demo> composer require "igaster/laravel-theme"
Using version ^2.0 for igaster/laravel-theme
./composer.json has been updated
Running composer update igaster/laravel-theme
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking igaster/laravel-theme (v2.0.17)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing igaster/laravel-theme (v2.0.17): Extracting archive
1 package suggestions were added by new dependencies, use `composer suggest` to see details.
Package swiftmailer/swiftmailer is abandoned, you should avoid using it. Use symfony/mailer instead.
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fruitcake/laravel-cors
Discovered Package: igaster/laravel-theme
Discovered Package: laravel/sail
Discovered Package: laravel/sanctum
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
77 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
> @php artisan vendor:publish --tag=laravel-assets --ansi --force
No publishable resources for tag [laravel-assets].
Publishing complete.
PS E:\wwwroot\laravel-theme-demo>

3、安装 Laravel Mix,执行:yarn install,运行所有的 Mix 任务,执行:npm run dev。

PS E:\wwwroot\laravel-theme-demo> yarn install
yarn install v1.22.15
info No lockfile found.
[1/4] Resolving packages...
warning laravel-mix > node-libs-browser > url > querystring@0.2.0: The querystring API is considered Legacy. new code should use the URLSearchParams API instead.
[2/4] Fetching packages...
info fsevents@2.3.2: The platform "win32" is incompatible with this module.
info "fsevents@2.3.2" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > laravel-mix@6.0.39" has unmet peer dependency "@babel/core@^7.15.8".
warning " > laravel-mix@6.0.39" has unmet peer dependency "@babel/plugin-proposal-object-rest-spread@^7.15.6".
warning " > laravel-mix@6.0.39" has unmet peer dependency "@babel/plugin-syntax-dynamic-import@^7.8.3".
warning " > laravel-mix@6.0.39" has unmet peer dependency "@babel/plugin-transform-runtime@^7.15.8".
warning " > laravel-mix@6.0.39" has unmet peer dependency "@babel/preset-env@^7.15.8".
warning " > laravel-mix@6.0.39" has unmet peer dependency "webpack@^5.60.0".
warning " > laravel-mix@6.0.39" has unmet peer dependency "webpack-cli@^4.9.1".
[4/4] Building fresh packages...
success Saved lockfile.
warning Your current version of Yarn is out of date. The latest version is "1.22.17", while you're on "1.22.15".
Done in 87.72s.
PS E:\wwwroot\laravel-theme-demo> npm run dev

> dev
> npm run development


> development
> mix


● Mix █████████████████████████ emitting (98%)
 after emit




● Mix █████████████████████████ done (99%) plugins
 WebpackBar:done
















✔ Mix
  Compiled successfully in 3.82s


































   Laravel Mix v6.0.39


✔ Compiled Successfully in 3717ms
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────┐
│                                                                                                       File │ Size    │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────┤
│                                                                                                 /js/app.js │ 606 KiB │
│                                                                                                css/app.css │ 1 bytes │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────┘

● Mix █████████████████████████ done (99%)
 plugins

webpack compiled successfully
PS E:\wwwroot\laravel-theme-demo>

4、将配置文件发布到您的应用程序

PS E:\wwwroot\laravel-theme-demo> php artisan vendor:publish --provider="Igaster\LaravelTheme\themeServiceProvider"
Copied File [\vendor\igaster\laravel-theme\src\Config\themes.php] To [\config\themes.php]
Publishing complete.

5、创建主题,执行命令:theme:create。生成目录结构,如法炮制,生成主题 t2。如图1

图1

PS E:\wwwroot\laravel-theme-demo> php artisan theme:create

 Give theme name::
 > t1

 Where will views be located? [t1]:
 >

 Where will assets be located? [t1]:
 >

 Extends an other theme? (yes/no) [no]:
 >

Summary:
- Theme name: t1
- Views Path: E:\wwwroot\laravel-theme-demo\resources\views/t1
- Asset Path: E:\wwwroot\laravel-theme-demo\public\t1
- Extends Theme: No

 Create Theme? (yes/no) [yes]:
 > yes
PS E:\wwwroot\laravel-theme-demo> php artisan theme:create

 Give theme name::
 > t2

 Where will views be located? [t2]:
 >

 Where will assets be located? [t2]:
 >

 Extends an other theme? (yes/no) [no]:
 >

Summary:
- Theme name: t2
- Views Path: E:\wwwroot\laravel-theme-demo\resources\views/t2
- Asset Path: E:\wwwroot\laravel-theme-demo\public\t2
- Extends Theme: No

 Create Theme? (yes/no) [yes]:
 > yes

6、打开首页:http://laravel-theme-demo.local/ ,默认的界面。如图2

图2

7、使用中间件设置主题,执行命令:php artisan make:middleware SetTheme

PS E:\wwwroot\laravel-theme-demo> php artisan make:middleware SetTheme
Middleware created successfully.

8、在 app\Http\Kernel.php 文件中注册你的中间件。 例如,如果您想将主题应用于所有路由,则应将其添加到 web 数组中。

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */    protected $middlewareGroups = [
        'web' => [
            // ...
            \App\Http\Middleware\SetTheme::class,
        ],
    ];

9、添加文件:public/images/test.gif、public/t1/images/test.gif、public/t2/images/test.gif,编辑主题文件:resources/views/welcome.blade.php、resources/views/welcome.blade.php、resources/views/welcome.blade.php,其内容完全一样

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel</title>

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">

    <!-- Styles -->
    <style>
        /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}a{background-color:transparent}[hidden]{display:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*,:after,:before{box-sizing:border-box;border:0 solid #e2e8f0}a{color:inherit;text-decoration:inherit}svg,video{display:block;vertical-align:middle}video{max-width:100%;height:auto}.bg-white{--bg-opacity:1;background-color:#fff;background-color:rgba(255,255,255,var(--bg-opacity))}.bg-gray-100{--bg-opacity:1;background-color:#f7fafc;background-color:rgba(247,250,252,var(--bg-opacity))}.border-gray-200{--border-opacity:1;border-color:#edf2f7;border-color:rgba(237,242,247,var(--border-opacity))}.border-t{border-top-width:1px}.flex{display:flex}.grid{display:grid}.hidden{display:none}.items-center{align-items:center}.justify-center{justify-content:center}.font-semibold{font-weight:600}.h-5{height:1.25rem}.h-8{height:2rem}.h-16{height:4rem}.text-sm{font-size:.875rem}.text-lg{font-size:1.125rem}.leading-7{line-height:1.75rem}.mx-auto{margin-left:auto;margin-right:auto}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.ml-2{margin-left:.5rem}.mt-4{margin-top:1rem}.ml-4{margin-left:1rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-6xl{max-width:72rem}.min-h-screen{min-height:100vh}.overflow-hidden{overflow:hidden}.p-6{padding:1.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-8{padding-top:2rem}.fixed{position:fixed}.relative{position:relative}.top-0{top:0}.right-0{right:0}.shadow{box-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px 0 rgba(0,0,0,.06)}.text-center{text-align:center}.text-gray-200{--text-opacity:1;color:#edf2f7;color:rgba(237,242,247,var(--text-opacity))}.text-gray-300{--text-opacity:1;color:#e2e8f0;color:rgba(226,232,240,var(--text-opacity))}.text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.text-gray-500{--text-opacity:1;color:#a0aec0;color:rgba(160,174,192,var(--text-opacity))}.text-gray-600{--text-opacity:1;color:#718096;color:rgba(113,128,150,var(--text-opacity))}.text-gray-700{--text-opacity:1;color:#4a5568;color:rgba(74,85,104,var(--text-opacity))}.text-gray-900{--text-opacity:1;color:#1a202c;color:rgba(26,32,44,var(--text-opacity))}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.w-5{width:1.25rem}.w-8{width:2rem}.w-auto{width:auto}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:640px){.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:h-20{height:5rem}.sm\:ml-0{margin-left:0}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:text-left{text-align:left}.sm\:text-right{text-align:right}}@media (min-width:768px){.md\:border-t-0{border-top-width:0}.md\:border-l{border-left-width:1px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (prefers-color-scheme:dark){.dark\:bg-gray-800{--bg-opacity:1;background-color:#2d3748;background-color:rgba(45,55,72,var(--bg-opacity))}.dark\:bg-gray-900{--bg-opacity:1;background-color:#1a202c;background-color:rgba(26,32,44,var(--bg-opacity))}.dark\:border-gray-700{--border-opacity:1;border-color:#4a5568;border-color:rgba(74,85,104,var(--border-opacity))}.dark\:text-white{--text-opacity:1;color:#fff;color:rgba(255,255,255,var(--text-opacity))}.dark\:text-gray-400{--text-opacity:1;color:#cbd5e0;color:rgba(203,213,224,var(--text-opacity))}.dark\:text-gray-500{--tw-text-opacity:1;color:#6b7280;color:rgba(107,114,128,var(--tw-text-opacity))}}
    </style>

    <style>
        body {
            font-family: 'Nunito', sans-serif;
        }
    </style>
</head>
<body class="antialiased">
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
    @if (Route::has('login'))
        <div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
            @auth
                <a href="{{ url('/home') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Home</a>
            @else
                <a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</a>

                @if (Route::has('register'))
                    <a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</a>
                @endif
            @endauth
        </div>
    @endif

    <div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
        <div class="flex justify-center pt-8 sm:justify-start sm:pt-0">
            This is the "{{Theme::get()}}" theme.
        </div>
        <div class="flex justify-center pt-8 sm:justify-start sm:pt-0">
            <img src={{Theme::url('images/test.gif')}}>
        </div>

    </div>
</div>
</body>
</html>

10、打开首页,主题名为空。如图3

图3

11、编辑 app/Http/Middleware/SetTheme.php,设置主题为 t1,打开首页,主题名为 t1。如图4

图4

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetTheme
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */    public function handle(Request $request, Closure $next)
    {
        $themeName = "t1";
        if(\Theme::exists($themeName)){
            \Theme::set($themeName);
        }

        return $next($request);
    }
}

12、编辑 app/Http/Middleware/SetTheme.php,设置主题为 t2,打开首页,主题名为 t2。如图5

图5

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class SetTheme
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse)  $next
     * @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
     */    public function handle(Request $request, Closure $next)
    {
        $themeName = "t2";
        if(\Theme::exists($themeName)){
            \Theme::set($themeName);
        }

        return $next($request);
    }
}

永夜