Fork me on GitHub

spring boot解决如何同时上传多个图片和下载的问题

spring boot解决如何同时上传多个图片和下载的问题

在平时的业务场景中,避免不了,要搭建文件上传服务器,作为公共服务。一般情况,只做了单个文件的上传,实际业务场景中,却发现单个文件上传,并不能满足一些业务需求,因此我们需要解决如何写一个同时上传多个文件的借口,并返回可下载的文件地址;

废话不多讲,不再从头建立一个Spring boot项目,如果不知道的话,请直接前往官网查看实例。

下面我们以上传图片为例,示例相对简单,仅供参考:

1 后端上传图片接口逻辑

UploadController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.zz.controllers.fileUpload;

import com.zz.Application;
import com.zz.model.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.UUID;

import static com.zz.config.ConfigConstants.getFileDir;

@RestController
@Configuration
public class UploadController {

private static final Logger log = LoggerFactory.getLogger(Application.class);

@Value("${server.port}")
private String port;

//获取当前IP地址
public String getIp() {
InetAddress localhost = null;
try {
localhost = Inet4Address.getLocalHost();
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
return localhost.getHostAddress();
}

@PostMapping(value = "/upload", consumes = {"multipart/form-data"})
public Response upload(@RequestParam("file") MultipartFile[] files, Response response) {
log.info("上传多个文件");
StringBuilder builder = new StringBuilder();
// file address
String fileAddress ="http://"+ getIp()+ ":" + port + File.separator;

ArrayList<String> imgUrls = new ArrayList<String>();
try {
for (int i = 0; i < files.length; i++) {
// old file name
String fileName = files[i].getOriginalFilename();
// new filename
String generateFileName = UUID.randomUUID().toString().replaceAll("-", "") + fileName.substring(fileName.lastIndexOf("."));
// store filename
String distFileAddress = fileAddress + generateFileName;
builder.append(distFileAddress+",");
imgUrls.add(distFileAddress);
// generate file to disk
files[i].transferTo(new File(getFileDir() + generateFileName));
}
} catch (Exception e) {
e.printStackTrace();
}
response.setMsg("success");
log.info(builder.toString());
response.setData(imgUrls);
return response;
}
}

相对于单个文件的接收,我们这里直接接受多个file对象,然后遍历生成每个对应的地址。

其中:

getFileDir 设置存放图片的地址,我选择存在项目外的其他地方

com.zz.config.ConfigConstants.getFileDir

1
2
3
4
5
6
7
8
9
10
11
package com.zz.config;

public class ConfigConstants {

public static String fileDir;

public static String getFileDir() {
fileDir = "/Users/wz/projects/blog/uploadFile/";
return fileDir;
}
}

当我们把文件生成到指定的文件夹后,我们如何配置在当前server下访问项目外的静态文件图片资源呢?

这个我们就要利用spring boot配置文件 application.yml, 当前还有其他方法比如 WebMvcConfigurer 这里不再赘述。

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pring:
jpa:
show-sql: true
hibernate:
ddl-auto: update

servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB

profiles:
active: dev

# 静态资源配置
mvc:
static-path-pattern: /**
resources:
static-locations: file:/Users/wz/projects/blog/uploadFile/,classpath:/static/,classpath:/resources/,classpath:/file/,classpath:/templates/

server:
use-forward-headers: true
tomcat:
remote-ip-header: X-Real-IP
protocol-header: X-Forwarded-Proto

#自定义
my:
tokenURL: "55555"
authURL: "88888"

这样之后我们在生成的结果中的 http://192.168.31.77:8080/a7ef32e3922b46aea256a93dd53de288.png,这样的地址就可以把文件实质性的指向了file:/Users/wz/projects/blog/uploadFile/,这样大致就是一个简单文件服务器的配置了,当然远不及此,还有压缩一类的功能,后续再聊。

后端逻辑已经很清晰;

那前端如何向后端同时发送多个file对象呢?

2 前端多个文件上传如何传参

html

1
<input type="file" multiple class="el-upload" accept="image/*" name="file">

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//upload
var uploadBtn = document.querySelector('.el-upload');

uploadBtn.onchange = function (e) {
let files = this.files;
console.log(this.files);

const xhr = new XMLHttpRequest();
xhr.open("post", "/upload", true);
// xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
console.log(JSON.parse(this.responseText));
const {data} = JSON.parse(this.responseText);
if(!data) return;
const imageList = data.slice(0);
let imageStr = '';
imageList.forEach(img=>{
imageStr += `<img src="${img}" />`;
});
document.getElementById("result").innerHTML = imageStr;
}
};

const formData = new FormData();

// 多个file 同时上传
if(files && files.length){
for (let i=0;i<files.length;i++) {
formData.append("file", files[i])
}
}

console.log(formData);

xhr.send(formData);
};

前端通过FormData传参数发送POST请求;

区别于之前的单个formData.append(); 这里我们可以同时append多个相同名字的文件二进制文件流;

image-20191123234150228

image-20191123234325831

image-20191123234358451

image-20191123234557023

如图结果正常显示,当我们部署到服务器的时候,这个就可以当作一个web服务器供大家使用。

spring boot从零搭建登录注册功能并进行所有接口验证

spring boot从零搭建登录注册功能并进行所有接口验证

[TOC]

目前大多项目是前后端分离。在后台接口服务开发过程中,往往我们需要先搭建一个基础服务,比如登录注册功能、自动对所有的接口进行token的安全校验等,这样可以防范安全问题的出现。并且这样后续的同事可以只关注业务代码的开发,不需要关心基础架构服务的实现。

这次我准备搭建一个简单的后台服务,用的是spring boot + mysql + mybatis

1、搭建Spring boot项目

首先我们使用IDEA自带的初始化项目功能,创建一个Spring boot项目,如图:

image-20191207125203878

或者在线生成,点击进入

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>com.zz</groupId>
<artifactId>rest-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rest-api</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
<scope>compile</scope>
</dependency>

<!-- Use MySQL Connector-J -->

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>


<!-- image to base64 -->

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

<!-- jjwt支持 -->

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>

<!--commons-codec -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>

<dependency>
<groupId>com.github.terran4j</groupId>
<artifactId>terran4j-commons-api2doc</artifactId>
<version>1.0.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>com.itextpdf.tool</groupId>
<artifactId>xmlworker</artifactId>
<version>5.5.10</version>
</dependency>

<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.15</version>
</dependency>

<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.15</version>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

maven更改为国内阿里云镜像,这样比较快

settings.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">

<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>

Mybatis 推荐插件如下:

image-20191207125851843

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# mysql
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: update
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
profiles:
active: dev
# 静态资源配置
mvc:
static-path-pattern: /**
resources:
static-locations: file:/Users/wz/projects/blog/uploadFile/,classpath:/static/,classpath:/resources/,classpath:/file/,classpath:/templates/

mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.zz.entity
#自定义
my:
tokenURL: "55555"
authURL: "88888"

application-dev.yml

1
2
3
4
5
6
7
8
9
# mysql
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: ******
server:
port: 8080

大概目录结构如下

image-20191207133758025

搭建细节不再赘述;

2、读取自定义配置文件

com.zz.config.MyConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.zz.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "my")
public class MyConfiguration {

private String tokenURL;

private String authURL;

public String getAuthURL() {
return this.authURL;
}

public void setAuthURL(String authURL) {
this.authURL = authURL;
}

public String getTokenURL() {
return this.tokenURL;
}

public void setTokenURL(String tokenURL) {
this.tokenURL = tokenURL;
}
}

3、web服务配置

com.zz.config.MyConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zz.config;

import com.zz.common.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@Configuration
public class WebConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有请求,通过判断是否有 @passToken 注解 决定是否需要跳过登录
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}

@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}

}

4、自定义返回统一的实体类Response

com.zz.model.Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.zz.model;


public class Response {

private int code;
private String msg;
private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}
}

5、Utils公共方法类

com.zz.utils.HttpUtils

获取Request、 Response、session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.zz.utils;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
* 获取 Request 和 Response
*/
public class HttpUtils {

// 获取 request
public static HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return null;
return requestAttributes.getRequest();
}

// 获取 response
public static HttpServletResponse getResponse() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) return null;
return requestAttributes.getResponse();
}

// 获取 session
public static HttpSession getSession(){
HttpServletRequest request = getRequest();
if(request == null) return null;
return request.getSession();
}
}

com.zz.utils.JWTUtils

JWT 生成token, 验证token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.zz.utils;

import com.zz.entity.User;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JWTUtils {

// 生成签名的时候使用的秘钥secret
private static final String SECRETKEY = "KJHUhjjJYgYUllVbXhKDHXhkSyHjlNiVkYzWTBac1Yxkjhuad";

// expirationDate 生成jwt的有效期,单位秒
private static long expirationDate = 2 * 60 * 60;


/**
* 由字符串生成加密key
*
* @return SecretKey
*/
private static SecretKey generalKey(String stringKey) {
byte[] encodedKey = Base64.decodeBase64(stringKey);
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}

/**
* 创建 jwt
*
* @param user 登录成功后的用户信息
* @return jwt token
*/
public static String createToken(User user) {

// 指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

// 生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getUserId());
claims.put("userName", user.getUserName());
claims.put("password", user.getPassword());

// 生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了
SecretKey key = generalKey(SECRETKEY + user.getPassword());

// 生成签发人
// json形式字符串或字符串,增加用户非敏感信息存储,如用户id或用户账号,与token解析后进行对比,防止乱用
HashMap<String, Object> storeInfo = new HashMap<String, Object>();
storeInfo.put("userId", user.getUserId());
storeInfo.put("userName", user.getUserName());
String subject = storeInfo.toString();

// 下面就是在为payload添加各种标准声明和私有声明了
// 这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 唯一随机UUID
// 设置JWT ID:是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
.setId(UUID.randomUUID().toString())
// jwt的签发时间
.setIssuedAt(now)
// 代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志
.setSubject(subject)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, key);

if (expirationDate >= 0) {
long expMillis = nowMillis + expirationDate * 1000;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}

/**
* 解密token,获取声明的实体
*
* @param token 加密后的token
* @return claims
*/
public static Claims parseToken(String token, User user) {
// 签名秘钥,和生成的签名的秘钥要保持一模一样
SecretKey key = generalKey(SECRETKEY + user.getPassword());

// 获取私有声明
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(key)
// 设置需要解析的token
.parseClaimsJws(token).getBody();

return claims;
}


/**
* 校验token
*
* @param token 加密后的token
* @param user 用户信息
* @return true|false
*/
public static Boolean verify(String token, User user) {

// 获取私有声明的实体
Claims claims = parseToken(token, user);

return claims.get("password").equals(user.getPassword());
}
}

6、查询实体类 query

所有的服务查询都采用统一的各自的实体类

比如:

com.zz.query.UserQuery

用户查询实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.zz.query;

public class UserQuery {

private String userName;

private String password;

private long userId;

private boolean showPassword;

public boolean isShowPassword() {
return showPassword;
}

public void setShowPassword(boolean showPassword) {
this.showPassword = showPassword;
}

public long getUserId() {
return userId;
}

public void setUserId(long userId) {
this.userId = userId;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

7、查询后返回实体类

所有的服务查询返回都采用统一的各自的实体类

比如:

com.zz.entity.User

用户数据返回实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.zz.entity;

public class User {

private long userId;

private String userName;

private String token;

private String password;

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

public long getUserId() {
return userId;
}

public void setUserId(long userId) {
this.userId = userId;
}

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

}

8 、接口实现三层架构

我们这采取的是三层架构:controller —> service —> mapper;

如果我们要写一个User类接口,先声明一个UserController路由控制层,然后这个里调用UserService实现类方法,然后再调用mapper持久层去CRUD(mysql增查删改)。

9、开始搭建注册用户功能

基础搭建先暂停,开始实质业务的推演;

mysql的连接就不多说啦;

让我们开始实现之旅吧;

com.zz.newController.UserController

用户注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com.zz.newController;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;


/**
* 登录
* author: wz
*/
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

/*
* @param userName
* @param password
* @return response
*/
@PostMapping("/add")
@PassToken
public Response addUser(@RequestParam String userName, @RequestParam String password, Response response) {
UserQuery query = new UserQuery();
User userData = null;

query.setUserName(userName);
query.setPassword(password);

int result;
String message = "";

// 判断用户是否已经存在
UserQuery findUserQuery = new UserQuery();
findUserQuery.setUserName(userName);
User existUser = this.userService.findUserByName(findUserQuery);
if (existUser == null) {

// 插入用户
try {
result = this.userService.addUser(query);
message = "success";
} catch (Exception e) {
result = 0;
message = "error";
e.printStackTrace();
}

// 插入用户成功后返回用户信息
if (result == 1) {
userData = this.userService.findUserByName(findUserQuery);

// 生成token
String token = null;

// 当前用户
User currentUser = new User();
if (userData != null) {
currentUser.setUserId(userData.getUserId());
currentUser.setUserName(userData.getUserName());
currentUser.setPassword(password);
token = JWTUtils.createToken(currentUser);
}

if (token != null) {
userData.setToken(token);

// 获取token用户信息
// Claims userDataFromToken = JWTUtils.parseToken(token, currentUser);
}
}

} else {
message = "用户已经存在";
}

response.setData(userData);
response.setMsg(message);
return response;
}

}

com.zz.service.UserService

Interface 用户接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.zz.service;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;
import java.util.Map;

public interface UserService {

// 添加用户
int addUser(UserQuery query);


}

com.zz.service.impl.UserServiceImpl

用户接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.zz.service.impl;

import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public int addUser(UserQuery query){
return this.userMapper.insert(query);
}

}

com.zz.mapper.UserMapper

mapper

1
2
3
4
5
6
7
8
9
10
11
12
package com.zz.mapper;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;

public interface UserMapper {

int insert(UserQuery query);

}

resources/mapper/UserMapper.xml

前后名字一定对应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">

<resultMap id="BaseResult" type="com.zz.entity.User">
<id column="user_id" property="userId"></id>
<id column="user_name" property="userName"></id>
</resultMap>

<sql id="base">
user_id,
user_name
<if test="showPassword">
, password
</if>
</sql>

<sql id="base_condition">
<where>
<if test="userName!=null and userName!=''">
user_name=#{userName}
</if>
<if test="password!=null and password!=''">
and password=#{password}
</if>
</where>

</sql>

<insert id="insert">
INSERT INTO user(
user_name,
password
) VALUES (
#{userName},
#{password}
)
</insert>


</mapper>

到此,整个接口书写过程已全部完成,这就是在当前架构下写一个接口的全部过程。

10、搭建web实例 ——注册用户

由于我们在配置文件里已经配置静态资源的路径,所以我们可以在resources里面写一个不分离的we b实例进行访问。

resources/static/regist.html

注册页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<title>注册用户</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="css/regist.css"/>
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">

</head>
<body>
<div class="container">
<div class="page form_page js_show">
<div class="weui-form">
<div class="weui-form__text-area">
<h2 class="weui-form__title">注册新用户</h2>
</div>
<div class="weui-form__control-area">
<div class="weui-cells__group weui-cells__group_form">
<div class="weui-cells weui-cells_form">
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">用户名</label></div>
<div class="weui-cell__bd">
<input id="js_input——user" class="weui-input" placeholder="请输入要设置的用户名">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">密码</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd" type="password" class="weui-input" placeholder="请输入要设置的密码">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">确认密码</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd2" type="password" class="weui-input" placeholder="请再次输入设置的密码" type="number" pattern="[0-9]*">
</div>
</div>
</div>
</div>
</div>
<!-- <div class="weui-form__tips-area">-->
<!-- <p class="weui-form__tips">-->
<!-- 表单页提示,居中对齐-->
<!-- </p>-->
<!-- </div>-->
<div class="weui-form__opr-area">
<a class="weui-btn weui-btn_primary" href="javascript:" id="submit">确定</a>
</div>

<div class="weui-form__extra-area">
<div class="weui-footer">
<!-- <p class="weui-footer__links">-->
<!-- <a href="javascript:void(0);" class="weui-footer__link">底部链接文本</a>-->
<!-- </p>-->
<p class="weui-footer__text">Copyright © 2019 alex wong</p>
</div>
</div>
</div>
<div id="js_toast" style="display: none;">
<div class="weui-mask_transparent"></div>
<div class="weui-toast">
<i class="weui-icon-success-no-circle weui-icon_toast"></i>
<p class="weui-toast__content">已完成</p>
</div>
</div>
</div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/regist.js"></script>
</html>

static/js/dataService.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
const APIURL = '/';

window.dataService = {

//GET
get: (url, params = {}) => {

const searchArr = [];

Object.keys(params).forEach(n => {
searchArr.push(`${n}=${params[n]}`);
});

const searchStr = searchArr.length ? '?' + searchArr.join('&') : '';
const token = utils.getCookie('token');

return fetch(APIURL + url + searchStr, {
method: 'GET',
headers: {
token
}
}).then(res => res.json());
},

//POST
post: (url, params = {}) => {

const formData = new FormData();

Object.keys(params).forEach(n => {
formData.append(n, params[n]);
});

const token = utils.getCookie('token');

return fetch(APIURL + url, {
method: 'POST',
headers: {
token
},
body: formData
}).then(res => res.json());
},

// 注册
addUser(params) {
return this.post('user/add', params);
},

// 登录
login(params) {
return this.post('user/login', params);
},

// 用户信息
getUserInfo(params) {
return this.get('user/info', params);
},

};

static/js/utils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
window.utils = {

// md5
generateMd5(userName, password) {
const salt = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik9ol0p@!.";
const asciStr = userName + salt + password;
const asciArr = asciStr.split('');
const asciResult = [];
asciArr.forEach(n => {
asciResult.push(n.charCodeAt());
});
const ascireusltStr = asciResult.join(salt);
return hex_md5(ascireusltStr);
},

// setCookie
setCookie(name, value) {
var time = 2 * 60 * 60 * 1000;
var exp = new Date();
exp.setTime(exp.getTime() + time);
document.cookie = name + "=" + escape(value) + ";expires=" + exp.toGMTString();
},

// getCookie
getCookie(name) {
var arr, reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if (arr = document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}

};

static/js/regist.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 获取相关用户信息
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const passwordConfirmInput = document.getElementById("js_input——pwd2");
const submitBtn = document.getElementById("submit");

// submit
submitBtn.onclick = () => {

const userName = userNameInput.value;
const password = passwordInput.value;
const confirmPassword = passwordConfirmInput.value;

// verify
if (!userName) {
weui.topTips('用户姓名不能为空');
return;
} else if (!password) {
weui.topTips('用户密码不能为空');
return;
} else if (confirmPassword !== password) {
weui.topTips('前后密码不一致,请重试');
return;
}

// 加密密码
const newPassword = utils.generateMd5(userName, password);

// 注册
dataService.addUser({
userName,
password: newPassword,
}).then(res => {
const {code, data, msg} = res;
if (!data) {
weui.topTips(msg);
} else {
weui.topTips(`注册成功,欢迎 ${data.userName}`);
window.location.href = location.origin + '/login.html';
}
})
};

效果如图:

增加一些基本的校验

image-20191207145008765

用户密码加密传输,并验证新用户是否已经注册

image-20191207145612200

image-20191207150016196

mysql 查看下用户表

image-20191207150530465

11、后端-用户登录功能

按上面第9步骤所述,下面的添加内容,请直接添加到上述服务中,不再全部展示代码。

com.zz.newController.UserController

首先要判断用户是否存在,如果存在,返回基本信息并返回用户凭证token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 登录
*
* @param userName 用户名
* @param password 密码
* @return {}
*/
@PostMapping("/login")
@PassToken
public Response login(@RequestParam String userName, @RequestParam String password, Response response) {

UserQuery query = new UserQuery();
query.setUserName(userName);
query.setPassword(password);

// 验证用户和密码
try {
// 判断用户是否已经存在
User existUser = this.userService.findUserByName(query);

// 生成token
String token = null;

// 当前用户
User currentUser = new User();
if (existUser != null) {
currentUser.setUserId(existUser.getUserId());
currentUser.setUserName(existUser.getUserName());
currentUser.setPassword(password);
// 生成用户凭证
token = JWTUtils.createToken(currentUser);
if (token != null) {
existUser.setToken(token);
}
response.setMsg("success");
response.setData(existUser);
} else {
// 登录失败
response.setMsg("登录失败,请检查用户名和密码");
response.setData(null);
}

} catch (Exception e) {
response.setMsg("login failed");
response.setData(null);
e.printStackTrace();
}
return response;
}

com.zz.service.UserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.zz.service;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;
import java.util.Map;

public interface UserService {

// 添加用户
int addUser(UserQuery query);

//查找单个用户
User findUserById(UserQuery query);

User findUserByName(UserQuery query);

}

com.zz.service.impl.UserServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.zz.service.impl;

import com.zz.entity.User;
import com.zz.mapper.UserMapper;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public int addUser(UserQuery query){
return this.userMapper.insert(query);
}

@Override
public User findUserById(UserQuery query) {
return this.userMapper.findUserById(query);
}

@Override
public User findUserByName(UserQuery query) {
return this.userMapper.findUserByName(query);
}

@Override
public List<User> findAllUser(UserQuery query) {
return this.userMapper.findAllUser(query);
}
}

com.zz.mapper.UserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.zz.mapper;

import com.zz.entity.User;
import com.zz.query.UserQuery;

import java.util.List;

public interface UserMapper {

int insert(UserQuery query);

User findUserById(UserQuery query);

User findUserByName(UserQuery query);

List<User> findAllUser(UserQuery query);

}

mapper/UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zz.mapper.UserMapper">

<resultMap id="BaseResult" type="com.zz.entity.User">
<id column="user_id" property="userId"></id>
<id column="user_name" property="userName"></id>
</resultMap>

<sql id="base">
user_id,
user_name
<if test="showPassword">
, password
</if>
</sql>

<sql id="base_condition">
<where>
<if test="userName!=null and userName!=''">
user_name=#{userName}
</if>
<if test="password!=null and password!=''">
and password=#{password}
</if>
</where>

</sql>

<!-- 查询所有user -->
<select id="findAllUser" resultMap="BaseResult">
select
<include refid="base"/>
from user
</select>

<!-- 查询user -->
<select id="findUserById" resultMap="BaseResult">
select
<include refid="base"/>
from user
where
user_id = #{userId}
</select>

<select id="findUserByName" resultMap="BaseResult">
select
<include refid="base"/>
from user
<include refid="base_condition"/>
</select>

<insert id="insert">
INSERT INTO user(
user_name,
password
) VALUES (
#{userName},
#{password}
)
</insert>


</mapper>

12、搭建web实例 ——登录用户

static/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
<title>login</title>
<!-- 引入样式 -->
<link rel="stylesheet" href="css/regist.css"/>
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.1.3/weui.min.css">

</head>
<body>
<div class="container">
<div class="page form_page js_show">
<div class="weui-form">
<div class="weui-form__text-area">
<h2 class="weui-form__title">登录</h2>
</div>
<div class="weui-form__control-area">
<div class="weui-cells__group weui-cells__group_form">
<div class="weui-cells weui-cells_form">
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">用户名</label></div>
<div class="weui-cell__bd">
<input id="js_input——user" class="weui-input" placeholder="请输入用户名">
</div>
</div>
<div class="weui-cell">
<div class="weui-cell__hd"><label class="weui-label">密码</label></div>
<div class="weui-cell__bd">
<input id="js_input——pwd" type="password" class="weui-input" placeholder="请输入密码">
</div>
</div>

</div>
</div>
</div>
<!-- <div class="weui-form__tips-area">-->
<!-- <p class="weui-form__tips">-->
<!-- 表单页提示,居中对齐-->
<!-- </p>-->
<!-- </div>-->
<div class="weui-form__opr-area">
<a class="weui-btn weui-btn_primary" href="javascript:" id="submit">确定</a>
</div>

<div class="weui-form__extra-area">
<div class="weui-footer">
<!-- <p class="weui-footer__links">-->
<!-- <a href="javascript:void(0);" class="weui-footer__link">底部链接文本</a>-->
<!-- </p>-->
<p class="weui-footer__text">Copyright © 2019 alex wong</p>
</div>
</div>
</div>
<div id="js_toast" style="display: none;">
<div class="weui-mask_transparent"></div>
<div class="weui-toast">
<i class="weui-icon-success-no-circle weui-icon_toast"></i>
<p class="weui-toast__content">已完成</p>
</div>
</div>
</div>
</div>
</body>
<script src="js/md5.js"></script>
<script src="js/utils.js"></script>
<script src="js/dataService.js"></script>
<script type="text/javascript" src="https://res.wx.qq.com/open/libs/weuijs/1.2.1/weui.min.js"></script>
<script src="js/login.js"></script>
</html>

static/js/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 获取相关用户信息
const userNameInput = document.getElementById("js_input——user");
const passwordInput = document.getElementById("js_input——pwd");
const submitBtn = document.getElementById("submit");

// submit
submitBtn.onclick = () => {

const userName = userNameInput.value;
const password = passwordInput.value;

// verify
if (!userName) {
weui.topTips('用户姓名不能为空');
return;
} else if (!password) {
weui.topTips('用户密码不能为空');
return;
}

// 加密密码
const newPassword = utils.generateMd5(userName, password);

// 注册
dataService.login({
userName,
password: newPassword,
}).then(res => {
const {code, data, msg} = res;
if (!data) {
weui.topTips(msg);
} else {
weui.topTips(`登录成功,欢迎 ${data.userName}`);
utils.setCookie('token', data.token);
location.href = location.origin + '/home.html';
}
})
};

image-20191207152034072

image-20191207152244301

image-20191207152343686

登录接口返回用户凭证token,后续用来校验用户接口,增加安全性。

13、增加自定义注解和拦截器

在常规的业务开发中,切记不可把接口服务暴露给任何人都可以访问,不然别人可以任意查看或者修改你的数据,这是很严重的事情。除了常规从网段IP方面限制固定客户端IP的范围,接口本身也要增加安全验证,这个时候我们就需要用到之前生成的用户凭证token;

问题是我们如果自定义控制,哪些接口是需要经过验证,哪些接口是不需要通过验证的呢?有人可能会说,直接全部验证不就可以啦,何苦纠结。但是在真实的业务中,有些接口是不能强制校验的,比如一些用户分享到微信的那种接口,是不能增加验证,否则分享的页面无法正常显示。

所以我们可以自定义注解@PassToken, 添加这个注解的接口,就可以不用进行token验证了。

com.zz.common.annotation.PassToken

PassToken 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.zz.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 是否跳过token验证
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}

添加拦截器

com.zz.common.interceptor.AuthenticationInterceptor

在发送请求的时候,在请求头里面加token, 然后验证的时候token从头部获取

如果没有token, 进行无token提示;

如果存在,就用 JWT 校验 token 是否存在,并且校验用户密码是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package com.zz.common.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.mongodb.util.JSON;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.configurationprocessor.json.JSONException;
import org.springframework.boot.configurationprocessor.json.JSONObject;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

// 拦截器
public class AuthenticationInterceptor implements HandlerInterceptor {

@Autowired
private UserService userService;

/**
* response返回信息
*
* @param code
* @param message
* @return
* @throws JSONException
*/
public JSONObject getJsonObject(int code, String message) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", message);
jsonObject.put("code", code);
return jsonObject;
}

@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {

// 从 http 请求头中取出 token
String token = BaseApplicationController.getToken();
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}

HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有PassToken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}

// 默认执行认证
httpServletResponse.setContentType("application/json;charset=UTF-8");
if (token == null || token.equals("null")) {
JSONObject jsonObject = getJsonObject(403, "无token,请重新登录");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("无token,请重新登录");
}

// 获取 token 中的 user id
long userId;
try {
userId = BaseApplicationController.getCurrentUserId();
} catch (JWTDecodeException j) {
JSONObject jsonObject = getJsonObject(500, "访问异常, token不正确,请重新登录");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("访问异常!");
}

// 验证用户是否存在
UserQuery query = new UserQuery();
query.setUserId(userId);
query.setShowPassword(Boolean.TRUE);
User user = userService.findUserById(query);

if (user == null) {
JSONObject jsonObject = getJsonObject(500, "用户不存在,请重新登录");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("用户不存在,请重新登录");
}

// 验证token是否有效
Boolean verify = JWTUtils.verify(token, user);
if (!verify) {
JSONObject jsonObject = getJsonObject(500, "非法访问,请重新登录");
httpServletResponse.getWriter().write(jsonObject.toString());
return false;
// throw new RuntimeException("非法访问!");
}

return true;
}
}

下面让我们实例看下效果:

com.zz.newController.UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
package com.zz.newController;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zz.common.annotation.PassToken;
import com.zz.common.base.BaseApplicationController;
import com.zz.entity.User;
import com.zz.model.Response;
import com.zz.query.UserQuery;
import com.zz.service.UserService;
import com.zz.utils.JWTUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;


/**
* 登录
* autho: wangzhao
*/
@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

/*
* @param userName
* @param password
* @return response
*/
@PostMapping("/add")
@PassToken
public Response addUser(@RequestParam String userName, @RequestParam String password, Response response) {
UserQuery query = new UserQuery();
User userData = null;

query.setUserName(userName);
query.setPassword(password);

int result;
String message = "";

// 判断用户是否已经存在
UserQuery findUserQuery = new UserQuery();
findUserQuery.setUserName(userName);
User existUser = this.userService.findUserByName(findUserQuery);
if (existUser == null) {

// 插入用户
try {
result = this.userService.addUser(query);
message = "success";
} catch (Exception e) {
result = 0;
message = "error";
e.printStackTrace();
}

// 插入用户成功后返回用户信息
if (result == 1) {
userData = this.userService.findUserByName(findUserQuery);

// 生成token
String token = null;

// 当前用户
User currentUser = new User();
if (userData != null) {
currentUser.setUserId(userData.getUserId());
currentUser.setUserName(userData.getUserName());
currentUser.setPassword(password);
token = JWTUtils.createToken(currentUser);
}

if (token != null) {
userData.setToken(token);

// 获取token用户信息
// Claims userDataFromToken = JWTUtils.parseToken(token, currentUser);
}
}

} else {
message = "用户已经存在";
}

response.setData(userData);
response.setMsg(message);
return response;
}

/**
* 登录
*
* @param userName 用户名
* @param password 密码
* @return {}
*/
@PostMapping("/login")
@PassToken
public Response login(@RequestParam String userName, @RequestParam String password, Response response) {

UserQuery query = new UserQuery();
query.setUserName(userName);
query.setPassword(password);

// 验证用户和密码
try {
// 判断用户是否已经存在
User existUser = this.userService.findUserByName(query);

// 生成token
String token = null;

// 当前用户
User currentUser = new User();
if (existUser != null) {
currentUser.setUserId(existUser.getUserId());
currentUser.setUserName(existUser.getUserName());
currentUser.setPassword(password);
token = JWTUtils.createToken(currentUser);
if (token != null) {
existUser.setToken(token);
}
response.setMsg("success");
response.setData(existUser);
} else {
// 登录失败
response.setMsg("登录失败,请检查用户名和密码");
response.setData(null);
}

} catch (Exception e) {
response.setMsg("login failed");
response.setData(null);
e.printStackTrace();
}
return response;
}

/**
* 获取个人信息
*
* @return {}
*/
@GetMapping("/info")
public Response getUserInfo(Response response) {
// 获取token
String token = BaseApplicationController.getToken();
User userData2 = BaseApplicationController.getCurrentUser();
Map<String, Object> headerData = BaseApplicationController.getHeader();
if (token != null && !token.equals("null")) {
User userData = new User();
DecodedJWT claims = JWT.decode(token);
userData.setUserName(claims.getClaim("userName").asString());
userData.setUserId(claims.getClaim("userId").asLong());
response.setData(userData);
response.setMsg("success");
} else {
response.setMsg("token不存在");
}
return response;
}

}

我们新写了一个接口,获取用户信息,如上,其余代码不再赘述;

成功获取用户信息

image-20191207154357109

删除token

image-20191207154505224

image-20191207154542523

Token 故意改错

image-20191207154655011

image-20191207154738162

image-20191207154806418

到此,验证过程完美成功;

14、总结

以上过程,只是一个简单服务的搭建,真实的服务还需要更多配置,比如XSS配置防止XSS攻击等。好了,本篇文章到此为止,如果有哪里描述得不清楚,请多多包涵。

Spring boot 生成动态验证码并前后端校验

Spring boot 生成动态验证码并前后端校验

最近需要生成一个动态的验证码,在登录页面使用,并在前后端进行校验;

image-20190728183409195

后端生成动态二维码,存储在 session 里面;

前端调取接口,展示在登录页面;

前端登录时候,把验证码传给后端,后端和 session 里面的值进行对比。

1 生成动态验证码图片

新建一个 classValidateCode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package hello;

import org.apache.commons.io.FileUtils;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

// 生成随机验证码
public class ValidateCode {

private static Random random = new Random();
private int width = 160;// 宽
private int height = 40;// 高
private int lineSize = 30;// 干扰线数量
private int stringNum = 4;//随机产生字符的个数

private String randomString = "0123456789abcdefghijklmnopqrstuvwxyz";

private final String sessionKey = "RANDOMKEY";


/*
* 获取字体
*/
private Font getFont() {
return new Font("Times New Roman", Font.ROMAN_BASELINE, 40);
}

/*
* 获取颜色
*/
private static Color getRandomColor(int fc, int bc) {

fc = Math.min(fc, 255);
bc = Math.min(bc, 255);

int r = fc + random.nextInt(bc - fc - 16);
int g = fc + random.nextInt(bc - fc - 14);
int b = fc + random.nextInt(bc - fc - 12);

return new Color(r, g, b);
}

/*
* 绘制干扰线
*/
private void drawLine(Graphics g) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(20);
int yl = random.nextInt(10);
g.drawLine(x, y, x + xl, y + yl);
}

/*
* 获取随机字符
*/
private String getRandomString(int num) {
num = num > 0 ? num : randomString.length();
return String.valueOf(randomString.charAt(random.nextInt(num)));
}

/*
* 绘制字符串
*/
private String drawString(Graphics g, String randomStr, int i) {
g.setFont(getFont());
g.setColor(getRandomColor(108, 190));
System.out.println(random.nextInt(randomString.length()));
String rand = getRandomString(random.nextInt(randomString.length()));
randomStr += rand;
g.translate(random.nextInt(3), random.nextInt(6));
g.drawString(rand, 40 * i + 10, 25);
return randomStr;
}

/*
* 生成随机图片
*/
public void getRandomCodeImage(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();
g.fillRect(0, 0, width, height);
g.setColor(getRandomColor(105, 189));
g.setFont(getFont());

// 绘制干扰线
for (int i = 0; i < lineSize; i++) {
drawLine(g);
}

// 绘制随机字符
String random_string = "";
for (int i = 0; i < stringNum; i++) {
random_string = drawString(g, random_string, i);
}

System.out.println(random_string);

g.dispose();

session.removeAttribute(sessionKey);
session.setAttribute(sessionKey, random_string);

String base64String = "";
try {
// 直接返回图片
ImageIO.write(image, "PNG", response.getOutputStream());

} catch (Exception e) {
e.printStackTrace();
}
}

}

接下来写个 Controller , 提供个接口给前端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package hello;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/v1/user")
public class ValidateCodeController {


// 生成验证码图片
@RequestMapping("/getCaptchaImage")
@ResponseBody
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) {

try {

response.setContentType("image/png");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expire", "0");
response.setHeader("Pragma", "no-cache");

ValidateCode validateCode = new ValidateCode();

// 直接返回图片
validateCode.getRandomCodeImage(request, response);

} catch (Exception e) {
System.out.println(e);
}

}

}

2 前端调取接口

image-20190728191505258

结果如图:

image-20190728192207836

image-20190728192227358

3 返回 base64 字符串

有时候我们不能直接返回图片,需要返回一个 json 的数据比如:

image-20190728192558243

这时候我们就需要把 image 转化为 base64

具体代码如下:

在之前的 ValidateCode 类中添加一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* 生成随机图片,返回 base64 字符串
*/
public String getRandomCodeBase64(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
// BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();
g.fillRect(0, 0, width, height);
g.setColor(getRandomColor(105, 189));
g.setFont(getFont());

// 绘制干扰线
for (int i = 0; i < lineSize; i++) {
drawLine(g);
}

// 绘制随机字符
String random_string = "";
for (int i = 0; i < stringNum; i++) {
random_string = drawString(g, random_string, i);
}

System.out.println(random_string);

g.dispose();

session.removeAttribute(sessionKey);
session.setAttribute(sessionKey, random_string);

String base64String = "";
try {
// 直接返回图片
// ImageIO.write(image, "PNG", response.getOutputStream());
//返回 base64
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ImageIO.write(image, "PNG", bos);

byte[] bytes = bos.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
base64String = encoder.encodeToString(bytes);

} catch (Exception e) {
e.printStackTrace();
}

return base64String;
}

在 Controller 添加另外一个路由接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 生成验证码,返回的是 base64
@RequestMapping("/getCaptchaBase64")
@ResponseBody
public Object getCaptchaBase64(HttpServletRequest request, HttpServletResponse response) {

Map result = new HashMap();
Response response1 = new Response();

try {

response.setContentType("image/png");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expire", "0");
response.setHeader("Pragma", "no-cache");

ValidateCode validateCode = new ValidateCode();

// 直接返回图片
// validateCode.getRandomCode(request, response);

// 返回base64
String base64String = validateCode.getRandomCodeBase64(request, response);
result.put("url", "data:image/png;base64," + base64String);
result.put("message", "created successfull");
System.out.println("test=" + result.get("url"));
response1.setData(0, result);

} catch (Exception e) {
System.out.println(e);
}

return response1.getResult();
}

调用结果:

image-20190728192558243

image-20190728193013770

在前端页面中,只要把 URL 放到 imageURL 中,即可显示,这里不再演示。

3 验证验证码

image-20190728195121612

image-20190728195058507

RESETful API 设计规范

RESETful API 设计规范

文章参考了目前比较常见的 RESETful API 设计,文档不一定十分完善,如果遗漏或者不准确,请包涵和提出意见。

关于「能愿动词」的使用

为了避免歧义,文档大量使用了「能愿动词」,对应的解释如下:

  • 必须 (MUST):绝对,严格遵循,请照做,无条件遵守;
  • 一定不可 (MUST NOT):禁令,严令禁止;
  • 应该 (SHOULD) :强烈建议这样做,但是不强求;
  • 不该 (SHOULD NOT):强烈不建议这样做,但是不强求;
  • 可以 (MAY)可选 (OPTIONAL) :选择性高一点,在这个文档内,此词语使用较少;

参考 RFC 文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Status of this Memo

This document specifies an Internet Best Current Practices for the
Internet Community, and requests discussion and suggestions for
improvements. Distribution of this memo is unlimited.

Abstract

In many standards track documents several words are used to signify
the requirements in the specification. These words are often
capitalized. This document defines these words as they should be
interpreted in IETF documents. Authors who follow these guidelines
should incorporate this phrase near the beginning of their document:

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
RFC 2119.

Note that the force of these words is modified by the requirement
level of the document in which they are used.

1. MUST This word, or the terms "REQUIRED" or "SHALL", mean that the
definition is an absolute requirement of the specification.

2. MUST NOT This phrase, or the phrase "SHALL NOT", mean that the
definition is an absolute prohibition of the specification.

3. SHOULD This word, or the adjective "RECOMMENDED", mean that there
may exist valid reasons in particular circumstances to ignore a
particular item, but the full implications must be understood and
carefully weighed before choosing a different course.

4. SHOULD NOT This phrase, or the phrase "NOT RECOMMENDED" mean that
there may exist valid reasons in particular circumstances when the
particular behavior is acceptable or even useful, but the full
implications should be understood and the case carefully weighed
before implementing any behavior described with this label.

Protocol

客户端在通过 API 与后端服务通信的过程中,应该 使用 HTTPS 协议。

API Root URL

API 的根入口点应尽可能保持足够简单,这里有两个常见的 URL 根例子:

  • api.example.com/*
  • example.com/api/*

如果你的应用很庞大或者你预计它将会变的很庞大,那 应该API 放到子域下(api.example.com)。这种做法可以保持某些规模化上的灵活性。

Versioning

所有的 API 必须保持向后兼容,你 必须 在引入新版本 API 的同时确保旧版本 API 仍然可用。所以 应该 为其提供版本支持。

目前比较常见的两种版本号形式:

在 URL 中嵌入版本编号

1
api.example.com/v1/*

这种做法是版本号直观、易于调试;另一种做法是,将版本号放在 HTTP Header 头中:

通过媒体类型来指定版本信息

1
Accept: application/vnd.example.com.v1+json

其中 vnd 表示 Standards Tree 标准树类型,有三个不同的树: xprsvnd。你使用的标准树需要取决于你开发的项目

  • 未注册的树(x)主要表示本地和私有环境
  • 私有树(prs)主要表示没有商业发布的项目
  • 供应商树(vnd)主要表示公开发布的项目

后面几个参数依次为应用名称(一般为应用域名)、版本号、期望的返回格式。

至于具体把版本号放在什么地方,这个问题一直存在很大的争议,但由于我们大多数时间都在使用 Laravel 开发,应该 使用 dingo/api 来快速构建应用,它采用第二种方式来管理 API 版本,并且已集成了标准的 HTTP Response

Endpoints

端点就是指向特定资源或资源集合的 URL。在端点的设计中,你 必须 遵守下列约定:

  • URL 的命名 必须 全部小写
  • URL 中资源(resource)的命名 必须 是名词,并且 必须 是复数形式
  • 必须 优先使用 Restful 类型的 URL
  • URL 必须 是易读的
  • URL 一定不可 暴露服务器架构

至于 URL 是否必须使用连字符(-) 或下划线(_),不做硬性规定,但 必须 根据团队情况统一一种风格。

来看一个反例

再来看一个正列

HTTP 动词

对于资源的具体操作类型,由 HTTP 动词表示。常用的 HTTP 动词有下面五个(括号里是对应的 SQL 命令)。

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

其中

1 删除资源 必须DELETE 方法 2 创建新的资源 必须 使用 POST 方法 3 更新资源 应该 使用 PUT 方法 4 获取资源信息 必须 使用 GET 方法

针对每一个端点来说,下面列出所有可行的 HTTP 动词和端点的组合

请求方法 URL 描述
GET /zoos/list 列出所有的动物园(ID和名称,不要太详细)
POST /zoos/update 新增一个新的动物园
GET /zoos/detail?id=1 获取指定动物园详情
PUT /zoos/{zoo} 更新指定动物园(整个对象)
PATCH /zoos/{zoo} 更新动物园(部分对象)
DELETE /zoos/{zoo} 删除指定动物园
GET /zoos/{zoo}/animals 检索指定动物园下的动物列表(ID和名称,不要太详细)
GET /animals 列出所有动物(ID和名称)。
POST /animals 新增新的动物
GET /animals/detail 获取指定的动物详情
PUT /animals/{animal} 更新指定的动物(整个对象)
PATCH /animals/{animal} 更新指定的动物(部分对象)
GET /animal_types 获取所有动物类型(ID和名称,不要太详细)
GET /animal_types/{type} 获取指定的动物类型详情
GET /employees 检索整个雇员列表
GET /employees/{employee} 检索指定特定的员工
GET /zoos/employees/list 检索在这个动物园工作的雇员的名单(身份证和姓名)
POST /employees 新增指定新员工
POST /zoos/{zoo}/employees 在特定的动物园雇佣一名员工
DELETE /zoos/{zoo}/employees/{employee} 从某个动物园解雇一名员工

超出 Restful 端点的,应该 模仿上表的方式来定义端点。

Filtering

如果记录数量很多,服务器不可能都将它们返回给用户。API 应该 提供参数,过滤返回结果。下面是一些常见的参数。

  • ?limit=10:指定返回记录的数量
  • ?offset=10:指定返回记录的开始位置。
  • ?page=2&rows0:指定第几页,以及每页的记录数。
  • ?order_by=me&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
  • ?animal_type_id=1:指定筛选条件

所有 URL 参数 必须 是全小写,必须 使用下划线类型的参数形式。

分页参数 必须 固定为 pagerows

经常使用的、复杂的查询 应该 标签化,降低维护成本。如

1
2
3
4
GET /trades?status=closed&order_by=name&order=asc

# 可为其定制快捷方式
GET /trades/recently_closed

Authentication

应该 使用 OAuth2.0 的方式为 API 调用者提供登录认证。必须 先通过登录接口获取 Access Token 后再通过该 token 调用需要身份认证的 API

Oauth 的端点设计示列

  • RFC 6749 /token
  • Twitter /oauth2/token
  • Fackbook /oauth/access_token
  • Google /o/oauth2/token
  • Github /login/oauth/access_token
  • Instagram /oauth/authorize

客户端在获得 access token 的同时 必须 在响应中包含一个名为 expires_in 的数据,它表示当前获得的 token 会在多少 后失效。

1
2
3
4
5
{
"access_token": "token....",
"token_type": "Bearer",
"expires_in": 3600
}

客户端在请求需要认证的 API 时,必须 在请求头 Authorization 中带上 access_token

1
Authorization: Bearer token...

当超过指定的秒数后,access token 就会过期,再次用过期/或无效的 token 访问时,服务端 应该 返回 invalid_token 的错误或 401 错误码。

1
2
3
4
5
6
7
8
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
"error": "invalid_token"
}

开发中,应该 使用 JWT 来为管理你的 Token,并且 一定不可api 中间件中开启请求 session

Response

所有的 API 响应,必须 遵守 HTTP 设计规范,必须 选择合适的 HTTP 状态码。一定不可 所有接口都返回状态码为 200HTTP 响应,如:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
"code": 0,
"msg": "success",
"data": {
"username": "username"
}
}

1
2
3
4
5
6
7
8
HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
"code": -1,
"msg": "该活动不存在",
}

下表列举了常见的 HTTP 状态码

状态码 描述
1xx 代表请求已被接受,需要继续处理
2xx 请求已成功,请求所希望的响应头或数据体将随此响应返回
3xx 重定向
4xx 客户端原因引起的错误
5xx 服务端原因引起的错误

只有来自客户端的请求被正确的处理后才能返回 2xx 的响应,所以当 API 返回 2xx 类型的状态码时,前端 必须 认定该请求已处理成功

必须强调的是,所有 API 一定不可 返回 1xx 类型的状态码。当 API 发生错误时,必须 返回出错时的详细信息。目前常见返回错误信息的方法有两种:

1、将错误详细放入 HTTP 响应首部;

1
2
3
X-MYNAME-ERROR-CODE: 4001
X-MYNAME-ERROR-MESSAGE: Bad authentication token
X-MYNAME-ERROR-INFO: http://docs.example.com/api/v1/authentication

2、直接放入响应实体中;

1
2
3
4
5
6
7
8
9
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:02:59 GMT
Connection: keep-alive

{"error_code":40100,"message":"Unauthorized"}

考虑到易读性和客户端的易处理性,我们 必须 把错误信息直接放到响应实体中,并且错误格式 应该 满足如下格式:

1
2
3
4
{
"message": "您查找的资源不存在",
"error_code": 404001
}

其中错误码(error_code必须HTTP 状态码对应,也方便错误码归类,如:

1
2
3
4
5
6
7
8
9
HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:15:52 GMT
Connection: keep-alive

{"error_code":429001,"message":"你操作太频繁了"}

应该 在返回的错误信息中,同时包含面向开发者和面向用户的提示信息,前者可方便开发人员调试,后者可直接展示给终端用户查看如:

1
2
3
4
5
6
7
8
{
"message": "直接展示给终端用户的错误信息",
"error_code": "业务错误码",
"error": "供开发者查看的错误信息",
"debug": [
"错误堆栈,必须开启 debug 才存在"
]
}

下面详细列举了各种情况 API 的返回说明。

200 ok

200 状态码是最常见的 HTTP 状态码,在所有 成功GET 请求中,必须 返回此状态码。HTTP 响应实体部分 必须 直接就是数据,不要做多余的包装。

错误示例:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 200 ok
Content-Type: application/json
Server: example.com

{
"user": {
"id":1,
"nickname":"fwest",
"username": "example"
}
}

正确示例:

1、获取单个资源详情

1
2
3
4
5
{
"id": 1,
"username": "godruoyi",
"age": 88,
}

2、获取资源集合

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"id": 1,
"username": "godruoyi",
"age": 88,
},
{
"id": 2,
"username": "foo",
"age": 88,
}
]

3、额外的媒体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"data": [
{
"id": 1,
"avatar": "https://lorempixel.com/640/480/?32556",
"nickname": "fwest",
"last_logined_time": "2018-05-29 04:56:43",
"has_registed": true
},
{
"id": 2,
"avatar": "https://lorempixel.com/640/480/?86144",
"nickname": "zschowalter",
"last_logined_time": "2018-06-16 15:18:34",
"has_registed": true
}
],
"meta": {
"pagination": {
"total": 101,
"count": 2,
"per_page": 2,
"current_page": 1,
"total_pages": 51,
"links": {
"next": "http://api.example.com?page=2"
}
}
}
}

其中,分页和其他额外的媒体信息,必须放到 meta 字段中。

201 Created

当服务器创建数据成功时,应该 返回此状态码。常见的应用场景是使用 POST 提交用户信息,如:

  • 添加了新用户
  • 上传了图片
  • 创建了新活动

等,都可以返回 201 状态码。需要注意的是,你可以选择在用户创建成功后返回新用户的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:13:40 GMT
Connection: keep-alive

{
"id": 1,
"avatar": "https:\/\/lorempixel.com\/640\/480\/?32556",
"nickname": "fwest",
"last_logined_time": "2018-05-29 04:56:43",
"created_at": "2018-06-16 17:55:55",
"updated_at": "2018-06-16 17:55:55"
}

也可以返回一个响应实体为空的 HTTP Response 如:

1
2
3
4
5
6
HTTP/1.1 201 Created
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:12:20 GMT
Connection: keep-alive

这里我们 应该 采用第二种方式,因为大多数情况下,客户端只需要知道该请求操作成功与否,并不需要返回新资源的信息。

202 Accepted

该状态码表示服务器已经接受到了来自客户端的请求,但还未开始处理。常用短信发送、邮件通知、模板消息推送等这类很耗时需要队列支持的场景中;

返回该状态码时,响应实体 必须 为空。

1
2
3
4
5
6
HTTP/1.1 202 Accepted
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:25:15 GMT
Connection: keep-alive

204 No Content

该状态码表示响应实体不包含任何数据,其中:

  • 在使用 DELETE 方法删除资源 成功 时,必须 返回该状态码
  • 使用 PUTPATCH 方法更新数据 成功 时,也 应该 返回此状态码
1
2
3
4
HTTP/1.1 204 No Content
Server: nginx/1.11.9
Date: Sun, 24 Jun 2018 09:29:12 GMT
Connection: keep-alive

3xx 重定向

所有 API 不该 返回 3xx 类型的状态码。因为 3xx 类型的响应格式一般为下列格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 09:41:50 GMT
Location: https://example.com
Connection: keep-alive

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="refresh" content="0;url=https://example.com" />

<title>Redirecting to https://example.com</title>
</head>
<body>
Redirecting to <a href="https://example.com">https://example.com</a>.
</body>
</html>

所有 API 一定不可 返回纯 HTML 结构的响应;若一定要使用重定向功能,可以 返回一个响应实体为空的 3xx 响应,并在响应头中加上 Location 字段:

1
2
3
4
5
6
7
HTTP/1.1 302 Found
Server: nginx/1.11.9
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 24 Jun 2018 09:52:50 GMT
Location: https://godruoyi.com
Connection: keep-alive

400 Bad Request

由于明显的客户端错误(例如,请求语法格式错误、无效的请求、无效的签名等),服务器 应该 放弃该请求。

当服务器无法从其他 4xx 类型的状态码中找出合适的来表示错误类型时,都 必须 返回该状态码。

1
2
3
4
5
6
7
8
9
HTTP/1.1 400 Bad Request
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:22:36 GMT
Connection: keep-alive

{"error_code":40000,"message":"无效的签名"}

401 Unauthorized

该状态码表示当前请求需要身份认证,以下情况都 必须 返回该状态码。

  • 未认证用户访问需要认证的 API
  • access_token 无效/过期

客户端在收到 401 响应后,都 应该 提示用户进行下一步的登录操作。

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 401 Unauthorized
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
WWW-Authenticate: JWTAuth
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:17:02 GMT
Connection: keep-alive

{"message":"Token Signature could not be verified.","error_code": "40100"}

403 Forbidden

该状态码可以简单的理解为没有权限访问该请求,服务器收到请求但拒绝提供服务。

如当普通用户请求操作管理员用户时,必须 返回该状态码。

1
2
3
4
5
6
7
8
9
HTTP/1.1 403 Forbidden
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 13:05:34 GMT
Connection: keep-alive

{"error_code":40301,"message":"权限不足"}

404 Not Found

该状态码表示用户请求的资源不存在,如

  • 获取不存在的用户信息 (get /users/9999999)
  • 访问不存在的端点

必须 返回该状态码,若该资源已永久不存在,则 应该 返回 410 响应。

405 Method Not Allowed

当客户端使用的 HTTP 请求方法不被服务器允许时,必须 返回该状态码。

如客户端调用了 POST 方法来访问只支持 GET 方法的 API

该响应 必须 返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 405 Method Not Allowed
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Allow: GET, HEAD
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:30:57 GMT
Connection: keep-alive

{"message":"405 Method Not Allowed","error_code": 40500}

406 Not Acceptable

API 在不支持客户端指定的数据格式时,应该返回此状态码。如支持 JSONXML 输出的 API 被指定返回 YAML 格式的数据时。

Http 协议一般通过请求首部的 Accept 来指定数据格式

408 Request Timeout

客户端请求超时时 必须 返回该状态码,需要注意的时,该状态码表示 客户端请求超时,在涉及第三方 API 调用超时时,一定不可 返回该状态码。

409 Confilct

该状态码表示因为请求存在冲突无法处理。如通过手机号码提供注册功能的 API,当用户提交的手机号已存在时,必须 返回此状态码。

1
2
3
4
5
6
7
8
9
HTTP/1.1 409 Conflict
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:19:04 GMT
Connection: keep-alive

{"error_code":40900,"message":"手机号已存在"}

410 Gone

404 类似,该状态码也表示请求的资源不存在,只是 410 状态码进一步表示所请求的资源已不存在,并且未来也不会存在。在收到 410 状态码后,客户端 应该 停止再次请求该资源。

413 Request Entity Too Large

该状态码表示服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。

此种情况下,服务器可以关闭连接以免客户端继续发送此请求。

如果这个状况是临时的,服务器 应该 返回一个 Retry-After 的响应头,以告知客户端可以在多少时间以后重新尝试。

414 Request-URI Too Long

该状态码表示请求的 URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。

415 Unsupported Media Type

通常表示服务器不支持客户端请求首部 Content-Type 指定的数据格式。如在只接受 JSON 格式的 API 中放入 XML 类型的数据并向服务器发送,都 应该 返回该状态码。

该状态码也可用于如:只允许上传图片格式的文件,但是客户端提交媒体文件非法或不是图片类型,这时 应该 返回该状态码:

1
2
3
4
5
6
7
8
9
HTTP/1.1 415 Unsupported Media Type
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 12:09:40 GMT
Connection: keep-alive

{"error_code":41500,"message":"不允许上传的图片格式"}

429 Too Many Requests

该状态码表示用户请求次数超过允许范围。如 API 设定为 60次/分钟,当用户在一分钟内请求次数超过 60 次后,都 应该 返回该状态码。并且也 应该 在响应首部中加上下列头部:

1
2
3
4
X-RateLimit-Limit: 10 请求速率(由应用设定,其单位一般为小时/分钟等,这里是 10次/5分钟)
X-RateLimit-Remaining: 0 当前剩余的请求数量
X-RateLimit-Reset: 1529839462 重置时间
Retry-After: 120 下一次访问应该等待的时间(秒)

列子

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 429 Too Many Requests
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1529839462
Retry-After: 290
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 11:19:32 GMT
Connection: keep-alive

{"message":"You have exceeded your rate limit.","error_code":42900}

必须 为所有的 API 设置 Rate Limit 支持。

500 Internal Server Error

该状态码 必须 在服务器出错时抛出,对于所有的 500 错误,都 应该 提供完整的错误信息支持,也方便跟踪调试。

503 Service Unavailable

该状态码表示服务器暂时处理不可用状态,当服务器需要维护或第三方 API 请求超时/不可达时,都 应该 返回该状态码,其中若是主动关闭 API 服务,应该在返回的响应首部加上 Retry-After 头部,表示多少秒后可以再次访问。

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 503 Service Unavailable
Server: nginx/1.11.9
Content-Type: application/json
Transfer-Encoding: chunked
Cache-Control: no-cache, private
Date: Sun, 24 Jun 2018 10:56:20 GMT
Retry-After: 60
Connection: keep-alive

{"error_code":50300,"message":"服务维护中"}

其他 HTTP 状态码请参考 HTTP 状态码- 维基百科

nginx开启gzip、gzip_static 加速你的应用

nginx开启gzip、gzip_static 加速你的应用

[TOC]

gzip

gzip属于在线压缩,在资源通过http发送报文给客户端的过程中,进行压缩,可以减少客户端带宽占用,减少文件传输大小。

一般写在server或者location均可;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

server {
listen 6002;
server_name **.234.133.**;

gzip on;
gzip_proxied any;
gzip_types
text/css
text/javascript
text/xml
text/plain
image/x-icon
application/javascript
application/x-javascript
application/json;
}

不开启gzip:

1568810872445

这个时候 298KB 左右;

开启gzip:

1568811020029

1568811044342

开启之后,Content-Encoding: gzip; ETag: W/"~~~";
这里的ETag中的 W\ 就是区分是否是在线写入压缩的标识;

开启gzip298KB 可以减少到 104KB,效率还是不错的;
只是在线gzip比较占用CPU,相比gzip_static还是不太好。

gzip_static

在前端代码打包构建bundle的时候,一般都有根据一定的算法自动压缩代码成gz文件的webpack插件;

当我们不在 nginx 开启 gzip_static的时候,发现生产的gz文件并没有被运行;

gzip_static是会自动执行gz文件的,这样的就避免了通过gzip自动压缩;

比如上面图片的资源:

1568811548745

我们上面讲到通过gzip自动压缩是 104KB,而我们自己压缩的是90KB,所有如果运行了我们自己的gz文件,会更好。

1
gzip_static on;

image-20190918223750703

image-20190918223809667

ETag里面没有 \W, 就是使用的是我们自己的gz文件的,比gzip自动压缩的还减少了10KB

nginx服务器配置React的Browser路由模式,并避免出现404

nginx服务器配置React的Browser路由模式,并避免出现404

[TOC]

前言

React路由模式分为两种:

hashHistory:

比如 http://localhost:8080/#/login

browserHistory

比如 http://localhost:8080/login

browserHistory的好处大于hashHistory, 但是麻烦的地方就是,browserHistory路由模式,需要服务器的配置:

请求 http://localhost:8080/login 上的资源的时候,服务器会默认搜索当前目录下的login文件夹里的资源。但是logIn这个目录其实是不存在的,往往在刷新浏览器的时候,会==404Not fund==;

所以需要利用 nginx 里面的 try_files 去指定一个 fall back资源;

1、React router配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from 'react';
import { Route, Switch, BrowserRouter as Router } from 'react-router-dom';

import App from '@pages/App';

function About(props) {
console.log('about', props);
return <div>page: about</div>;
}

// 路由配置
const routerConfig = [
{
path: '/',
component: App
},
{
path: '/about',
component: About
}
];

function AppRouter() {
return (
// 只有当你的应用部署到服务器的二级目录的时候,才需要设置basename
<Router basename="/react">
<Switch>
{routerConfig.map(n => {
return <Route path={n.path} exact component={n.component}></Route>;
})}
</Switch>
</Router>
);
}

export default AppRouter;

我这里在服务器配置了二级目录 react 作为请求目录,所以这里的 basename 需要配置成 /react。如果你的静态资源已经是在根目录是不需要设置这个的。

启动本地服务:

1568608865270

这个时候从首页点击跳到 about ,就能看到这种路由模式的路径了;

此时如果你刷新了浏览器,将会提示找不到about目录:

1568609309790

此时可以在webpack.config.js里面增加:

1
2
3
devServer {
historyApiFallback: true
}

webpack 里面的 historyApiFallback 使用的是connect-history-api-fallback:

重启本地服务,再次刷新正常。

关于 connect-history-api-fallback

单页应用(SPA)一般只有一个index.html, 导航的跳转都是基于HTML5 History API,当用户在越过index.html 页面直接访问这个地址或是通过浏览器的刷新按钮重新获取时,就会出现404问题;

比如 直接访问/login, /login/online,这时候越过了index.html,去查找这个地址下的文件。由于这是个一个单页应用,最终结果肯定是查找失败,返回一个404错误。

这个中间件就是用来解决这个问题的

只要满足下面四个条件之一,这个中间件就会改变请求的地址,指向到默认的index.html:

1 GET请求

2 接受内容格式为text/html

3 不是一个直接的文件请求,比如路径中不带有 .

4 没有 options.rewrites 里的正则匹配

2、nginx 配置

1
2
3
4
5
6
7
8
9
10
11
location /react {
alias /project/react/;
# browserHistory模式 404问题
try_files $uri $uri/ /react/index.html;
index index.html;
autoindex on;
gzip on;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, POST, PUT, OPTIONS';
add_header Access-Control-Expose-Headers 'Accept-Ranges, Content-Encoding, Content-Length, Content-Range';
}

autoindex on; 开启这个,输入到/react 会直接定向到index.html;

try_files 主要解决的是,如果在一些目录下找不到 index.html, 会最终有一个保底资源的路径就是 /react/index.html;

1
try_files $uri $uri/ /react/index.html;

浏览器输入 http://62.234.133.41:6002/react/about

会先查找 http://62.234.133.41:6002/react/about 是否有文件about.html存在;再查找/about/下是否有文件存在,如果都不存在,启动 /react/index.html;

==try_files 增加 $uri/ 可以解决 try_filesautoindex同时存在的时候,再输入/react不会自动定向到index.html的问题==;

参考文档

  1. https://github.com/bripkens/connect-history-api-fallback

  2. http://nginx.org/en/docs/http/ngx_http_core_module.html#try_files

前端代码规范

前端代码规范

[TOC]

1 前言

随着团队人数的增加,每个人的代码编写喜好不同,代码风格也迥然不同。如果有一个大家的统一的愿意遵守的代码规范,肯定事半功倍,提高效率,避免代码Review重构

目前这只是一个基础的版本,如果有哪些规则不太好,大家可以提出issues 然后一起沟通协商规则。

其中一部分规则参考了 腾讯alloyteam团队的代码规范,如有错误,请指出,将会非常感谢。

坚持好的代码风格规范,从你我做起。

2 命名规范

1) 项目命名

全部采用小写方式, 以下划线分隔。

例:my_project_name

2 )目录命名

参照项目命名规则;

有复数结构时,要采用复数命名法。

例:pages, assets, directives, components, mixins, utils

3)javaScript 文件命名

参照项目命名规则。

例:account_model.js

4)CSS,less文件命名

参照项目命名规则。

例:retina_sprites.less

5)HTML文件命名

参照项目命名规则。

例:error_report.html

6) 如果使用Vue或者React技术栈,组件Component命名

所有组件名字需要首字母大写,然后驼峰格式

例:CalendarList.vue

3 HTML

1) 语法

  • 缩进使用soft tab(4个空格);
  • 嵌套的节点应该缩进;
  • 在属性上,使用双引号,不要使用单引号;
  • 属性名全小写,用中划线做分隔符;
  • 不要在自动闭合标签结尾处使用斜线(HTML5 规范 指出他们是可选的);
  • 不要忽略可选的关闭标签;
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>Page title</title>
</head>
<body>
<img src="images/company_logo.png" alt="Company" />

<h1 class="hello-world">Hello, world!</h1>
</body>
</html>

2) HTML5 doctype

在页面开头使用这个简单地doctype来启用标准模式,使其在每个浏览器中尽可能一致的展现;

虽然doctype不区分大小写,但是按照惯例,doctype大写 (关于html属性,大写还是小写)。

1
2
3
4
<!DOCTYPE html>
<html>
...
</html>

3) lang属性

根据HTML5规范:

应在html标签上加上lang属性。这会给语音工具和翻译工具帮助,告诉它们应当怎么去发音和翻译。

更多关于 lang 属性的说明在这里

在sitepoint上可以查到语言列表

但sitepoint只是给出了语言的大类,例如中文只给出了zh,但是没有区分香港,台湾,大陆。而微软给出了一份更加详细的语言列表,其中细分了zh-cn, zh-hk, zh-tw。

1
2
3
4
<!DOCTYPE html>
<html lang="en-us">
...
</html>

4) 字符编码

通过声明一个明确的字符编码,让浏览器轻松、快速的确定适合网页内容的渲染方式,通常指定为’UTF-8’。

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
...
</html>

5) IE兼容模式

用 `` 标签可以指定页面应该用什么版本的IE来渲染;

如果你想要了解更多,请点击这里

不同doctype在不同浏览器下会触发不同的渲染模式(这篇文章总结的很到位)。

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
</head>
...
</html>

6) 引入CSS, JS

根据HTML5规范, 通常在引入CSS和JS时不需要指明 type,因为 text/csstext/javascript 分别是他们的默认值。

HTML5 规范链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- External CSS -->
<link rel="stylesheet" href="code_guide.css">

<!-- In-document CSS -->
<style>
...
</style>

<!-- External JS -->
<script src="code_guide.js"></script>

<!-- In-document JS -->
<script>
...
</script>

7) 属性顺序

属性应该按照特定的顺序出现以保证易读性;

  • class
  • id
  • name
  • data-*
  • src, for, type, href, value , max-length, max, min, pattern
  • placeholder, title, alt
  • aria-*, role
  • required, readonly, disabled

class是为高可复用组件设计的,所以应处在第一位;

id 具体且应该尽量少使用,所以将它放在第二位。

1
2
3
4
5
<a class="..." id="..." data-modal="toggle" href="#">Example link</a>

<input class="form-control" type="text">

<img src="..." alt="...">

8) boolean属性

boolean属性指不需要声明取值的属性,XHTML需要每个属性声明取值,但是HTML5并不需要;

更多内容可以参考 WhatWG section on boolean attributes

boolean属性的存在表示取值为true,不存在则表示取值为false。

1
2
3
4
5
6
7
<input type="text" disabled>

<input type="checkbox" value="1" checked>

<select>
<option value="1" selected>1</option>
</select>

9) JS生成标签

在JS文件中生成标签让内容变得更难查找,更难编辑,性能更差。应该尽量避免这种情况的出现。

10) 减少标签数量

在编写HTML代码时,需要尽量避免多余的父节点;

很多时候,需要通过迭代和重构来使HTML变得更少。

1
2
3
4
5
6
7
<!-- 不建议这么做 -->
<span class="avatar">
<img src="...">
</span>

<!-- 建议这么做 -->
<img class="avatar" src="...">

11) 实用高于完美

尽量遵循HTML标准和语义,但是不应该以浪费实用性作为代价;

任何时候都要用尽量小的复杂度和尽量少的标签来解决问题。

4 css、less

1) 缩进

使用soft tab(4个空格)

1
2
3
4
5
6
7
8
9
.element {
position: absolute;
top: 10px;
left: 10px;

border-radius: 10px;
width: 50px;
height: 50px;
}

2)分号

每个属性声明末尾都要加分号。

1
2
3
4
5
6
.element {
width: 20px;
height: 20px;

background-color: red;
}

3)空格

以下几种情况不需要空格:

  • 属性名后
  • 多个规则的分隔符’,’前
  • !important ‘!’后
  • 属性值中’(‘后和’)’前
  • 行末不要有多余的空格

以下几种情况需要空格:

  • 属性值前
  • 选择器’>’, ‘+’, ‘~’前后
  • ‘{‘前
  • !important ‘!’前
  • @else 前后
  • 属性值中的’,’后
  • 注释’/‘后和’/‘前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* not good */
.element {
color :red! important;
background-color: rgba(0,0,0,.5);
}

/* good */
.element {
color: red !important;
background-color: rgba(0, 0, 0, .5);
}

/* not good */
.element ,
.dialog{
...
}

/* good */
.element,
.dialog {

}

/* not good */
.element>.dialog{
...
}

/* good */
.element > .dialog{
...
}

/* not good */
.element{
...
}

/* good */
.element {
...
}

/* not good */
@debug: true;

header {
background-color: (yellow)when(@debug = true);
}


/* good */
header {
background-color: (yellow) when (@debug = true);
}

4) 空行

以下几种情况需要空行:

  • 文件最后保留一个空行
  • ‘}’后最好跟一个空行,包括scss中嵌套的规则
  • 属性之间需要适当的空行,具体见属性声明顺序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* not good */
.element {
...
}
.dialog {
color: red;
&:after {
...
}
}

/* good */
.element {
...
}

.dialog {
color: red;

&:after {
...
}
}

5) 注释

注释统一用’/* */‘(scss中也不要用’//‘),具体参照右边的写法;

缩进与下一行代码保持一致;

可位于一个代码行的末尾,与代码间隔一个空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Modal header */
.modal-header {
...
}

/*
* Modal header
*/
.modal-header {
...
}

.modal-header {
/* 50px */
width: 50px;

color: red; /* color red */
}

6) 引号

最外层统一使用双引号;

url的内容要用引号;

属性选择器中的属性值需要引号。

1
2
3
4
5
6
7
8
.element:after {
content: "";
background-image: url("logo.png");
}

li[data-type="single"] {
...
}

7)命名

  • 类名使用小写字母,以中划线分隔
  • id采用驼峰式命名
  • less中的变量、函数以中划线分隔命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* class */
.element-content {
...
}

/* id */
#myDialog {
...
}

/* 变量 */
@color-black: #000;

/* mixins */
.my-mixin() {
color: black;
}

8)属性声明顺序

相关的属性声明按右边的顺序做分组处理,组之间需要有一个空行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.declaration-order {
display: block;
float: right;

position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;

border: 1px solid #e5e5e5;
border-radius: 3px;
width: 100px;
height: 100px;

font: normal 13px "Helvetica Neue", sans-serif;
line-height: 1.5;
text-align: center;

color: #333;
background-color: #f5f5f5;

opacity: 1;
}

书写顺序前后为:

(1)定位属性:position display float left top right bottom overflow clear z-index

(2)自身属性:width height padding border margin background

(3)文字样式:font-family font-size font-style font-weight font-varient color

(4)文本属性text-align vertical-align text-wrap text-transform text-indent text-decoration letter-spacing word-spacing white-space text-overflow

目的:减少浏览器reflow(回流),提升浏览器渲染dom的性能

原理:浏览器的渲染流程为:

1、解析html构建dom树,解析css构建css树:将html解析成树形的数据结构,将css解析成树形的数据结构

2、构建render树:DOM树和CSS树合并之后形成的render树。

3、布局render树:有了render树,浏览器已经知道那些网页中有哪些节点,各个节点的css定义和以及它们的从属关系,从而计算出每个节点在屏幕中的位置。

4、绘制render树:按照计算出来的规则,通过显卡把内容画在屏幕上。

css样式解析到显示至浏览器屏幕上就发生在234步骤,可见浏览器并不是一获取到css样式就立马开始解析而是根据css样式的书写顺序将之按照dom树的结构分布render样式,完成第2步,然后开始遍历每个树结点的css样式进行解析,此时的css样式的遍历顺序完全是按照之前的书写顺序。在解析过程中,一旦浏览器发现某个元素的定位变化影响布局,则需要倒回去重新渲染正如按照这样的书写书序:

1
2
3
4
5
6
7
.demo{
width: 100px;
height: 100px;
background-color: red ;

position: absolute;
}

当浏览器解析到position的时候突然发现该元素是绝对定位元素需要脱离文档流,而之前却是按照普通元素进行解析的,所以不得不重新渲染,解除该元素在文档中所占位置,然而由于该元素的占位发生变化,其他元素也可能会受到它回流的影响而重新排位。最终导致③步骤花费的时间太久而影响到④步骤的显示,影响了用户体验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
// 下面是推荐的属性的顺序
[
[
"display",
"visibility",
"float",
"clear",
"overflow",
"overflow-x",
"overflow-y",
"clip",
"zoom"
],
[
"table-layout",
"empty-cells",
"caption-side",
"border-spacing",
"border-collapse",
"list-style",
"list-style-position",
"list-style-type",
"list-style-image"
],
[
"-webkit-box-orient",
"-webkit-box-direction",
"-webkit-box-decoration-break",
"-webkit-box-pack",
"-webkit-box-align",
"-webkit-box-flex"
],
[
"position",
"top",
"right",
"bottom",
"left",
"z-index"
],
[
"margin",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"-webkit-box-sizing",
"-moz-box-sizing",
"box-sizing",
"border",
"border-width",
"border-style",
"border-color",
"border-top",
"border-top-width",
"border-top-style",
"border-top-color",
"border-right",
"border-right-width",
"border-right-style",
"border-right-color",
"border-bottom",
"border-bottom-width",
"border-bottom-style",
"border-bottom-color",
"border-left",
"border-left-width",
"border-left-style",
"border-left-color",
"-webkit-border-radius",
"-moz-border-radius",
"border-radius",
"-webkit-border-top-left-radius",
"-moz-border-radius-topleft",
"border-top-left-radius",
"-webkit-border-top-right-radius",
"-moz-border-radius-topright",
"border-top-right-radius",
"-webkit-border-bottom-right-radius",
"-moz-border-radius-bottomright",
"border-bottom-right-radius",
"-webkit-border-bottom-left-radius",
"-moz-border-radius-bottomleft",
"border-bottom-left-radius",
"-webkit-border-image",
"-moz-border-image",
"-o-border-image",
"border-image",
"-webkit-border-image-source",
"-moz-border-image-source",
"-o-border-image-source",
"border-image-source",
"-webkit-border-image-slice",
"-moz-border-image-slice",
"-o-border-image-slice",
"border-image-slice",
"-webkit-border-image-width",
"-moz-border-image-width",
"-o-border-image-width",
"border-image-width",
"-webkit-border-image-outset",
"-moz-border-image-outset",
"-o-border-image-outset",
"border-image-outset",
"-webkit-border-image-repeat",
"-moz-border-image-repeat",
"-o-border-image-repeat",
"border-image-repeat",
"padding",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"width",
"min-width",
"max-width",
"height",
"min-height",
"max-height"
],
[
"font",
"font-family",
"font-size",
"font-weight",
"font-style",
"font-variant",
"font-size-adjust",
"font-stretch",
"font-effect",
"font-emphasize",
"font-emphasize-position",
"font-emphasize-style",
"font-smooth",
"line-height",
"text-align",
"-webkit-text-align-last",
"-moz-text-align-last",
"-ms-text-align-last",
"text-align-last",
"vertical-align",
"white-space",
"text-decoration",
"text-emphasis",
"text-emphasis-color",
"text-emphasis-style",
"text-emphasis-position",
"text-indent",
"-ms-text-justify",
"text-justify",
"letter-spacing",
"word-spacing",
"-ms-writing-mode",
"text-outline",
"text-transform",
"text-wrap",
"-ms-text-overflow",
"text-overflow",
"text-overflow-ellipsis",
"text-overflow-mode",
"-ms-word-wrap",
"word-wrap",
"-ms-word-break",
"word-break"
],
[
"color",
"background",
"filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
"background-color",
"background-image",
"background-repeat",
"background-attachment",
"background-position",
"-ms-background-position-x",
"background-position-x",
"-ms-background-position-y",
"background-position-y",
"-webkit-background-clip",
"-moz-background-clip",
"background-clip",
"background-origin",
"-webkit-background-size",
"-moz-background-size",
"-o-background-size",
"background-size"
],
[
"outline",
"outline-width",
"outline-style",
"outline-color",
"outline-offset",
"opacity",
"filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
"-ms-interpolation-mode",
"-webkit-box-shadow",
"-moz-box-shadow",
"box-shadow",
"filter:progid:DXImageTransform.Microsoft.gradient",
"-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
"text-shadow"
],
[
"-webkit-transition",
"-moz-transition",
"-ms-transition",
"-o-transition",
"transition",
"-webkit-transition-delay",
"-moz-transition-delay",
"-ms-transition-delay",
"-o-transition-delay",
"transition-delay",
"-webkit-transition-timing-function",
"-moz-transition-timing-function",
"-ms-transition-timing-function",
"-o-transition-timing-function",
"transition-timing-function",
"-webkit-transition-duration",
"-moz-transition-duration",
"-ms-transition-duration",
"-o-transition-duration",
"transition-duration",
"-webkit-transition-property",
"-moz-transition-property",
"-ms-transition-property",
"-o-transition-property",
"transition-property",
"-webkit-transform",
"-moz-transform",
"-ms-transform",
"-o-transform",
"transform",
"-webkit-transform-origin",
"-moz-transform-origin",
"-ms-transform-origin",
"-o-transform-origin",
"transform-origin",
"-webkit-animation",
"-moz-animation",
"-ms-animation",
"-o-animation",
"animation",
"-webkit-animation-name",
"-moz-animation-name",
"-ms-animation-name",
"-o-animation-name",
"animation-name",
"-webkit-animation-duration",
"-moz-animation-duration",
"-ms-animation-duration",
"-o-animation-duration",
"animation-duration",
"-webkit-animation-play-state",
"-moz-animation-play-state",
"-ms-animation-play-state",
"-o-animation-play-state",
"animation-play-state",
"-webkit-animation-timing-function",
"-moz-animation-timing-function",
"-ms-animation-timing-function",
"-o-animation-timing-function",
"animation-timing-function",
"-webkit-animation-delay",
"-moz-animation-delay",
"-ms-animation-delay",
"-o-animation-delay",
"animation-delay",
"-webkit-animation-iteration-count",
"-moz-animation-iteration-count",
"-ms-animation-iteration-count",
"-o-animation-iteration-count",
"animation-iteration-count",
"-webkit-animation-direction",
"-moz-animation-direction",
"-ms-animation-direction",
"-o-animation-direction",
"animation-direction"
],
[
"content",
"quotes",
"counter-reset",
"counter-increment",
"resize",
"cursor",
"-webkit-user-select",
"-moz-user-select",
"-ms-user-select",
"user-select",
"nav-index",
"nav-up",
"nav-right",
"nav-down",
"nav-left",
"-moz-tab-size",
"-o-tab-size",
"tab-size",
"-webkit-hyphens",
"-moz-hyphens",
"hyphens",
"pointer-events"
]
]

9)颜色

颜色16进制用小写字母;

颜色16进制尽量用简写。

1
2
3
4
5
6
7
8
9
10
11
/* not good */
.element {
color: #ABCDEF;
background-color: #001122;
}

/* good */
.element {
color: #abcdef;
background-color: #012;
}

10)属性简写

属性简写需要你非常清楚属性值的正确顺序,而且在大多数情况下并不需要设置属性简写中包含的所有值,所以建议尽量分开声明会更加清晰;

marginpadding 相反,需要使用简写;

常见的属性简写包括:

  • font
  • background
  • transition
  • animation
1
2
3
4
5
6
7
8
9
10
11
12
/* not good */
.element {
transition: opacity 1s linear 2s;
}

/* good */
.element {
transition-delay: 2s;
transition-timing-function: linear;
transition-duration: 1s;
transition-property: opacity;
}

11)媒体查询

尽量将媒体查询的规则靠近与他们相关的规则,不要将他们一起放到一个独立的样式文件中,或者丢在文档的最底部,这样做只会让大家以后更容易忘记他们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.element {
...
}

.element-avatar{
...
}

@media (min-width: 480px) {
.element {
...
}

.element-avatar {
...
}
}

12)Less相关

每个模块应该有一个单独的less, 然后每个最外层的父类 className 应该写在第一位,所有子Node的样式都写在里面,这样是为了避免命名冲突。比如

1
2
3
4
5
6
7
8
9
//out: false
.parent-name{

.child-name{
...
}

...
}

@import 引入的文件不需要结尾的’.less’

LESS嵌套最多不能超过5层

不允许有空的规则;

元素选择器用小写字母;

去掉小数点前面的0;

去掉数字中不必要的小数点和末尾的0;

属性值’0’后面不要加单位;

同个属性不同前缀的写法需要在垂直方向保持对齐,具体参照右边的写法;

无前缀的标准属性应该写在有前缀的属性后面;

不要在同个规则里出现重复的属性,如果重复的属性是连续的则没关系;

不要在一个文件里出现两个相同的规则;

border: 0; 代替 border: none;

CSS选择器不要超过3层

尽量少用'*'选择器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* not good */
.element {
}

/* not good */
LI {
...
}

/* good */
li {
...
}

/* not good */
.element {
color: rgba(0, 0, 0, 0.5);
}

/* good */
.element {
color: rgba(0, 0, 0, .5);
}

/* not good */
.element {
width: 50.0px;
}

/* good */
.element {
width: 50px;
}

/* not good */
.element {
width: 0px;
}

/* good */
.element {
width: 0;
}

/* not good */
.element {
border-radius: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;

background: linear-gradient(to bottom, #fff 0, #eee 100%);
background: -webkit-linear-gradient(top, #fff 0, #eee 100%);
background: -moz-linear-gradient(top, #fff 0, #eee 100%);
}

/* good */
.element {
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;

background: -webkit-linear-gradient(top, #fff 0, #eee 100%);
background: -moz-linear-gradient(top, #fff 0, #eee 100%);
background: linear-gradient(to bottom, #fff 0, #eee 100%);
}

/* not good */
.element {
color: rgb(0, 0, 0);
width: 50px;
color: rgba(0, 0, 0, .5);
}

/* good */
.element {
color: rgb(0, 0, 0);
color: rgba(0, 0, 0, .5);
}

5 JavaScript

1)缩进

使用soft tab(4个空格)。

1
2
3
4
5
6
7
8
var x = 1,
y = 1;

if (x < y) {
x += 10;
} else {
x += 1;
}

2)分号

以下几种情况后需加分号:

  • 变量声明
  • 表达式
  • return
  • throw
  • break
  • continue
  • do-while
1
2
3
4
5
6
7
8
9
10
/* var declaration */
var x = 1;

/* expression statement */
x++;

/* do-while */
do {
x++;
} while (x < 10);

3)空格

以下几种情况不需要空格:

  • 对象的属性名后
  • 前缀一元运算符后
  • 后缀一元运算符前
  • 函数调用括号前
  • 无论是函数声明还是函数表达式,’(‘前不要空格
  • 数组的’[‘后和’]’前
  • 对象的’{‘后和’}’前
  • 运算符’(‘后和’)’前

以下几种情况需要空格:

  • 二元运算符前后
  • 三元运算符’?:’前后
  • 代码块’{‘前
  • 下列关键字前:else, while, catch, finally
  • 下列关键字后:if, else, for, while, do, switch, case, try,catch, finally, with, return, typeof
  • 单行注释’//‘后(若单行注释和代码同行,则’//‘前也需要),多行注释’*’后
  • 对象的属性值前
  • for循环,分号后留有一个空格,前置条件如果有多个,逗号后留一个空格
  • 无论是函数声明还是函数表达式,’{‘前一定要有空格
  • 函数的参数之间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// not good
var a = {
b :1
};

// good
var a = {
b: 1
};

// not good
++ x;
y ++;
z = x?1:2;

// good
++x;
y++;
z = x ? 1 : 2;

// not good
var a = [ 1, 2 ];

// good
var a = [1, 2];

// not good
var a = ( 1+2 )*3;

// good
var a = (1 + 2) * 3;

// no space before '(', one space before '{', one space between function parameters
var doSomething = function(a, b, c) {
// do something
};

// no space before '('
doSomething(item);

// not good
for(i=0;i<6;i++){
x++;
}

// good
for (i = 0; i < 6; i++) {
x++;
}

4)空行

以下几种情况需要空行:

  • 变量声明后(当变量声明在代码块的最后一行时,则无需空行)
  • 注释前(当注释在代码块的第一行时,则无需空行)
  • 代码块后(在函数调用、数组、对象中则无需空行)
  • 文件最后保留一个空行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// need blank line after variable declaration
var x = 1;

// not need blank line when variable declaration is last expression in the current block
if (x >= 1) {
var y = x + 1;
}

var a = 2;

// need blank line before line comment
a++;

function b() {
// not need blank line when comment is first line of block
return a;
}

// need blank line after blocks
for (var i = 0; i < 2; i++) {
if (true) {
return false;
}

continue;
}

var obj = {
foo: function() {
return 1;
},

bar: function() {
return 2;
}
};

// not need blank line when in argument list, array, object
func(
2,
function() {
a++;
},
3
);

var foo = [
2,
function() {
a++;
},
3
];

var foo = {
a: 2,
b: function() {
a++;
},
c: 3
};

5)换行

换行的地方,行末必须有’,’或者运算符;

以下几种情况不需要换行:

  • 下列关键字后:else, catch, finally
  • 代码块’{‘前

以下几种情况需要换行:

  • 代码块’{‘后和’}’前
  • 变量赋值后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// not good
var a = {
b: 1
, c: 2
};

x = y
? 1 : 2;

// good
var a = {
b: 1,
c: 2
};

x = y ? 1 : 2;
x = y ?
1 : 2;

// no need line break with 'else', 'catch', 'finally'
if (condition) {
...
} else {
...
}

try {
...
} catch (e) {
...
} finally {
...
}

// not good
function test()
{
...
}

// good
function test() {
...
}

// not good
var a, foo = 7, b,
c, bar = 8;

// good
var a,
foo = 7,
b, c, bar = 8;

6)单行注释

双斜线后,必须跟一个空格;

缩进与下一行代码保持一致;

可位于一个代码行的末尾,与代码间隔一个空格。

1
2
3
4
5
6
if (condition) {
// if you made it here, then all security checks passed
allowed();
}

var zhangsan = 'zhangsan'; // one space after code

7)多行注释

最少三行, ‘*’后跟一个空格,具体参照右边的写法;

建议在以下情况下使用:

  • 难于理解的代码段
  • 可能存在错误的代码段
  • 浏览器特殊的HACK代码
  • 业务逻辑强相关的代码
1
2
3
4
/*
* one space after '*'
*/
var x = 1;

8)文档注释

各类标签@param, @method等请参考usejsdocJSDoc Guide

建议在以下情况下使用:

  • 所有常量
  • 所有函数
  • 所有类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @func
* @desc 一个带参数的函数
* @param {string} a - 参数a
* @param {number} b=1 - 参数b默认值为1
* @param {string} c=1 - 参数c有两种支持的取值</br>1—表示x</br>2—表示xx
* @param {object} d - 参数d为一个对象
* @param {string} d.e - 参数d的e属性
* @param {string} d.f - 参数d的f属性
* @param {object[]} g - 参数g为一个对象数组
* @param {string} g.h - 参数g数组中一项的h属性
* @param {string} g.i - 参数g数组中一项的i属性
* @param {string} [j] - 参数j是一个可选参数
*/
function foo(a, b, c, d, g, j) {
...
}

9)引号

最外层统一使用单引号。

1
2
3
4
5
6
// not good
var x = "test";

// good
var y = 'foo',
z = '<div id="test"></div>';

10)变量命名

  • 标准变量采用驼峰式命名(除了对象的属性外,主要是考虑到cgi返回的数据)
  • ‘ID’在变量名中全大写
  • ‘URL’在变量名中全大写
  • ‘Android’在变量名中大写第一个字母
  • ‘iOS’在变量名中小写第一个,大写后两个字母
  • 常量全大写,用下划线连接
  • 构造函数,大写第一个字母
  • jquery对象必须以’$’开头命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var thisIsMyName;

var goodID;

var reportURL;

var AndroidVersion;

var iOSVersion;

var MAX_COUNT = 10;

function Person(name) {
this.name = name;
}

// not good
var body = $('body');

// good
var $body = $('body');

11)变量声明

一个函数作用域中所有的变量声明尽量提到函数首部,用一个var 声明,不允许出现两个连续的var声明。

1
2
3
4
5
6
7
8
9
10
11
function doSomethingWithItems(items) {
// use one var
var value = 10,
result = value + 10,
i,
len;

for (i = 0, len = items.length; i < len; i++) {
result += 10;
}
}

12)函数

无论是函数声明还是函数表达式,’(‘前不要空格,但’{‘前一定要有空格;

函数调用括号前不需要空格;

立即执行函数外必须包一层括号;

不要给inline function命名;

参数之间用’, ‘分隔,注意逗号后有一个空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// no space before '(', but one space before'{'
var doSomething = function(item) {
// do something
};

function doSomething(item) {
// do something
}

// not good
doSomething (item);

// good
doSomething(item);

// requires parentheses around immediately invoked function expressions
(function() {
return 1;
})();

// not good
[1, 2].forEach(function x() {
...
});

// good
[1, 2].forEach(function() {
...
});

// not good
var a = [1, 2, function a() {
...
}];

// good
var a = [1, 2, function() {
...
}];

// use ', ' between function parameters
var doSomething = function(a, b, c) {
// do something
};

13)数组、对象

对象属性名不需要加引号;

对象以缩进的形式书写,不要写在一行;

数组、对象最后不要有逗号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// not good
var a = {
'b': 1
};

var a = {b: 1};

var a = {
b: 1,
c: 2,
};

// good
var a = {
b: 1,
c: 2
};

14)括号

下列关键字后必须有大括号(即使代码块的内容只有一行):if, else,for, while, do, switch, try, catch, finally, with

1
2
3
4
5
6
7
8
// not good
if (condition)
doSomething();

// good
if (condition) {
doSomething();
}

15)null

适用场景:

  • 初始化一个将来可能被赋值为对象的变量
  • 与已经初始化的变量做比较
  • 作为一个参数为对象的函数的调用传参
  • 作为一个返回对象的函数的返回值

不适用场景:

  • 不要用null来判断函数调用时有无传参
  • 不要与未初始化的变量做比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// not good
function test(a, b) {
if (b `= null) {
// not mean b is not supply
...
}
}

var a;

if (a `= null) {
...
}

// good
var a = null;

if (a `= null) {
...
}

16)undefined

永远不要直接使用undefined进行变量判断;

使用typeof和字符串’undefined’对变量进行判断。

1
2
3
4
5
6
7
8
9
// not good
if (person `= undefined) {
...
}

// good
if (typeof person `= 'undefined') {
...
}

17)jshint

用’=', '!‘代替’`’, ‘!=’;

for-in里一定要有hasOwnProperty的判断;

不要在内置对象的原型上添加方法,如Array, Date;

不要在内层作用域的代码里声明了变量,之后却访问到了外层作用域的同名变量;

变量不要先使用后声明;

不要在一句代码中单单使用构造函数,记得将其赋值给某个变量;

不要在同个作用域下声明同名变量;

不要在一些不需要的地方加括号,例:delete(a.b);

不要使用未声明的变量(全局变量需要加到.jshintrc文件的globals属性里面);

不要声明了变量却不使用;

不要在应该做比较的地方做赋值;

debugger不要出现在提交的代码里;

数组中不要存在空元素;

不要在循环内部声明函数;

不要像这样使用构造函数,例:new function () { ... }, new Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// not good
if (a ` 1) {
a++;
}

// good
if (a `= 1) {
a++;
}

// good
for (key in obj) {
if (obj.hasOwnProperty(key)) {
// be sure that obj[key] belongs to the object and was not inherited
console.log(obj[key]);
}
}

// not good
Array.prototype.count = function(value) {
return 4;
};

// not good
var x = 1;

function test() {
if (true) {
var x = 0;
}

x += 1;
}

// not good
function test() {
console.log(x);

var x = 1;
}

// not good
new Person();

// good
var person = new Person();

// not good
delete(obj.attr);

// good
delete obj.attr;

// not good
if (a = 10) {
a++;
}

// not good
var a = [1, , , 2, 3];

// not good
var nums = [];

for (var i = 0; i < 10; i++) {
(function(i) {
nums[i] = function(j) {
return i + j;
};
}(i));
}

// not good
var singleton = new function() {
var privateVar;

this.publicMethod = function() {
privateVar = 1;
};

this.publicMethod2 = function() {
privateVar = 2;
};
};

18)杂项

不要混用tab和space;

不要在一处使用多个tab或space;

换行符统一用’LF’;

对上下文this的引用只能使用’_this’, ‘that’, ‘self’其中一个来命名;

行尾不要有空白字符;

switch的falling through和no default的情况一定要有注释特别说明;

不允许有空的代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// not good
var a = 1;

function Person() {
// not good
var me = this;

// good
var _this = this;

// good
var that = this;

// good
var self = this;
}

// good
switch (condition) {
case 1:
case 2:
...
break;
case 3:
...
// why fall through
case 4
...
break;
// why no default
}

// not good with empty block
if (condition) {

}

Mac下 Navicat Premium 12.1如何安装与激活

Mac下Navicat Premium 12.1如何安装与激活

[TOC]
###1 下载

下载地址 https://www.navicat.com/en/products

mac安装过程省略,一直按照下去最后放到Application即可;

navicat

2 破解

本次的破解使用的是 navicat-keygen

1)下载 Navicat-keygen 项目

1
alex:projects $ git clone https://github.com/DoubleLabyrinth/navicat-keygen

keygen

2) 进入项目

1
alex:projects $ cd navicat-keygen/

3) 切换 mac 分支

1
alex:navicat-keygen $ git checkout mac

4) 编译前准备

1
2
3
4
$ brew install openssl
$ brew install capstone
$ brew install keystone
$ brew install rapidjson

5) 进入到navicat-patcher 并编译

1
2
3
4
alex:navicat-keygen $ make all
//编译之后,会有着两个文件
alex:navicat-keygen $ ls bin
navicat-keygen navicat-patcher

6) 编译好navicat-keygen, navicat-patcher之后,记得备份你的app

nav_bk

甚至备份整个 Contents,都可以。

7) 使用navicat-patcher替换掉公钥:

1
2
Usage:
navicat-patcher <navicat executable file> [RSA-2048 PrivateKey(PEM file)]
  • <navicat executable file>: Navicat可执行文件的路径。

    这个参数必须指定。

  • [RSA-2048 PrivateKey(PEM file)]: RSA-2048私钥文件的路径。

    这个参数是可选的。 如果没有指定,navicat-patcher将会在当前目录下生成一个新的RSA-2048私钥文件RegPrivateKey.pem

我使用最简单的用法,不指定:

1
2
3
4
alex:navicat-keygen $ cd bin
alex:bin $ ls
navicat-keygen navicat-patcher
alex:bin $ ./navicat-patcher /Applications/Navicat\Premium.app/Contents/MacOS/Navicat\ Premium

image-20190727105419621

如上图:这只是样例生成RSA public key一部分。

bin里面生成了RegPrivateKey.pem

image-20190727105758411

仅对 Navicat Premium 版本 < 12.0.24 的说明:

如果你的Navicat版本小于12.0.24,那么navicat-patcher将会终止并且不会修改目标文件。

你必须使用openssl生成RegPrivateKey.pemrpk文件:

1
2
$ openssl genrsa -out RegPrivateKey.pem 2048
$ openssl rsa -in RegPrivateKey.pem -pubout -out rpk

接着用刚生成的rpk文件替换

1
/Applications/Navicat Premium.app/Contents/Resources/rpk

8) 重要的一步:生成一份自签名的代码证书,并总是信任该证书

用codesign对Navicat Premium.app重签名

1
$ codesign -f -s "Your self-signed code-sign certificate name" <path to Navicat Premium.app>

注意:

“Your self-signed code-sign certificate name”是你证书的名字,不是路径。

例如:

1
alex:bin $ codesign -f -s "master" /Applications/Navicat\ Premium.app/

9) 接下来使用navicat-keygen来生成 序列号激活码

1
2
Usage:
navicat-keygen <RSA-2048 PrivateKey(PEM file)>
  • <RSA-2048 PrivateKey(PEM file)>: RSA-2048私钥文件的路径。

    这个参数必须指定。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
alex:bin $ ./navicat-keygen ./RegPrivateKey.pem
Which is your Navicat Premium language?
0. English
1. Simplified Chinese
2. Traditional Chinese
3. Japanese
4. Polish
5. Spanish
6. French
7. German
8. Korean
9. Russian
10. Portuguese

(Input index)> 0
(Input major version number, range: 0 ~ 15, default: 12)> 12

Serial number:
N6VM-YWXC-8ZJU-99VH

Your name:

你可以使用这个 序列号 暂时激活Navicat。

接下来你会被要求输入用户名组织名;请随便填写,但不要太长

1
2
3
4
Your name: alex
Your organization: alex

Input request code (in Base64), input empty line to end:

之后你会被要求填入请求码。注意 不要关闭注册机.

10) 手动激活

断开网络 并打开Navicat。找到注册窗口,填入注册机给你的序列号。然后点击激活按钮

一般来说在线激活肯定会失败,这时候Navicat会询问你是否手动激活,直接选

手动激活窗口你会得到一个请求码,复制它并把它粘贴到keygen里。最后别忘了连按至少两下回车结束输入

image-20190727111438303

11) 成功激活

如果不出意外,你会得到一个看似用Base64编码的激活码。直接复制它,并把它粘贴到Navicat手动激活窗口,最后点激活按钮。如果没什么意外的话应该能成功激活

image-20190727111542953

求人不如求已系列工具:如何PSD文件自动标注切图和真机预览

求人不如求已系列工具:如何PSD文件自动标注切图和真机预览

[TOC]

前言

在很多时候,UI会给一部分切图,当你使用一个新的技术,需要的不再是png而是webp或者SVG时候,沟通变得异常复杂,沟通成本急剧增加。

很多时候在想,是否有一些工具,可以帮助我们解决这个尴尬的问题,也可以减少沟通成本;

当然很多公司专业的UI会通过专业工具Stetch帮你生成一套完成的UI备注文件。

可惜,有些时候,并没有。

所以,经过一些摸索,发现了一些比较不错的工具,来提高我们工作的效率;

废话不多说,开始介绍。

蓝湖工具

如上所有,如果没有UI帮你做或者没时间做UI备注,我们就采取其他的在线模式;

在线工具就是蓝湖

1572584058545

注册账号

注册一个免费账号

下载插件

ps应用商店下载蓝湖插件

1572584243307

1572584340880

Adobe Photoshop 插件下载

1572584396001

1572584534322

ps中点击蓝湖工具,然后登陆

1572584589448

自动备注

当我们只需要获取自动备注的时候,点击上传面板

1572584662525

1572584697681

1572584714320

点击去web端查看按钮,可以跳转到蓝湖网页版

1572584773187

1572584805139

右边的各种备注信息一览无余,很是方便。

切图

当你需要生成切图时候,如下图一步步操作

1572585054240

双击psd源文件,选中ICON图层,点击标记为切图

1572585107165

1572585128333

1572585175075

1572585187356

1572585200521

1572585228126

1572585256584

1572585347250

手机预览psd源文件

当你之前都已经把PSD文件上传到蓝湖后,appstore下载蓝湖APP,之后就可以直接预览psd文件了

1572585888368

Design Mirror

1572586034557

下载地址

首先下载 photoshop 插件, 然后下载 app,然后在ps里面登录注册好的账号,通过wifi或者USB,就可以app里面预览

1572586886728

1572587039851

1572587003083

这个预览和上一个蓝湖预览最大的不同就是这个完全是本地预览,不需要上传,比较方便。

1572587155495

甚至还可以点击上传图片,把手机上的图片同步到photoshop, 这个挺不错的

申请免费域名到搭建整个网站

申请免费域名到搭建整个网站

1 申请免费域名

可以到 freenom 申请免费或者便宜的域名

image-20190728075830521

注册 or 登陆一个账号……

image-20190728080047064

image-20190728080343581

后缀Tk\ml\ga\cf 大多可以免费申请到,如果想要一些付费的 com\cn, 也可以购买。

然后 Get it now, 在购物车里就可以看到:

image-20190728080923570

免费使用最大时间是12months, 如果再久就需要付费了。

2 域名解析

域名解析的话,国内的解析提供商我们一般用 DNSPod,国外的解析服务提供商一般用 CloudFlare,操作都大同小异,接下来以 dynadot 和 DNSPod 为例,讲解一下域名解析。

首先我们打开 DNSPod 的网站:www.dnspod.cn,如果没有账户,注册一个然后进行登陆。

登陆之后,添加我们要解析的域名。如图所示。

image-20190728083830772

然后检查是否要导入它自动扫描的解析记录,如果不是我们添加的,一般不导入

image-20190728084043364

我们选择取消,不导入,然后进入下一步,添加记录。

image-20190728084216418

如上图所示,最基本的话我们需要添加两条记录,一条是 WWW,一条是 @,这两条记录分别添加,记录类型都选择 A,线路类型选择默认即可,记录值填入你的服务器 IP 地址,权重和 MX优先级不用写,TTL 就默认的 600 就行,然后点击保存;

两条记录添加完后,我们再到域名注册商那边修改 NameServer

我们需要返回到 freenom;

在菜单中找到 my domains,然后点击你的域名,找到管理 name servers:

image-20190728085151478

之后我们输入 DNSPodNameServer,也就是:

  • f1g1ns1.dnspod.net
  • f1g1ns2.dnspod.net

修改完之后点击 Save 之类的按钮,保存即可。

至此,我们已经完成了整个解析过程,接下来需要做的就是等待解析生效,这个时间一般需要几分钟到十几个小时,取决于之前设置的 TTL。所以我们一般不建议把 TTL 设置的过大,默认即可。一般来说几分钟就能生效,如果几天还不生效,那多半是本地有了 DNS 缓存,我们可以清理一下缓存即可。

image-20190728085555060

之后的话,域名解析完成了,就可以上传网站文件;

JavaScript 运行环境

JavaScript 运行环境

[TOC]

我们在这里会深入了解浏览器的javaScript的运行机制。

我们将了解 chrome V8引擎如何格式化代码和如何通过 Event Loop 让代码运行在一个线程上,包括 同步异步

1、前言

当我们在浏览器里面访问一个网址,比如 ChromeEdgeFirefox 或者 safari。每一个浏览器都有一个 Javascript Running Environment 。在这个环境里,开发者可以访问构。一个程序的Web API

AJAXDOM树其他API,不是JavaScript的一部分,它们只是浏览器提供的可以在JS 运行环境里面运行的含有方法属性对象

当然,在运行环境中,是一个 js 引擎来格式化代码。每个浏览器都有自己的引擎版本。Chrome 使用 V8 JS 引擎,也就是我们接下来要分析的。

2、V8 JS 引擎

一旦Chrome 接收到web页面的javaScript 代码或者脚本,V8 js引擎就开始格式化代码。刚开始,它会部分的格式化代码来检查语法错误。如果没有找到语法错误,它将从上到下开始解读代码。它的最终目的是将JavaScript代码转化为机器可以读懂的机器码。但是在我们了解它到底对代码做了什么之前,我们必须先了解格式化代码的运行环境。

3、Javascript 运行环境

可以把js运行环境想象是一个大容器。在这个大容器里面还有其他的小容器。当 JS 引擎开始格式化代码时,代码被分块放入不同的容器中。

3.1 、Heap 堆

环境里面的第一个容器,也是V8 js 引擎的一部分,被称为“内存堆”。

JS 引擎在代码里碰到变量函数声明,它把它们储存在里。

3.2、Stack 栈

环境里面的第二个容器是 call stack 调用栈。它也是 JS V8 引擎的一部分。当JS引擎碰到一个调用指令,比如一个 function call

三分钟教会你自制IconFont字体图标库

三分钟教会你自制IconFont字体图标库

app开发的时候,我们大多会用到字体icon,下面我们就讲解一下,如何自定义生成 iconFont

一般情况,我们优先选择在 Iconfont-阿里巴巴矢量图标库 下载我们需要的矢量icon的svg格式, 如果这里没有你需要的icon,也可以自己切图把png格式的图片转化为svg;

1571900747688

svg 导入并生成 font

网址:https://icomoon.io/app/#/select

1571898904170

导入多个svg图片

1571898834932

点击右下角

1571898949883

记好每个icon的code

1571899023086

然后点击 download

如图:

1571899147150

然后我们写 less(或者css),定义这些icon的样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@/images: '..//images/fonts';
@version: '?v=1.1.0';

@font-face {
font-family: 'icomoon';
/* 自行安装第三方字体图标库 */
src: url('@{/images}/icomoon.eot@{version}');
src: url('@{/images}/icomoon.woff@{version}') format('woff'),
url('@{/images}/icomoon.ttf@{version}') format('truetype'),
url('@{/images}/icomoon.svg@{version}') format('svg');
font-weight: normal;
font-style: normal;
}

/* 根据第三方字体图标库编写 */
/* 举例:fa 就是 prefixClass 的值,下面的的图标 css class 命名都要用 fa- 开头 */
.fa {
display: inline-block;
/* 以下的 font 与上面 @font-face 的 font-family 要一致*/
font: normal normal normal 14px/1 icomoon;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.fa-home:before {
content: "\e900";
}

.fa-doc:before {
content: "\e903";
}

.fa-knowledge:before {
content: "\e906";
}

.fa-discuss:before {
content: "\e901";
}

.fa-question:before {
content: "\e902";
}

然后就可以把这个less引入到我们的入口文件里面;

调用举例:

1
2
3
<span className="fa fa-home"></span>
/**更改大小和颜色**/
<span className="fa fa-home" style="font-size:30;color: blue"></span>

实际效果:

1571900156424

Vue项目改造抛弃vue-cli配置,重撸webpack4和babel配置

Vue项目改造抛弃vue-cli配置,重撸webpack4和babel配置

抛弃自带的vue.config.js的配置模式,手动使用webpack进行构建:

[TOC]

webpack4

webpack4 相关loaders和plugins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
>>>>>>>>>>>>>>>>>>>>>>>相关loader<<<<<<<<<<<<<<<<<<<<<<

vue-loader
编译.vue文件

babel-loader
编译成ES5

file-loader
解决文件中 import/require() 的资源,转化为URL,再输出到指定文件夹内

url-loader
把图片转化成 base64 URLs, 可以根据limit大小自由控制

css-loader
css-loader 解释 @import and url() 比如 import/require() 然后解析他们

file-loader
file-loader 解析文件中的 import/require() 成一个URL 然后输出到输出文件中

vue-style-loader
style-loader
dev环境,把css注入到DOM

>>>>>>>>>>>>>>>>>>>>>>>相关plugin<<<<<<<<<<<<<<<<<<<<<<

mini-css-extract-plugin
提取css到单独的文件

clean-webpack-plugin
清理构建的资源

webpack-build-notifier
构建完成桌面提醒

html-webpack-plugin
生成html入口模板

optimize-css-/images-webpack-plugin
css去重压缩

purgecss-webpack-plugin
去除css中未使用的代码

webpack-dev-server
本地server

webpack-spritesmith
自动整合成雪碧图

compression-webpack-plugin
@gfx/zopfli
压缩代码,根据算法生成gzip

webpack-bundle-analyzer
生成bundle后分析报告,方便优化

progress-bar-webpack-plugin
显示构建进度条

安装依赖

1
yarn add -D webpack webpack-cli webpack-dev-server vue-loader babel-loader file-loader css-loader style-loader url-loader mini-css-extract-plugin  clean-webpack-plugin webpack-build-notifier html-webpack-plugin optimize-css-/images-webpack-plugin purgecss-webpack-plugin webpack-spritesmith compression-webpack-plugin webpack-bundle-analyzer

babel 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@babel/core
@babel/preset-env
@babel/cli
@babel/polyfill
// runtime
@babel/runtime
@babel/plugin-transform-runtime
// 动态插入
@babel/plugin-syntax-dynamic-import
// 支持 ...spread
@babel/plugin-syntax-object-rest-spread
// commonjs
@babel/plugin-transform-modules-commonjs
// 支持 vue jsx语法
@babel/plugin-syntax-jsx
babel-plugin-transform-vue-jsx

//支持 element-ui 按需加载
babel-plugin-component

//支持 lodash 按需加载
babel-plugin-lodash

// 移除 console.log
babel-plugin-transform-remove-console

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const removeConsolePlugin = [];

if (process.env.NODE_ENV === 'production') {
removeConsolePlugin.push('transform-remove-console');
}

module.exports = {
// presets: ['@vue/app'],
presets: [
[
'@babel/preset-env',
{
// transform any
loose: true
}
]
],
// 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的
plugins: [
// import
'@babel/plugin-syntax-dynamic-import',
// transform
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-modules-commonjs',
// vue jsx语法
'@babel/plugin-syntax-jsx',
'transform-vue-jsx',
'lodash',
// spread ...
// '@babel/plugin-syntax-object-rest-spread',
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk'
}
],
...removeConsolePlugin
]
};

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/**
* webpack 4 config
* @author master2011zhao@gmail.com
* @Date 20190910
*/
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// webpack4 使用 mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// extract 被废弃
// const ExtractTextPlugin = require('extract-text-webpack-plugin');
// clean project
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 压缩css
const OptimizeCss/imagesPlugin = require('optimize-css-/images-webpack-plugin');
// notifier
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
// 压缩代码
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const zopfli = require('@gfx/zopfli');
// vue loader
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// 图片整合成雪碧图
const SpritesmithPlugin = require('webpack-spritesmith');
// bundle分析
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

// path function
const resolve = src => {
return path.resolve(__dirname, src);
};

// nginx 配置二级目录 base url
let serverBaseUrl = '';

// customerTemplate
const templateFunction = function(data) {
// console.log('---', data)
const shared = `.sprite_ico { background-image: url(I);display:inline-block;background-size: Wpx Hpx;}`
.replace('I', data.sprites[0].image)
.replace('W', data.spritesheet.width)
.replace('H', data.spritesheet.height);

const perSprite = data.sprites
.map(function(sprite) {
return `.sprite_ico_N { width: Wpx; height: Hpx; background-position: Xpx Ypx;}`
.replace('N', sprite.name)
.replace('W', sprite.width)
.replace('H', sprite.height)
.replace('X', sprite.offset_x)
.replace('Y', sprite.offset_y);
})
.join('\n');

return '//out:false' + '\n' + shared + '\n' + perSprite;
};

module.exports = (env, argv) => {
const isProduction = argv.mode === 'production';

console.log('isProduction', isProduction);

// 传递给 babel.config.js
process.env.NODE_ENV = argv.mode;

// console.log(process.env.NODE_ENV);

let plugins = [new VueLoaderPlugin()];

// 生成模板
let HtmlTemplates = [];

// 生产环境
if (isProduction) {
// 清理项目, 清理不干净,需要使用 rm.sh
plugins.push(
new CleanWebpackPlugin({
dry: false,
verbose: true
})
);

// 雪碧图
plugins.push(
new SpritesmithPlugin({
src: {
//下面的路径,根据自己的实际路径配置
cwd: path.resolve(__dirname, 'src//images/icons'),
glob: '*.png'
},
// 输出雪碧图文件及样式文件
target: {
//下面的路径,根据自己的实际路径配置
image: path.resolve(__dirname, 'src//images/sprite.png'),
css: [
[
path.resolve(__dirname, 'src/less/sprite.less'),
{
format: 'function_based_template'
}
]
]
// css: path.resolve(__dirname, './src/less/sprite.less')
},
// 自定义模板
customTemplates: {
function_based_template: templateFunction
},
// 样式文件中调用雪碧图地址写法
apiOptions: {
// 这个路径根据自己页面配置
cssImageRef: './images/sprite.png'
},
spritesmithOptions: {
// algorithm: 'top-down'
padding: 5
}
})
);

// 构建完成提醒
plugins.push(
new WebpackBuildNotifierPlugin({
title: 'project build',
suppressSuccess: true,
suppressWarning: true,
messageFormatter: function() {
return 'build completely';
}
})
);

// 分离css
// plugins.push(new ExtractTextPlugin('css/[name].[hash:8].css'));
plugins.push(
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// all options are optional
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name].[hash:8].css',
publicPath: './' + serverBaseUrl,
ignoreOrder: false // Enable to remove warnings about conflicting order
})
);

// 去除重复的 less, 比如 common
plugins.push(
new OptimizeCss/imagesPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: {
removeAll: true
}
}
]
},
canPrint: true
})
);

//再次压缩代码
plugins.push(
new CompressionWebpackPlugin({
deleteOriginal/images: false,
test: /\.(js|css|html|woff|ttf|png|jpg|jpeg)$/,
compressionOptions: {
numiterations: 15
},
threshold: 10240,
minRatio: 0.8,
algorithm(input, compressionOptions, callback) {
return zopfli.gzip(input, compressionOptions, callback);
}
})
);

// 公共提取的chunk
const commonChunks = ['chunk-vendors', 'runtime', 'chunk-commons', 'css-commons'];

const minify = {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
};

// 生成模板
HtmlTemplates = [
new HtmlWebpackPlugin({
title: 'Index',
template: resolve('public/index.html'),
filename: 'index.html',
hash: true,
minify,
chunks: [...commonChunks, 'index'],
favicon: resolve('public/favicon.ico')
})
];

// 分析生成的包
plugins.push(
new BundleAnalyzerPlugin({
// 生成report.html
analyzerMode: 'static'
})
);
} else {
// 生成模板
HtmlTemplates = [
new HtmlWebpackPlugin({
title: 'Index',
template: resolve('./public/index.html'),
filename: 'index.html',
favicon: resolve('public/favicon.ico'),
chunks: ['index', 'runtime']
})
];
}

return {
entry: {
index: resolve('src/main.js')
},
output: {
path: resolve('cdn'),
filename: 'js/[name].[hash:8].js',
publicPath: isProduction ? './' + serverBaseUrl : ''
},
// 本地调试
devtool: !isProduction ? 'inline-source-map' : '',
devServer: {
port: 3000,
open: true,
hot: true,
// 配置 browserHistory 路由,防止刷新就 404
historyApiFallback: true,
compress: true,
contentBase: path.resolve(__dirname, ''),
noInfo: false,
overlay: {
warnings: true,
errors: true
},
proxy: {
'/api/v1': {
target: 'http://192.168.1.100:18080',
changeOrigin: true,
router: {
'/shareIndex': 'http://192.168.1.110:18080',
}
},
}
},
resolve: {
// 别名
alias: {
'@': resolve('src'),
'@c': resolve('src/components'),
'@less': resolve('src/less'),
'@util': resolve('src/utils'),
'@/images': resolve('src//images'),
'@pages': resolve('src/pages')
},
// 自动添加后缀
extensions: ['.vue', '.js', '.less']
},
module: {
rules: [
{
test: /\.vue?$/,
use: 'vue-loader'
},
{
test: /\.js?$/,
use: 'babel-loader'
},
{
test: /\.css?$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader',
{
loader: 'css-loader',
options: {}
}
]
},
{
test: /\.less$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : 'vue-style-loader',
'css-loader',
{
loader: 'less-loader',
options: {
javascriptEnabled: true
}
}
]
},
{
test: /\.(png|jpg|svg|gif|ico|woff|ttf)?$/,
use: [
{
loader: 'url-loader',
options: {
// 这里的options选项参数可以定义多大的图片转换为base64
fallback: 'file-loader',
limit: 10 * 1024, // 表示小于10kb的图片转为base64,大于10kb的是路径
outputPath: 'images', //定义输出的图片文件夹名字
publicPath: '../images', //css中的路径
// name: '[name].[contenthash:8].[ext]'
name: '[sha512:contenthash:base64:8].[ext]'
}
}
]
}
]
},
plugins: [...plugins, ...HtmlTemplates],
optimization: {
splitChunks: {
// 静态资源缓存
// test, priority and reuseExistingChunk can only be configured on cache group level.
cacheGroups: {
// 提取 node_modules 里面依赖的代码
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-vendors',
chunks: 'all',
minSize: 0,
minChunks: 2, //2个共享以及以上都提取
priority: -10 //优先级
},
// 提出每个模块公共的代码
commons: {
name: 'chunk-commons',
test: /\.js$/,
chunks: 'initial',
minChunks: 2, //两个共享以及以上都提取,
minSize: 0,
priority: -20, //优先级
reuseExistingChunk: true
},
css: {
name: 'css-commons',
test: /\.less$/,
minChunks: 2,
minSize: 0,
priority: -30,
chunks: 'initial',
reuseExistingChunk: true
}
}
},
// I pull the Webpack runtime out into its own bundle file so that the
// contentHash of each subsequent bundle will remain the same as long as the
// source code of said bundles remain the same.
runtimeChunk: 'single'
}
};
};

package.json

1
2
3
4
scripts:{
"dev": "webpack-dev-server --mode development",
"build": "webpack --mode production",
}

1569495769879

JavaScript - 并发模式和 Event Loop 事件循环解读

JavaScript - 并发模式和 Event Loop 事件循环解读

[TOC]

1、Runtime concepts 执行相关的概念

Javascript 有一个基于Event Loop 事件循环的并发模型;

下面讲解一个理论模型,讲解现代浏览器javascript 引擎实现机制和讲解一下描述的一些语义词;

可视模型代表:

1.1、stack 栈

函数调用形成了一个栈帧

1
2
3
4
5
6
7
8
9
10
11
function foo(b) {
var a = 10;
return a + b + 11;
}

function bar(x) {
var y = 3;
return foo(x * y);
}

console.log(bar(7)); // 返回 42

stack

简单介绍下函数调用的过程:

当调用bar(7)时,建立了第一个 stack framebar (包含参数7和本地变量);当 bar 调用 foo 时候,建立了第二个 stack framefoo(包含参数 3* 7 和本地变量), 并且放置在 bar上方,也就是栈的顶部了。

foo(21) 执行完毕 返回 42 的时候,foo 这个栈帧会被移除掉,只剩下了 bar(7);然后再执行 bar, 有返回后,整个栈都是空的。

1.2、Heap 堆

对象都被关联在Heap里面,即用于表示一大块非结构化的内存区域。

1.3、Queue 消息队列

一个 Javascript 运行时使用一系列待处理消息的消息队列。每个消息关联一个函数去处理消息。

如果一个消息,比如click事件却没有回调函数,是不会被加入消息队列的

在事件循环的一些时刻,运行时从最先进入队列的消息开始处理队列中的消息。这样做的话,消息从队列中被移除,并作为输入参数调用与之关联的函数。就如上面所说,调用一个函数总是为其创造一个的栈帧。

函数的执行一直会持续到 stack 变成 空的。然后如果消息队列还有消息的话,事件循环将会执行消息队列的下一个消息。

总之就是:

Queue队列中的消息,会以一个个执行;首先会判断 stack 会不会为空,如果为空就执行下一个消息;如果不为空,等待上一个消息处理完。

2、Event Loop 事件循环

之所以称为事件循环,是因为他的执行实现的方式如下:

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

如果当前没有消息,queue.waitForMessage()会同步等待消息的到达。

事件循环是指等待队列同步接收消息的过程的术语。 事件循环移入的增量称为’tick’,每次’ticks’时它检查调用堆栈是否为空,如果是,它将事件队列中的top函数添加到调用堆栈并执行它。 完成处理此功能后,它会再次开始计时。

1
2
3
4
5
6
7
8
9
function init() {
var link = document.getElementById("foo");

link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}

init();

在这个例子中:

当用户点击 元素 foo 和 触发 onClick 事件时,一个 message (and callback, changeColor) 被添加到消息队列中。

当这个消息被移除时,它的回调函数 changeColor 被调用,当 changeColor returns 时(哪怕返回的是一个错误),call stack 清空,事件循环再次开始;

只要 changeColor 方法存在,然后指定为元素 foo 的点击回调函数,后续的点击元素会引起更多的消息(关联到回调函数 changeColor)被加入到消息队列。

2.1、Run-to-completion 运行到结束

每一个消息都被完全执行结束后,才回去执行下一个消息的处理。

这为程序的分析提供了一些优秀的特性,包括:无论何时执行一个函数,都不会被抢占,并且会在其他代码执行之前就已经被完全执行(并且可以修改函数操作的数量)。

这个和 C语言不太一样,比如,如果一个函数运行在一个线程中,一些时候,会被执行系统因在别的线程执行其他代码中断。

这个模型的缺点时,当一个消息需要太长时间去执行的时候,web用户就无法处理一些,比如click, srcoll的交互。浏览器会弹出一个 “a script is taking too long to run” 这样的对话框来缓解这个情况。一个好的解决办法就是,缩短消息处理的时间,或者把一个消息分割成多个消息

2.2、Adding messages 添加消息

在web浏览器里面,只要有事件发生并且有监听器绑定的时候,一定会增加一个消息。如果没有监听器,则事件消失。所以,一个元素的点击并且带有点击事件处理,一定会增加一个消息到消息队列中去。

setTimeout 函数有两个参数:添加队列的消息时间(默认 0 ),这个时间值代表着这个消息被添加到消息队列最小的延迟时间。如果消息队列中,没有别的消息,这个消息会在延迟时间达到之后,立马会被处理。如果消息队列有别的消息,setTimeout 这个消息一定要等到别的消息被处理完后才能执行。由于这个原因,所以第二参数表明了最小的时间间隔,而非确切的时间

举例说明,当第二个参数的时间过期后,setTimeout 不会被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
const s = new Date().getSeconds();

setTimeout(function() {
// prints out "2", meaning that the callback is not called immediately after 500 milliseconds.
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);

while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}

举例说明添加消息的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f() {
console.log("foo");
setTimeout(g, 0);
console.log("baz");
h();
}

function g() {
console.log("bar");
}

function h() {
console.log("blix");
}

f();

由于 setTimeout 的非阻塞特性,它的回调函数将在 0 毫米后触发,并且不会作为 f()这个消息的一部分来处理。

在这个例子中,调用 setTimeout ,传递回调函数 g 和 时间延迟 0 ms; 当指定的延迟时间到了的时候,一个以 g 为回调函数的单独的消息会被加入到消息队列中。

生成结果的控制台活动是 “foo”, “baz”, “bilx” 和在下一个事件循环中产生的结果:“bar”;

如果同一个调用帧,对 setTimeout 进行两次调用,并且 timeout 时间相同,则按执行顺序进行处理。

2.3、Zero delays 零延迟

零延迟不是真实代表着在0毫秒后回调函数会执行。

setTimeout 的零延迟,在给定的时间间隔后不会执行回调函数。

是否执行决定于消息队列中的等待任务的数量。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function() {

console.log('this is the start');

setTimeout(function cb() {
console.log('Callback 1: this is a msg from call back');
});

console.log('this is just a message');

setTimeout(function cb1() {
console.log('Callback 2: this is a msg from call back');
}, 0);

console.log('this is the end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// 当前函数 note that function return, which is undefined, happens here
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

this is just a message 虽然在回调之后,却会在回调之前输出到打印台上,这是因为这个零延迟只是处理请求的最小延迟,并非一个保证的精确的时间。

一般地,setTimeout 需要等待所有其他消息队列的代码执行完之后,才会执行,即时你设置了特殊的时间间隔。

构建工具之webpack

构建工具之webpack

入门小试牛刀

本章节我们讲解一下 webpack 构建详细过程。

作为 module bundle,越来越多的人使用webpack

不需要像 gulp grunt 一样了解每个task过程,只需要配置入口文件等即可。

这里使用yarn

1
2
yarn init -y
yarn add webpack webpack-cli -D

假如使用 npm:

1
2
npm init -y
npm install webpack webpack-cli --save-dev

现在,我们将创建以下目录结构、文件和内容:

project

1
2
3
4
5
6
  webpack4
|- package.json
|- yarn.lock
+ |- index.html
+ |- /src
+ |- index.js

package.json中,去除 main, 增加 private,防止被误提交:

1
2
3
4
5
6
7
8
9
10
{
"name": "webpack4",
"version": "1.0.0",
"private": true,
"license": "MIT",
"devDependencies": {
"webpack": "^4.33.0",
"webpack-cli": "^3.3.4"
}
}

我们接下来的代码需要用到 lodash ,所以事先安装一下:

1
2
3
4
5
6
7
8
9
10
11
12
yarn add lodash

这里有一个小知识点:
yarn add lodash ==VS== yarn add lodash -D
抑或
npm install lodash --save ==VS== npm install lodash --save-dev
的区别:

在安装一个 package,而此 package 要打包到生产环境 bundle 中时,你应该使用
npm install --save。
如果你在安装一个用于开发环境目的的 package 时(例如,linter, 测试库等),你应该使用
npm install --save-dev

index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webpack</title>
</head>
<body>
<script src="src/index.js"></script>
</body>
</html>

src/index.js

1
2
3
4
5
6
7
8
9
10
11
import _ from 'lodash';

function Component() {

let element = document.createElement('div');
element.innerText = _.join(['Hello', 'World']);

return element;
}

document.body.appendChild(Component());

这时候运行,肯定不能显示,我们要先打包编译一下。

执行 npx webpack,会将我们的脚本 src/index.js 作为 入口起点,也会生成 dist/main.js作为 输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
alex:webpack4 $ npx webpack
Hash: 918b00f8361c58df4bff
Version: webpack 4.33.0
Time: 2673ms
Built at: 2019-06-12 23:49:02
Asset Size Chunks Chunk Names
main.js 70.4 KiB 0 [emitted] main
Entrypoint main = main.js
[1] ./src/index.js 199 bytes {0} [built]
[2] (webpack)/buildin/global.js 472 bytes {0} [built]
[3] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

htmljs 的引用地址改为 ‘’dist/main.js”;

浏览器打开就能看到“Hello World”。

为了更加方便的配置项目,我们还是需要配置 webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
const path = require('path');
module.exports = {
entry:{
index: './src/index.js'
//记得加 ./ 不然报错 ERROR in Entry module not found: Error: Can't resolve 'src/index.js' in '/webpack4'
},
output:{
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
}

package.json 配置:

1
2
3
"scripts": {
"build": "webpack"
}

打包:

1
yarn build

通过在 npm run build 命令和你的参数之间添加两个中横线,可以将自定义参数传递给 webpack,例如:npm run build -- --colors

loadsh 小插曲

不知道大家有没有注意到一个事情:我们只使用了lodash的一个join方法,却打包进去了整个版本 70K

这对于优化来说,是个极其不划算的事情。

1
2
 Asset      Size  Chunks             Chunk Names
main.js 70.4 KiB 0 [emitted] main

当然我们首先

第一种方法

可以只引用 join 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// import _ from 'lodash';
import join from 'lodash/join';

function component() {

let element = document.createElement('div');
// element.innerHTML = _.join(['Hello', 'World']);
element.innerHTML = join(['Hello', 'World']);

return element;

}

const div = component();

document.body.append(div);
1
2
3
4
5
6
Hash: e68d39f2999aafcdc772
Version: webpack 4.33.0
Time: 304ms
Built at: 2019-06-13 00:42:35
Asset Size Chunks Chunk Names
main.js 1.17 KiB 0 [emitted] index

70.4 KiB —> 1.17 KiB效果显著。

但是一般情况,我们需要很多方法要用,不想如此麻烦,那么我们又该如何是好呢?

第二种方法:

使用 lodash-webpack-plugin

1
yarn add lodash-webpack-plugin babel-core babel-loader babel-plugin-lodash babel-preset-env

window和document各种宽高计算

window和document各种宽高计算

[TOC]

1、盒子模型

img

所谓CSS盒子模型是:

margin + border + padding + content

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<style>
*{
margin: 0;
padding: 0;
}
.box{
width: 200px;
height: 200px;
padding: 20px;
margin: 20px;
border: 2px solid red;
}
.boxItem{
width: 100%;
height: 100%;
background: green;
}
</style>
<div class="box">
<div class="boxItem"></div>
</div>

如图所示:

可视区域高度: clientHeight = height + padding = 240;

正文全文高度:scrollHeight = height + padding = 240;

可见区域偏移高度:offsetHeight = height + padding + border = 244;

此时 scrollHeightclientHeight 好像看起来并没有什么区别。

如果我们上面的代码做以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
.box{
width: 200px;
height: 200px;
padding: 20px;
margin: 20px;
border: 2px solid red;
overflow:auto;
}
.boxItem{
width: 100%;
height: 1000px;
background: green;
}

可视区域高度: clientHeight = height + padding = 240;

正文全文高度:scrollHeight = height + padding = 1040;

可见区域偏移高度:offsetHeight = height + padding + border = 244;

这下就可以看到之间的区别了,其他计算属性如下:

1
2
3
4
5
6
clientLeft: 2;
clinetTop: 2;
offsetLeft: 20;
offsetTop: 20;
scrollLeft:0;
scrollTop:0;

总结:

当前div.box元素的称为当前元素,当前元素的offsetParent(父类节点)在这里是 body

Client

clientleft:元素的内边距的外边缘和元素边框的外边缘的距离,实际就是边框的左边框宽度

clienttop:同理边框的上边框的宽度

clientwidth:用于描述元素内尺寸宽度,是指 元素内容+内边距 大小,不包括边框、外边距、滚动条部分

clientheight:同理 用于描述元素内尺寸高度,是指 元素内容+内边距 大小,不包括边框、外边距、滚动条部分

Offset

offsetleft: 元素的边框的外边缘距离与已定位的父容器(offsetparent)的左边距离(不包括元素的边框和父容器的边框)。

offsettop:同理是指元素的边框的外边缘距离与已定位的父容器(offsetparent)的上边距离(不包括元素的边框和父容器的边框)。

offsetwidth:描述元素外尺寸宽度,是指 元素内容宽度+内边距宽度(左右两个)+边框(左右两个),不包括外边距和滚动条部分。

offsetheight:同理 描述元素外尺寸高度,是指 元素内容高度+内边距高度(上下两个)+边框(上下两个),不包括外边距和滚动条部分

深入了解 HTML5 History API,前端路由的生成,解读 webpack-dev-server 的 historyApiFallback 原理

深入了解 HTML5 History API,前端路由的生成,解读 webpack-dev-server 的 historyApiFallback 原理

[TOC]

1、history

History 接口,允许操作浏览器的 session history,比如在当前tab下浏览的所有页面或者当前页面的会话记录。

history属性

1568966948334

1、length(只读)

返回一个总数,代表当前窗口下的所有会话记录数量,包括当前页面。如果你在新开的一个tab里面输入一个地址,当前的length1,如果再输入一个地址,就会变成2

假设当前总数已经是6,无论是浏览器的返回还是 history.back(), 当前总数都不会改变。

2、scrollRestoration(实验性API)

允许web应用在history导航下指定一个默认返回的页面滚动行为,就是是否自动滚动到页面顶部;默认是 auto, 另外可以是 manual(手动)

3、 state (当前页面状态)

返回一个任意的状态值,代表当前处在历史记录`栈`里最高的状态。其实就是返回当前页面的`state`,默认是 null

history 方法

History不继承任何方法;

1、 back()

返回历史记录会话的上一个页面,同浏览器的返回,同 history.go(-1)

2、forward()

前进到历史会话记录的下一个页面,同浏览器的前进,同 history.go(1)

3、go()

session history里面加载页面,取决于当前页面的相对位置,比如 go(-1) 是返回上一页,go(1)是前进到下一个页面。
如果你直接一个超过当前总length的返回,比如初始页面,没有前一个页面,也没有后一个页面,这个时候 go(-1)go(1),都不会有任何作用;
如果你不指定任何参数或者go(0),将会重新加载当前页面;

4、pushState(StateObj, title, url)

把提供的状态数据放到当前的会话栈里面,如果有参数的话,一般第二个是title,第三个是URL。
这个数据被DOM当做透明数据;你可以传任何可以序列号的数据。不过火狐现在忽略 title 这个参数;
这个方法引起会话记录length的增长。

5、replaceState(StateObj, title, url)

把提供的状态数据更新到当前的会话栈里面最近的入口,如果有参数的话,一般第二个是title,第三个是URL
这个数据被DOM当做透明数据;你可以传任何可以序列号的数据。不过火狐现在忽略 title 这个参数;
这个方法不会引起会话记录length的增长。


综上所述,pushStatereplaceState 是修改当前session history的两个方法,他们都会触发一个方法 onpopstate 事件;

1
history.pushState({demo: 12}, "8888", "en-US/docs/Web/API/XMLHttpRequest")

1568969522699

如图 pushState 会改变当你在后面建立的页面发起XHR请求的时候,请求header里面的 referrer;这个地址就是你在pushState里面的URL;

另外URL en-US/docs/Web/API/XMLHttpRequest(并非真实存在的URL), 在pushState完成之后,并不触发页面的重新加载或者检查当前URL的目录是否存在

只有当你此刻从这个页面跳转到 google.com, 然后再点击返回按钮,此时的页面就是你现在pushState的页面,state也会是当前的state, 也同时会加载当前的页面资源,oops,此刻会显示不存在;

1568970176102

replaceState 同理;

关于 onpopstate:

1
2
3
4
5
6
7
8
9
10
11

window.onpopstate = function(event) {
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};

history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // alerts "location: http://example.com/example.html, state: null
history.go(2); // alerts "location: http://example.com/example.html?page=3, state: {"page":3}

2、两种路由模式的生成

以下说明仅存在于当前路由是 history 模式;
说道 webpack-dev-serverhistoryApiFallback 就不得不说下 VUE 前端路由,路由跳转原理;

传统的web开发中,大多是多页应用,每个模块对应一个页面,在浏览器输入相关页面的路径,然后服务端处理相关浏览器的请求,通过HTTP把资源返回给客户端浏览器进行渲染。

传统开发,后端定义好路由的路径和请求数据的地址;

随着前端的发展,前端也承担着越来越大的责任,比如Ajax局部刷新数据,前端可以操控一些历史会话,而不用每次都从服务端进行数据交互。

history.pushStatehistory.replaceState ,这两个history新增的api,为前端操控浏览器历史栈提供了可能性

1
2
3
4
5
6
7
8

/**
* @data {object} state对象 最大640KB, 如果需要存很大的数据,考虑 sessionStorage localStorage
* @title {string} 标题
* @url {string} 必须同一个域下,相对路径和绝对路径都可以
*/
history.pushState(data, title, url) //向浏览器历史栈中增加一条记录。
history.replaceState(data, title, url) //替换历史栈中的当前记录。

这两个Api都会操作浏览器的历史栈,而不会引起页面的刷新。不同的是,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录。所需的参数相同,在将新的历史记录存入栈后,会把传入的data(即state对象)同时存入,以便以后调用。同时,这俩api都会更新或者覆盖当前浏览器的titleurl为对应传入的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13

// 假设当前的URL: http://test.com

history.pushState(null, null, "/login");
// http://test.com ---->>> http://test.com/login

history.pushState(null, null, "http://test.com/regiest");
// http://test.com ---->>> http://test.com/regiest


// 错误用法
history.pushState(null, null, "http://baidu.com/regiest");
// error 跨域报错

也正是基于浏览器的hitroy,慢慢的衍生出来现在的前端路由比如vuehistory路由,reactBrowseHistory

==现在让我们手动写一个history路由模式==:

Html

1
2
3
4
5
<div>
<a href="javascript:;" data-link="/">login</a>
<a href="javascript:;" data-link="/news">news</a>
<a href="javascript:;" data-link="/contact">contact</a>
</div>

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// history 路由
class HistoryRouter {
constructor(options = {}) {
// store all router
this.routers = {};
// 遍历路由参数,保存到 this.routers
if (options.router) {
options.router.forEach(n => {
this.routers[n.path] = () => {
document.getElementById("content").innerHTML = n.component;
}
});
}
// 绑定到 this.routers
this.updateContent = this.updateContent.bind(this);
// 初始化事件
this.init();
this.bindClickEvent();
}
init() {
// 页面初始化的时候,初始化当前匹配路由
// 监听 load
window.addEventListener('load', this.updateContent, false);
// pushState replaceState 不能触发 popstate 事件
// 当浏览器返回前进或者刷新,都会触发 popstate 更新
window.addEventListener("popstate", this.updateContent, false);
}
// 更新内容
updateContent(e) {
alert(e ? e.type : "click");
const currentPath = location.pathname || "/";
this.routers[currentPath] && this.routers[currentPath]();
}
// 绑定点击事件
bindClickEvent() {
const links = document.querySelectorAll('a');
Array.prototype.forEach.call(links, link => {
link.addEventListener('click', e => {
const path = e.target.getAttribute("data-link");
// 添加到session history
this.handlePush(path);
})
});
}
// pushState 不会触发 popstate
handlePush(path){
window.history.pushState({path}, null, path);
this.updateContent();
}
}
// 实例
new HistoryRouter({
router: [{
name: "index",
path: "/",
component: "Index"
}, {
name: "news",
path: "/news",
component: "News"
}, {
name: "contact",
path: "/contact",
component: "Contact"
}]
});

第一次渲染的时候,会根据当前的 pathname 进行更新对应的 callback 事件,然后更新 content , 这个时候无需服务器的请求;

如果这个时候,我们点击浏览器的返回🔙前进按钮,发现依然会依次渲染相关 content ,这就是history历史堆栈的魅力所在。

最后我们发现当我们切换到非loading page的时候,我们刷新页面,会报出 Get 404,这个时候就是请求了server , 却发现不存在这个目录的资源;

这个时候我们就需要 historyApiFallback


3、historyApiFallback

Webpack-dev-server 的背后的是connect-history-api-fallback

关于 connect-history-api-fallback 中间件,解决这个404问题

单页应用(SPA)一般只有一个index.html, 导航的跳转都是基于HTML5 History API,当用户在越过index.html 页面直接访问这个地址或是通过浏览器的刷新按钮重新获取时,就会出现404问题;

比如 直接访问/login, /login/online,这时候越过了index.html,去查找这个地址下的文件。由于这是个一个单页应用,最终结果肯定是查找失败,返回一个404错误

这个中间件就是用来解决这个问题的

只要满足下面四个条件之一,这个中间件就会改变请求的地址,指向到默认的index.html:

1 GET请求

2 接受内容格式为text/html

3 不是一个直接的文件请求,比如路径中不带有 .

4 没有 options.rewrites 里的正则匹配


connect-history-api-fallback 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
'use strict';

var url = require('url');

exports = module.exports = function historyApiFallback(options) {
options = options || {};
var logger = getLogger(options);

return function(req, res, next) {
var headers = req.headers;
if (req.method !== 'GET') {
logger(
'Not rewriting',
req.method,
req.url,
'because the method is not GET.'
);
return next();
} else if (!headers || typeof headers.accept !== 'string') {
logger(
'Not rewriting',
req.method,
req.url,
'because the client did not send an HTTP accept header.'
);
return next();
} else if (headers.accept.indexOf('application/json') === 0) {
logger(
'Not rewriting',
req.method,
req.url,
'because the client prefers JSON.'
);
return next();
} else if (!acceptsHtml(headers.accept, options)) {
logger(
'Not rewriting',
req.method,
req.url,
'because the client does not accept HTML.'
);
return next();
}

var parsedUrl = url.parse(req.url);
var rewriteTarget;
options.rewrites = options.rewrites || [];
for (var i = 0; i < options.rewrites.length; i++) {
var rewrite = options.rewrites[i];
var match = parsedUrl.pathname.match(rewrite.from);
if (match !== null) {
rewriteTarget = evaluateRewriteRule(parsedUrl, match, rewrite.to);
logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
req.url = rewriteTarget;
return next();
}
}

if (parsedUrl.pathname.indexOf('.') !== -1 &&
options.disableDotRule !== true) {
logger(
'Not rewriting',
req.method,
req.url,
'because the path includes a dot (.) character.'
);
return next();
}

rewriteTarget = options.index || '/index.html';
logger('Rewriting', req.method, req.url, 'to', rewriteTarget);
req.url = rewriteTarget;
next();
};
};

function evaluateRewriteRule(parsedUrl, match, rule) {
if (typeof rule === 'string') {
return rule;
} else if (typeof rule !== 'function') {
throw new Error('Rewrite rule can only be of type string of function.');
}

return rule({
parsedUrl: parsedUrl,
match: match
});
}

function acceptsHtml(header, options) {
options.htmlAcceptHeaders = options.htmlAcceptHeaders || ['text/html', '*/*'];
for (var i = 0; i < options.htmlAcceptHeaders.length; i++) {
if (header.indexOf(options.htmlAcceptHeaders[i]) !== -1) {
return true;
}
}
return false;
}

function getLogger(options) {
if (options && options.logger) {
return options.logger;
} else if (options && options.verbose) {
return console.log.bind(console);
}
return function(){};
}

其实代码也挺简单的,最主要先符合上面四个原则,然后先匹配自定义rewrites规则,再匹配点文件规则;

getLogger, 默认不输出,options.verbose如果为true,则输出,默认console.log.bind(console)

如果req.method != 'GET',结束
如果!headers || !headers.accept != 'string' ,结束
如果headers.accept.indexOf('application/json') === 0 结束

acceptsHtml函数a判断headers.accept字符串是否含有[‘text/html’, ‘/‘]中任意一个
当然不够这两个不够你可以自定义到选项options.htmlAcceptHeaders
!acceptsHtml(headers.accept, options),结束

然后根据你定义的选项rewrites, 没定义就相当于跳过了
按定义的数组顺序,字符串依次匹配路由rewrite.from,匹配成功则走rewrite.to,to可以是字符串也可以是函数,结束

判断dot file,即pathname中包含.(点),并且选项disableDotRule !== true,即没有关闭点文件限制规则, 结束

rewriteTarget = options.index || '/index.html'

大致如此;

webpack4配置React项目,同时配置DEV和PROD环境

webpack4配置React项目,同时配置DEV和PROD环境

[TOC]

1、生成一个react项目

1
2
yarn add create-react-app -global
npx create-react-app my-app

2、从零配置webapck

安装:

1
yarn add webpack webpack-cli webpack-dev-server

在package.json增加

1
2
3
4
5
6
"scripts": {
"dev": "webpack-dev-server --mode development",
"build": "webpack --mode production",
},
# yarn dev 去启动本地server
# yarn build 去生成生产代码

生成配置文件:

1
touch webpack.config.js

webpack.config.js增加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
// path function
const resolve = src => {
return path.resolve(__dirname, src);
};

module.exports = (env, argv) => {

//argv 里面的 mode 分别是之前执行命令的的,development production
// 传递给 babel.config.js
process.env.NODE_ENV = argv.mode;

return ({
entry: {
"login": "./src/login",
"index": "./src/index",
},
output: {
path: resolve("cdn"),
filename: 'js/[name].[hash:8].js',
publicPath: '/',
},
//解析 jsx
rules: [{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
}],
plugins:[
// 生成最终需要的html模板
new HtmlWebpackPlugin({
title: "Login",
template: resolve("public/index.html"),
filename: "login.html",
hash: true,//增加hash
minify:true,//压缩html代码
chunks: ['login'],
favicon: resolve("public/favicon.ico")
}),
new HtmlWebpackPlugin({
title: "Index",
template: resolve("public/index.html"),
filename: "index.html",
hash: true,
minify: true,
chunks: ['index'],
favicon: resolve("public/favicon.ico")
})
]
})

}

此时还需要配置 babel7,把 ES6\7转化为浏览器直接解析的语法;

webpack 4: transpiling Javascript ES6 with Babel 7

webpack 4: transpiling Javascript ES6 with Babel

babel-loader把ES6甚至更高的版本,编译成ES5,这样浏览器就能解析了。

babel core
babel loader
babel preset env for compiling Javascript ES6 code down to ES5

1
yarn add @babel/core babel-loader @babel/preset-env @babel/preset-react

下一步,我们生成一个babel.config.js配置文件

1
touch babel.config.js

babel.config.js

在这里可以除无用的 console.log()来减少文件的体积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const removeConsolePlugin = [];

console.log("babel", process.env.NODE_ENV)
//移除console
if (process.env.NODE_ENV == "production") {
console.log("====remove-console=====");
removeConsolePlugin.push([
"transform-remove-console",
{
"exclude": ["error", "warn"]
}
]);
}

module.exports = {
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": [
"last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 iOS versions",
"last 1 Android version",
"last 1 ChromeAndroid version",
"ie 11"
]
}
}
],
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
...removeConsolePlugin
]
}

这个时候yarn dev,我们启动本地server,应该是成功的了。

3、设置alias别名和自动填写后缀

1
2
3
4
5
6
7
8
9
10
11
12
resolve: {
// 别名
alias: {
"@": resolve('src'),
"@c": resolve('src/components'),
"@less": resolve('src/less'),
"@util": resolve('src/utils'),
"@/images": resolve('src//images'),
},
// 自动添加后缀
extensions: ['.jsx', '.js', '.less']
}

4、配置 devServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
devServer: {
port: 3001,
open: true,
hot: true,
compress: true,
contentBase: path.join(__dirname, './'),
noInfo: false,
overlay: {
warnings: true,
errors: true
},
proxy: {
'/api': {
target: 'http://****',
changeOrigin: true,
},
}
}

5、解析分离less到单独的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// webpack4 使用 mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// 分离css
# plugins 增加
plugins.push(new MiniCssExtractPlugin({
// Options similar to the same options webpackOptions.output
// all options are optional
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name].[hash:8].css',
ignoreOrder: false
}));

#rules 增加
rules:[
{
test: /\.less$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
"less-loader"
]
// webpack4 废弃
// use: ExtractTextPlugin.extract({
// fallback: "style-loader",
// use: [
// 'css-loader',
// "less-loader"
// ]
// })
}
]

6、url-loader, file-loader 解析图片地址,并导出到指定文件

1
2
3
4
5
6
7
8
9
10
11
12
{
test: /\.(png|jpg|svg|gif|ico)?$/,
use: [{
loader: 'url-loader',
options: { // 这里的options选项参数可以定义多大的图片转换为base64
fallback: "file-loader",
limit: 10 * 1024, // 表示小于10kb的图片转为base64,大于10kb的是路径
outputPath: 'images', //定义输出的图片文件夹
name: '[name].[contenthash:8].[ext]'
}
}]
}

7、多个less文件公用的common.less,需要删除合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 去除重复的 less, 比如 common.less里面的内容
plugins.push(new OptimizeCss/imagesPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: {
removeAll: true
}
}
],
},
canPrint: true
}));

8、提取各个模块的公共代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
optimization: {
splitChunks: {
// 静态资源缓存
// test, priority and reuseExistingChunk can only be configured on cache group level.
cacheGroups: {
// 提取 node_modules 里面依赖的代码
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-vendors',
chunks: 'all',
minChunks: 2, //2个共享以及以上都提取
priority: -10 //优先级
},
// 提出每个模块公共的代码
commons: {
name: 'chunk-commons',
test: /\.js$/,
chunks: 'initial',
minChunks: 2, //两个共享以及以上都提取,
minSize: 0,
priority: -20, //优先级
reuseExistingChunk: true
},
css: {
name: 'css-commons',
test: /\.less$/,
minChunks: 2,
minSize: 0,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true,
}
}
},
// I pull the Webpack runtime out into its own bundle file so that the
// contentHash of each subsequent bundle will remain the same as long as the
// source code of said bundles remain the same.
runtimeChunk: "single"
}

全部代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
/**
* webpack 4 config
* @author master2011zhao@gmail.com
* @Date 20190910
*/
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");
// webpack4 使用 mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// extract 被废弃
// const ExtractTextPlugin = require('extract-text-webpack-plugin');
// clean project
const {
CleanWebpackPlugin
} = require("clean-webpack-plugin");
// 压缩css
const OptimizeCss/imagesPlugin = require('optimize-css-/images-webpack-plugin');
// notifier
const WebpackBuildNotifierPlugin = require('webpack-build-notifier');

// path function
const resolve = src => {
return path.resolve(__dirname, src);
};


module.exports = (env, argv) => {

const isProduction = argv.mode === "production";

console.log("isProduction", isProduction);

// 传递给 babel.config.js
process.env.NODE_ENV = argv.mode;

// console.log(process.env.NODE_ENV);

let plugins = [];

// 生成模板
let HtmlTemplates = [];

// 生产环境
if (isProduction) {
// 清理项目, 清理不干净,需要使用 rm.sh
plugins.push(new CleanWebpackPlugin({
dry: false,
verbose: true,
}));

// 构建完成提醒
plugins.push(new WebpackBuildNotifierPlugin({
title: "react project build",
suppressSuccess: true,
suppressWarning: false,
messageFormatter: function () {
return "build completely"
}
}));

// 分离css
// plugins.push(new ExtractTextPlugin('css/[name].[hash:8].css'));
plugins.push(new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// all options are optional
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name].[hash:8].css',
ignoreOrder: false, // Enable to remove warnings about conflicting order
}));

// 去除重复的 less, 比如 common
plugins.push(new OptimizeCss/imagesPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: [
'default',
{
discardComments: {
removeAll: true
}
}
],
},
canPrint: true
}));

// 公共提取的chunk
const commonChunks = ["chunk-vendors", "runtime", "chunk-commons", "css-commons"];

const minify = {
collapseWhitespace: true,
removeComments: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
useShortDoctype: true
}

// 生成模板
HtmlTemplates = [
new HtmlWebpackPlugin({
title: "Login",
template: resolve("public/index.html"),
filename: "login.html",
hash: true,
minify,
chunks: [...commonChunks, 'login'],
favicon: resolve("public/favicon.ico")
}),
new HtmlWebpackPlugin({
title: "Index",
template: resolve("public/index.html"),
filename: "index.html",
hash: true,
minify,
chunks: [...commonChunks, 'index'],
favicon: resolve("public/favicon.ico")
})
]


} else {
// 生成模板
HtmlTemplates = [
new HtmlWebpackPlugin({
title: "Login",
template: resolve("public/index.html"),
filename: "login.html",
favicon: resolve("public/favicon.ico"),
chunks: ['login'], //指定入口
}),
new HtmlWebpackPlugin({
title: "Index",
template: resolve("./public/index.html"),
filename: "index.html",
favicon: resolve("public/favicon.ico"),
chunks: ['index'], //指定入口
})
]
}

return {
entry: {
"login": "./src/login",
"index": "./src/index",
},
output: {
path: resolve("cdn"),
filename: 'js/[name].[hash:8].js',
publicPath: '/',
},
devServer: {
port: 3001,
open: true,
hot: true,
compress: true,
contentBase: path.join(__dirname, './'),
noInfo: false,
overlay: {
warnings: true,
errors: true
},
proxy: {
'/api': {
target: 'http://*****:8093',
changeOrigin: true,
},
}
},
resolve: {
// 别名
alias: {
"@": resolve('src'),
"@c": resolve('src/components'),
"@less": resolve('src/less'),
"@util": resolve('src/utils'),
"@/images": resolve('src//images'),
},
// 自动添加后缀
extensions: ['.jsx', '.js', '.less']
},
module: {
rules: [{
test: /\.jsx?$/,
use: 'babel-loader',
exclude: /node_modules/,
},
{
test: /\.less$/,
use: [
isProduction ? MiniCssExtractPlugin.loader : "style-loader",
"css-loader",
"less-loader"
]
// webpack4 废弃
// use: ExtractTextPlugin.extract({
// fallback: "style-loader",
// use: [
// 'css-loader',
// "less-loader"
// ]
// })
},
{
test: /\.(png|jpg|svg|gif|ico)?$/,
use: [{
loader: 'url-loader',
options: { // 这里的options选项参数可以定义多大的图片转换为base64
fallback: "file-loader",
limit: 10 * 1024, // 表示小于10kb的图片转为base64,大于10kb的是路径
outputPath: 'images', //定义输出的图片文件夹
name: '[name].[contenthash:8].[ext]'
}
}]
},
// {
// test: /\.html$/,
// use: [{
// loader: "html-loader",
// options: {
// minimize: true,
// removeComments: true,
// collapseWhitespace: true
// }
// }]
// }
]
},
plugins: [
...plugins,
...HtmlTemplates
],
optimization: {
splitChunks: {
// 静态资源缓存
// test, priority and reuseExistingChunk can only be configured on cache group level.
cacheGroups: {
// 提取 node_modules 里面依赖的代码
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'chunk-vendors',
chunks: 'all',
minChunks: 2, //2个共享以及以上都提取
priority: -10 //优先级
},
// 提出每个模块公共的代码
commons: {
name: 'chunk-commons',
test: /\.js$/,
chunks: 'initial',
minChunks: 2, //两个共享以及以上都提取,
minSize: 0,
priority: -20, //优先级
reuseExistingChunk: true
},
css: {
name: 'css-commons',
test: /\.less$/,
minChunks: 2,
minSize: 0,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true,
}
}
},
// I pull the Webpack runtime out into its own bundle file so that the
// contentHash of each subsequent bundle will remain the same as long as the
// source code of said bundles remain the same.
runtimeChunk: "single"
}

};
}

关于webpack的面试题总结

关于webpack的面试题总结

[TOC]

为什么要总结webpack相关的面试题

随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,如react的jsx代码必须编译后才能在浏览器中使用;又如sass和less的代码浏览器也是不支持的。 而如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack脱颖而出成为了当今最流行的前端构建工具。 然而大多数的使用者都只是单纯的会使用,而并不知道其深层的原理。希望通过以下的面试题总结可以帮助大家温故知新、查缺补漏,知其然而又知其所以然。

FAQ 问题列表

  1. webpack与grunt、gulp的不同?
  2. 与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?
  3. 有哪些常见的Loader?他们是解决什么问题的?
  4. 有哪些常见的Plugin?他们是解决什么问题的?
  5. Loader和Plugin的不同?
  6. webpack的构建流程是什么?从读取配置到输出文件这个过程尽量说全
  7. 是否写过Loader和Plugin?描述一下编写loader或plugin的思路?
  8. webpack的热更新是如何做到的?说明其原理?
  9. 如何利用webpack来优化前端性能?(提高性能和体验)
  10. 如何提高webpack的构建速度?
  11. 怎么配置单页应用?怎么配置多页应用?
  12. npm打包时需要注意哪些?如何利用webpack来更好的构建?
  13. 如何在vue项目中实现按需加载?

解答:

1、webpack与grunt、gulp的不同?

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流

不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件。

gruntgulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。

webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。

gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系
webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工。

2、与webpack类似的工具还有哪些?谈谈你为什么最终选择(或放弃)使用webpack?

同样是基于入口的打包工具还有以下几个主流的:

从应用场景上来看:

  • webpack适用于大型复杂的前端站点构建
  • rollup适用于基础库的打包,如vue、react
  • parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于parcel在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用parcel。

3.有哪些常见的Loader?他们是解决什么问题的?

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

4.有哪些常见的Plugin?他们是解决什么问题的?

  • define-plugin:定义环境变量

  • commons-chunk-plugin:提取公共代码

  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码

前端面试测试题目

[TOC]

JavaScript

1 基本类型

介绍一下基本类型有几种?

怎么判断数据类型?

Object 和 Array,怎么区分?

null == undefined ?

2 变量作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var name = "Hello body";
var a = {
name: "A",
sayHi: function(){
console.log(this.name)
}
};
var b = {
name: 'B'
};
var sayHi = a.sayHi;
name = "Hello C";

console.log(name)

a.sayHi()
sayHi()
a.sayHi.call(b)

3 闭包

4 call 和 apply的区别

5 浅拷贝 和 深拷贝

6 const 和 let 的区别

7 写出Es6的几种用法

8 Vue 生命周期实现过程?

9 vuex中 mumation 和 action 有什么不同? 实现原理是什么?

10 vue v-if 和 v-show 的区别? Key的作用?

11 vue keep-alive 用法?

12 bind 和 call apply 区别

13 Promise 和 async await 区别?

14 本地存储

15 跨域

16 Event Loop

17 事件委托机制

18 异步请求

19 Vue 双向数据绑定的原理

CSS

1 盒子模型

2、垂直居中问题

{% include 'gitalk.ejs' %}
  • © 2014-2020 Alex Wong
  • Powered by Hexo Theme Ayer
  • PV: UV:

请我喝杯咖啡吧~~~

支付宝
微信