Write the Code. Change the World.

分类目录
3月 02

vue 这边是可以登录了。可是登录态没有保存。在这里,我们仅仅只要知道用户已经登录或已经注册了就好。然后再通过接口去获取用户信息,然后通过状态管理去处理这些信息。

这里不是通过 token 的方式维护状态。所以登录态仅仅加个标志和生命时间。想象一下,这就是一个独立的后台页面。结合 vue 的生态,它的流程是什么样子的呢。

  1. 判断用户是否登录过了。登录过了就直接请求用户信息。这里的用户信息包括用户自身的信息以及后台菜单的信息。所有权限角色都由后端控制和生成。如果还有其他配置信息,也可以连带或再单独发送请求。 这些请求是异步的,这些请求都在 auth 中间件的守护下。如果 cookie 是过期的,后端会返回 401,前端会重定向到登录页面。如果能正常拿到数据,那么就 next,渲染后台页面(左侧菜单,顶部,中间内容部分)。
  2. 用户没登录过。前端直接重定向到登录页面。

流程图如下:

先完成服务端逻辑

用户信息的获取

增加路由,路由应遵循 restful 的风格。通过请求方式 + 资源名 + 参数的方式。比如,如果是 put 请求,表示该请求是修改已存在的资源。

# routes/admin.php

Route::group([
    'middleware' => ['auth:sanctum'],
], function () {
    Route::get('userinfos', [UserController::class, 'getUserInfo'])->name('admin.api.getUserInfo');
});

增加控制器逻辑

# app/Http/Controllers/Admin/UserController
public function getUserInfo(Request $request)
{
    $user = $request->user();

    return response()->json(['user' => $user, 'menus' => []]);
}

这里可以直接拿到用户信息,是因为有 sanctum 中间件的守护。如果用户的认证未通过,在中间件环节就已经返回 401 了。这里是测试,所以菜单,直接给个空的数组。

json 没有指定 http状态码,默认就是 200。

完成 vue 的逻辑

vue 中的状态管理这里用 pinia。

删除默认的 stores/counter.js

增加 stores/userinfo.js

import { defineStore } from 'pinia'

export const useUserinfo = defineStore('userinfo', {
    state: () =>({
        isLogin: localStorage.getItem('isLogin') || false,
        name: '',
        email: ''
    }),
    actions: {
        login() {
            localStorage.setItem('isLogin', true)
            this.isLogin = true
        },
        loginOut() {
            localStorage.removeItem('isLogin')
            this.isLogin = false
        },
        setUserinfo(value) {
            const keys = ['name', 'email']
            keys.forEach(item => {
                if (value[item]) {
                    this[item] = value[item]
                } else {
                    this[item] = ''
                }
            })
        }
    }
})

现在的 pinia 是以前的 vuex 新版。用起来更方便,如果不是它的功能,感觉就自己组件内的调用。而且,也不像以前那样多很多 modules 的概念。

状态处理好了,我们现在来处理路由守护。将以前的路由里边加上守护逻辑就好。

router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import { useUserinfo } from '@/stores/userinfo.js'
import { GetUserinfo }from '@/api/request'

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

router.beforeEach(async (to, from, next) => {
  const whiteList = ['/login']
  const userinfo = useUserinfo()
  const isLogin = userinfo.isLogin
  if (isLogin) {
    GetUserinfo().then(
      res => {
        userinfo.setUserinfo(res.data.user)
        next()
      }
    ).catch(() => {
      userinfo.loginOut()
      next(`/login?redirect=${to.path}`)
    })
  } else {
    if (whiteList.some(value => value == to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})
export default router

路由守护做好了,我们只需要在登录成功后调用状态的 login 方法,并跳转到首页就好。

关于这个跳转,体验做到更好的话,不是非要强制跳转到首页的。比如本来用户是在看后台的用户信息,这个时候刚好 session 过期了,需要重新登录。登录成功后跳转到之前的页面,就是后台的用户信息页面才是最好的。

关于这个路由,我们可以在登录组件中,通过路由的监听获取到。

最后,我们修改下 header 组件。如果用户登录了,就显示用户的称呼。否则显示登录注册按钮。

<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 v-if="userinfo.name">
                <span>{{ userinfo.name }}</span>
            </n-space>

            <n-space v-else 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'

import { useUserinfo } from '@/stores/userinfo.js'

const userinfo = useUserinfo()

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>

退出功能没做。这里就不做了,这里仅仅是 demo。不是真正的项目。

其实,注册按钮也是不需要的。后台我就不想让用户注册。还有登录这里,我其实也不想要用户输入,直接让用户扫码还更快。现在谁没个手机,现在谁没个微信。就是这么霸道,只要一个登录二维码就可以。

这里,我还是把注册相关的删掉吧。删除注册组件,删除注册按钮,删除注册路由信息。

好了。现在打包一下项目。拿到 php 那边去测试一下。

pnpm run build

效果如图所示。

到此,已经验证了。 项目主网站和vue做的spa后台网站,可以公用登录态。下一步就是做移动端(app、小程序等)的接口逻辑。这个接口适用 token 的方式来验证登录态,不再是 cookie和session的方式。

提交代码。清洗车子。收拾房子。

git add .
git commit -m '登录功能彻底完成'

git remote add origin https://gitlab.com/demo9885/vue3_backstage.git

git push -u origin main

代码仓库: https://gitlab.com/demo9885/vue3_backstage

这个只是一个简单的架构。离完整的项目还差很多。比如多语言,多主题(白天,晚上等),组件库,图表,大屏展示等等。

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