Maven
Maven是一个项目管理工具,可以对Java项目进行自动化的构建和依赖管理
Maven的作用
- 项目构建:提供标准的,跨平台的自动化构建项目的方式
- 依赖管理:方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突等问题
- 统一开发结构:提供标准的,统一的项目开发结构,如下图所示
运行 Maven 的时候,Maven 所需要的任何构件都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构件至本地仓库。
本地仓库配置
修改maven安装包中的conf/settings.xml文件,指定本地仓库位置。
远程仓库配置
maven默认连接的远程仓库位置并不在国内,因此有时候下载速度非常慢,我们可以配置一个国内站点镜像,可用于加速下载资源。
与IDEA集成
SpringBoot快速上手
SpringBoot介绍
- Spring Boot是由Pivotal团队提供的基于Spring的全新框架,旨在简化Spring应用的初始搭建和开发过程。
- Spring Boot是所有基于Spring开发项目的起点。
- Spring Boot就是尽可能地简化应用开发的门槛,让应用开发、测试、部署变得更加简单。
SpringBoot特点
- 遵循“约定优于配置”的原则,只需要很少的配置或使用默认的配置。
- 能够使用内嵌的Tomcat、Jetty服务器,不需要部署war文件。
- 提供定制化的启动器Starters,简化Maven配置,开箱即用。
- 纯Java配置,没有代码生成,也不需要XML配置。
- 提供了生产级的服务监控方案,如安全监控、应用监控、健康检测等。
快速创建SpringBoot应用
开发环境热部署
- 在实际的项目开发调试过程中会频繁地修改后台类文件,导致需要重新编译、重新启动,整个过程非常麻烦,影响开发效率。
- Spring Boot提供了spring-boot-devtools组件,使得无须手动重启Spring Boot应用即可重新编译、启动项目,大大缩短编译启动的时间。
- devtools会监听classpath下的文件变动,触发Restart类加载器重新加载该类,从而实现类文件和属性文件的热部署。
- 并不是所有的更改都需要重启应用(如静态资源、视图模板),可以通过设置spring.devtools.restart.exclude属性来指定一些文件或目录的修改不用重启应用
步骤
- 在pom.xml配置文件中添加dev-tools依赖。
- 使用optional=true表示依赖不会传递,即该项目依赖devtools;其他项目如果引入此项目生成的JAR包,则不会包含devtools
- 在application.properties中配置devtools。
Optional
- 如果使用了Eclipse,那么在修改完代码并保存之后,项目将自动编译并触发重启,而如果使用了IntelliJ IDEA,还需要配置项目自动编译。
- 打开Settings页面,在左边的菜单栏依次找到Build,Execution,Deployment→Compile,勾选Build project automatically
- 按Ctrl+Shift+Alt+/快捷键调出Maintenance页面,单击Registry,勾选compiler.automake.allow.when.app.running复选框。
Finally
- 做完这两步配置之后,若开发者再次在IntelliJ IDEA中修改代码,则项目会自动重启。
- 项目创建成功后会默认在resources目录下生成application.properties文件。该文件包含Spring Boot项目的全局配置。
- 配置格式如下:
系统配置
Web开发基础
Web入门
- Spring Boot将传统Web开发的mvc、json、tomcat等框架整合,提供了spring-boot-starter-web组件,简化了Web应用配置。
- 创建SpringBoot项目勾选Spring Web选项后,会自动将spring-boot-starter-web组件加入到项目中。
- spring-boot-starter-web启动器主要包括web、webmvc、json、tomcat等基础依赖组件,作用是提供Web开发场景所需的所有底层依赖。
- webmvc为Web开发的基础框架,json为JSON数据解析组件,tomcat为自带的容器依赖。
控制器
- Spring Boot提供了@Controller和@RestController两种注解来标识此类负责接收和处理HTTP请求。
- 如果请求的是页面和数据,使用@Controller注解即可;如果只是请求数据,则可以使用@RestController注解。
@Controller的用法
- 示例中返回了hello页面和name的数据,在前端页面中可以通过${name}参数获取后台返回的数据并显示。
- @Controller通常与Thymeleaf模板引擎结合使用。
@RestController的用法
- @RestController的用法
@RestController和@Controller的区别
@RestController注解等价于@ResponseBody + @Controller。@RestController和@Controller的共同点是都用来表示Spring某个类是否可以接收HTTP请求,二者区别: @RestController无法返回指定页面,而@Controller可以;前者可以直接返回数据,后者需要@ResponseBody辅助。
① 是否可以返回页面
答:@RestController无法返回指定页面,而@Controller可以。
解析:对于Controller, 如果只是使用@RestController注解,则其方法无法返回指定页面,此时配置的视图解析器 InternalResourceViewResolver不起作用,返回的内容就是 return 里的内容。 如果需要返回到指定页面,则需要用 @Controller配合视图解析器InternalResourceViewResolver才行。
② 返回内容
如果需要返回JSON,XML或自定义mediaType内容到页面,@RestController自己就可以搞定,这个注解对于返回数据比较方便,因为它会自动将对象实体转换为JSON格式。而@Controller需要在对应的方法加上@ResponseBody注解。
import java.util.HashMap;
import java.util.Map;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/difference")
public class DifferenceController {
// 跳转到上传文件的页面
@RequestMapping(value = "/goToSuccessPage", method = RequestMethod.GET)
public String goToSuccessPage() {
// 跳转到 视图层 success.html失败
return "success";
}
@RequestMapping(value = "findAll", method = RequestMethod.GET)
public Map<String, String> findAll() {
Map<String, String> all = new HashMap<>();
all.put("remark", "可以返回json,xml或自定义mediaType内容到页面");
return all;
}
}
@Controller
@RequestMapping("/login")
public class LoginController {
@GetMapping(value = "/login")
public String login() {
// 跳转到 视图层 login.html
return "login";
}
@RequestMapping(value = "/getJson", method = RequestMethod.GET)
@ResponseBody
public Map<String, String> getJson() {
Map<String, String> all = new HashMap<>();
all.put("remark", "结合注解 @ResponseBody 返回接送");
return all;
}
}
路由映射
-
@RequestMapping注解主要负责URL的路由映射。它可以添加在Controller类或者具体的方法上。
-
如果添加在Controller类上,则这个Controller中的所有路由映射都将会加上此映射规则,如果添加在方法上,则只对当前方法生效。
-
@RequestMapping注解包含很多属性参数来定义HTTP的请求映射规则。常用的属性参数如下:
-
value: 请求URL的路径,支持URL模板、正则表达式
-
method: HTTP请求方法
-
consumes: 请求的媒体类型(Content-Type),如application/json
-
produces: 响应的媒体类型
-
params,headers: 请求的参数及请求头的值
-
@RequestMapping的value属性用于匹配URL映射,value支持简单表达式@RequestMapping(“/user”)
-
@RequestMapping支持使用通配符匹配URL,用于统一映射某些URL规则类似的请求:@RequestMapping(“/getJson/*.json”),当在浏览器中请求/getJson/a.json或者/getJson/b.json时都会匹配到后台的Json方法
-
@RequestMapping的通配符匹配非常简单实用,支持“*”“?”“**”等通配符
-
符号“*”匹配任意字符,符号“**”匹配任意路径,符号“?”匹配单个字符。
-
有通配符的优先级低于没有通配符的,比如/user/add.json比/user/*.json优先匹配。
-
有“**”通配符的优先级低于有“*”通配符的
-
Method匹配
RequestMapping分别和GetMapping、PostMapping的区别
GetMapping和PostMapping可以用RequestMapping表示,例如
- 以get的方式请求
@RequestMapping(value=“/login”, method=RequestMethod.GET)等价于@GetMapping(“/login”)
- 以post的方式请求
@RequestMapping(value=“/login”, method=RequestMethod.POST)等价于@PostMapping(“/login”)
参数传递
- @RequestParam将请求参数绑定到控制器的方法参数上,接收的参数来自HTTP请求体或请求url的QueryString,当请求的参数名称与Controller的业务方法参数名称一致时,@RequestParam可以省略
- @PathVaraible:用来处理动态的URL,URL的值可以作为控制器中处理方法的参数
- @RequestBody接收的参数是来自requestBody中,即请求体。一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:
application/json
、application/xml
等类型的数据
Web开发进阶
静态资源访问
- 使用IDEA创建Spring Boot项目,会默认创建出classpath:/static/目录,静态资源一般放在这个目录下即可。
- 如果默认的静态资源过滤策略不能满足开发需求,也可以自定义静态资源过滤策略。
- 在application.properties中直接定义过滤规则和静态资源位置:
- 过滤规则为/static/**,静态资源位置为classpath:/static/
文件上传
原理
- 表单的enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码。
- 当表单的enctype=“application/x-www-form-urlencoded”(默认)时,form表单中的数据格式为:key=value&key=value
- 当表单的enctype="multipart/form-data"时,其传输数据形式如下
SpirngBoot实现文件上传功能
- Spring Boot工程嵌入的tomcat限制了请求的文件大小,每个文件的配置最大为1Mb,单次请求的文件的总数不能大于10Mb。
- 要更改这个默认值需要在配置文件(如application.properties)中加入两个配置
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MBA
- 当表单的enctype="multipart/form-data"时,可以使用MultipartFile 获取上传的文件数据,再通过transferTo方法将其写入到磁盘中
package com.example.helloworld.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
@RestController
public class FileController {
@PostMapping("/upload")
public String upload(String nickname, MultipartFile f, HttpServletRequest request) throws IOException {
System.out.println(nickname);
System.out.println("文件大小:"+f.getSize());
System.out.println(f.getContentType());
System.out.println(f.getOriginalFilename());
//ServletContext().getRealPath() 是从当前servlet 在tomcat 中的存放文件夹开始计算起的
String path = request.getServletContext().getRealPath("/upload/");
System.out.println(path);
System.out.println(f);
saveFile(f, path);
return "success";
}
public void saveFile(MultipartFile f, String path) throws IOException{
File upDir = new File(path);
if(!upDir.exists()){
upDir.mkdir();
}
File file = new File(path+f.getOriginalFilename());
f.transferTo(file);
}
}
拦截器
- 拦截器在Web系统中非常常见,对于某些全局统一的操作,我们可以把它提取到拦截器中实现。总结起来,拦截器大致有以下几种使用场景:
- 权限检查:如登录检测,进入处理程序检测是否登录,如果没有,则直接返回登录页面。
- 性能监控:有时系统在某段时间莫名其妙很慢,可以通过拦截器在进入处理程序之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间
- 通用行为:读取cookie得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有提取Locale、Theme信息等,只要是多个处理程序都需要的,即可使用拦截器实现。
SpringBoot定义了HandlerInterceptor接口来实现自定义拦截功能,此接口定义了preHandle, postHandle, afterCompletion三个方法,通过重写这三个方法可以实现请求前、请求后等操作。
拦截器的定义
public class LoginInterceptor implement HandlerInterceptor{
/**
*在请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
object handler) throws Exception{
if(条件){
System.out.println("通过");
return true;
} else{
System.out.println("不通过");
return false;
}
}
}
拦截器注册
- addPathPatterns方法定义拦截地址
- excludePathPatterns方法定义排除某些地址不被拦截
- 添加的一个拦截器没有addPathPatterns任何一个url则默认拦截所有请求
- 如果没有excludePathPatterns任何一个请求,则默认不放过任何一个请求
@Configuration
public class WebConfigurer implements WebMvcConfigurer{
@override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/user/**")
}
}
构建RESTful服务
RESTful介绍
- RESTful是目前流行的互联网软件服务架构设计风格。
- REST(Representational State Transfer,表述性状态转移)一词是由Roy Thomas Fielding在2000年的博士论文中提出的,它定义了互联网软件服务的架构原则,如果一个架构符合REST原则,则称之为RESTful架构。
- REST并不是一个标准,它更像一组客户端和服务端交互时的架构理念和设计原则,基于这种架构理念和设计原则的Web API更加简洁,更有层次。
RESTful的特点
- 每一个URI代表一种资源
- 客户端使用GET、POST、PUT、DELETE四种表示操作方式的动词对服务端资源进行操作:GET用于获取资源,POST用于新建资源(也可以用于更新资源),PUT用于更新资源,DELETE用于删除资源。
- 通过操作资源的表现形式来实现服务端请求操作。
- 资源的表现形式是JSON或者HTML。
- 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都包含必需的信息。
RESTful API
- 符合RESTful规范的Web API需要具备如下两个关键特性:
- 安全性:安全的方法被期望不会产生任何副作用,当我们使用GET操作获取资源时,不会引起资源本身的改变,也不会引起服务器状态的改变。
- 幂等性:幂等的方法保证了重复进行一个请求和一次请求的效果相同(并不是指响应总是相同的,而是指服务器上资源的状态从第一次请求后就不再改变了),在数学上幂等性是指N次变换和一次变换相同。
HTTP Method
- HTTP提供了POST、GET、PUT、DELETE等操作类型对某个Web资源进行Create、Read、Update和Delete操作。
- 一个HTTP请求除了利用URI标志目标资源之外,还需要通过HTTP Method指定针对该资源的操作类型,一些常见的HTTP方法及其在RESTful风格下的使用:
HTTP状态码
- HTTP状态码就是服务向用户返回的状态码和提示信息,客户端的每一次请求,服务都必须给出回应,回应包括HTTP状态码和数据两部分。
- HTTP定义了40个标准状态码,可用于传达客户端请求的结果。状态码分为以下5个类别:
- 1xx:信息,通信传输协议级信息
- 2xx:成功,表示客户端的请求已成功接受
- 3xx:重定向,表示客户端必须执行一些其他操作才能完成其请求
- 4xx:客户端错误,此类错误状态码指向客户端
- 5xx:服务器错误,服务器负责这写错误状态码
RESTful API中使用HTTP状态码来表示请求执行结果的状态,适用于REST API设计的代码以及对应的HTTP方法。
构建RESTful应用接口
Spring Boot实现RESTful API
- Spring Boot提供的spring-boot-starter-web组件完全支持开发RESTful API,提供了与REST操作方式(GET、POST、PUT、DELETE)对应的注解。
- @GetMapping:处理GET请求,获取资源。
- @PostMapping:处理POST请求,新增资源。
- @PutMapping:处理PUT请求,更新资源。
- @DeleteMapping:处理DELETE请求,删除资源。
- @PatchMapping:处理PATCH请求,用于部分更新资源。
在RESTful架构中,每个网址代表一种资源,所以URI中建议不要包含动词,只包含名词即可,而且所用的名词往往与数据库的表格名对应。
用户管理模块API示例:
package com.example.helloworld.controller;
import com.example.helloworld.entity.User;
import org.springframework.web.bind.annotation.*;
@RestController
public class UserController {
@GetMapping("/user/{id}")
public String getUserById(@PathVariable int id) {
return "根据ID获取用户";
}
@PostMapping("/user")
public String save(User user) {
return "添加用户";
}
@PutMapping("/user")
public String update(User user) {
return "更新用户";
}
@DeleteMapping("/user/{ud}")
public String delete(@PathVariable int id){
return "根据ID删除用户";
}
}
使用Swagger生成Web API文档
什么是Swagger
- Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务,是非常流行的API表达工具。
- Swagger能够自动生成完善的RESTful API文档,,同时并根据后台代码的修改同步更新,同时提供完整的测试页面来调试API。
使用Swagger生成Web API文档
在Spring Boot项目中集成Swagger同样非常简单,只需在项目中引入springfox-swagger2和springfox-swagger-ui依赖即可。
配置Swagger
注意事项
Spring Boot 2.6.X后与Swagger有版本冲突问题,需要在application.properties中加入以下配置:
使用 Swagger2 进行接口测试
启动项目访问 http://127.0.0.1:8080/swagger-ui.html ,即可打开自动生成的可视化测试页面
Swagger常用注解
Swagger提供了一系列注解来描述接口信息,包括接口说明、请求方法、请求参数、返回信息等
MybatisPlus快速上手
ORM介绍
- ORM(Object Relational Mapping,对象关系映射)是为了解决面向对象与关系数据库存在的互不匹配现象的一种技术。
- ORM通过使用描述对象和数据库之间映射的元数据将程序中的对象自动持久化到关系数据库中。
- ORM框架的本质是简化编程中操作数据库的编码。
MyBatis-Plus介绍
- MyBatis是一款优秀的数据持久层ORM框架,被广泛地应用于应用系统。
- MyBatis能够非常灵活地实现动态SQL,可以使用XML或注解来配置和映射原生信息,能够轻松地将Java的POJO(Plain Ordinary Java Object,普通的Java对象)与数据库中的表和字段进行映射关联。
- MyBatis-Plus是一个 MyBatis 的增强工具,在 MyBatis 的基础上做了增强,简化了开发。
<!--Mybatis Plus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--MySQL 驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据连接池druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false
spring.datasource.druid.username=root
spring.datasource.druid.password=root
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
@SpringBootApplication
@MapperScan("com.demo.mapper")
public class MybatisPlusDemo Application{
public static void main(String [] args){
SpringApplication.run(MybatisPlusApplication.class, args);
}
}
MyBatis-Plus CRUD操作
//package com.example.demo.mapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper {
@Select("select * from user")
public List<User> findAll();
@Insert("insert into user values(#{id}, #{name}, #{age})")
public int add(User user);
@Update("update user set id=#{id}, name=#{name}, age=#{age}")
public int update(User user);
@Delete("delete from user where id=#{id}")
public int delete(int id);
@Select("select * from user where id=#{id}")
public User findById(int id);
}
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/user")
public List<User> findAll() {
return userMapper.findAll();
}
@PostMapping("/saveUser")
public String save(User user){
int flag = userMapper.add(user);
if (flag > 0)
return "插入成功";
else
return "插入失败";
}
}
Spring的三种注入方式:
构造注入,getter和setter注入,自动注入
package com.example.demo.entity;
public class User {
private int id;
private String name;
private int age;
public User(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
数据库字段和实体类字段映射规则
MybatisPlus注解
- @TableName,当表名与实体类名称不一致时,可以使用@TableName注解进行关联。
- @TableField,当表中字段名称与实体类属性不一致时,使用@TableField进行关联
- @TableId,用于标记表中的主键字段,MybatisPlus也提供了主键生成策略。
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
多表查询及分页查询
多表查询
实现复杂关系映射,可以使用@Results注解,@Result注解,@One注解,@Many注解组合完成复杂关系的配置。
分页查询
编写配置文件
package com.example.demo.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor paginationInterceptor(){
MybatisPlusInterceptor intercepter = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
intercepter.addInnerInterceptor(paginationInnerInterceptor);
return intercepter;
}
}
测试
package com.example.demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class UserController {
@Resource
private UserMapper userMapper;
@GetMapping("/user")
public List<User> findAll() {
return userMapper.findAll();
}
@PostMapping("/saveUser")
public String save(User user) {
int flag = userMapper.add(user);
if (flag > 0)
return "插入成功";
else
return "插入失败";
}
@GetMapping("/user-orders")
public List<User> getUserAllOrders() {
return userMapper.selectAllUserAndOrders();
}
// 条件查询
@GetMapping("/user/find")
public List<User> findByCond() {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("name", "zhangsan");
return userMapper.selectList(queryWrapper);
}
// 分页查询
@GetMapping("/user/findByPage")
public IPage findByPage() {
// 设置起始值及每页条数
Page<User> page = new Page<>(0, 2);
IPage ipage = userMapper.selectPage(page, null);
return ipage;
}
}
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.Date;
@TableName("orders")
public class Order {
private int oid;
private Date date;
private int cid;
@TableField(exist = false)
private User user;
public int getOid() {
return oid;
}
public void setOid(int oid) {
this.oid = oid;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public int getCid() {
return cid;
}
public void setCid(int cid) {
this.cid = cid;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
@Override
public String toString() {
return "Order{" +
"oid=" + oid +
", date=" + date +
", cid=" + cid +
'}';
}
}
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.util.List;
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private int id;
private String name;
private int age;
//告诉Mybatis不做映射
@TableField(exist = false)
private List<Order> orders;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List<Order> getOrders() {
return orders;
}
public void setOrders(List<Order> orders) {
this.orders = orders;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.Order;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
@Select("select * from orders where cid = #{cid}")
public List<Order> selectByUid(int id);
// 查询所有的订单,同时查询订单的用户
@Select("select * from order")
@Results({
@Result(column = "oid", property = "oid"),
@Result(column = "date", property = "date"),
@Result(column = "uid", property = "user", javaType = User.class,
one = @One(select = "com.example.demo.mapper.UserMapper.selectById"))
})
public List<Order> selectAllOrdersAndUser();
}
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select * from user")
public List<User> findAll();
@Insert("insert into user values(#{id}, #{name}, #{age})")
public int add(User user);
@Update("update user set id=#{id}, name=#{name}, age=#{age}")
public int update(User user);
@Delete("delete from user where id=#{id}")
public int delete(int id);
@Select("select * from user where id=#{id}")
public User findById(int id);
// 查询用户所有订单
@Select("select * from user")
@Results(
{
@Result(column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "age", property = "age"),
@Result(column = "id", property = "orders", javaType = List.class,
many = @Many(select = "com.example.demo.mapper.OrderMapper.selectByUid"))
}
)
public List<User> selectAllUserAndOrders();
}
Vue快速上手
前端环境准备
- 编码工具:VSCode
- 依赖管理:NPM
- 项目构建:VueCli
Vue框架介绍
- Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
- Vue.js提供了MVVM数据绑定和一个可组合的组件系统,具有简单、灵活的API。
- 其目标是通过尽可能简单的API实现响应式的数据绑定和可组合的视图组件。
MVVM模式
- MVVM是Model-View-ViewModel的缩写,它是一种基于前端开发的架构模式,其核心是提供对View和ViewModel的双向数据绑定。
- Vue提供了MVVM风格的双向数据绑定,核心是MVVM中的VM,也就是ViewModel,ViewModel负责连接View和Model,保证视图和数据的一致性。
Vue快速入门
<script src="https://unpkg.com/vue@next"></script>
<div id="app">
{{message}}
</div>
const Hello = {
//指定数据源,即MVVM中的Model
data: function(){
return {
"message": "Hello Vue!"
}
}
}
const app = Vue.createApp(Hello)
app.mount('#app') //指定当前Vue实例要控制页面的哪个区域
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<!-- 1. 导入 vue 的脚本文件 -->
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<!-- 2. 声明要被 vue 所控制的 DOM 区域 -->
<div id="app">
{{message}}
</div>
<!-- 3. 创建 vue 的实例对象 -->
<script>
const hello = {
// 指定数据源,既MVVM中的Model
data: function() {
return {
message: 'Hello Vue!'
}
}
}
const app = Vue.createApp(hello)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>
<p>{{desc}}</p>
<!--将html元素字符串转换成标签-->
<p v-html="desc"></p>
</div>
<script>
const vm = {
data: function(){
return {
username: 'zhangsan',
gender: '男',
desc: '<a href="http://www.baidu.com">百度</a>'
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<a :href="link">百度</a>
<input type="text" :placeholder="inputValue">
<img :src="imgSrc" :style="{width:w}" alt="">
</div>
<script>
const vm = {
data: function(){
return {
link:"http://www.baidu.com",
// 文本框的占位符内容
inputValue: '请输入内容',
// 图片的 src 地址
imgSrc: './images/demo.png',
w:'500px'
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
<p>{{number + 1}}</p>
<p>{{ok ? 'True' : 'False'}}</p>
<p>{{message.split('').reverse().join('')}}</p>
<p :id="'list-' + id">xxx</p>
<p>{{user.name}}</p>
</div>
<script>
const vm = {
data: function(){
return {
number: 9,
ok: false,
message: 'ABC',
id: 3,
user: {
name: 'zs',
}
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<h3>count 的值为:{{count}}</h3>
<button v-on:click="addCount">+1</button>
<button @click="count+=1">+1</button>
<!--
<button @click="addCount">+1</button>
-->
</div>
<script>
const vm = {
data: function(){
return {
count: 0,
}
},
methods: {
// 点击按钮,让 count 自增 +1
addCount() {
this.count += 1
},
},
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<button @click="flag = !flag">Toggle Flag</button>
<!--当状态只更改一次时,建议用v-if,当状态频繁更改时,建议用v-show-->
<p v-if="flag">请求成功 --- 被 v-if 控制</p>
<p v-show="flag">请求成功 --- 被 v-show 控制</p>
</div>
<script>
const vm = {
data: function(){
return {
flag: false,
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<p v-if="num > 0.5">随机数 > 0.5</p>
<p v-else>随机数 ≤ 0.5</p>
<hr />
<p v-if="type === 'A'">优秀</p>
<p v-else-if="type === 'B'">良好</p>
<p v-else-if="type === 'C'">一般</p>
<p v-else>差</p>
<div v-show="a">
测试
</div>
<button @click="!a">点击</button>
</div>
<script>
const vm = {
data: function(){
return {
// 生成 1 以内的随机数
num: Math.random(),
// 类型
type: 'A',
a : false
}
},
methods:{
f:function(){
this.a = !this.a
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<ul>
<li v-for="(user, i) in userList">索引是:{{i}},姓名是:{{user.name}}</li>
</ul>
</div>
<script>
const vm = {
data: function(){
return {
userList: [
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' },
{ id: 3, name: 'wangwu' },
],
}
},
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<!-- 添加用户的区域 -->
<div>
<input type="text" v-model="name">
<button @click="addNewUser">添加</button>
</div>
<!-- 用户列表区域 -->
<ul>
<li v-for="(user, index) in userlist" :key="user.id">
<input type="checkbox" />
姓名:{{user.name}}
</li>
</ul>
</div>
<script>
const vm = {
data: function(){
return {
// 用户列表
userlist: [
{ id: 1, name: 'zhangsan' },
{ id: 2, name: 'lisi' }
],
// 输入的用户名
name: '',
// 下一个可用的 id 值
nextId: 3
}
},
methods: {
// 点击了添加按钮
addNewUser() {
this.userlist.unshift({ id: this.nextId, name: this.name })
this.name = ''
this.nextId++
}
}
}
const app = Vue.createApp(vm)
app.mount('#app')
</script>
</body>
</html>
组件化开发
NPM使用
npm简介
- NPM(Node Package Manager)是一个NodeJS包管理和分发工具。
- NPM以其优秀的依赖管理机制和庞大的用户群体,目前已经发展成为整个JS领域的依赖管理工具
- NPM最常见的用法就是用于安装和更新依赖。要使用NPM,首先要安装Node工具。
nodeJS安装
- Node.js 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境。
- Node中包含了NPM包管理工具。
- 下载地址:https://nodejs.org/zh-cn/
npm使用
Vue Cli 使用
- Vue CLI是Vue官方提供的构建工具,通常称为脚手架。
- 用于快速搭建一个带有热重载(在代码修改后不必刷新页面即可呈现修改后的效果)及构建生产版本等功能的单页面应用。
- Vue CLI基于 webpack 构建,也可以通过项目内的配置文件进行配置。
- 安装:npm install -g @vue/cli
组件化开发
- 组件(Component)是Vue.js最强大的功能之一。组件可以扩展HTML元素,封装可重用的代码。
- Vue的组件系统允许我们使用小型、独立和通常可复用的组件构建大型应用。
组件构成
- Vue 中规定组件的后缀名是 .vue
- 每个 .vue 组件都由 3 部分构成,分别是
- template,组件的模板结构,可以包含HTML标签及其他的组件
- script,组件的 JavaScript 代码
- style,组件的样式
解决eslint报错
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'eslint:recommended'
],
parserOptions: {
parser: '@babel/eslint-parser'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
// 在rules中添加自定义规则
// 关闭组件命名规则
'vue/multi-word-component-names': 'off'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
第三方组件
组件间的传值
- 组件可以由内部的Data提供数据,也可以由父组件通过prop的方式传值。
- 兄弟组件之间可以通过Vuex等统一数据源提供数据共享。
Demo
<template>
<div id="app">
<Movie
v-for="movie in movies"
:key="movie.id"
:title="movie.title"
:rating="movie.rating"
></Movie>
<Hello></Hello>
</div>
</template>
<script>
import Movie from "./components/Movie.vue";
import Hello from "./components/Hello.vue";
export default {
name: "App",
data: function () {
return {
movies: [
{ id: 1, title: "金刚狼", rating: 8.7 },
{ id: 2, title: "雷神", rating: 7.6 },
{ id: 3, title: "蜘蛛侠", rating: 8.9 },
],
};
},
components: {
Movie,
Hello,
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<div>
<h1>{{title}}</h1>
<span>{{rating}}</span>
<button @click="fun">点击收藏</button>
</div>
</template>
<script>
export default {
name:"Movie",
props: ["title", "rating"],
data: function(){
return {
}
},
methods: {
fun(){
alert("收藏成功")
}
}
}
</script>
<template>
<h3>hello</h3>
</template>
element-ui介绍
- Element是国内饿了么公司提供的一套开源前端框架,简洁优雅,提供了Vue、React、Angular等多个版本。
- 文档地址:https://element.eleme.cn/#/zh-CN/
组件的使用
- 安装:npm i element-ui
- 引入 Element:
图标的使用
- 由于Element UI提供的字体图符较少,一般会采用其他图表库,如著名的Font Awesome
- Font Awesome提供了675个可缩放的矢量图标,可以使用CSS所提供的所有特性对它们进行更改,包括大小、颜色、阴影或者其他任何支持的效果。
- 文档地址:http://fontawesome.dashgame.com/
- 安装:npm install font-awesome
- 使用:import ‘font-awesome/css/font-awesome.min.css’
Axios网络请求
Axios的使用
Axios简介
- 在实际项目开发中,前端页面所需要的数据往往需要从服务器端获取,这必然涉及与服务器的通信。
- Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。
- Axios 在浏览器端使用XMLHttpRequests发送网络请求,并能自动完成JSON数据的转换 。
- 安装:npm install axios
- 地址:https://www.axios-http.cn/
发送网络请求
发送get请求
发送post请求
异步回调问题
async/await
其他请求方式
参考:https://axios-http.com/zh/docs/req_config
与Vue整合
- 在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:
- 每个组件中都需要导入 axios
- 每次发请求都需要填写完整的请求路径
- 可以通过全局配置的方式解决上述问题:
跨域
为什么会出现跨域问题
- 为了保证浏览器的安全,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源,称为同源策略,同源策略是浏览器安全的基石
- 同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能
- 所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
- 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域,此时无法读取非同源网页的 Cookie,无法向非同源地址发送 AJAX 请求
跨域问题解决
- CORS(Cross-Origin Resource Sharing)是由W3C制定的一种跨域资源共享技术标准,其目的就是为了解决前端的跨域请求。
- CORS可以在不破坏即有规则的情况下,通过后端服务器实现CORS接口,从而实现跨域通信。
- CORS将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。
简单请求
满足以下条件的请求即为简单请求:
- 请求方法:GET、POST、HEAD
- 除了以下的请求头字段之外,没有自定义的请求头:
- Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type
- Content-Type的值只有以下三种:
- text/plain、multipart/form-data、application/x-www-form-urlencoded
简单请求的服务器处理
对于简单请求,CORS的策略是请求时在请求头中增加一个Origin字段,
服务器收到请求后,根据该字段判断是否允许该请求访问,如果允许,则在HTTP头信息中添加Access-Control-Allow-Origin字段。
非简单请求
- 对于非简单请求的跨源请求,浏览器会在真实请求发出前增加一次OPTION请求,称为预检请求(preflight request)
- 预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到HTTP头信息字段中,询问服务器是否允许这样的操作。
- 例如一个GET请求:
- Access-Control-Request-Method表示请求使用的HTTP方法,Access-Control-Request-Headers包含请求的自定义头字段
- 服务器收到请求时,需要分别对Origin、Access-Control-Request-Method、Access-Control-Request-Headers进行验证,验证通过后,会在返回HTTP头信息中添加:
- Access-Control-Allow-Methods、Access-Control-Allow-Headers:真实请求允许的方法、允许使用的字段
- Access-Control-Allow-Credentials: 是否允许用户发送、处理cookie
- Access-Control-Max-Age: 预检请求的有效期,单位为秒,有效期内不会重复发送预检请求。
- 当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。
- 在传统的Java EE开发中,可以通过过滤器统一配置,而Spring Boot中对此则提供了更加简洁的解决方案
前端路由管理
Vue Router 的安装及使用
- Vue路由vue-router是官方的路由插件,能够轻松的管理 SPA 项目中组件的切换。
- Vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来
- vue-router 目前有 3.x 的版本和 4.x 的版本,vue-router 3.x 只能结合 vue2 进行使用,vue-router 4.x 只能结合 vue3 进行使用
- 安装:npm install vue-router@4
一. 路由(Vue2)
1. SPA 与前端路由
路由是根据不同的url地址来显示不同的页面或内容的功能,这个概念很早是由后端提出的,既浏览器向不同的地址发送请求,后端返回相应的内容。
SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。
前端路由通常是通过监听URL hash属性值的变化,切换页面,hash 属性是一个可读可写的字符串
,该字符串是 URL 的锚部分(从 # 号开始的部分)。
前端路由的工作方式
前端路由,指的是 Hash 地址与组件之间的对应关系。
- 用户点击了页面上的路由链接
- 导致了 URL 地址栏中的 Hash 值发生了变化
- 前端路由监听了到 Hash 地址的变化
- 前端路由把当前 Hash 地址对应的组件渲染都浏览器中
2. vue-router基本使用
vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。
vue-router 目前有 3.x 的版本和 4.x 的版本。其中:
- vue-router 3.x 只能结合 vue2 进行使用
- vue-router 4.x 只能结合 vue3 进行使用
官方文档:https://router.vuejs.org/zh/installation.html
vue-router的基本使用步骤:
- 在项目中安装 vue-router
- 定义路由组件
- 声明路由链接和占位符
- 创建路由模块
- 导入并挂载路由模块
vue-router安装
npm install vue-router@3
创建路由组件
在项目中定义 Discover.vue、Friends.vue、My.vue 三个组件,将来要使用 vue-router 来控制它们的展示与切换:
Discover.vue:
<template>
<div>
<h1>发现音乐</h1>
</div>
</template>
Friends.vue:
<template>
<div>
<h1>关注</h1>
</div>
</template>
My.vue:
<template>
<div>
<h1>我的音乐</h1>
</div>
</template>
声明路由链接和占位标签
可以使用 <router-link>
标签来声明路由链接,并使用 <router-view>
标签来声明路由占位符。示例代码如下:
App.vue:
<template>
<div>
<h1>APP组件</h1>
<!-- 声明路由链接 -->
<router-link to="/discover">发现音乐</router-link>
<router-link to="/my">我的音乐</router-link>
<router-link to="/friend">关注</router-link>
<!-- 声明路由占位标签 -->
<router-view></router-view>
</div>
</template>
创建路由模块
在项目中创建 index.js 路由模块,加入以下代码:
import VueRouter from 'vue-router'
import Vue from 'vue'
import Discover from '@/components/Discover.vue'
import Friends from '@/components/Friends.vue'
import My from '@/components/My.vue'
//将VueRouter设置为Vue的插件
Vue.use(VueRouter)
const router = new VueRouter({
// 指定hash属性与组件的对应关系
routes: [
{ path: '/discover', component: Discover },
{ path: '/friends', component: Friends },
{ path: '/my', component: My },
]
})
export default router
挂载路由模块
在main.js中导入并挂载router
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router:router
}).$mount('#app')
3. vue-router进阶
路由重定向
路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。
通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:
const router = new VueRouter({
// 指定hash属性与组件的对应关系
routes: [
// 当用户访问 / 时,跳转到 /discover
{ path: '/', redirect: '/discover'},
{ path: '/discover', component: Discover },
{ path: '/friends', component: Friends },
{ path: '/my', component: My },
]
})
嵌套路由
在 Discover.vue 组件中,声明 toplist和 playlist的子路由链接以及子路由占位符。示例代码如下:
<template>
<div>
<h1>发现音乐</h1>
<!-- 子路由链接 -->
<router-link to="/discover/toplist">推荐</router-link>
<router-link to="/discover/playlist">歌单</router-link>
<hr>
<router-view></router-view>
</div>
</template>
在 src/router/index.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:
const router = new VueRouter({
// 指定hash属性与组件的对应关系
routes: [
{ path: '/', redirect: '/discover'},
{
path: '/discover',
component: Discover,
// 通过children属性,嵌套声明子路由
children: [
{path:"toplist",component:TopList},
{path:"playlist",component:PlayList},
]
},
{ path: '/friends', component: Friends },
{ path: '/my', component: My },
]
})
动态路由
思考:有如下 3 个路由链接:
<router-link to="/product/1">商品1</router-link>
<router-link to="/product/2">商品2</router-link>
<router-link to="/product/3">商品3</router-link>
const router = new VueRouter({
// 指定hash属性与组件的对应关系
routes: [
{ path: '/product/1', component: Product },
{ path: '/product/2', component: Product },
{ path: '/product/3', component: Product },
]
})
上述方式复用性非常差。
动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用英文的冒号(:)来定义路由的参数项。示例代码如下:
{ path: '/product/:id',component:Product }
通过动态路由匹配的方式渲染出来的组件中,可以使用 $route.params 对象访问到动态匹配的参数值,比如在商品详情组件的内部,根据id值,请求不同的商品数据。
<template>
<h1>Product组件</h1>
<!-- 获取动态的id值 -->
<p>{{$route.params.id}}</p>
</template>
<script>
export default {
// 组件的名称
name: 'Product'
}
</script>
为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参。示例代码如下:
{ path: '/product/:id',component: Product, props: true }
<template>
<h1>Product组件</h1>
<!-- 获取动态的id值 -->
<p>{{id}}</p>
</template>
<script>
export default {
// 组件的名称
name: 'Product',
props : [id]
}
</script>
编程式导航
声明式 | 编程式 |
---|---|
<router-link :to="..."> |
router.push(...) |
除了使用 <router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
想要导航到不同的 URL,则使用 router.push
方法。这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。
当你点击 <router-link>
时,这个方法会在内部调用,所以说,点击 <router-link :to="...">
等同于调用 router.push(...)
。
<template>
<button @click="gotoProduct(2)">跳转到商品2</button>
</template>
<script>
export default {
methods:{
gotoProduct: function(id){
this.$router.push('/movie/${id}')
}
}
}
</script>
导航守卫
导航守卫可以控制路由的访问权限。示意图如下:
全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。
你可以使用 router.beforeEach
注册一个全局前置守卫:
router.beforeEach((to, from, next) => {
if (to.path === '/main' && !isAuthenticated) {
next('/login')
}
else {
next()
}
})
-
**to**
: 即将要进入的目标 -
**from**
: 当前导航正要离开的路由 - 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由!
- 直接放行:next()
- 强制其停留在当前页面:next(false)
- 强制其跳转到登录页面:next(‘/login’)
二. 状态管理
1. 简单状态管理
经常被忽略的是,Vue 应用中响应式 data
对象的实际来源——当访问数据对象时,一个组件实例只是简单的代理访问。所以,如果你有一处需要被多个实例间共享的状态,你可以使用一个 reactive 方法让对象作为响应式对象。
const { createApp, reactive } = Vue
const sourceOfTruth = reactive({
message: 'Hello'
})
const appA = createApp({
data() {
return sourceOfTruth
}
}).mount('#app-a')
const appB = createApp({
data() {
return sourceOfTruth
}
}).mount('#app-b')
<div id="app-a">App A: {{ message }}</div>
<div id="app-b">App B: {{ message }}</div>
现在当 sourceOfTruth
发生变更,appA
和 appB
都将自动地更新它们的视图。虽然现在我们有了一个真实数据来源,但调试将是一场噩梦。应用的任何部分都可以随时更改任何数据,而不会留下变更过的记录。
const appB = createApp({
data() {
return sourceOfTruth
},
mounted() {
sourceOfTruth.message = 'Goodbye' // both apps will render 'Goodbye' message now
}
}).mount('#app-b')
为了解决这个问题,可以采用一个简单的 store 模式:
const store = {
debug: true,
state: reactive({
message: 'Hello!'
}),
setMessageAction(newValue) {
if (this.debug) {
console.log('setMessageAction triggered with', newValue)
}
this.state.message = newValue
},
clearMessageAction() {
if (this.debug) {
console.log('clearMessageAction triggered')
}
this.state.message = ''
}
}
需要注意,所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,现在也会有一个 log 记录 bug 之前发生了什么。
此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
<div id="app-a">{{sharedState.message}}</div>
<div id="app-b">{{sharedState.message}}</div>
const appA = createApp({
data() {
return {
privateState: {},
sharedState: store.state
}
},
mounted() {
store.setMessageAction('Goodbye!')
}
}).mount('#app-a')
const appB = createApp({
data() {
return {
privateState: {},
sharedState: store.state
}
}
}).mount('#app-b')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1KSsN0eb-1664286794923)(2_路由与状态管理.assets/state.png#crop=0&crop=0&crop=1&crop=1&id=g9T6Z&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
随着我们进一步扩展约定,即组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,最终达成了 Flux 架构。这样约定的好处是,能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。
2. Vuex介绍
由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。为了解决这个问题,Vue 提供 Vuex:我们有受到 Elm 启发的状态管理库。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
示例
让我们从一个简单的 Vue 计数应用开始:
const Counter = {
// 状态
data () {
return {
count: 0
}
},
// 视图
template: `
<div>{{ count }}</div>
`,
// 操作
methods: {
increment () {
this.count++
}
}
}
createApp(Counter).mount('#app')
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kR1BZHTv-1664286794923)(2_路由与状态管理.assets/flow.png#crop=0&crop=0&crop=1&crop=1&id=EmKeb&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)]
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
这就是 Vuex 背后的基本思想,借鉴了 Flux、Redux 和 The Elm Architecture。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
什么情况下应该使用 Vuex?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:
Flux 架构就像眼镜:您自会知道什么时候需要它。
安装
npm install vuex@3
3. 最简单的 Store
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
安装Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutation:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
现在,你可以通过 store.state
来获取状态对象,并通过 store.commit
方法触发状态变更:
store.commit('increment')
console.log(store.state.count) // -> 1
为了在 Vue 组件中访问 this.$store
property,你需要为 Vue 实例提供创建好的 store。Vuex 提供了一个从根组件向所有子组件,以 store
选项的方式“注入”该 store 的机制:
new Vue({
el: '#app',
store: store,
})
在 Vue 组件中, 可以通过 this.$store
访问store实例。现在我们可以从组件的方法提交一个变更:
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count
,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。
由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的 methods 中提交 mutation。
4. State
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
单状态树和模块化并不冲突——在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中。
在 Vue 组件中获得 Vuex 状态
那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过 this.$store
访问到。让我们更新下 Counter
的实现:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState
辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
对象展开运算符
mapState
函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed
属性。但是自从有了对象展开运算符,我们可以极大地简化写法:
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
5. Getter
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
Getter 接受 state 作为其第一个参数:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: (state) => {
return state.todos.filter(todo => todo.done)
}
}
})
通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
mapGetters
辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
6. Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation 处理函数。这个选项更像是事件注册:“当触发一个类型为 increment
的 mutation 时,调用此函数。”要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
提交载荷(Payload)
你可以向 store.commit
传入额外的参数,即 mutation 的载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type
属性的对象:
store.commit({
type: 'increment',
amount: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:
store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。
为了处理异步操作,让我们来看一看Action。
7. Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
让我们来注册一个简单的 action:
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit
很多次的时候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
分发 Action
Action 通过 store.dispatch
方法触发:
store.dispatch('increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。
在组件中分发 Action
你在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
):
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
一个 store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
8. Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
同样,对于模块内部的 action,局部状态通过 context.state
暴露出来,根节点状态则为 context.rootState
:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
模块动态注册
在 store 创建之后,你可以使用 store.registerModule
方法注册模块
import { createStore } from 'vuex'
const store = createStore({ /* 选项 */ })
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
之后就可以通过 store.state.myModule
和 store.state.nested.myModule
访问模块的状态。
参数传递
子路由
导航守卫
- vue-router提供的导航守卫主要用来拦截导航,让它完成跳转或取消。
- to:Route:即将要进入的目标路由。
- from:Route:当前导航正要离开的路由。
- next:在守卫方法中如果声明了next形参,则必须调用 next() 函数,否则不允许用户访问任何一个路由
- 直接放行:next(),强制其跳转到登录页面:next(‘/login’)
状态管理VueX
Vuex介绍
- 对于组件化开发来说,大型应用的状态往往跨越多个组件。在多层嵌套的父子组件之间传递状态已经十分麻烦,而Vue更是没有为兄弟组件提供直接共享数据的办法。
- 基于这个问题,许多框架提供了解决方案——使用全局的状态管理器,将所有分散的共享数据交由状态管理器保管,Vue也不例外。
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理库,采用集中式存储管理应用的所有组件的状态。
- 简单的说,Vuex用于管理分散在Vue各个组件中的数据。
- 安装:npm install vuex@next
状态管理
- 每一个Vuex应用的核心都是一个store,与普通的全局对象不同的是,基于Vue数据与视图绑定的特点,当store中的状态发生变化时,与之绑定的视图也会被重新渲染。
- store中的状态不允许被直接修改,改变store中的状态的唯一途径就是显式地提交(commit)mutation,这可以让我们方便地跟踪每一个状态的变化。
- 在大型复杂应用中,如果无法有效地跟踪到状态的变化,将会对理解和维护代码带来极大的困扰。
- Vuex中有5个重要的概念:State、Getter、Mutation、Action、Module。
State
- State用于维护所有应用层的状态,并确保应用只有唯一的数据源
- 在组件中,可以直接使用this.$store.state.count访问数据,也可以先用mapState辅助函数将其映射下来
Getter
- Getter维护由State派生的一些状态,这些状态随着State状态的变化而变化
- 在组件中,可以直接使用this.$store.getters.doneTodos,也可以先用mapGetters辅助函数将其映射下来,代码如下:
Mutation
- Mutation提供修改State状态的方法。
- 在组件中,可以直接使用store.commit来提交mutation
- 也可以先用mapMutation辅助函数将其映射下来
Action
- Action类似Mutation,不同在于:
- Action不能直接修改状态,只能通过提交mutation来修改,Action可以包含异步操作
- 在组件中,可以直接使用this.$store.dispatch(‘xxx’)分发 action,或者使用mapActions辅助函数先将其映射下来
Module
- 由于使用单一状态树,当项目的状态非常多时,store对象就会变得十分臃肿。因此,Vuex允许我们将store分割成模块(Module)
- 每个模块拥有独立的State、Getter、Mutation和Action,模块之中还可以嵌套模块,每一级都有着相同的结构。
Vuex安装与使用
前端数据模拟MockJS
mockjs介绍
- Mock.js 是一款前端开发中拦截Ajax请求再生成随机数据响应的工具,可以用来模拟服务器响应.
- 优点是非常简单方便, 无侵入性, 基本覆盖常用的接口数据类型.。
- 支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。
- 安装:npm install mock
mockjs基本使用
1. 什么是mockjs
Mock.js 是一款前端开发中拦截Ajax请求再生成随机数据响应的工具.可以用来模拟服务器响应. 优点是非常简单方便, 无侵入性, 基本覆盖常用的接口数据类型.
- 前后端分离
- 开发无侵入(不需要修改既有代码,就可以拦截 Ajax 请求,返回模拟的响应数据。)
- 数据类型丰富(支持生成随机的文本、数字、布尔值、日期、邮箱、链接、图片、颜色等。)
- 增加单元测试的真实性(通过随机数据,模拟各种场景。)
- 用法简单、符合直觉的接口。
- 方便扩展(支持支持扩展更多数据类型,支持自定义函数和正则。)
安装
npm install mockjs
2. 快速入门
在项目中创建mock目录,新建index.js文件
//引入mockjs
import Mock from 'mockjs'
//使用mockjs模拟数据
Mock.mock('/product/search', {
"ret":0,
"data":
{
"mtime": "@datetime",//随机生成日期时间
"score|1-800": 1,//随机生成1-800的数字
"rank|1-100": 1,//随机生成1-100的数字
"stars|1-5": 1,//随机生成1-5的数字
"nickname": "@cname",//随机生成中文名字
//生成图片
"img":"@image('200x100', '#ffcc33', '#FFF', 'png', 'Fast Mock')"
}
});
main.js里面引入
import './mock'
xxx.vue文件中调用mock.js中模拟的数据接口,这时返回的response就是mock.js中用Mock.mock(‘url’,data)中设置的data了
import axios from 'axios'
export default {
mounted:function(){
axios.get("/product/search").then(res => {
console.log(res)
})
}
}
注意,如果get
请求如果带参数,会以?a=b&c=d
,形式拼接到url上,这时mock请把接口url写为正则匹配,否则匹配不到就报错Mock.mock(RegExp(API_URL + ".*")
),如:
import axios from 'axios'
export default {
mounted:function(){
///product/search?id=2
axios.get("/product/search",{params:{id:2}}).then(res => {
console.log(res)
})
}
}
在url后面加上.*
mockjs.mock(RegExp("/product/search"+ ".*"),'get',option=>{
console.log(option.url) // 请求的url
console.log(option.body)// body为post请求参数
return {
status:200,
message:"获取数据成功"
}
})
3. 核心方法
Mock.mock( rurl?, rtype?, template|function( options ) )
这是mock的核心方法,根据数据模板生成模拟数据。
- rurl ,可选。
表示需要拦截的 URL,可以是 URL 字符串或 URL 正则。例如:正则+变量写法 ->RegExp(API_URL.LOGIN + ".*")
、正则写法 ->/\/domain\/list\.json/
、字符串写法 ->'/domian/list.json'
。 - rtype,可选。
表示需要拦截的 Ajax 请求类型。例如GET
、POST
、PUT
、DELETE
等。 - template,可选。
表示数据模板,可以是对象或字符串。例如{ 'data|1-10':[{}] }
、'@EMAIL'
。 - function(options),可选。
表示用于生成响应数据的函数。 - options
指向本次请求的 Ajax 选项集,含有url
、type
和body
三个属性,参见 XMLHttpRequest 规范。
具体使用:
- Mock.mock( template ) :根据数据模板生成模拟数据。
- Mock.mock( rurl, template ) : 记录数据模板。当拦截到匹配
rurl
的 Ajax 请求时,将根据数据模板template
生成模拟数据,并作为响应数据返回。 - Mock.mock( rurl, function( options ) ) : 记录用于生成响应数据的函数。当拦截到匹配
rurl
的 Ajax 请求时,函数function(options)
将被执行,并把执行结果作为响应数据返回。 - Mock.mock( rurl, rtype, template ) : 记录数据模板。当拦截到匹配
rurl
和rtype
的 Ajax 请求时,将根据数据模板template
生成模拟数据,并作为响应数据返回。 - Mock.mock( rurl, rtype, function( options ) ) : 记录用于生成响应数据的函数。当拦截到匹配
rurl
和rtype
的 Ajax 请求时,函数function(options)
将被执行,并把执行结果作为响应数据返回。
4. 设置延时请求到数据
不设置延时很有可能遇到坑,这里需要留意,因为真实的请求是需要时间的,mock不设置延时则是马上拿到数据返回,这两个情况不同可能导致在接口联调时出现问题。所以最好要先设置延时请求到数据。
//延时400ms请求到数据
Mock.setup({
timeout: 400
})
//延时200-600毫秒请求到数据
Mock.setup({
timeout: '200-600'
})
数据生成规则
1. 数据模板DTD
mock的语法规范包含两层规范:
- 数据模板 (DTD)
- 数据占位符 (DPD)
基本语法
数据模板中的每个属性由 3 部分构成:属性名name、生成规则rule、属性值value:
'name|rule': value
属性名和生成规则之间用竖线 | 分隔,生成规则是可选的
生成规则 有 7 种格式:
'name|min-max': value
'name|count': value
'name|min-max.dmin-dmax': value
'name|min-max.dcount': value
'name|count.dmin-dmax': value
'name|count.dcount': value
'name|+step': value
- 生成规则的含义需要依赖属性值的类型才能确定。
- 属性值中可以含有
@占位符
。 - 属性值还指定了最终值的初始值和类型。
生成规则和示例
- 属性值是字符串 String
//通过重复 string 生成一个字符串,重复次数大于等于 min,小于等于 max。
'name|min-max': string
//通过重复 string 生成一个字符串,重复次数等于 count。
'name|count': string
var data = Mock.mock({
'name1|1-3':'a', //重复生成1到3个a(随机)
'name2|2':'b' //生成bb
})
- 属性值是数字 Number
//属性值自动加 1,初始值为 number。
'name|+1': number
//生成一个大于等于 min、小于等于 max 的整数,属性值 number 只是用来确定类型。
'name|min-max': number
//生成一个浮点数,整数部分大于等于 min、小于等于 max,小数部分保留 dmin 到 dmax 位。
'name|min-max.dmin-dmax': number
Mock.mock({
'number1|1-100.1-10': 1,
'number2|123.1-10': 1,
'number3|123.3': 1,
'number4|123.10': 1.123
})
//结果:
{
"number1": 12.92,
"number2": 123.51,
"number3": 123.777,
"number4": 123.1231091814
}
var data = Mock.mock({
'name1|+1':4, //生成4,如果循环每次加1
‘name2|1-7':2, //生成一个数字,1到7之间
'name3|1-4.5-8':1 生成一个小数,整数部分1到4,小数部分5到8位,数字1只是为了确定类型
})
- 属性值是布尔型 Boolean
//随机生成一个布尔值,值为 true 的概率是 1/2,值为 false 的概率同样是 1/2。
'name|1': boolean
//随机生成一个布尔值,值为 value 的概率是 min / (min + max),值为 !value 的概率是 max / (min + max)。
'name|min-max': value
var data = Mock.mock({
'name|1':true, //生成一个布尔值,各一半
'name1|1-3':true //1/4是true,3/4是false
})
- 属性值是对象 Object
//从属性值 object 中随机选取 count 个属性。
'name|count': object
//从属性值 object 中随机选取 min 到 max 个属性。
'name|min-max': object
var obj = {
a:1,
b:2,
c:3,
d:4
}
var data = Mock.mock({
'name|1-3':obj, //随机从obj中寻找1到3个属性,新对象
'name|2':obj //随机从onj中找到两个属性,新对象
})
- 属性值是数组 Array
//从属性值 array 中随机选取 1 个元素,作为最终值。
'name|1': array
//从属性值 array 中顺序选取 1 个元素,作为最终值。
'name|+1': array
//通过重复属性值 array 生成一个新数组,重复次数大于等于 min,小于等于 max。
'name|min-max': array
//通过重复属性值 array 生成一个新数组,重复次数为 count。
'name|count': array
Mock.mock({
//通过重复属性值 array 生成一个新数组,重复次数为 1-3次。
"favorite_games|1-3": [3,5,4,6,23,28,42,45],
});
var arr = [1,2,3];
var data = Mock.mock({
'name1|1':arr, //从数组里随机取出1个值
'name2|2':arr, //数组重复count次,这里count为2
'name3|1-3':arr, //数组重复1到3次
})
- 属性值是函数 Function
执行函数 function,取其返回值作为最终的属性值,函数的上下文为属性 'name' 所在的对象。
'name': function
var fun = function(x){
return x+10;
}
var data = Mock.mock({
'name':fun(10) //返回函数的返回值20
})
- 属性值是正则表达式 RegExp
根据正则表达式 regexp 反向生成可以匹配它的字符串。用于生成自定义格式的字符串。
'name': regexp
Mock.mock({
'regexp1': /[a-z][A-Z][0-9]/,
'regexp2': /\w\W\s\S\d\D/,
'regexp3': /\d{5,10}/
})
// =>
{
"regexp1": "pJ7",
"regexp2": "F)\fp1G",
"regexp3": "561659409"
}
2. 数据占位符DPD
占位符只是在属性值字符串中占个位置,并不出现在最终的属性值中。
占位符的格式为:
@占位符
@占位符(参数 [, 参数])
关于占位符需要知道以下几点
- 用
@
标识符标识后面的字符串是占位符 - 占位符 引用的是
Mock.Random
中的方法。 - 可以通过
Mock.Random.extend()
来扩展自定义占位符。 - 占位符 也可以引用 数据模板 中的属性。
- 占位符 会优先引用 数据模板 中的属性。
- 占位符 支持 相对路径 和 绝对路径。
//引入mockjs
import Mock from 'mockjs'
//使用mockjs模拟数据
Mock.mock('/api/msdk/proxy/query_common_credit', {
"ret":0,
"data":
{
"mtime": "@datetime",//随机生成日期时间
"score": "@natural(1, 800)",//随机生成1-800的数字
"rank": "@natural(1, 100)",//随机生成1-100的数字
"stars": "@natural(0, 5)",//随机生成1-5的数字
"nickname": "@cname",//随机生成中文名字
}
});
3. 用例
基础随机内容的生成
{
"string|1-10": "=", // 随机生成1到10个等号
"string2|3": "=", // 随机生成2个或者三个等号
"number|+1": 0, // 从0开始自增
"number2|1-00.1-3": 1, // 生成一个小数,小数点前面1到10,小数点后1到3位
"boolean": "@boolean( 1, 2, true )", // 生成boolean值 三个参数,1表示第三个参数true出现的概率,2表示false出现的概率
"name": "@cname", // 随机生成中文姓名
"firstname": "@cfirst", // 随机生成中文姓
"int": "@integer(1, 10)", // 随机生成1-10的整数
"float": "@float(1,2,3,4)", // 随机生成浮点数,四个参数分别为,整数部分的最大最小值和小数部分的最大最小值
"range": "@range(1,100,10)", // 随机生成整数数组,三个参数为,最大最小值和加的步长
"natural": "@natural(60, 100)", // 随机生成自然数(大于零的数)
"email": "@email", // 邮箱
"ip": "@ip" ,// ip
"datatime": "@date('yy-MM-dd hh:mm:ss')" // 随机生成指定格式的时间
// ......
}
列表数据
{
"code": "0000",
"data": {
"pageNo": "@integer(1, 100)",
"totalRecord": "@integer(100, 1000)",
"pageSize": 10,
"list|10": [{
"id|+1": 1,
"name": "@cword(10)",
"title": "@cword(20)",
"descript": "@csentence(20,50)",
"price": "@float(10,100,10,100)"
}]
},
"desc": "成功"
}
图片
mockjs可以生成任意大小,任意颜色块,且用文字填充内容的图片,使我们不用到处找图片资源就能轻松实现图片的模拟展示
{
"code": "0000",
"data": {
"pageNo": "@integer(1, 100)",
"totalRecord": "@integer(100, 1000)",
"pageSize": 10,
"list|10": [{
// 参数从左到右依次为,图片尺寸,背景色,前景色(及文字颜色),图片格式,图片中间的填充文字内容
"image": "@image('200x100', '#ffcc33', '#FFF', 'png', 'Fast Mock')"
}]
},
"desc": "成功"
}
4. Mock.Random
Mock.Random 是一个工具类,用于生成各种随机数据。
Mock.Random 的方法在数据模板中称为『占位符』,书写格式为 @占位符(参数 [, 参数]) 。
用法示例:
var Random = Mock.Random
Random.email()
// => "n.clark@miller.io"
Mock.mock('@email')
// => "y.lee@lewis.org"
Mock.mock( { email: '@email' } )
// => { email: "v.lewis@hall.gov" }
Mock.Random 提供的完整方法(占位符)如下:
Type | Method |
---|---|
Basic | boolean, natural, integer, float, character, string, range, date, time, datetime, now |
Image | image, dataImage |
Color | color |
Text | paragraph, sentence, word, title, cparagraph, csentence, cword, ctitle |
Name | first, last, name, cfirst, clast, cname |
Web | url, domain, email, ip, tld |
Address | area, region |
Helper | capitalize, upper, lower, pick, shuffle |
Miscellaneous | guid, id |
Basic
- Random.boolean(min?max?current?)
随机生成布尔值
var bool1 = Random.boolean(); //true false各一半
var bool2 = Random.boolean(1,2,false) //1/3的可能性是false 2/3是true
- Random.natural(min?,max?)
随机生成一个自然数,什么叫自然数,就是大于等于0的
var natural1 = Random.natural(); //默认值最大为 9007199254740992
var natural2 = Random.natural(4); //随机出来的最小值是4
var natural3 = Random.natural(6,9);
- Random.Integer(min?,max?)
生成一个随机的整数,可以是负数。
var integer1 = Random.integer();
var integer2 = Random.integer(-10); //随机最小值是-10
var integer3 = Random.integer(-10,20);
- Random.float(min?,max?,dmin?,dmax?)
随机生成一个小数浮点数,四个参数分别为,整数部分最小值最大值,小数部分最小值最大值。
var float1 = Random.float();
var float2 = Random.float(3,8);
var float3 = Random.float(1,3,5,7)
- Random.character(pool?)
随机生成一个字符,pool的值可以是:
- upper: 26个大写字母
- lower: 26个小写字母
- number: 0到9十个数字
- sympol: “!@#$%^&*()[]”
var character1 = Random.character();
var character2 = Random.character('lower');
var character3 = Random.character('upper');
var character4 = Random.character('symbol');
- Random.string(pool?,min?,max?)
随机生成一个字符串,pool的值同上边四个。
var str1 = Random.string(); //长度3到7位
var str2 = Random.string(5); //长度5位
var str3 = Random.string('lower',7); //长度7位,小写
var str4 = Random.string(4,6); //长度4到
var str5 = Random.string('新的字符串会从这里选择4到5位',4,6); //从第一个参数里选择4到5位
- Random.range(start?,stop,step?)
返回一个整型数组
- start,可选,数组起始值,闭区间
- stop,必选,数据结束值,开区间
- step,可选,数据每一项间隔值
var range1 = Random.range(10); //[0,1,2,3,4,5,6,7,8,9]
var range2 = Random.range(14,20); //[14,15,16,17,18,19]
var range3 = Random.range(3,13,2); //[3,5,7,9,11]
- Random.date(format?)
返回一个随机日期的字符串
format的格式是‘yyyy-MM-dd’,可以随机组合
var date1 = Random.date();
var date2 = Random.date('yyyy-MM-dd');
var date3 = Random.date('y-M-d');
var date4 = Random.date('yy-MM-dd');
- Random.time(format?)
返回时间字符串
format的格式是‘HH-mm-ss’
var time1 = Random.time();
var time2 = Random.time('HH-mm-ss');
var time3 = Random.time('J-m-s');
- Random.datetime(format?)
上边两个的结合版
var dt1 = Random.datetime();
var dt2 = Random.datetime('yyyy-MM-dd HH-mm-ss');
Random.now(unit?,format?)
返回当前时间的字符串
Image
一般情况下,使用dataImage更好,因为更简单,但是如果要生成高度自定义的图片,则最好用image。另外,dataImage生成的是base64编码
- Random.image(size?,background?,foreground?,format?text?)
- size 图片宽高,格式是’宽x高’
- background:图片的背景色,默认值#000000
- foreground:图片的文字前景色,默认#FFFFFF
- format:图片的格式,默认’.png’
- text:图片上的默认文字,默认值为参数size
其中size的取值范围是
[
'300x250', '250x250', '240x400', '336x280',
'180x150', '720x300', '468x60', '234x60',
'88x31', '120x90', '120x60', '120x240',
'125x125', '728x90', '160x600', '120x600',
'300x600'
]
图片的格式可以选择.png
、 .gif
、 .jpg
var image1 = Random.image();
var image2 = Random.image('128x90');
var image3 = Random.image('120x660','#ccc'); //前景色#ccc
var image4 = Random.image('226x280','#eee','第三个参数是文字不是前景色');
var image5 = Random.image('66x31','#ddd','#123456','四个参数的时候第三个参数是前景色');
var image6 = Random.image('240x400','#333','#1483dc','.gif','全部参数的情况下');
- Random.dataImage(size?,text?)
返回一段base64编码,两个参数同上。
var di1 = Random.dataImage();
var di2 = Random.datImage('300x600');
var di3 = Random.dataImage('180x150','hahahaha');
Color
- Random.color()
有好几个相关的方法
var color = Random.color(); //格式'#rrggbb'
var hex = Random.hex(); //好像和color没什么不同
var rgb = Random.rgb(); //生成格式如rgb(133,233,244)
var rgba = Random.rgba(); //生成个事如rgba(111,222,233,0.5)
var hsl = Random.hsl(); //生成格式(345,82,71)
Text
- Random.paragraph(in?,max?,len?)
随机生成一段文本,
var para1 = Random.paragraph(); //随机生成一短文本,范围3到7
var para2 = Random.paragraph(10); //随机生成长度是10的文本
var para3 = Random.paragraph(9,12); //随机生成9到11位长度的文本
- Random.sentence(min?,max?,len?)
随机生成一个句子,第一个单词的首字母大写
var sen1 = Random.sentence(); //默认长度12到18
var sen2 = Random.sentence(10); //随机生成一个单词个数为10的句子
var sen3 = Random.sentence(5,10); //随机生成一个5到9单词个数的句子
- Random.word(min?,max?,len?)
随机生成一个单词
var word1 = Random.word(); //默认长度3到10
var word2 = Random.word(7); //随机生成长度是7的单词
var word3 = Random.word(2,12); //随机生成2到11位长度的单词
- Random.title(min?,max?,len?)
随机生成一段标题,每个单词的首字母大写
var title1 = Random.title(); //title中的单词个数
var title2 = Random.title(6); //title六个单词
var title3 = Random.title(7,12); //title7到11个单词
另外还有四个方法,四个方法前边加一个**c**
,返回中文内容
-
Random.cparagraph
, 返回中文文本 -
Random.csentence
, 返回中文句子 -
Random.cword
, 返回中文文字 -
Random.ctitle
, 返回中文标题
Name
var first = Random.first() 随机生成常见英文名
var last = Random.last() 随机生成常见英文姓
var name = Random.name() 随机生成常见英文姓名
var cfirst = Random.cfirst() 随机生成常见中文姓
var clast = Random.clast() 随机生成常见中文名
var cname = Random.cname() 随机生成一个常见中文姓名
Web
- Random.url(protocol?,host?)
随机生成一个url
-
protocol
可选参数,表示网络协议,如http
。 -
host
表示域名和端口号
var url1 = Random.url();
var url2 = Random.url('http');
var url3 = Random.url('http','58.com');
- Random.protocol()
随机生成一个域名
var protocol = Random.protocol()
protocol可以选的值,‘http’、‘ftp’、‘gopher’、‘mailto’、‘mid’、‘cid’、‘news’、‘nntp’、‘prospero’、‘telnet’、‘rlogin’、‘tn3270’、‘wais’。
- Random.domin()
随机生成一个域名
- Random.tld()
随机生成一个顶级域名
var domain = Random.domain()
var tld = Random.tld()
- Random.email(domain?)
随机生成一个email地址,domain表示域名
var email1 = Random.email();
var email2 = Random.email('58.com') //生成xxxx@58.com
- Random.ip()
随机生成一个ip地址
var ip = Random.ip()
Address
- Random.region()
随机生成一个中国的大区,如华北,西南
var region = Random.region();
- Random.province()
随机生成一个中国省直辖市自治区特别行政区
var province = Random.province()
- Random.city(prefix?)
随机生成一个中国城市,prefix布尔值,表示是否标注所在省
var city1 = Random.city();
var city2 = Random.city(ture);
Random.country(prefix?)
随机生成一个中国县,prefix
布尔值,表示是否显示所属的省市
var county1 = Random.county();
var county2 = Random.county(ture);
- Random.zip()
随机生成一个六位数邮政编码
var zip = Random.zip();
Helper
- Random.capitlize(word)
把第一个字母转成大写
var capitalize = Random.capitalize('hello')
- Random.upper(str)
转成大写
var upper = Random.upper('zhang');
- Random.lower(str)
转成小写
var lower = Random.lower('JINGWEI');
- Random.pick(arr)
从数组中随机选取一个元素
var arr = [1,4,5,6,7,8];
var pick = Random.pick(arr);
- Random.shuffle(arr);
打乱数组的顺序并返回
var arr = [1,2,3,4,6];
var shuffle = Random.shuffle(arr);
Miscellaneous
- Random.guid()
随机生成一个GUID
- Random.id()
随机生成一个18位身份证id
var guid = Random.guid();
var id = Random.id();
扩展
Mock.Random 中的方法与数据模板的 @占位符
一一对应,在需要时还可以为 Mock.Random 扩展方法,然后在数据模板中通过 @扩展方法
引用。例如:
Random.extend({
constellation: function(date) {
var constellations = ['白羊座'`, '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座']
return this.pick(constellations)
}
})
Random.constellation()
// => "水瓶座"
Mock.mock('@CONSTELLATION')
// => "天蝎座"
Mock.mock({
constellation: '@CONSTELLATION'
})
// => { constellation: "射手座" }
企业级后台集成方案
vue-element-admin介绍
- vue-element-admin 是一个后台前端解决方案,它基于 vue 和 element-ui实现。
- 内置了 i18 国际化解决方案,动态路由,权限验证,提炼了典型的业务模型,提供了丰富的功能组件。
- 可以快速搭建企业级中后台产品原型。
- 地址:https://panjiachen.github.io/vue-element-admin-site/zh/guide/
安装与使用
- 克隆项目,git clone https://github.com/PanJiaChen/vue-element-admin.git
- 进入项目目录,cd vue-element-admin
- 安装依赖,npm install
- 使用淘宝镜像,npm install --registry=https://registry.npm.taobao.org
- 本地开发 启动项目,npm run dev
注意事项
- 若依赖安装不成功,很大概率是node-sass安装失败。
- node-sass因为其历史原因,安装时极易失败,同时node-sass依赖了Python环境,因此电脑上需要安装配置Python2。
- 建议直接使用课程提供的包含有依赖包的项目,简化学习曲线。
源码解读
- 项目提供了完成的构建过程,适合前端技术的学习:https://juejin.cn/post/6844903476661583880
- 本课程对其内部的核心流程进行分析。
跨域认证
Session认证
互联网服务离不开用户认证。一般流程是下面这样。
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
- Session认证流程
session 认证的方式应用非常普遍,但也存在一些问题,扩展性不好,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session,针对此种问题一般有两种方案:
- 一种解决方案是session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。
- 一种方案是服务器不再保存 session 数据,所有数据都保存在客户端,每次请求都发回服务器。Token认证就是这种方案的一个代表。
Token认证
Token 是在服务端产生的一串字符串,是客户端访问资源接口(API)时所需要的资源凭证,流程如下:
- 客户端使用用户名跟密码请求登录,服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端
- 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 token
- 服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据
- token认证流程。
token认证的特点
- 基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。
- 用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
- token 完全由应用管理,所以它可以避开同源策略
JWT认证
- JSON Web Token(简称 JWT)是一个token的具体实现方式,是目前最流行的跨域认证解决方案。
- JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,具体如下:
- 用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。
- 为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
JWT
JWT 的由三个部分组成,依次如下:
-
Header(头部)
- Header 部分是一个 JSON 对象,描述 JWT 的元数据
- alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
- typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
- 最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串
- Header 部分是一个 JSON 对象,描述 JWT 的元数据
-
Payload(负载)
- Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- 注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
- 这个 JSON 对象也要使用 Base64URL 算法转成字符串。
- Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
-
Signature(签名)
- Signature 部分是对前两部分的签名,防止数据篡改。
- 首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户
- 然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
三部分最终组合为完整的字符串,中间使用 . 分隔,如下:
- Header.Payload.Signature
JWT的特点
- 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
- 客户端每次与服务器通信,都要带上这个 JWT,可以把它放在 Cookie 里面自动发送,但是这样不能跨域。
- 更好的做法是放在 HTTP 请求的头信息
Authorization
字段里面,单独发送。
JWT的实现
- 加入依赖