若依框架-补充篇:Vuex全局状态管理Axios二次封装

news2024/9/23 11:21:11

        在上一篇《若依框架:前端登录组件与图像验证码|用户登录逻辑》中的篇末,对Vuex全局状态管理、Axios二次封装部分介绍的较为粗略,因此就有了这个补充篇。

目录

Vuex全局状态管理

Vuex是什么?

如何理解“状态管理模式”?

Vuex的注册和配置

主模块index.js代码

子模块index.js代码

Vuex状态值的获取

Axios与网络请求

什么是Axios?

Axios的配置规则

 Axios二次封装  

后端接口状态码定义

前端Axios二次封装


Vuex全局状态管理

Vuex是什么?

        Vuex的官网地址为:Vuex 是什么? | Vuex。

        Vuex是什么呢?Vuex是一个专门为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。其实它就类似于Java后端框架中的Spring容器的概念,只不过Spring容器是用于专门管理各种Bean实例的,而Vuex则是前端应用中专门用于管理全局共享状态的。

Vuex特点

        早期的前端Web应用开发,针对那些全局共享的变量,一般会将其挂载到window对象上,因为JavaScript是单线程的,所以也无需担心状态不一致的问题。Vuex可以被视为是用于替代这套原始方案的一种前端框架。在前后端分离模式的Vue前端应用开发过程中,例如:有一个全局共享的User用户信息对象,那么就可以将其存储到Vuex的store中,在需要获取时,直接去调用Vuex暴露出来的commit()/dispatch()方法;在需要获取User用户对象信息时,直接去调用getters方法即可。这是一种可以简化全局状态管理方案的前端应用框架。

如何理解“状态管理模式”?

        一个包含状态管理模式的应用可以分为以下三个部分:

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

        如下图所示,由于Vuex中存储的状态值是响应式的,因此,当state数据源变化时,就会推动Vue组件进行Render重新渲染view视图,以更新页面中的显示的值。

        而state数据源的变化,则需要由用户交互事件来进行触发,例如:点击了某个按钮、鼠标划过了某个DOM元素等等,导致在组件内调用Vuex暴露出来的Dispatch()进行异步更新、调用Vuex暴露出来的Commit进行同步更新,最终使得state数据源产生变化。这就是actions的一个触发过程。

        而状态管理模式,则是将与state数据源相关的部分进行封装,同时提供了获取和更新状态值的方法,最后将这个对象作为一个单例对象存在于Vue前端应用中。这种状态管理模式的优点在于:

        ①在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!

        ②通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。

Vuex的注册和配置

        使用Vuex时,需要注意:在一个模块化的打包系统中,您必须显式地通过 Vue.use() 来安装 Vuex。通常地,我们会在项目根目录下创建文件——src/store/index.js,用于对Vuex进行安装和配置操作。示例代码如下,其中:针对Vuex的核心概念——State、Getters、Mutations、Actions、Modules都做了代码注释。

主模块index.js代码

import Vuex from "vuex";
import Vue from "vue";
import user from "./modules/user";
import { getStatus } from "@/apis/home";

//注册Vuex
Vue.use(Vuex);

/**
 * 配置全局状态管理模式
 * 每一个Vuex应用的核心就是store仓库对象,它本质上就是一个容器,包含着应用中大部分的状态 (state)。
 * Vuex 和单纯的全局对象有以下两点不同:
 * [1] Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
 * [2] 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。
 */
const store = {
  //state:存储响应式的全局状态值
  state: {
    count: 0,
    name: "Vuex",
    status: {
      code: 200,
      msg: "获取成功",
      data: [
        {
          name: "Tom",
          age: 18,
          address: "London",
        },
      ],
    },
  },
  /**
   *
   * getters:用于从state中的全局状态值中获取派生数据,像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
   * 有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数
   * 每一个getters下的函数,可以接收如下参数:
   *    参数1:state,可以通过state参数获取state配置项中的状态值
   *    参数2:getters,可以通过getters参数调用getters配置项中的getXXX()方法
   */
  getters: {
    //判断数据是否获取成功
    getStatus(state) {
      const { code, data, msg } = state.status;
      if (code === 200) {
        return data;
      } else {
        return msg;
      }
    },
  },
  /**
   * keyPoints:。在 Vuex 中,mutation 都是同步事务,因此,mutation 必须是同步函数
   * 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
   * Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。
   * 这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
   *
   * 要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法,例如:store.commit("setName");
   * 并且,可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload),这样可以使用payload载荷来更新目标值,例如:store.commit("setName","Lily"),这样state.name的值就被更新为Lily
   */
  mutations: {
    setName(state, payload) {
      state.name = payload;
    },
    setCount(state, payload) {
      state.count = payload;
    },
    setStatus(state, payload) {
      state.status = payload;
    },
  },
  /**
   * Action 类似于 mutation,不同在于:
        Action 提交的是 mutation,而不是直接变更状态。
        Action 可以包含任意异步操作。
     
   */
  actions: {
    //在方法内部异步调用后端接口,来更新status的值
    asyncSetStatus({ commit }, payload) {
      console.log(`payload=${payload}`);
      return new Promise((resolve, reject) => {
        getStatus(payload)
          .then((result) => {
            const { data } = result;
            commit("setStatus", data);
            resolve(data);
          })
          .catch((error) => {
            reject(error);
          });
      });
    },
  },
  /**
   * Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
   */
  modules:{
    user:user,
  }
};

export default new Vuex.Store(store);

子模块index.js代码

/**
 * Vuex-store子模块定义
 */
const user = {
  namespaced: true,//定义独立的命名空间
  state:()=>({
    name:"user",
  }),
  getters:{
    getName(state){
      return state.name;
    }
  },
  mutations:{
    setUserName(state,name){
      state.name = name;
    }
  },
  actions:{
    asyncSetUserName({commit},payload){
        commit("setUserName",payload);
    }
  }
}
export default user;

Vuex状态值的获取

        Vuex状态值的获取可以通过Vuex暴露出来的mappers接口来简化代码书写,也可以直接从Vue实例的原型对象上获取。以下是示例代码,以及打印结果,

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
// @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
import { mapState,mapGetters, mapMutations, mapActions } from 'vuex';

export default {
  name: 'Home',
  components: {
    HelloWorld
  },
  computed:{
    //计算属性写法
    count(){
      return this.$store.state.count;
    },  
    name(){
      return this.$store.state.name;
    },
    status(){
      return this.$store.state.status;
    },
    /**
     * Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值
     * 通过getters暴露出来的store.getters对象获取status对应的派生数据
     */
    status_data(){
        return this.$store.getters.getStatus;
    },
    //mapState辅助函数写法
    // ...mapState({
    //   count:state=>state.count,
    //   name:state=>state.name,
    //   status:state=>state.status,
    // }),
    /**
     * mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
     */
    ...mapGetters({
      get_status_data:"getStatus",
      get_userName:"user/getName",
    })
  },
  created(){
    console.log(this.$store)
    console.log(this.name);
    console.log(this.status);
    console.log(this.status_data)
    console.log(this.get_status_data);
    console.log(this.get_userName);
    //使用mapMutations -更新count的值
    this.setCount(15); //也可以直接修改count的值: this.$store.commit("setCount",15);
    console.log(this.count);
    //修改actions的值
    // this.asyncSetStatus(256).then(data=>{
    //   console.log(data);
    // });
    this.$store.dispatch("asyncSetStatus",256).then(data=>{
      console.log(data);
    })
  },
  methods:{
    ...mapMutations({
      setCount:"setCount",
    }),
    ...mapActions({
      asyncSetStatus:"asyncSetStatus"
    })
  }
}
</script>

        

Axios与网络请求

什么是Axios?

        Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

Axios的配置规则

        Axios类似于传统项目开发时使用的Ajax的替代品,它也有自己的一套默认配置规则,也支持开发者自定义请求的规则,详情可见Axios官网介绍:

请求配置 | Axios 中文文档 | Axios 中文网https://www.axios-http.cn/docs/req_config
请求配置 | Axios 中文文档 | Axios 中文网https://www.axios-http.cn/docs/req_config

 Axios二次封装  

        在实际开发中,我们较少直接使用原生的Axios(默认配置)去做网络请求操作,而是会对其进行二次封装。像若依前端项目中,就在@/src/utils/request.js脚本中对Axios进行了二次封装。

        当然,我们也可以根据实际项目需求,进行自定义的二次封装。

后端接口状态码定义

        而谈到Axios的二次封装,通常也离不开后端接口状态码这个话题。因为很多请求的响应结果,在前端进行处理时,是需要根据状态码来判断是否获取到了正确的结果,以此为依据执行不同的逻辑处理流程。

        在上一篇文章中有谈到若依后端项目中对于状态码的封装,形成了一个HttpStatus类。方便起见,我们就根据这个类的定义,在下一部分来对Axios进行二次封装。

package com.ruoyi.common.constant;

/**
 * 返回状态码
 * 
 * @author ruoyi
 */
public class HttpStatus
{
    /**
     * 操作成功
     */
    public static final int SUCCESS = 200;

    /**
     * 对象创建成功
     */
    public static final int CREATED = 201;

    /**
     * 请求已经被接受
     */
    public static final int ACCEPTED = 202;

    /**
     * 操作已经执行成功,但是没有返回数据
     */
    public static final int NO_CONTENT = 204;

    /**
     * 资源已被移除
     */
    public static final int MOVED_PERM = 301;

    /**
     * 重定向
     */
    public static final int SEE_OTHER = 303;

    /**
     * 资源没有被修改
     */
    public static final int NOT_MODIFIED = 304;

    /**
     * 参数列表错误(缺少,格式不匹配)
     */
    public static final int BAD_REQUEST = 400;

    /**
     * 未授权
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * 访问受限,授权过期
     */
    public static final int FORBIDDEN = 403;

    /**
     * 资源,服务未找到
     */
    public static final int NOT_FOUND = 404;

    /**
     * 不允许的http方法
     */
    public static final int BAD_METHOD = 405;

    /**
     * 资源冲突,或者资源被锁
     */
    public static final int CONFLICT = 409;

    /**
     * 不支持的数据,媒体类型
     */
    public static final int UNSUPPORTED_TYPE = 415;

    /**
     * 系统内部错误
     */
    public static final int ERROR = 500;

    /**
     * 接口未实现
     */
    public static final int NOT_IMPLEMENTED = 501;

    /**
     * 系统警告消息
     */
    public static final int WARN = 601;
}

前端Axios二次封装

        对Axios的二次封装流程,通常是包括:①Axios实例的创建;②拦截器配置;③通用网络请求方法的封装;④重复的异步请求的过滤处理。

        以下,我们进行Axios的二次封装,实现上述的:①、②、④的需求,示例代码如下,

import axios from "axios";
import { MessageBox, Message, Notification } from "element-ui";
import { getToken, removeToken } from "@/utils/auth";

//请求池-用于存储请求接口-防止重复提交
const pendingRequestPool = new Map();
//间隔时间小于interval的,被视为重复提交的请求
const interval = 1000;

console.log(process.env);
//创建Axios实例
const request = axios.create({
  baseURL: process.env.VUE_APP_BASE_URL,
  timeout: 1000 * 60, //60s
  headers: { "X-Custom-Header": "foobar" },
});

//Axios请求拦截器
request.interceptors.request.use(
  (config) => {
    //在发送请求之前做些什么
    const isToken = Boolean((config.headers || {}).isToken); //根据请求配置是否需要为请求头添加token
    const isRepeated = Boolean((config.headers || {}).isRepeated); //根据请求配置判断是否需要防止重复提交请求
    let cancelFunction = undefined; //引用取消请求的cancelToken
    //挂载请求key
    config.requestKey = `${config.method}-${config.url}`;
    if (!!isToken && getToken()) {
      //添加token到请求头
      config.headers["token"] = getToken();
    }
    if (!!isRepeated) {
      //防止重复提交请求
      const requestObject = {
        url: config.url,
        data:
          typeof config.data === "object"
            ? JSON.stringify(config.data)
            : config.data,
        time: Date.now(),
      };
      const sessionObejct = pendingRequestPool.get(config.requestKey);
      //创建一个 cancel token
      config.cancelToken = new axios.CancelToken((cancel) => {
        cancelFunction = cancel;
      });
      if (sessionObejct === undefined || sessionObejct === null) {
        //请求池中不存在相同的请求-将其加入请求池
        pendingRequestPool.set(config.requestKey, requestObject);
      } else {
        //请求池中存在相同的请求-根据时间间隔interval判断是否为重复的请求
        const { url, data, time } = sessionObejct;
        if (Date.now() - time <= interval) {
          //重复请求-取消当前请求
          cancelFunction();
          Promise.reject(new Error(`数据正在处理,请勿重复提交`));
        }
      }
    }
    return Promise.resolve(config);
  },
  (error) => {
    //对请求错误做些什么
    return Promise.reject(error);
  }
);

//Axios响应拦截器
request.interceptors.response.use(
  (response) => {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    const requestKey = response.config.requestKey;
    pendingRequestPool.delete(requestKey);
    console.log(response);
    //对不同的状态码进行判断处理
    const { code, data, msg } = response.data;
    //判断状态码
    if (code === 401) {
      Message({ message: `登录状态已过期`, type: "error" });
      removeToken(); //移除Cookie中的缓存Token信息
      return Promise.reject(`无效的会话,会话已过期,请重新登录`);
    } else if (code === 500) {
      Message({ message: `系统内部错误,请联系管理员!`, type: "error" });
      return Promise.reject(new Error(msg));
    } else if (code === 601) {
      Message({ message: `系统未知警告,请稍后再试!`, type: "warning" });
      return Promise.reject(new Error(msg));
    } else if (code !== 200) {
      Notification.error({ title: "未知错误,请联系管理员!" });
      return Promise.reject("error");
    } else {
      return Promise.resolve(response);
    }
  },
  (error) => {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  }
);

//导出Axios实例
export default request;

        测试接口如下,

import request from "@/utils/request";

/**
 * 获取状态码status
 * @param {*} url 
 * @param {*} code 
 * @returns 
 */
export const getStatus = (code)=>{
  return request({
    url:"/app/status",
    method:"get",
    params:{
      code:code,
    },
    headers:{
      isToken:true,
      isRepeated:true,
    }
  });
}

        响应结果如下,

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/133378.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java语法】之String类练习1

目录 1.字符串中的第一个唯一字符 2. 最后一个单词的长度 58. 最后一个单词的长度 3.验证回文串 4.字符串相加 5.小结&#xff1a; 1.字符串中的第一个唯一字符387. 字符串中的第一个唯一字符https://leetcode.cn/problems/first-unique-character-in-a-string/ 给定一个字符…

【免费开放源码】审批类小程序项目实战(活动申请详解)

第一节&#xff1a;什么构成了微信小程序、创建一个自己的小程序 第二节&#xff1a;微信开发者工具使用教程 第三节&#xff1a;深入了解并掌握小程序核心组件 第四节&#xff1a;初始化云函数和数据库 第五节&#xff1a;云数据库的增删改查 第六节&#xff1a;项目大纲以及制…

Mac下安装go

1.下载地址 ​​​​​​https://golang.google.cn/dl/ 2.安装Go 3.查看安装效果 go version go env 4.安装vscode和插件 4.1.安装vscode https://code.visualstudio.com/Download 4.2.安装GO插件 4.3.设置goproxy 执行命令&#xff1a;vim ~/.bash_profile export GO1…

数值分布的分散程度对迭代次数的影响

( A, B )---1*30*2---( 1, 0 )( 0, 1 ) 让网络的输入只有1个节点&#xff0c;AB各由7张二值化的图片组成&#xff0c;排列组合A和B的所有可能性&#xff0c;固定收敛误差为7e-4&#xff0c;统计收敛迭代次数 1 2 3 4 5 6 7 迭代次数 1b 1b 1b 1b 1b 1b 0 0*0*0…

PHP---文件上传

目录 一、文件上传的概念 二、文件上传的步骤 &#xff08;1&#xff09;表单的制作 三、$_FILES详解 &#xff08;1&#xff09;name &#xff08;2&#xff09;tmp_name &#xff08;3&#xff09;type &#xff08;4&#xff09;error &#xff08;5&#xff09;si…

YOLO v6:一个硬件友好的目标检测算法

本文来自公众号“AI大道理” YOLOv6 是美团视觉智能部研发的一款目标检测框架&#xff0c;致力于工业应用。 YOLOv6支持模型训练、推理及多平台部署等全链条的工业应用需求&#xff0c;并在网络结构、训练策略等算法层面进行了多项改进和优化&#xff0c;在 COCO 数据集上&…

一文轻松明白 Base64 编码原理

把图片丢进浏览器&#xff0c;打开sources能看到一长串字符串&#xff0c;这是图片的Base64编码。这一长串编码到底是怎么生成的呢&#xff1f; 我们接下来探索一下base64编码的原理 Base64 名称的由来 Base64编码要求把3个8位的字节&#xff08;3824&#xff09;转化为4个6…

C++代码编程学习(2):类和对象封装部分的两个案例-立方体与点圆位置

C类与对象 封装的学习 挺有趣的&#xff01; 一、前言 昨日有点事忙了些&#xff0c;今天把昨天学习的两个案例给整理一下&#xff0c;C确实比较原始基础&#xff0c;在学习过程中需要好好总结分析与记录。 二、效果展示 案例一&#xff1a;设计立方体 立方体的面积和体积 用…

阿里微服务质量保障系列(一):微服务知多少

年初买了一本集团巨佬联合出的书《阿里测试之道》&#xff0c;然后认真拜读了下&#xff0c;我相信看过的同学都会获益匪浅&#xff0c;此书分享了阿里在大促保障、移动App测试、大数据测试、AI系统测试、云计算测试、资损防控、物流类测试等领域的方法、技术和工具平台&#x…

十一、Properties、多线程

Properties集合 Properties作为Map集合的使用 介绍 是一个Map体系的集合类Properties可以保存到流中或从流中加载属性列表中的每个键及其对应的值都是一个字符串 基本使用 public static void main(String[] args) {Properties prop new Properties();//增prop.put("…

Pytorch c++ 部署报错解决方案

目录 1. Only the versions between 2017 and 2019 (inclusive) are supported! 2. Cannot find cuDNN library. Turning the option off C 部署的时候&#xff0c;demo 写完之后&#xff0c;提示如下错误 1. Only the versions between 2017 and 2019 (inclusive) are sup…

使用Kubernetes部署xxl-job-admin及xxl-job执行器服务

部署环境 xxl-job-2.4.0kubernetes-1.26 这里以xxl-job官方的2.4.0的代码为例子&#xff0c;在官方编写的Dockerfile基础上使用dockerkubernetes进行部署&#xff0c;xxl-job-admin和执行器的Dockerfile、application等配置文件并不是关键&#xff0c;所以这里示例安装以官方…

Linux系统初始化进程及文件(带命令)

作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​​ 目录 前言 一.系统初始化进程及文件 1.init 进程 2.Systemd概述 3.SysVi…

【Java语言】— 循环结构 :for循环

循环结构&#xff1a;for循环 1.for循环 控制一段代码反复执行很多次。 for循环的格式如下&#xff1a; for (初始化语句;循环条件;迭代语句){循环体语句(重复执行的代码);}下面我们通过案例&#xff1a;输出3次HelloWorld感受一下。 //需求&#xff1a;输出3次HelloWorld…

OpenOCD 不同仿真器使用操作总结记录

针对不同的仿真器使用 OpenOCD 时候的设置操作总结 ...... 矜辰所致目录前言一、OpenOCD 环境搭建二、OpenOCD 基本测试三、Makefile 中仿真器配置3.1 ST-link3.2 Jlink3.2 CMSIS-DAP&#xff08;待更新&#xff09;结语前言 在使用 GCC 环境开发 ARM 系列芯片的时候&#x…

ArcGIS基础实验操作100例--实验35等高线生成DEM

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验35 等高线生成DEM 目录 一、实验背景 二、实验数据 三、实验步骤 方法一 &#xff…

4.5、静态路由配置及其可能产生的路由环路问题

静态路由配置是指用户或网络管理员使用路由器的相关命令给路由器人工配置路由表\color{red}人工配置路由表人工配置路由表。 这种人工配置方式简单、开销小。但不能及时适应网络状态&#xff08;流量、拓扑等&#xff09;的变化。\color{red}但不能及时适应网络状态&#xff08…

我的世界Bukkit服务器插件开发教程(十三)资源包与玩家资料

十三、资源包与玩家资料 1.资源包&#xff08;Resource Pack&#xff09; 早期的 Minecraft 并没有资源包一说&#xff0c;而是被叫做材质包。有些服务器为了让玩家拥有更好的游戏体验&#xff0c;一般会在自己特制的客户端中存放一些资源包供玩家加载。 显然&#xff0c;使用…

自动梯度计算

神经网络的参数主要通过梯度下降来进行优化&#xff0e; 当确定了风险函数以及网络结构后&#xff0c; 我们就可以手动用链式法则来计算风险函数对每个参数的梯度&#xff0c; 并用代码进行实现&#xff0e; 但是手动求导并转换为计算机程序的过程非常琐碎并容易出错&#xff0…

二叉树的基础oj题(单值二叉树、相同的树、对称二叉树、二叉树的前序、中序、后序遍历、另一棵树的子树、二叉树的构建和遍历、翻转二叉树)

今天&#xff0c;我带来二叉树的基础oj题 目录单值二叉树&#xff1a;[链接](https://leetcode.cn/problems/univalued-binary-tree/)相同的树&#xff1a;[链接](https://leetcode.cn/problems/same-tree/)对称二叉树&#xff1a;[链接](https://leetcode.cn/problems/symmetr…