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 '配置输出文件夹,路由前缀等'
2月 01

视频以及动画是一个比较好的视觉体验。可是,移动端 h5 的 video 标签不能自动播放视频。下边总结下处理方案。其实,就是在网上找到别人的解决方案来解决自己的事情。

处理处理

video 标签

<video class="video" poster="xxx.jpg" autoplay loop muted 
        playsinline="true"
        x5-playsinline="true" 
        webkit-playsinline="" 
        x-webkit-airplay="allow" 
        x5-video-player-type="h5"
        preload="auto">
                <source src="xxx.mp4" type="video/mp4">
        </video>

方法1,使用类似 gif 图的方式,来实现是视频播放的效果。

https://blog.csdn.net/qq_33929420/article/details/114962347

方法2,使用 js 来手动处理播放

//其他移动端浏览器只能让用户自己触发
$(window).on('touchstart', function(){
    $("video")[0].play();
});
// 兼容微信(微信可以自动播放)
document.addEventListener("WeixinJSBridgeReady", function () {
    $("video")[0].play();
}, false);
12月 13

2022年 10月25日后发布的小程序,无论通过 wx.getUserProfile 还是 wx.getUserInfo 获取到的的用户的信息中(之前也仅能获取头像和昵称。性别地区啥的都会没有),只会有昵称为微信用户,头像为灰色头像的有用信息。说实话,对于开发者,这种搞法太过恶心。

但实践中发现有部分小程序,在用户刚打开小程序时就要求收集用户的微信昵称头像,或者在支付前等不合理路径上要求授权。如果用户拒绝授权,则无法使用小程序或相关功能。在已经获取用户的 openId 与 unionId 信息情况下,用户的微信昵称与头像并不是用户使用小程序的必要条件。为减少此类不合理的强迫授权情况,作出如下调整。

你把你审核的锅摔给开发者,你可以审核的时候不让这种小程序上架不就可以了。记得2019年的时候,就有这样的说明。必须登录注册才能使用的用户不给上架的说明。现在搞什么飞机。

官方相关说明和解决方法

https://developers.weixin.qq.com/community/develop/doc/00022c683e8a80b29bed2142b56c01

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/userProfile.html

11月 30

数组

sort() 方法对数组的项目进行排序。
排序顺序可以是按字母或数字,也可以是升序(向上)或降序(向下)。
默认情况下,sort() 方法将按字母和升序将值作为字符串进行排序。

如:

let fruits = ["Banana", "Orange", "Apple", "Mango"];

fruits.sort();

# 输出 ['Apple', 'Banana', 'Mango', 'Orange']

fruits.reverse();
# 输出 ['Orange', 'Mango', 'Banana', 'Apple']

不过基于数字的排序,是按字符串来排序的。比如 33 排序比 123 要后。 如果非要基于数字大小来排。可以通过传入函数的方式来解决。

比如:

let arr = [1, 123, 22,5, 32];
arr.sort((a,b)=>(a-b));
# 输出 1, 5, 22, 32, 123
# 这样就可以基于数字来排序了

如果是基于数组里的 object ,也可以通过这种方式来排序。

Object 呢

对 object 按键值排序,一般在做加密验证的时候会用到。

有了数组的排序,object 也一样可以。

let param = {
    name: 'vini123',
    gender: 1,
    time: 1669796346
}

let sorted = {};
Object.keys(param).sort().forEach(key => {
    sorted[key] = param[key];
});
11月 19

在微信环境,通过分享方式(非用户搜索,非小程序盒子), 能打开目标小程序,进入目标页面的方式。除了扫小程序码还有小程序链接方式。生成小程序性链接的能力有各种控制,链接的作用和生命也各有控制。做一个体验好,效果好的分享,得细心和用心。还得处处找一切能突破微信自己的阻碍。

主要是小程序分享到朋友圈从来就没一个好的体验。可以通过海报的方式,但用户得长按海报。不是所有人都知道要长按,也不是所有人都愿意长按。其他途径,比如小程序链接。虽然用户可以点击进入,但每次都弹个窗让用户确认。从体验感来说是不好,从用户意愿度(广告)来讲,算是一次告知。可是,既然用户愿意点了,意愿度还是有的,何必弹个窗来恶心人呢。每个人都有每个人的考虑,人在屋檐下,不得不低头。

https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/url-scheme/queryScheme.html

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-scheme.html

开始

看了下边的各种说明,只想说在微信内,打开小程序的途径 scheme 码和 urllink 都不好。这种码 A 打开了,其他人就打不开了。怎么发朋友圈宣传。短链接看起来是最好的,可短链接不是谁都有权限生成的,只有特殊服务类目才可以。就像获取用户当前位置一样,需要特殊服务目录(这个还得去申请接口权限)才有资格用。说来说去,微信内朋友圈宣传只能短链接或海报了。只能去争取申请能获取该接口的服务了。

微信外打开小程序 scheme 码 ios 是直接可以,android 需要借助 h5。 urllink 必须都得先跳转到 h5。只是跳转到 h5,然后再跳转到小程序。不必在 h5 那边再点击再跳转。就是 scheme 码和 urllink 只能一个人打开就有点恶心。

其实还有一种方式,是可以的。就是使用微信开放标签。需要借助服务号(个人可以借助云开发的静态网站),需要额外调用 js。请看下边的链接。

https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html#%E5%BC%80%E6%94%BE%E6%A0%87%E7%AD%BE%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3

https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/staticstorage/introduction.html

scheme 码

该接口用于获取小程序 scheme 码,适用于短信、邮件、外部网页、微信内等拉起小程序的业务场景。通过该接口,可以选择生成到期失效和永久有效的小程序码,有数量限制,目前仅针对国内非个人主体的小程序开放

# 重点说明
自 2022 年 4 月 11 日起,URL Scheme有效期最长 30 天,不再支持永久有效的URL Scheme、不再区分短期有效URL Scheme与长期有效URL Scheme。若在微信外打开,用户可以在浏览器页面点击进入小程序。每个独立的URL Scheme被用户访问后,仅此用户可以再次访问并打开对应小程序,其他用户无法再次通过相同URL Scheme打开该小程序。 在本次规则调整生效前已经生成的URL Scheme,如果有效期超过30天或长期会被降级为30天有效,只能被1个用户访问,开始时间从调整日期开始计算。

每天生成 URL Scheme 和 URL Link 总数量上限为50万

iOS系统支持识别 URL Scheme,可在短信等应用场景中直接通过 Scheme 跳转小程序。
Android系统不支持直接识别 URL Scheme,用户无法通过 Scheme 正常打开小程序,开发者需要使用 H5 页面中转,再跳转到 Scheme 实现打开小程序

urllink

获取小程序 URL Link,适用于短信、邮件、网页、微信内等拉起小程序的业务场景。通过该接口,可以选择生成到期失效和永久有效的小程序链接,有数量限制,目前仅针对国内非个人主体的小程序开放

# 重点说明
自 2022 年 4 月 11 日起,URL Link有效期最长 30 天,不再支持永久有效的URL Link、不再区分短期有效URL Link与长期有效URL Link。若在微信外打开,用户可以在浏览器页面点击进入小程序。每个独立的URL Link被用户访问后,仅此用户可以再次访问并打开对应小程序,其他用户无法再次通过相同URL Link打开该小程序。 在本次规则调整生效前已经生成的URL Link,如果有效期超过30天或长期会被降级为30天有效,只能被1个用户访问,开始时间从调整日期开始计算。 

每天生成 URL Scheme 和 URL Link 总数量上限为50万

只能生成已发布的小程序的 URL Link。
在微信内或者安卓手机打开 URL Link 时,默认会先跳转官方 H5 中间页,如果需要定制 H5 内容,可以使用云开发静态网站。

short link

获取小程序 Short Link,适用于微信内拉起小程序的业务场景。目前只开放给电商类目(具体包含以下一级类目:电商平台、商家自营、跨境电商)。通过该接口,可以选择生成到期失效和永久有效的小程序短链

通过服务端接口可以获取打开小程序任意页面的 Short Link。适用于微信内拉起小程序的业务场景。

目前只开放给电商类目小程序,具体包含以下一级类目:电商平台、商家自营、跨境电商。

最终

说了好多啰嗦的话。微信内推广,宣传,短链接肯定是最好用的。可得去申请服务类目。官方说只对电商开放。不过,看到其他非电商类目的也可以(比如招聘)。