Write the Code. Change the World.

分类目录
3月 02

服务端 admin/api 的注册登录逻辑处理。

开始

先创建基本的控制器和Request文件。

php artisan make:controller Admin/UserController

php artisan make:request Admin/AdminRequest

php artisan make:request Admin/RegisterRequest

php artisan make:request Admin/LoginRequest

我们先把 request 完成起来。 AdminRequest 作为其他 Request 的父类,需要做两件事。

一是 authorize 永远返回 true。这个是给接口用的,不是网页自己用。没必要去验证通过。验证的逻辑会在路由和中间件中完成。

二是重写 failedValidation 方法。默认的 failedValidation 会发起重定向。这里是接口,也是不需要的。

所以 AdminRequest.php 是这个样子。

<?php

namespace App\Http\Requests\Admin;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Validation\ValidationException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\JsonResponse;

class AdminRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json([
            'error' => (new ValidationException($validator))->errors()
        ], JsonResponse::HTTP_UNPROCESSABLE_ENTITY));
    }
}

再看其他 request。

#LoginRequest

<?php

namespace App\Http\Requests\Admin;

class LoginRequest extends AdminRequest
{
    public function rules(): array
    {
        return [
            'email' =>  ['required', 'string', 'email', 'max:255'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function messages()
    {
        return [];
    }

    public function attributes()
    {
        return [
            'email' => '邮箱',
            'password' => '密码'
        ];
    }
}

# RegisterRequest

<?php

namespace App\Http\Requests\Admin;

class RegisterRequest extends AdminRequest
{
    public function rules(): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
            'email' =>  ['required', 'string', 'email', 'max:255', 'unique:users'],
            'password' => ['required', 'string', 'min:8'],
        ];
    }

    public function messages()
    {
        return [];
    }

    public function attributes()
    {
        return [
            'name' => '称呼',
            'email' => '邮箱',
            'password' => '密码'
        ];
    }
}

在 UserController.php 中创建登录注册接口。

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\LoginRequest;
use App\Http\Requests\Admin\RegisterRequest;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
use App\Models\User;

use Illuminate\Http\Request;

class UserController extends Controller
{
    public function login(LoginRequest $request)
    {
        $data = $request->only(['email', 'password']);

        $remember = true;

        if (Auth::attempt($data, $remember)) {
            $user = auth()->user();
            return response()->json($user);
        } else {
            return response()->json(['message' => '账号或密码错误'], 403);
        }
    }

    public function register(RegisterRequest $request)
    {
        $data = $request->only(['name', 'email', 'password']);

        $data['password'] = Hash::make($data['password']);

        $user = User::create($data);

        Auth::guard()->login($user);

        return response()->json($user);
    }
}

最后,看看路由。

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\UserController;

// 登录
Route::post('login', [UserController::class, 'login'])->name('admin.api.login');

// 注册
Route::post('register', [UserController::class, 'register'])->name('admin.api.register');

csrf-token 路由是 Sanctum 自己提供的。我们只需要再 config/sanctum.php 中配置上前缀就好。

    …
    'prefix' => 'admin/api'

到了这里,服务端的逻辑算是好了。现在用之前创建的 vue 项目来测试登录注册接口。

我们先从简单的入手。那就是登录。登录页面和注册页面很香香。是这样子的。

<template>
    <div class="container">
        <div class="form-wrap">
            <n-card title="登录">
                <n-form
                    ref="formRef"
                    :model="model"
                    :rules="rules"
                    label-placement="left"
                    label-width="auto"
                    require-mark-placement="right-hanging"
                    :style="{
                        maxWidth: '640px'
                    }">
                    <n-form-item label="邮箱" path="email">
                        <n-input v-model:value="model.email" :placeholder="rules.email.message" />
                    </n-form-item>
                    <n-form-item label="密码" path="password">
                        <n-input type="password" v-model:value="model.password" :placeholder="rules.password.message" />
                    </n-form-item>
                    <n-form-item label=" " :show-feedback="false" class="login-item">
                        <n-button strong type="primary" @click="submit">登录</n-button>
                    </n-form-item>
                </n-form>
            </n-card>
        </div>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { GetCsrfCookie, Login } from '@/api/request.js'

const router = useRouter()

const formRef = ref(null)

const model = reactive({
    email: null,
    password: null
})

const rules = {
    email: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入邮箱'
    },
    password: {
        required: true,
        trigger: ['blur', 'input'],
        min: 8,
        message: '请输入密码'
    }
}

const submit = () => {
    formRef.value
        ?.validate((errors) => {
            if (errors) {
                console.error(errors)
            }
        })
        .then(() => {
            doLogin()
        })
}

const doLogin = () => {
    GetCsrfCookie(null).then(() => {
        Login({ email: model.email.trim(), password: model.password.trim() }).then(() => {
            // 登录完成,就跳转到首页吧
            router.push({ name: 'home' })
        })
    })
}
</script>

<style lang="scss" scoped>
.container {
    display: flex;
    justify-content: center;

    .form-wrap {
        display: flex;
        margin: 30px 0;
        min-width: 640px;
    }

    .login-item {
        margin-top: 12px;
    }
}
</style>

好了,我们打个包,试一试。

pnpm run build

然后将打包好的 admin 这个文件夹移动到 laravel 项目的 public 下。

浏览器访问:http://ypb2.com/admin/

ok,显示正常。然后刷新也是没有问题的。再点点登录注册。也可以。好吧,那来登录吧。因为之前通过 laravel 已经注册过一个账号了,就用那个账号登录。

演示如下图:

通过这个可以看到 vue 编写的 spa 页面和 laravel 自己的页面,共同持有 cookie。保证了登录的一致性。

这样做的目的就是想 laravel 的页面是官网。 vue 写的是后台。后台和官网有各自的特点。也有对应的方式去实现(后台用 vue、react 这样实现起来比较好)。

到了这里,还不算完成。虽然 vue 页面登录了。但是对登录的用户信息没有保存处理。下一步,就是对登录的信息进行保存处理。

3月 02

这里来安装 naiveui,将上一步的测试页面搭建完成。

官网: https://www.naiveui.com/zh-CN/os-theme

安装

pnpm add -D naive-ui

流行的组件库,都支持按需引入。我们通过 unplugin-auto-import插件来完成。

pnpm add -D unplugin-vue-components

pnpm add -D unplugin-auto-import

然后再 vite.config.js 中进行配置。

import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      imports: ['vue']
    }),
    Components({
      resolvers: [
        NaiveUiResolver()
      ]
    })
  ],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

通过上边的配置,就可以在项目中,直接使用 naiveui 的组件了。

现在,我们修改下 header.vue。来实现登录注册首页三个页面的跳转。

# header.vue
<template>
    <header>
        <div>
      <n-avatar round size="large" @click="goHome">
        LOGO
      </n-avatar>
    </div>

        <div class="nav-bar">
            <n-space class="left-nav">
                <n-button quaternary @click="goHome">首页</n-button>
                <n-button quaternary>产品介绍</n-button>
                <n-button quaternary>关于我们</n-button>
            </n-space>

            <n-space class="right-nav">
                <n-button type="success" size="small" style="font-size: 12px;" @click="goLogin">登录</n-button>
                <n-button type="success" size="small" style="font-size: 12px" @click="goRegister">注册</n-button>
            </n-space>
        </div>
    </header>
</template>

<script setup>

import { useRouter } from 'vue-router'

const router = useRouter()

const goHome = () => {
    router.push({name: 'home'})
}

const goLogin = () => {
  router.push({name: 'login'})
}

const goRegister = () => {
  router.push({name: 'register'})
}
</script>

<style lang="scss" scoped>
header {
    display: flex;
    align-items: center;
    box-sizing: border-box;
    padding: 0 30px;
    width: 100%;
    height: 72px;
    background-color: #fff;
    box-shadow: 0 0.125rem 0.25rem #00000013 !important;

  .nav-bar {
    display: flex;
    align-items: center;
    width: 100%;
    box-sizing: border-box;
    padding-left: 50px;

    .left-nav {
      flex:1;
    }
  }
}
</style>

效果如下:

提交代码。

git add .
git commit -m '安装 Naive以及配置顶部 Ui'

顶部ui以及路由跳转完成了。现在做注册页面的 ui 以及相关逻辑。

注册页面

对注册页面 ui 简单的进行一个布局,对表单数据只进行了基础的验证,就是只要存在就好。更详细的规则没配置。

<template>
    <div class="container">
        <div class="form-wrap">
            <n-card title="注册">
                <n-form
                    ref="formRef"
                    :model="model"
                    :rules="rules"
                    label-placement="left"
                    label-width="auto"
                    require-mark-placement="right-hanging"
                    :style="{
                        maxWidth: '640px'
                    }">
                    <n-form-item label="称呼" path="name">
                        <n-input v-model:value="model.name" :placeholder="rules.name.message" />
                    </n-form-item>
                    <n-form-item label="邮箱" path="email">
                        <n-input v-model:value="model.email" :placeholder="rules.email.message" />
                    </n-form-item>
                    <n-form-item label="密码" path="password">
                        <n-input type="password" v-model:value="model.password" :placeholder="rules.password.message" />
                    </n-form-item>
                    <n-form-item label="确认密码" path="confirmPassword">
                        <n-input type="password" v-model:value="model.confirmPassword" :placeholder="rules.confirmPassword.message" />
                    </n-form-item>

                    <n-form-item label=" " :show-feedback="false" class="register-item">
                        <n-button strong type="primary" @click="submit">注册</n-button>
                    </n-form-item>
                </n-form>
            </n-card>
        </div>
    </div>
</template>

<script setup>
import { ref, reactive } from 'vue'

const formRef = ref(null)

const model = reactive({
    name: null,
    email: null,
    password: null,
    confirmPassword: null
})

const rules = {
    name: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入称呼'
    },
    email: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入邮箱'
    },
    password: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入密码'
    },
    confirmPassword: {
        required: true,
        trigger: ['blur', 'input'],
        message: '请输入确认密码'
    }
}

const submit = () => {
    formRef.value?.validate((errors) => {
        if (errors) {
            console.error(errors)
        }
    }).then(() => {

  })
}
</script>

<style lang="scss" scoped>
.container {
    display: flex;
    justify-content: center;

    .form-wrap {
        display: flex;
        margin: 30px 0;
        min-width: 640px;
    }

    .register-item {
        margin-top: 12px;
    }
}
</style>

效果如下:

提交代码。

git add .
git commit -m '注册页面的搭建'

开始写数据请求部分的逻辑了。一般用 axios、flyio 这类库。这里用 axios。

安装、配置、使用 axios

pnpm add axios

安装好后,然后简单封装一个请求库,src/api/request.js
如下:

import axios from 'axios'

// 全局基本配置
axios.defaults.baseURL = 'http://ypb2.com/admin/api/'
axios.withCredentials = true
axios.timeout = 20000

// 请求拦截器
axios.interceptors.request.use(
    (config) => {
        config.headers = {
            Authorization: `Bearer ${GetToken()}`
        }
        return config
    },
    (error) => {
        return Promise.reject(error)
    }
)

// 响应拦截器
axios.interceptors.response.use(
    (response) => {
        return response
    },
    (error) => {
        return Promise.reject(error)
    }
)

// 获取 token
function GetToken() {
    const token = localStorage.getItem('token')
    const now = new Date().getTime() / 1000
    if (token && token.expires_at > now) {
        return token.token
    }
    return null
}

// scrf-cookie
const GetCsrfCookie = () => {
    return axios.get('csrf-cookie')
}

// 登录
const Login = (data) => {
    return axios.post('login', data)
}

// 注册
const Register = (data) => {
    return axios.post('register', data)
}

export {
    GetCsrfCookie,
    Login,
    Register
}

然后就可以在 register.vue 中用起来了

import { ref, reactive, onMounted } from 'vue'
import { Register } from '@/api/request.js'


…
onMounted(() => {
    Register(null).then()
})

当然,这样肯定是有问题的。一个是跨域,另外一个也是不符合服务端 Sanctum 认证的场景的。默认进来必须先进行一个 csrf-cookie 请求。再进行登录或注册的逻辑。

为了更贴合服务端的请求。我们将 base 设置为 admin,并且将 vue-router 的base也设置成 admin。

# vite.config.js
export default defineConfig({
  base: '/admin',
  build: {
    outDir: 'admin'
  },
  …


# router/index.js
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  base: '/admin/',
  routes: [
  …

于是,访问的地址就变成:http://ypb2.com/admin/

这样在 vite 这里是可以的。但是在 nginx 那边就不行。打开页面再刷新就找不到资源了。对 nginx 也需要配置。这里先配置好。

cd /etc/nginx/sites-enabled

sudo vim ypb2.com

# 添加下边的配置
    location ~* ^\/admin\/((?!api\/).) {
        try_files $uri $uri/ /admin/index.html;
    }

# 重启 nginx
sudo nginx -s reload

这个配置很重要的哈。到此,算是前端打包的配置完成。git 版本控制中将 admin 目录设置为忽略。提交版本控制。

git add .
git commit -m '配置输出文件夹,路由前缀等'
3月 01

Sanctum 已经配置了,那么现在就做一个单页面,来进行测试。主要是做登录、注册的调用。这里使用 vue3。这里仅仅是测试,不作为项目代码的一部分,可以理解为是一个单独项目,但是调用的接口是项目文件里构建的。

新建 vue3 项目

# 创建项目
pnpm create vue

#配置如下图所示
# 不要 ts
# 也不要支持 jsx
# 需要 router
# 需要 pinia
# 许愿单元测试
# 不需要端到端测试
# 需要 eslint检查
# 需要美丽的格式化代码

然后执行下边命令,把项目运行起来

cd yuepaibao
pnpm install
pnpm format
pnpm dev

在浏览器中打开 http://localhost:5173/ 就可以访问到站点了。看到效果了。

我们养成好习惯,把代码添加到版本控制中。

git init -b main
git add .
git commit -m 'vue3 initialize'

对 eslint 和 prettier 进行配置

配置 eslint,修改 .eslintrc.cjs 文件

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
    root: true,
    extends: [
        './.eslintrc-auto-import.json',
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/eslint-config-prettier'
    ],
    parserOptions: {
        ecmaVersion: 'latest'
    },
    rules: {
        'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
        'comma-dangle': 0,
        'vue/multi-word-component-names': 'off',
        semi: ['warn', 'never']
    }
}

comma-dangle 为0,禁止在数据的尾部追随逗号
semi: ['warn', 'never'] 禁止尾部使用分号
'vue/multi-word-component-names': 'off' 允许组件命名为单个单词。比如 header.vue。

配置 prettier,修改 .prettierrc.json 文件

{
    "printWidth": 120,
    "tabWidth": 2,
    "useTabs": true,
    "semi": false,
    "singleQuote": true,
    "quoteProps": "as-needed",
    "bracketSpacing": true,
    "bracketSameLine": true,
    "arrowParens": "always",
    "vueIndentScriptAndStyle": false,
    "endOfLine": "auto",
    "trailingComma": "none"
}

整理页面

安装 sass

pnpm add -D sass

去掉不需要组件页面,删除main.css 和 base.css。再重新建立一个 main.scss 文件。到此,只有一个干净的 App.vue 文件和 main.scss 文件。这个时候肯定会报错的。我们接着整。

我们新建以下几个页面(习惯把公用的组件成为组件,其他的叫页面)。

.
├── home
│   └── index.vue
├── layouts
│   ├── footer.vue
│   └── header.vue
└── user
    ├── login.vue
    └── register.vue

然后,往里边加一些简单代码。如下:

header.vue

<template>
    <header>
      <h2>this is header</h2>
    </header>
  </template>

  <script setup>
  </script>

  <style lang="scss" scoped>
  header {
      display: flex;
      box-sizing: border-box;
      padding: 0 30px;
      width: 100%;
      height: 72px;
      background-color: #fff;
      box-shadow: 0 .125rem .25rem #00000013!important;
  }
  </style>

footer.vue

<template>
  <footer>
    <h2>this is footer</h2>
  </footer>
</template>

<script setup>
</script>

<style lang="scss" scoped>
footer {
    display: flex;
    width: 100%;
    height: 220px;
    background-color: #383838;
    box-sizing: border-box;
    padding: 0 30px;

    h2 {
        color: #fff;
    }
}
</style>

home.vue

<template>
  <div class="container">
    <h2>this is home</h2>
  </div>
</template>

<script setup>
</script>

<style lang="scss" scoped>
.container {
    box-sizing: border-box;
    padding: 0 30px;
}
</style>

login.vue

<template>
  <div class="">login</div>
</template>

<script setup>
</script>

<style lang="scss" scoped></style>

register.vue

<template>
  <div class="">register</div>
</template>

<script setup>
</script>

<style lang="scss" scoped></style>

然后修改 router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/home/index.vue'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('../views/user/login.vue')
    },
    {
      path: '/register',
      name: 'register',
      component: () => import('../views/user/register.vue')
    }
  ]
})

export default router

修改 App.vue,配合创建的这些页面

<script setup>
import { RouterView } from 'vue-router'

import Header from '@/views/layouts/header.vue'
import Footer from '@/views/layouts/footer.vue'

</script>

<template>
  <div class="main">
    <Header />
    <RouterView />
  </div>

  <Footer />
</template>

<style scoped>
</style>

最后修改 main.js,引入 css 改为引入 scss。就是文件名改一下。main.scss 填入以下 code。

html,body {
    margin: 0;
    padding: 0;
}

#app {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    min-height: 100vh;

    .main {
        display: flex;
        flex-direction: column;
        flex:1;
    }
}

这样,一个基本的架构页面就出来了。如下图:

好了,提交代码。

git add .
git commit -m '构建基础页面'

下一步,就是找一个好看的 ui 库。写登录和注册表单逻辑。 elemeui 看多了,and design 也看多了,这里用 naiveui。好就这样。

3月 01

bootstrap 脚手架带了登录注册相关的页面和逻辑。这里来测测。然后使用 Sanctum 来处理授权相关的逻辑。它不仅可以处理 web 页面的请求,还可以使用令牌的方式来处理api请求 。

这里是一个提供多平台服务的项目。有来自 app,多家小程序的接口请求,也有后台页面(单spa页面)的请求,还有基于官网的请求。所以 Sanctum 是一个很好的选择

.env 文件中配置好了数据库的信息,并且也创建了数据库后

# 执行迁移生成对应的数据表
php artisan migrate 

打开首页,点击注册页面。输入对应信息,点击注册。成功后会重定向到 /home uri 下。只是,我们之前删除了 home 路由。修改登录后重定向的地址就可以正确重定向了。

# app/Providers/RouteServiceProvider.php

# 修改(之前是 public const HOME = '/home';)
public const HOME = '/';

以上是 laravel 的默认认证方式。还有截图看到的注册页面,在 container 样式后边加了一个 mt-4。因为 app.blade.php 中是没有任何间隙的。

以上只是一个演示。下边就所有的用户认证都用 sanctum 处理。

sanctum 阶段

官网文档: https://laravel.com/docs/10.x/sanctum

laravel10 默认已经安装了 Sanctum 。我们只需要使用 vendor:publish 来发布 Sanctum 的配置和迁移文件。

php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"

其实,默认已经创建了的。

修改 config/auth.php

我们先在 config/auth.php 增加一个 'api' 的守护,这个是给app、小程序这些接口适用的。默认的 'web' 这个是给 web 页面和后台共同使用的,他们公用 cookie,并且支持 csrf 验证。 后台也是通过接口的方式进行调用的。只是这种方式限定了后台的主域和网站的主域要一致。

    'guards' => [
        //原来的
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        // 新增的
        'api' => [
            'driver' => 'sanctum',
            'provider' => 'users',
            'hash' => false,
        ]
    ],

修改 .env.example

在末尾追加配置

# .env.example 文件,参与版本控制
# sanctum 白名单
SANCTUM_STATEFUL_DOMAINS=

同样,在 .env 末尾也追加配置

# sanctum 白名单
SANCTUM_STATEFUL_DOMAINS=.xr.com

修改 app/Http/Kernel.php

$middlewareGroups 修改。

…
        'api' => [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

$middlewareAliases 修改,在末尾追加下边两个配置。这个不是必须的,有用到就配置。

    protected $middlewareAliases = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        …
        'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
        'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,

增加路由服务的配置

修改 app/Providers/RouteServiceProvider.php, 在 boot 中增加一个后台的配置。修改 api 接口的配置。后台将使用 vue 或 react 做成前 spa 页面。将使用前后端分离的方式。这里虽然看起来是一起,在 nginx 中,会做处理。

            Route::prefix('admin/api')
                ->middleware('web')
                ->group(base_path('routes/admin.php'));

然后,在 routes 文件夹下,增加一个 admin.php 的路由控制文件。

最后对 User 模型进行配置

app/Models/User.php 中增加

protected $guard_name = 'web';

其实,默认就是的了。这里可以不要。如果配置改名了,这里就得用上了。

到此,基于网站的,以及基于后台的(接口),还有基于app,小程序的(接口)用户认证已经配置完成了。

# 网站
ypb2.com

# 后台
ypb2.com/admin/api

# app、小程序接口
ypb2.com/api

后边,将对这三种情景进行测试和验证。

3月 01

laravel10 + bootstrap5 自适应站点[3] 是多余的,是临时看到一个赏析悦目的站点的一个冲动。还是回到正常的思路上来。我们这里先完成一个 loading 的功能。这里使用 nprogress。

开始吧

# 安装
pnpm add nprogress

# 配置 css (resources/sass/app.scss),加入以下代码
@import "nprogress/nprogress.css";

/* Loading 颜色 */
#nprogress .bar {
    background: $primary-color !important;
}

# 配置 js (resources/js/app.js),加入一下代码
import NProgress from "nprogress";

// 进度条
NProgress.configure({ showSpinner: false });

NProgress.start();

window.onload = () => {
    NProgress.done();
}

# 编译
pnpm run build

到此,配置和使用完毕。刷新页面,就可以在顶部看到 loading 的效果了。这是一个粗略简单的效果。其实,就想要这种简单多的就好。

提交代码

git add .
git commit -m '创建页面 loading nprogress'
2月 20

安装好了 bootstrap,我们先确定好主题色,然后开始创建网站的公用模块(头部和底部)。

前奏

路由调整,功能连贯

我们先删掉默认的 welcome.blade.php 文件和 HomeController 文件。

再创建一个默认的页面控制器,我喜欢叫 PageController,并且是放在 web 文件夹下的。后边会有 api 的控制器,所以才这样分开。

php artisan make:controller web/PageController

# 在里边添加一个方法,输出一个前端页面
/**
 * Show the application dashboard.
 *
 * @return \Illuminate\Contracts\Support\Renderable
 */
public function home()
{
    return view('home');
}

这里,不需要构造器。在之前删除的构造器中有构造器 ,并指定了 auth 的中间件。这样用户只要不是登录态访问页面,都会默认跳转到登录页面。

修改路由文件 web.php

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\web\PageController;

Route::get('/', [PageController::class, 'home'])->name('home');

Auth::routes();

在浏览器访问 http://xr.com 就可以看到顶部带有注册登录按钮的页面了。

本地化(多语言)

这个时候,注册和登录是英文的。是多语言功能没配置好。可以在 resources 下创建 lang 目录,lang 下创建 en 和 zh-CN 目录。里边分别创建 messages.php。然后填充下边的内容。

# en/messages.php
<?php

return [
    'login' => 'Login',
    'register' => 'Register',
    'logout' => 'Logout',
];

# zh-CN/messages.php
<?php

return [
    'login' => '登录',
    'register' => '注册',
    'logout' => '登出',
];

然后 修改 app.blade.php 中的 __('Login') 为 __('messages.login'),再打开站点。

发现英文的已经变成了中文了。

默认,lang 是直接在项目根目录下的,也就是和 resources 同级。个人不喜欢在根目录放太多玩意,就放在 resources 下。
多语言还可以使用 json 文件,还可以配置传参,也可以配置安装包的多语言。更多信息请看官方文档。
https://laravel.com/docs/10.x/localization

这里站点是纯中文的,后边也不考虑英文。就直接删除 lang。以及在 blade 中直接使用中文,不使用多语言调用了。

基本构成(思路)

经过前边的一些调整。我们来好好处理下 app.blade.php 文件。这个是整个站点的灵魂和基础。传统的网站,头部和底部都是公用的,中间区域随着页面不同而不同。这里就单独把头部和底部抽离出来。中间部分定义好插槽即可。这个插槽和 vue 项目中又有不同。理解为占坑就可。可以为 style 样式位置占坑,可以为 html 位置占坑,也可以为 js 占坑。

一点一点的来。

还有一个细节。上述所描述的一个容器,如果遇到页面高度比浏览器窗口的高度小很多的情况呢。页面那么多,看过有的网站在这种情况下。底部之后,会有很大一片空白(页面是从上到下的平面)。这样很难看。在这里,约定父容器的最小高度就是 100vh。页面减去顶部和中间部分的,就是底部的。想办法让顶部和中间撑开到自适应不就看可以了吗。这样会好看一点。后边出一个图。

头部

  1. 头部我喜欢用抽屉式(Offcanvas)。
  2. 头部我预想有一个 logo,几个主菜单,还有搜索框登录注册按钮构成。就是三部分。其实,这里实例中已经给出了结构。

底部

  1. 底部放一列友情链接
  2. 底部放一列联系方式
  3. 底部放一列二维码(最多两个)

上边就是心中所想。然后得用 bootstrap 的结构来表达,并且是融入 laravel 的 blade 中的。

基本构成(文件)

  1. app.blade.php
  2. header.blade.php
  3. footer.blade.php

header.blade.php

这样算比较简洁。因为是官网,不是论坛博客性质的,还是去掉搜索相关功能。还有多语言,也要准备起来。

<header class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
    <div class="container-fluid px-5">
        <a class="navbar-brand" href="{{ url('/') }}">
            Logo
        </a>

        <button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvasNavbar"
            aria-controls="offcanvasNavbar">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasNavbar" aria-labelledby="offcanvasNavbarLabel">
            <div class="offcanvas-body">
                <ul class="navbar-nav flex-grow-1 px-3 main-nav">
                    <li class="nav-item active">
                        <a class="nav-link" aria-current="page" href="#">首页</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">约拍</a>
                    </li>

                    <li class="nav-item">
                        <a class="nav-link" href="#">瞬间</a>
                    </li>

                    <li class="nav-item">
                        <a class="nav-link" href="#">关于我们</a>
                    </li>
                </ul>

                <ul class="navbar-nav pe-3">
                @guest
                    @if (Route::has('login'))
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('login') }}">登录</a>
                        </li>
                    @endif

                    @if (Route::has('register'))
                        <li class="nav-item">
                            <a class="nav-link" href="{{ route('register') }}">注册</a>
                        </li>
                    @endif
                @else
                    <li class="nav-item dropdown">
                        <a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button"
                            data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
                            {{ Auth::user()->name }}
                        </a>

                        <div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="{{ route('logout') }}"
                                onclick="event.preventDefault();
                                             document.getElementById('logout-form').submit();">
                                登出
                            </a>

                            <form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
                                @csrf
                            </form>
                        </div>
                    </li>
                @endguest
                </ul>
            </div>
        </div>
    </div>
</header>

header.blade.php

这里先空着,占个位置

<footer>

</footer>

app.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">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    @vite(['resources/sass/app.scss', 'resources/js/app.js'])
</head>
<body>
    <div id="app">
        <main>
            @include('layouts.header')
            @yield('content')
        </main>
        @include('layouts.footer')
    </div>
</body>
</html>

当然,对应的 css 也要的。

我们先在 resources/sass/app.scss 中添加。

# 定义你的主题色
$primary-color: #ECB34D;

html,
body {
    height: 100%;
    color: #000000;
    background: #f4f4f4;
    font-family: "PingFang SC", "微软雅黑", "Microsoft YaHei", Helvetica,
        "Helvetica Neue", Tahoma, Arial, sans-serif;
    font: 14px "PingFang SC", "微软雅黑", "Microsoft YaHei", Helvetica,
        "Helvetica Neue", Tahoma, Arial, sans-serif;
}

a,
a:link,
a:active,
a:hover,
a:visited,
a:focus {
    text-decoration: none;
    list-style: none;
    outline: none;
    cursor: pointer;
}

ul {
    list-style: none;
    list-style-type: none;
}

button:focus,
button:active:focus,
button.active:focus,
button.focus,
button:active.focus,
button.active.focus {
    outline: none;
}

.btn-primary {
    color: #fff !important;
}

#app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
    height: 100%;

    main {
        display: flex;
        flex-direction: column;
        flex: 1 0 auto;
    }
}

header {
    height: 72px;

    // 导航
    .main-nav {
        .nav-item {
            display: flex;
            padding: 7px 16px;
            border-radius: 4px;
            color: #383838;
            margin-right: 16px;

            .nav-link {
                padding: 0 !important;
                background-color: inherit !important;
                font-weight: 300 !important;
                font-size: 16px !important;
                line-height: 20px !important;
                color: #383838;
            }
        }

        .nav-item.active {
            background-color: $primary-color;
        }

        .nav-item.active>.nav-link {
            color: #fff;
        }

        .nav-item:not(.active):hover {
            background-color: $primary-color;
        }

        .nav-item:not(.active):hover>.nav-link {
            color: #fff;
        }
    }
}

footer {
    width: 100%;
    height: 220px;
    background-color: #383838;
}

最后,修改下 home.blade.php

@extends('layouts.app')

@section('content')

<div class="container pt-4">
    <div class="row justify-content-center">
        <h2>home</h2>
    </div>
</div>
@endsection

然后,在打开首页看看。

一些说明

域名其实我自己配置的是 ypb2.com。所以看到的是截图上的样子。 然后访问是 http,上服务器是要配置成 https 的。还有 favicon 也没有配置。以及看代码 keywords 等都没有处理。后边都会要完善的。以及页面的 loading 也没处理。虽然那么多年过去了,还是喜欢用 nprogress 那样的方式。

提交代码

git add .
git commit  -m '处理页面的连接以及基本配置'
2月 18

做网站用 php,做自适应网站用 bootstrap。很早的时候,这是一个不错的组合。喜欢 laravel,那么现在用 laravel10 + bootstrap5 + vue 就不错。

创建一个 laravel 项目

laravel10 release 版本已经发布,可以这样创建

composer create-project laravel/laravel xxx10 --prefer-dist

### 查看当前版本(当前显示 10.0.3)
cd xxx10
php artisan --version

# git 初始化,方便版本管理
git init
git add .
git commit -m 'laravel initialize 10.0.3'

修改配置

这里我 homestead 配置的域名是 xr.com,数据库名是 xr

.env

vim .env
# 修改 APP_URL 以及数据库配置
APP_URL=http://xr.com
DB_DATABASE=xr
DB_USERNAME=homestead
DB_PASSWORD=secret

config/app.php

vim config/app.php

# 修改以下内容
'timezone' => 'Asia/Shanghai',
'locale' => 'zh-CN',
'faker_locale' => 'zh_CN',

添加辅助函数

加辅助函数集,用来安放无副作用的函数。增加到 app 下。

vim app/helpers.php

# 增加一个辅助测试函数
<?php 

    function test($str = 'world') {
        return 'Hello ' . $str;
    }

# 将该文件增加到 composer 的自动加载配置中。 autoload->files 就是需要 composer 自动帮我们加载的函数库(不含类)

vim composer.json

    "autoload": {
        "psr-4": {
            "App\\": "app/",
            "Database\\Factories\\": "database/factories/",
            "Database\\Seeders\\": "database/seeders/"
        },
        "files": [
           "app/helpers.php" 
        ]
    },

# 然后 dump (这一步是不可少的)
composer dump-autoload

加好了之后,就可以来尝试一下了。这里打开 tinker 来试试。

php artisan tinker;

test();

test('bird')

最后,提交代码。

git  add .
git commit -m '修改基础配置以及创建辅助函数集'

安装 ui 包

composer require laravel/ui

php artisan ui bootstrap --auth

# 现在前端就用 pnpm + vite 组合
pnpm install 

pnpm run dev

如果报 System limit for number of file watchers reached 错误,请看 https://blog.vini123.com/837

这样就 ok 了。 注意看 resources/views/layouts/app.blade.php。如果站点效果都统一,可以其他页面都继承该模板。blade 模板真的很好用啊,可以继承,可以重写,可以插槽。

跟随时代的潮流,用上 vite 来构建前端项目。之前,laravel9 就开始使用 vite 来构建了。

因为是自己构建的,所以可以定义一些 bootstrap 的一些特征。比如 primary 的颜色。这样就很爽。

# resources/sass/_variables.scss 中,在尾部追加下边的代码(这样你的 primary 色就是红色了 。),当然还有更多的定义。

$primary: #f00;

https://v5.bootcss.com/docs/customize/color/

https://www.runoob.com/bootstrap5/bootstrap5-colors.html

相关

vite 中文文档 https://cn.vitejs.dev/guide/

bootstrap5 中文文档:https://v5.bootcss.com/docs/getting-started/introduction/

bootstrap5 教程 https://www.runoob.com/bootstrap5/bootstrap5-tutorial.html

11月 19

时间,日期相关的处理, carbon 的确是个好东西。感觉离不开呀。

Carbon 是对 PHP DateTime 模块的二次扩展;提供时间格式化,时间计算的功能;

官方主页:http://carbon.nesbot.com/;

github: https://github.com/briannesbitt/Carbon

参考

https://www.jianshu.com/p/7e45bc4d0006

https://www.php.cn/phpkj/laravel/484191.html

https://learnku.com/articles/62292

11月 11

只要 websocket 服务。最简单化的 websocket 服务。不需要其他基于 swoole 的其他框架应用。仅仅 swoole 扩展 + php + nginx 完成 websocket 的服务。就是想最简单简洁的实现。

就像官方的实例一样 https://wiki.swoole.com/#/start/start_ws_server,就这么些代码。

$ws = new Swoole\WebSocket\Server('0.0.0.0', 9502);

//监听WebSocket连接打开事件
$ws->on('Open', function ($ws, $request) {
    $ws->push($request->fd, "hello, welcome\n");
});

//监听WebSocket消息事件
$ws->on('Message', function ($ws, $frame) {
    echo "Message: {$frame->data}\n";
    $ws->push($frame->fd, "server: {$frame->data}");
});

//监听WebSocket连接关闭事件
$ws->on('Close', function ($ws, $fd) {
    echo "client-{$fd} is closed\n";
});

$ws->start();

我们不喜欢用 ip 来连接,我们用域名。我们不喜欢用 http,我们用 https。我们不喜欢加端口号,我们用代理。这里我们用 nginx 来搞定。比如吧,我们自己的域名是: xx.com, nginx 就可以像下边这样配置:

map $http_upgrade $connection_upgrade {  
    default upgrade;  
    '' close;  
}

server 
{    
    listen 443 ssl http2; 
    server_name xx.com;

    ssl_certificate         /usr/local/server/nginx/conf/ssl/xx.com.pem;
    ssl_certificate_key     /usr/local/server/nginx/conf/ssl/xx.com.key;

    charset utf-8;
    index index.php index.html index.htm;
    root /www/xx/xx.com/public;

    location /wss {
        # 代理到上边的自定义的地址
        proxy_pass http://127.0.0.1:9502;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location / {
        try_files $uri $uri/  /index.php?$query_string; 
    }

    location ~ \.php($|/) {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param HTTPS $https if_not_empty;
        fastcgi_split_path_info ^(.+\.php)(.*)$;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

服务端的配置构建的重要部分就算好了。需要注意的是,创建 websocket 的时候,一定不要带上 ssl 相关的参数和证书。这样你 nginx 这边请求代理过去是 http 请求,就会出错。

websocket 这里没有心跳功能,可以手动模拟创建一个,去实现类似的业务。

还有,在服务端可以使用 swoole 自己的存储结构。 https://wiki.swoole.com/#/memory/table

旧的文章

https://blog.vini123.com/405

https://blog.vini123.com/482

https://blog.vini123.com/294