vue3微信公众号商城项目实战系列(6)用户登录
1. 一个商城要实现购物的功能,需要能识别用户的身份,这样才能完成加购物车,下单,付款等操作。
但微信公众号商城和PC端商城有些不一样,区别在于微信公众号商城使用微信支付的时候需要一个openid的参数(以后再具体讲)
这个参数必须访问微信公众号提供的接口才能获取到,基于这个原因,用户登录的时候必须额外调用微信公众号的接口获取openid并保存下来,
以便付款的时候使用。
注:openid也可以在付款这个环节去获取,即时获取即时用,推荐在登录的时候处理。
2. 为了简化业务,将重点放在vue3前端技术上,我们的微信公众号商城设计尽可能简洁,用户表结构如下:
表名 | 字段 | 功能 |
userinfo |
user_id (int) 用户编号 nickname (varchar) 昵称 acc (varchar) 账号 pwd (varchar) 密码 open_id (varchar) 微信openid mobile (varchar) 联系电话 address (varchar) 收货地址 |
用户信息表 |
3. 对于一个商城来说,当用户不登录时是可以以游客的身份浏览商品的,当加购物车的时候再跳转到登录页,
所以登录页有2种类型的入口,一种是在 "我的" tab页,需要放置一个【登录】的连接;
另一种就是在进行【加购物车】等操作需要用户身份的时候自动跳转到登录页;
另外,登录页还需要提供注册功能,页面如下图:
当用户输入账号密码的时候,去调用后端的接口完成登录验证,如果验证通过,就将token及用户信息返回,
前端页面将返回的信息保存到本地,这样后续调用需要用户身份的接口时就不需要重复登录了。
4. vue3项目和一般的web服务器项目比如 jsp 、 php 、asp.net 有少许不同,后者不但可以编写页面,还可以直接访问数据库,
而vue3是一个前端框架,它只负责页面的呈现、响应用户的操作,这个过程中需要或产生的数据要以 ajax 的形式通过 webapi 来和数据库打交道。
vue3中官方推荐的 ajax 包是 axios , 可以用它来处理各种服务端的 webapi 请求,
官网地址:https://www.axios-http.cn/docs/intro,官方文档截图如下:
5. 使用 axios ,分2步:
第1步:安装 axios 包 , 命令: npm install axios,如下图:
安装完成:
第2步:导入 axios 并使用 Axios API 调用 后端的 webapi ,具体可以参考文档。
本系列中会对 Axios API 做封装,方便后续维护。
另外,用户登陆后需要在本地存储 token等信息以便其他接口使用,还需要对 token等信息做持久化处理。
6. 完整步骤如下:
第1步:新增如下文件夹和文件
storage.js持久化登录后的用户信息,request.js封装axios请求,api.js 记录具体的api接口,Login.vue是登录页面,代码分别如下。
storage.js 代码
const storage = { set:function(val) { localStorage.removeItem("userinfo"); localStorage.setItem("userinfo", JSON.stringify(val)); }, get:function(){ let info = localStorage.getItem("userinfo"); if(!info || typeof info == "undefined"){ return {}; } try{ return JSON.parse(info); } catch(e){ return {}; } }, clear:function(){ localStorage.removeItem("userinfo") }, check:function(){ let info = localStorage.getItem("userinfo"); if(info && info.length>1){ return true; } return false; } } export default storage;
定义了一个storage对象,该对象有4个方法,分别用于管理本地的用户信息,localStorage 是浏览器上的全局对象
,我们用 storage 做了一下封装,如果以后要更换持久化的方式,在这里改就可以了,外部的调用不变。
注:
1. 微信浏览器内核是QQ浏览器X5内核。
2. localStorage的几个方法都需要一个key值,我们这里固定为 userinfo,实际项目也可以给 storage 对应的方法加参数,这样可以从外部传入key值。
request.js 代码
import axios from 'axios'; import storage from '@/http/storage.js'; //配置默认的Content-Type格式 axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded'; //创建axios实例 const service = axios.create({ timeout: 60*1000, //请求超时时间60秒 baseURL: '/dev-api', }) //请求拦截器 service.interceptors.request.use(config => { //***在发送请求之前做些什么*** if(storage.check()){ //如果已登录就把token信息加到请求头中,不同的webapi这里的添加方式不一样,这是ASP.NET的写法 let info = storage.get(); //获取存储在本地的token信息 config.headers['Authorization'] = 'Bearer ' + info.token } return config; }, error => { //***对请求错误做些什么*** return Promise.reject(error); }) //响应拦截 service.interceptors.response.use(response => { //***2xx 范围内的状态码都会触发该函数*** //***对响应数据做点什么*** return response.data.data; }, error => { //***超出 2xx 范围的状态码都会触发该函数*** //***对响应错误做点什么*** return Promise.reject(error); }) //导出实例对象 export default service;
这里的代码基本上是从axios上复制而来,只在请求拦截器上加了一个校验,如果 持久化存储中的 userinfo 有值
,就把 token 加到请求头中,userinfo 的 json 格式如下(微信建议不要将 open_id 下发到客户端,实际项目中请去掉,这里只为演示):
{ userId:111, nickname:'张三', acc:'abcd1', pwd:'1', open_id:'xxxxx', mobile:'15900008888', address:'阳光大道123号豫王府2栋1单元1601', token:'xxxxxxxxxxxxxxxxx' }
每段代码都有注释,重点说一下 "baseURL: '/dev-api'," 这一段,baseURL 是请求的基地址 , 先看 官方文档上的解释:
它可以是一个完整api接口前缀 , 也可以是本文上面配置的 "/dev-api" ,配置完后,我们调用接口的时候
,实际的请求地址会和 baseURL 拼接成完整的地址作为最终的接口地址,这里我们为什么要配置成 "/dev-api"的形式
,而不用完整的地址呢,接下来会讲到这样做的目的。
api.js 代码:
import request from './request.js' //登录接口 //write by: 屏风马 //date : 2023-4-17 export function signin(data) { return request({ url: '/sign/signin', method: 'post', data: data }) }
这里对后端接口做了封装,统一放置在这个文件中,如果有其他接口,只需要在这里加方法就可以了
,"import request from './request.js'" 中的 request 就是 request.js 中导出的 service 对象,它代表的是一个配置好的 axios 实例对象
,return request( ) 传入的参数是一个 json 对象,url 是接口地址 , 实际请求会和 baseURL 拼接后再发送出去
,如果 baseURL = 'https://some-domain.com/api/' ,那么实际请求地址就是 'https://some-domain.com/api/sign/signin'
,相应的, 如果baseURL = '/dev-api' ,那么实际请求地址就是 '/dev-api/sign/signin' 。
method 和 data 分别表示请求的方法和参数。Login.vue页面调用的时候只需要传入请求的参数就可以,非常简洁,因为所有接口都在这个文件中,后期如果
要改接口对应的路径,比如部分路径不符合 RESTful 风格可能要调整,直接到这个文件修改就可以了,否则,大量的接口 url 散落在项目的各个文件中
,维护起来就比较头大。
Login.vue 代码
1 <template> 2 <div class="login"> <br /> 3 <div><span>用户登录</span></div> <br /> 4 <div> 5 <dl> 6 <dd>账号</dd> 7 <dt><input name="acc" v-model="loginForm.acc" /></dt> 8 <dd>密码</dd> 9 <dt><input name="pwd" v-model="loginForm.pwd" /></dt> 10 <dt><input type="button" value="登录" @click="onLogin" /></dt> 11 <dt>没有账号?去注册</dt> 12 </dl> 13 </div> 14 </div> 15 </template> 16 17 <script setup> 18 import { reactive } from 'vue'; 19 import { useRouter } from "vue-router"; 20 import { signin } from "@/http/api"; 21 import storage from '@/http/storage.js'; 22 23 const router = useRouter(); 24 const loginForm = reactive({ 25 acc: 'abcde', 26 pwd: '1' 27 }) 28 29 function onLogin() { 30 signin(loginForm).then(res=>{ 31 storage.set(res); 32 router.push({name:'home'}); 33 }); 34 } 35 </script> 36 37 <style> 38 .login{ 39 width: 370px; 40 margin: 0 auto; 41 text-align: center; 42 } 43 </style>
我们逐行分析一下这里的代码:
第7行和9行是一个输入框对象,上面有一个 v-model的属性 , 它对应的是 24 行定义的 loginForm 对象的2个属性,这种写法是 vue3 的绑定语法
,任何一个 json对象都可以用 vue3 框架内置的 reactive() 函数处理一下,这样做的结果就是 <script>代码块中的 loginForm 对象的2个属性和
输入框的 v-model 属性进行了绑定,当用户在页面上输入值时 , loginForm 对象中的属性被同步修改了,反之亦然, 即做到了输入框的值和
代码中的值完全同步 !这就是 vue3 的响应式编程的双向数据绑定,如果用传统的3件套来写,取值赋值及onblur事件都要写一堆的代码,效率很低。
而 vue3 只要用 reactive( ) 把一个 json对象处理一个就解决了全部的问题。
第10行是一个按钮,上面有一个click事件,只要在 click 前面加上 @ 符号,后面的字符串 "onLogin" 就和 <script>代码块中的
函数 function onLogin() { }绑定在一起了,当用户点击登录按钮的时候就会执行 onLogin() 这个方法。
第17行,<script>代码块中有一个 setup 关键字,这表示组件的书写方式是组合式API(Composition API)
,另一种方式是选项式API(Options API),下图是官网上的说明,通过对比组合式API更扁平化、更清晰,本系列全部用组合式API 。
|
|
第18行 , reactive 是 vue 框架中的函数,使用前要先导入进来后再使用。
第19行,useRouter 是路由对象 vue-router 中的函数,如果不用 <router-link>标签来导航而用编程的方式(类似 location.href )做跳转需要用到这个函数。
第20行,导入 api.js 中的 signin 方法,因为接下来登录会用到它,如果要用到多个接口方法,用逗号隔开就可以了。
js 文件导入时可以省略 .js 的后缀名。from 后的 @ 代表路径 "/src" ,因为它的使用频率很高,就用 @ 代替,这是在 vite.config.js中配置的,如下:
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
第21行,导入 storage 这个对象来存储登录成功后 接口端传递过来的用户信息,其 json 格式上面有介绍。
第29行,这个函数响应用户的登录操作,signin( ) 是 api.js中暴露出来的登录接口对应的函数,只需要传入 loginForm 就可以了
,因为它是一个响应对象 ,同步记录了用户的输入,不需要额外的处理就得到了输入框的数据,一些都是那么简单
,signin() 的返回值是一个 Promise 对象,执行成功后用 .then( ) 来处理返回值,传入一个箭头函数
,先把返回值 存储在了 localStorage 中,然后用 23 行生成的 路由对象的 push 方法跳转到 home 这个页面。
关于 router 对象:
1. 导航到另一个页面用 router.push() 方法,从方法名可以看到除了导航到目标页面外,它还把页面压入栈中
,这样当用户点返回按钮的时候就能退回到上一个页面,而不需要我们在程序中做任何追踪处理。
2. router.push() 跳转时 可以带参数,这个后面根据情况具体讲。
3. 这里的 router 是根据路由表中定义的名称来导航的,见 /src/router/routerx.js 中的配置:
const routes = [ {path:"/",name:'welcome',component:()=>import("../views/Welcome.vue")}, {path:"/home",name:'home',component:()=>import("../views/Home.vue")}, {path:"/login",name:'login',component:()=>import("../views/Login.vue")}, ]
我们还可以根据 path 属性来导航,为了整个项目风格统一,用 name 就可以了。
4. 每一个页面都需要在上面的 routers 数组中预先配置好,这样不至于导航的时候找不到对应的页面。登录 login.vue 能够访问是因为这里已做了配置。
接下来,我们在 chrome 中模拟访问 /login 页面, 如下:
页面报错了,这是因为在 request.js 中把 baseURL 设置成了 "/dev-api",这个路径因为没有完整的URL
,axios 默认给我们把当前开发 web 服务器的 ip和端口(localhost:5173) 加上去了,如下图:
实际项目中,后端接口可能是另一个小组的同事在写,为了开发方便,这个接口写好后通常会部署在公网的一台开发服务器上,方便前端同事调用
,其接口地址类似:https://some-domain.com/api/sign/signin,所以我们需要配置后端接口的url地址,打开 vite.config.js , 加入如下的配置信息:
server: { host: true, port:80, open:true, proxy:{ '/dev-api':{ target: 'http://testapi.xxx.com/api', changeOrigin: true, secure: false, rewrite:(path)=> path.replace('\/dev-api', '') }, } },
host 可以是开发服务器所在的 ip 地址 , 如果不限 ip , 用 true 就可以。port 是端口号,默认是 5173,这里我们改成 80 ,这样chrome中调试就可以省略端口号了。
proxy 用于配置代理服务器信息。在开发阶段,后端接口服务地址前缀一般是这样的 "http://testapi.xxx.com/api/" ,我们本机开发环境运行vue3项目是 localhost:80
, 去访问远程服务器时会存在跨域问题,这是因为 vue3 项目本质是运行在浏览器内核上的,如果访问的服务器域名+端口+协议(https和http是不同的协议)不同
,浏览器就认为跨域了,会禁止访问,所以在这里配置代理信息,当我们的访问网址中含有 "/dev-api" 这个路径的时候
,就以代理服务器 'http://testapi.xxx.com/api' 的名义去访问
,即 "http://localhost:5173/dev-api/sign/signin" 会被变成 "http://testapi.xxx.com/api/dev-api/sign/signin"
,配置中的属性 rewrite 的值是一个箭头函数,作用是用正则表达将路径中的 "'/dev-api'" 替换成空字符串
,所以实际的代理地址是 "http://testapi.xxx.com/api/sign/signin",这正是我们要访问的地址。
在 signin中加入log信息,如下红色字体:
function onLogin() { signin(loginForm).then(res=>{ console.log(res); storage.set(res); router.push({name:'home'}); }); }
将浏览器地址中的端口号去掉,刷新页面后点登录按钮如下:
登录成功,打印出了后端接口返回的用户数据及token信息,并跳转到了 Home.vue 页面。