Write the Code. Change the World.

3月 04

后台的接口授权问题已经解决。现在做移动端的接口。移动端接口路由写在 routes/api.php 中。测试用 postman 就可以。不用去做 ui 界面。

定义一个登录接口和获取用户信息接口

移动端一般都是使用微信授权登录或手机号登录,这样比较贴近场景和方便。为了测试,这里使用邮箱和密码登录。

routes/api.php

<?php

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

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

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

当前,UserController.php 文件还没有,我们创建一个。

php artisan make:controller Api/UserController

里边加入相应的逻辑。

<?php

namespace App\Http\Controllers\Api;

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

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

        if (!Auth::validate($data)) {
            return response()->json(['message' => '账号密码错误'], 403);
        }

        $user = User::where('email', $data['email'])->first();

        $user['token'] = $user->createToken('auth')->plainTextToken;

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

    public function getUserInfo(Request $request)
    {
        $user = $request->user();

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

这里依然使用了 LoginRequest 来进行字段的验证。

下边,打开 postman 来进行验证验证。

故意写错密码。返回了错误提示,http 状态码是指定的 403。故意不写 401,401 有它独有的作用。然后修改为正确密码再测试。


这次,返回了正常的数据,也返回了 token。下一步,就通过 token 来获取用户的信息。

这个是在 header 中没有加入 token,返回的结果。这里存在两个问题,第一不能返回正确的结果,第二这里返回的是 html。我们接口通常需要返回的是 json 。给个 html 算啥。这个有两个解决方法,第一就是在发起请求的时候,在 header 中指定 accept 为 application/json 就好。这样框架会知道接口想要什么样的数据。测试如下。

还有一种方式,服务端在数据请求之前主动在header加上 accept。因为很 明确这里是做接口的,那我就干脆直接加上。这个场景,我们新建一个前置中间件就可以完成。

php artisan make:middleware AcceptHeader

# 主动加入 accept

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class AcceptHeader
{
    public function handle(Request $request, Closure $next)
    {
        $request->headers->set('Accept', 'application/json');

        return $next($request);
    }
}

然后,在 $middlewareGroups 的 api 的第一个位置,配置上这个中间件就可以。

App/Http/Kernel.php

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

再来试试,这样就可以了。

最后,我们加上正确的 token。就能正常的获取的用户信息了。

到此。一个简单多端授权使用的功能初步完成了。但对于一个完整的项目,这还远远不够。需要去舔砖加瓦,写更多符合场景需要的业务。

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'