REEEEX

KEEP GOING


  • 首页

  • 归档

  • 标签

  • 关于

Spring Security | Note-1

发表于 2018-08-24

Spring Security Note-1


认证和授权

对于服务的安全考虑,认证和授权是一个非常重要,并且有难度的点;

它的最终表现形式之一是登录;

解决问题

同时支持多种认证方式(传统,手机短信,微信,QQ等);

同时支持多种前端渠道(浏览器,APP);

支持集群环境,跨应用工作,SESSION控制,控制用户权限,防护与身份认证相关的攻击;

目的

打造可重用的,企业级的,认证和授权模块;

涉及技术

Spring Security,Spring Social,Spring Security OAuth;

目标

理解Spring Security以及相关框架的原理、功能;

基于Spring Security以及相关框架独立开发认证授权相关功能;

掌握抽象和封装的常用技巧,编写可重用的模块;


项目结构

security:主模块(统一执行命令)

security-core:核心业务逻辑(实现基本安全的方式)

security-browser:浏览器安全逻辑

security-app:app安全逻辑

security-demo:样例程序


工程搭建

在创建完所有的过程后;

首先对security的pom进行配置,引入dependencyManagement管理Maven项目的版本(自动管理其他依赖的版本,只需要告诉项目什么依赖即可,不需要指定版本,保证互相兼容);

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
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!--引入插件,maven的编译工具,指定JDK和编译的版本为1.8-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-complier-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

<modules>
<module>../security-app</module>
<module>../security-browser</module>
<module>../security-core</module>
<module>../security-demo</module>
</modules>

接下来,在core项目中,添加pom依赖,依赖过多,不与展示;

添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--不需要添加version,由主项目的dependencyManagement控制版本,保证互相兼容-->
<dependencies>
<!--Spring Security & OAuth2认证-->

<!--Redis缓存-->

<!--JDBC数据库-->

<!--MYSQL驱动-->

<!--Spring Social-->

<!--commons工具包-->

</dependencies>

接下来,在app和browser项目中,添加core依赖即可;

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.imooc.security</groupId>
<artifactId>imooc-security-core</artifactId>
<version>${imooc.security.version}</version>
</dependency>
</dependencies>

browser项目与app有略微不同,是一个web项目,所以需要添加spring-session依赖;

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>

demo项目则引用browser依赖;

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.imooc.security</groupId>
<artifactId>imooc-security-browser</artifactId>
<version>${imooc.security.version}</version>
</dependency>
</dependencies>

Hello项目

首先依赖的有关数据库的包,需要先对数据库连接进行配置;

暂时先对spring-session的配置,暂时关闭;

1
2
3
4
5
6
7
8
9
# datasource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxx?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=xxxx
# session
spring.session.store-type=none
# spring-security
security.basic.enabled=false

此时访问http://localhost:8081/hello 需要帐号密码才能访问;

这是由于spring-security的默认配置,这里我们先暂时关闭;

如果我们需要将项目打包成可上线的包的形式,则需要在demo的pom文件中,添加一个插件;

用作打包的插件,按照Spring Boot的特有方式,进行打包;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.3.3.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<finalName>demo</finalName>
</build>

此时maven进行 clean package的命令;

在demo文件下的target的目录下,有两个人jar包;

一个叫demo.jar(可执行jar包),一个demo.jar.original(原始文件);

Wechat Mini | Note-9

发表于 2018-08-23

微信小程序开发 Note-9


关注接口

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
// Service
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UsersMapper userMapper;
@Autowired
private UsersLikeVideosMapper usersLikeVideosMapper;
@Autowired
private UsersFansMapper usersFansMapper;
@Autowired
private Sid sid;

@Transactional(propagation = Propagation.REQUIRED)
@Override
public void saveUserFanRelation(String userId, String fanId) {
UsersFans usersFan = new UsersFans();
usersFan.setId(sid.nextShort());
usersFan.setUserId(userId);
usersFan.setFanId(fanId);
usersFansMapper.insert(usersFan);

userMapper.addFansCount(userId);
userMapper.addFollersCount(fanId);
}

@Transactional(propagation = Propagation.REQUIRED)
@Override
public void deleteUserFanRelation(String userId, String fanId) {
Example example = new Example(UsersFans.class);
Example.Criteria criteria = example.createCriteria();
criteria.andEqualTo("userId",userId);
criteria.andEqualTo("fanId",fanId);
usersFansMapper.deleteByExample(example);

userMapper.reduceFansCount(userId);
userMapper.reduceFollersCount(fanId);
}
}
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
// Controller
@RestController
@Api(value = "USER FUNCTION API", tags = {"USER FUNCTION CONTROLLER"})
@RequestMapping("/user")
public class UserController extends BasicController {

@Autowired
private UserService userService;

@ApiOperation(value = "FOLLOW USER", notes = "FOLLOW USER API")
@PostMapping("/beyourfans")
public IMoocJSONResult beyourfans(String userId,String fanId) {
if (StringUtils.isBlank(userId) || StringUtils.isBlank(fanId)) {
return IMoocJSONResult.errorMsg("信息不存在(无ID)");
}
userService.saveUserFanRelation(userId,fanId);
return IMoocJSONResult.ok("关注成功");
}

@ApiOperation(value = "UNFOLLOW USER", notes = "UNFOLLOW USER API")
@PostMapping("/dontbeyourfans")
public IMoocJSONResult dontbeyourfans(String userId,String fanId) {
if (StringUtils.isBlank(userId) || StringUtils.isBlank(fanId)) {
return IMoocJSONResult.errorMsg("信息不存在(无ID)");
}
userService.deleteUserFanRelation(userId,fanId);
return IMoocJSONResult.ok("取消关注成功");
}
}

关注联调

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
// mine.js
Page({
data: {
isMe:true,
isFollow:false,
publisherId:""
},
// 加载
onLoad: function(params) {
var me = this;

// var user = app.userInfo;
// 修改原有的全局对象为本地缓存
var user = app.getGlobalUserInfo();
var userId = user.id;

var publisherId = params.publisherId;
if (publisherId != null && publisherId != "" && publisherId != undefined){
// 进入发布者的页面
userId = publisherId;
me.setData({
isMe:false,
publisherId: publisherId
});
}
...
});
}
// 关注事件
followMe:function(e){
var me = this;
var publisherId = me.data.publisherId;
var user = app.getGlobalUserInfo();
var userId = user.id;

var followType = e.currentTarget.dataset.followtype;
var url = '';
if(followType == '1'){
// 关注
url = '/user/beyourfans?userId=' + publisherId + "&fanId=" + userId;
}else{
// 取消关注
url = '/user/dontbeyourfans?userId=' + publisherId + "&fanId=" + userId;
}

wx.showLoading({
title: '提交中',
});

wx.request({
url: app.serverUrl + url,
method:'POST',
header: {
'content-type': 'application/json',
'userId': user.id,
'userToken': user.userToken
},
success:function(){
wx.hideLoading();
if (followType == '1') {
// 关注
me.setData({
isFollow:true
});
} else {
// 取消关注
me.setData({
isFollow: false
});
}
}
})
}
})

关注功能完善

关注与已关注的实时刷新;

1
2
3
4
5
6
7
8
9
10
11
12
13
if (followType == '1') {
// 关注
me.setData({
isFollow:true,
fansCounts: ++me.data.fansCounts
});
} else {
// 取消关注
me.setData({
isFollow: false,
fansCounts: --me.data.fansCounts
});
}

用户头像的事件触发修改;

1
2
3
4
5
6
7
8
<block wx:if="{{isMe}}">
<image src="{{faceUrl}}" class="face" bindtap='changeFace'></image>
</block>

<block wx:if="{{!isMe}}">
<image src="{{faceUrl}}" class="face"></image>
</block>
<label class='nickname'>{{nickname}}</label>

作品 收藏 关注 tab动态切换

通过三个不同的List和不同的Flag的值,进行不同List的获取;

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
const app = getApp()
Page({
data: {
faceUrl: "../resource/images/noneface.png",
isMe: true,
isFollow: false,
publisherId: "",

videoSelClass: "video-info",
isSelectedWork: "video-info-selected",
isSelectedLike: "",
isSelectedFollow: "",

myVideoList: [],
myVideoPage: 1,
myVideoTotal: 1,

likeVideoList: [],
likeVideoPage: 1,
likeVideoTotal: 1,

followVideoList: [],
followVideoPage: 1,
followVideoTotal: 1,

myWorkFlag: false,
myLikesFlag: true,
myFollowFlag: true
}
// 选择作品tab
doSelectWork: function() {
this.setData({
isSelectedWork: "video-info-selected",
isSelectedLike: "",
isSelectedFollow: "",

myWorkFlag: false,
myLikesFlag: true,
myFollowFlag: true,

myVideoList: [],
myVideoPage: 1,
myVideoTotal: 1,

likeVideoList: [],
likeVideoPage: 1,
likeVideoTotal: 1,

followVideoList: [],
followVideoPage: 1,
followVideoTotal: 1
});

this.getMyVideoList(1);
},
// 选择收藏tab
doSelectLike: function() {
},
// 选择关注tab
doSelectFollow: function() {
},
getMyVideoList: function(page) {
var me = this;

// 查询视频信息
wx.showLoading();
// 调用后端
var serverUrl = app.serverUrl;
wx.request({
url: serverUrl + '/video/showAll/?page=' + page + '&pageSize=6',
method: "POST",
data: {
userId: me.data.userId
},
header: {
'content-type': 'application/json' // 默认值
},
success: function(res) {
console.log(res.data);
var myVideoList = res.data.data.rows;
wx.hideLoading();

var newVideoList = me.data.myVideoList;
me.setData({
myVideoPage: page,
myVideoList: newVideoList.concat(myVideoList),
myVideoTotal: res.data.data.total,
serverUrl: app.serverUrl
});
}
})
},
// 我的作品视频列表
getMyVideoList: function(page) {
var me = this;

// 查询视频信息
wx.showLoading();
// 调用后端
var serverUrl = app.serverUrl;
wx.request({
url: serverUrl + '/video/showAll/?page=' + page + '&pageSize=6',
method: "POST",
data: {
userId: me.data.userId
},
header: {
'content-type': 'application/json' // 默认值
},
success: function(res) {
console.log(res.data);
var myVideoList = res.data.data.rows;
wx.hideLoading();

var newVideoList = me.data.myVideoList;
me.setData({
myVideoPage: page,
myVideoList: newVideoList.concat(myVideoList),
myVideoTotal: res.data.data.total,
serverUrl: app.serverUrl
});
}
})
}
})

微信API菜单操作

wx.showActionSheet(OBJECT) 显示操作菜单

OBJECT参数说明:

参数 类型 必填 说明
itemList String Array 是 按钮的文字数组,数组长度最大为6个
itemColor HexColor 否 按钮的文字颜色,默认为”#000000”
success Function 否 接口调用成功的回调函数,详见返回参数说明
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
shareMe:function(){
wx.showActionSheet({
itemList: ['下载到本地', '举报用户', '分享到朋友圈','分享到QQ空间','分享到微博'],
success: function (res) {
console.log(res.tapIndex);
if(res.tapIndex == 0){
// 下载
}else if(res.tapIndex == 1){
// 举报用户
}else{
// 未开放
wx.showToast({
title: '功能暂未开发',
icon:'loading'
})
}
}
})
}

保存举报信息接口

1
2
3
4
5
6
7
8
//Service
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void reportUser(UsersReport usersReport) {
usersReport.setId(sid.nextShort());
usersReport.setCreateDate(new Date());
usersReportMapper.insert(usersReport);
}
1
2
3
4
5
6
7
8
// Controller
@ApiOperation(value = "REPORT USER", notes = "REPORT USER API")
@PostMapping("/reportUser")
public IMoocJSONResult reportUser(@RequestBody UsersReport usersReport) {
// 保存举报信息
userService.reportUser(usersReport);
return IMoocJSONResult.errorMsg("举报成功,相关人员会及时处理");
}

举报联调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
else if (res.tapIndex == 1) {
// 举报用户
var videoInfo = JSON.stringify(me.data.videoInfo);
var realUrl = '../videoinfo/videoinfo#videoInfo@' + videoInfo;
if (user == null || user == undefined || user == '') {
wx.navigateTo({
url: '../userLogin/login?redirectUrl=' + realUrl,
});
} else {
var publishUserId = me.data.videoInfo.userId;
var videoId = me.data.videoInfo.id;
wx.navigateTo({
url: '../report/report?videoId=' + videoId + "&publishUserId=" + publishUserId
});
}
}

分享好友或微信群

onShareAppMessage(Object)

监听用户点击页面内转发按钮(<button> 组件 open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容。

注意:只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮

Object 参数说明:

参数 类型 说明
from String 转发事件来源。 button:页面内转发按钮; menu:右上角转发菜单
target Object 如果 from 值是 button,则 target 是触发这次转发事件的 button,否则为 undefined
webViewUrl String 页面中包含<web-view>组件时,返回当前<web-view>的url
1
2
3
4
5
6
7
8
9
10
// 分享
onShareAppMessage: function(res) {
var me = this;
var videoInfo = me.data.videoInfo;

return {
title: '短视频内容分享',
path: 'pages/videoinfo/videoinfo?videoInfo=' + JSON.stringify(videoInfo)
}
}

下载视频到本地

wx.downloadFile(OBJECT)

下载文件资源到本地,客户端直接发起一个 HTTP GET 请求,返回文件的本地临时路径;

OBJECT参数说明:

参数 类型 必填 必填
url String 是 下载资源的 url
header Object 否 HTTP 请求 Header,header 中不能设置 Referer
success Function 否 下载成功后以 tempFilePath 的形式传给页面,res = {tempFilePath: ‘文件的临时路径’}
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)

注:文件的临时路径,在小程序本次启动期间可以正常使用,如需持久保存,需在主动调用 wx.saveFile,才能在小程序下次启动时访问得到;

注:请在 header 中指定合理的 Content-Type 字段,以保证客户端正确处理文件类型;

wx.saveVideoToPhotosAlbum(OBJECT)

保存视频到系统相册

需要用户授权 scope.writePhotosAlbum

OBJECT参数说明:

参数名 类型 必填 说明
filePath String 是 视频文件路径,可以是临时文件路径也可以是永久文件路径
success Function 否 接口调用成功的回调函数
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (res.tapIndex == 0) {
// 下载
wx.showLoading({
title: '下载中',
});
wx.downloadFile({
url: app.serverUrl + me.data.videoInfo.videoPath,
success: function(res) {
// 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
if (res.statusCode === 200) {
wx.saveVideoToPhotosAlbum({
filePath: res.tempFilePath,
success:function(res) {
console.log(res.errMsg)
wx.hideLoading();
}
});
}
}
});
}

Wechat Mini | Note-8

发表于 2018-08-22

微信小程序开发 Note-8


搜索功能整合 & 首页联调

通过修改index.js中对于getAllVideoList,加入对应的参数,搜索内容和分页记录;

以达到搜索返回视频列表的目标;(上拉刷新和下拉刷新不进行刷新)

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
// 共用方法
getAllVideoList: function (page, isSaveRecord) {
var me = this;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '视频加载中...',
});
var searchContent = me.data.searchContent;
wx.request({
url: serverUrl + '/video/showAll?page=' + page + "&isSaveRecord=" + isSaveRecord,
method: 'POST',
data:{
videoDesc:searchContent
},
success: function(res) {
wx.hideLoading();
wx.hideNavigationBarLoading();
wx.stopPullDownRefresh();
console.log(res.data);
// 获取数据
// 判断当前页page是否为第一页,第一页则清空videoList
if (page === 1) {
me.setData({
videoList: []
});
}
var videoList = res.data.data.rows;
var newVideoList = me.data.videoList;
me.setData({
videoList: newVideoList.concat(videoList),
page: page,
totalPage: res.data.data.total,
serverUrl: serverUrl
});
}
});
}

热搜查询联调 & 视频对象播放与暂停

Q:对视频对象进行mute设置时,跳转到搜索页面和主页时,视频依然播放音乐;

A:在JS中进行相应的修改;

步骤: 在生命周期的onLoad(),获取videoContext,onShow()对视频进行播放,

​ 在跳转时,onHide()进行视频暂停;

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
const app = getApp()
Page({
data: {
cover:"cover"
},
videoCtx:{},
onLoad:function(){
var me = this;
me.videoCtx = wx.createVideoContext("myVideo", me);
},
onShow:function(){
var me = this;
me.videoCtx.play();
},
onHide:function(){
var me = this;
me.videoCtx.pause();
},
// 搜索
showSearch:function(){
wx.navigateTo({
url: '../searchVideo/searchVideo'
})
}
})

上传视频功能复用 & 测试

在pages同级目录下,创建一个utils,videoUtil.js

1
2
3
4
5
6
7
8
9
10
11
12
// videoUtil.js
// 视频上传-跳转至BGM选择页面
function uploadVideo() {
var me = this;
wx.chooseVideo({
...
});
}
// videoUtil.js 导出
module.exports = {
uploadVideo: uploadVideo
}
1
2
3
4
5
6
7
8
9
var videoUtil = require('../../utils/videoUtil.js') // 引用
const app = getApp()
Page({
...
// 上传视频
upload:function(){
videoUtil.uploadVideo();
}
})

首页进入视频展示页

从首页中的对象,获取到JSON数据,然后通过转换的形式,转换成字符串,传到videoinfo,在videoinfo中,再转换回JSON对象,进行数据的获取和赋值;

1
2
3
4
5
6
7
8
9
10
11
// index.js
// 具体的视频信息-跳转
showVideoInfo: function(e) {
var me = this;
var videoList = me.data.videoList;
var arrindex = e.target.dataset.arrindex;
var videoInfo = JSON.stringify(videoList[arrindex]);
wx.redirectTo({
url: '../videoinfo/videoinfo?videoInfo=' + videoInfo
})
}
1
2
3
4
5
6
7
8
9
10
11
12
// videoinfo.js
onLoad: function(params) {
var me = this;
me.videoCtx = wx.createVideoContext("myVideo", me);
// 获取上一页面传入的参数
var videoInfo = JSON.parse(params.videoInfo);
me.setData({
videoId: videoInfo.id,
src: app.serverUrl + videoInfo.videoPath,
videoInfo: videoInfo
});
}

页面重定向

对于没有登录的用户,可以搜索,但是不能进行个人信息页面的跳转和视频的上传;

所以需要对于没有登录的用户,进行页面的拦截和重定向;

1
2
3
4
5
6
7
8
9
10
11
12
showMine: function() {
var user = app.getGlobalUserInfo();
if (user == null || user == undefined || user == "") {
wx.navigateTo({
url: '../userLogin/login',
});
} else {
wx.navigateTo({
url: '../mine/mine',
});
}
}

在从视频详细页面跳转到登录页面之后,我们希望登录后,再次跳转回到之前的视频详细页面;

那么我们需要将原视频详细页面的信息传到登录页面,再进行页面的重定向跳转;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// videoInfo.js
Page({
// 上传视频
upload: function() {
var me = this;
var user = app.getGlobalUserInfo();
// 重定向数据定义
var videoInfo = JSON.stringify(me.data.videoInfo);
var realUrl = '../videoinfo/videoinfo#videoInfo@' + videoInfo;
if (user == null || user == undefined || user == "") {
wx.navigateTo({
url: '../userLogin/login?redirectUrl=' + realUrl,
});
} else {
videoUtil.uploadVideo();
}
}
})
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
// login.js
const app = getApp()

Page({
data: {

},
// 视频页面重定向跳转到登录页面,做参数的处理(接收与反传)
onLoad: function(params) {
var me = this;
var redirectUrl = params.redirectUrl;
redirectUrl = redirectUrl.replace(/#/g, "?");
redirectUrl = redirectUrl.replace(/@/g, "=");
me.redirectUrl = redirectUrl;
},
// 登录
doLogin: function(e) {
...
if (username.length == 0 || password.length == 0) {
...
} else {
...
// BackEnd
wx.request({
...
success: function(res) {
...
// app.userInfo = res.data.data;
// fixme 修改原有全局对象为本地缓存
app.setGlobalUserInfo(res.data.data);

// 页面跳转
var redirectUrl = me.redirectUrl;
if (redirectUrl != null && redirectUrl != undefined && redirectUrl != "") {
wx.redirectTo({
url: redirectUrl,
});
} else {
wx.redirectTo({
url: '../mine/mine',
});
}
} else if (status == 500) {
...
});
}
}
})
}
}
})

拦截器配置与注册

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
// 模拟拦截器配置(对所有请求进行拦截)
public class MiniInterceptor implements HandlerInterceptor {
/**
* 拦截请求判断,预处理(Controller之前)
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
if (true) {
System.out.println("请求拦截");
return false;
}
// 返回false:请求被拦截,返回
// 返回true:请求允许,继续执行
return true;
}

/**
* 请求Controller之后,渲染视图之前
*/
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}

/**
* 请求和视图渲染之后
*/
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 拦截器注册
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Bean
public MiniInterceptor miniInterceptor(){
return new MiniInterceptor();
}
/**
* 注册中心
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 对UserController进行拦截注册
registry.addInterceptor(miniInterceptor()).addPathPatterns("/user/**");
super.addInterceptors(registry);
}
}

完善登录拦截 & 限制单台手机登录

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
public class MiniInterceptor implements HandlerInterceptor {
@Autowired
private RedisOperator redis;
public static final String USER_REDIS_SESSION = "user-redis-session";

/**
* 拦截请求判断,预处理(Controller之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
String userId = request.getHeader("userId");
String userToken = request.getHeader("userToken");

if (StringUtils.isNotBlank(userId) && StringUtils.isNotBlank(userToken)) {
String uniqueToken = redis.get(USER_REDIS_SESSION + ":" + userId);
// Token超时限制的判断 重新登录
if (StringUtils.isEmpty(uniqueToken) && StringUtils.isBlank(uniqueToken)) {
System.out.println("请登录");
returnErrorResponse(response, IMoocJSONResult.errorTokenMsg("请登录"));
return false;
} else {
// 限制一台手机登录
if (!uniqueToken.equals(userToken)) {
System.out.println("帐号异点登录");
returnErrorResponse(response, IMoocJSONResult.errorTokenMsg("帐号异点登录"));
return false;
}
}
} else {
returnErrorResponse(response, IMoocJSONResult.errorTokenMsg("请登录"));
return false;
}
return true;
}

/**
* 公用返回JSON格式
*/
public void returnErrorResponse(HttpServletResponse response, IMoocJSONResult result) throws IOException, UnsupportedEncodingException {
OutputStream out = null;
try {
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
out = response.getOutputStream();
out.write(JsonUtils.objectToJson(result).getBytes("utf-8"));
out.flush();
} finally {
if (out != null) {
out.close();
}
}
}
}

拦截器联调

对于拦截器方法中,需要获取用户的USERID以及USERTOKEN,所以在用户请求时,在header中加入相应数据,以实现拦截器的功能;

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
Page({
// 加载
onLoad: function() {
...
// BackEnd
wx.request({
...
header: {
'content-type': 'application/json',
'userId': user.id,
'userToken': user.userToken
},
success: function(res) {
...
} else if (res.data.status == 502) {
wx.showToast({
title: res.data.msg,
icon: 'none',
duration: 2000,
success: function() {
wx.redirectTo({
url: '../userLogin/login',
});
}
});
}
}
});
}
})

点赞接口

非常简单的接口,不加以展示;

首先对Mapper进行方法的编写,然后修改对于的XML的语句;

在VideoServiceImpl进行对于方法的实现即可userLikeVideo & userUnLikeVideo;

以及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
41
42
43
44
45
46
47
48
49
50
51
52
var videoUtil = require('../../utils/videoUtil.js')
const app = getApp()
Page({
data: {
cover: "cover",
videoId: "",
src: "",
videoInfo: {},
userLikeVideo: false
},
// 点赞与取消点赞
likeVideoOrNot: function(e) {
var me = this;
var videoInfo = me.data.videoInfo;
var user = app.getGlobalUserInfo();
// 判断登录

if (user == null || user == undefined || user == "") {
wx.navigateTo({
url: '../userLogin/login',
});
} else {
// 点赞与取消点赞的request
var userLikeVideo = me.data.userLikeVideo;
var url = '/video/userLike?userId=' + user.id + "&videoId=" + videoInfo.id + "&videoCreaterId=" + videoInfo.userId;
if (userLikeVideo) {
// 取消点赞
url = '/video/userUnLike?userId=' + user.id + "&videoId=" + videoInfo.id + "&videoCreaterId=" + videoInfo.userId;
}
// request
var serverUrl = app.serverUrl;
wx.showLoading({
title: '提交中',
})
wx.request({
url: serverUrl + url,
method: 'POST',
header: {
'content-type': 'application/json',
'userId': user.id,
'userToken': user.userToken
},
success:function(res){
wx.hideLoading();
me.setData({
userLikeVideo: !userLikeVideo
});
}
})
}
}
})

视频展示页

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
var videoUtil = require('../../utils/videoUtil.js')
const app = getApp()
Page({
data: {
cover: "cover",
videoId: "",
src: "",
videoInfo: {},
userLikeVideo: false
},
onLoad: function(params) {
// 获取视频发布者信息,点赞关系
var serverUrl = app.serverUrl;
var user = app.getGlobalUserInfo();
var loginUserId = "";
if (user != null && user != undefined && user != "") {
loginUserId = user.id;
}
wx.request({
url: serverUrl + '/user/queryPublisher?loginUserId=' + loginUserId + "&videoId=" + videoInfo.id + "&publishUserId=" + videoInfo.userId,
method:'POST',
success:function(res){
console.log(res.data);
var publisher = res.data.data.publisher;
var userLikeVideo = res.data.data.userLikeVideo;
me.setData({
serverUrl: serverUrl,
publisher: publisher,
userLikeVideo: userLikeVideo
});
}
});
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// UserController
@ApiOperation(value = "QUERY VIDEO PUBLISHER INFORMATION", notes = "QUERY VIDEO PUBLISHER INFORMATION API")
@PostMapping("/queryPublisher")
public IMoocJSONResult queryPublisher(String loginUserId,String videoId,String publishUserId) {
if (StringUtils.isBlank(publishUserId)) {
return IMoocJSONResult.errorMsg("信息不存在(无ID)");
}
// 查询发布者用户个人信息
Users userInfo = userService.queryUserInfo(publishUserId);
UsersVO publisher = new UsersVO();
BeanUtils.copyProperties(userInfo, publisher);

// 查询登录用户与点赞关系
boolean userLikeVideo = userService.isUserLikeVideo(loginUserId,videoId);
PublisherVideo bean = new PublisherVideo();
bean.setPublisher(publisher);
bean.setUserLikeVideo(userLikeVideo);

return IMoocJSONResult.ok(bean);
}

Wechat Mini | Note-7

发表于 2018-08-21

微信小程序开发 Note-7


小程序首页视频列表

首页是底色加封面图,并不是可播放的视频;

自定义mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="queryAllVideos" resultMap="BaseResultMap" parameterType="String">
select v.*,u.face_image as face_image,u.nickname as nickname from videos v
left join users u on u.id = v.user_id
where
1 = 1
<if test=" videoDesc != null and videoDesc != '' ">
and v.video_desc like '%${videoDesc}%'
</if>
<if test=" userId != null and userId != '' ">
and v.user_id = #{userId}
</if>
and v.status = 1
order by v.create_time desc
</select>

视频列表分页查询接口

Pagehelper原理:

从数据库中select数据,在查询之前做一层拦截;

拦截后的select语句,将会由Ph做一个拼接,拼接成复合分页要求的select(limit)语句;

拼接出来的新select语句将替代原select,获取数据;

1
2
3
4
// 查询第几页,每页显示几条数据
PageHelper.startPage(page,pageSize);
List<User> list = userMapper.find();
PageInfo page = new PageInfo<list>;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;
@Autowired
private VideosMapperCustom videosMapperCustom;
@Override
public PagedResult getAllVideos(Integer page, Integer pageSize) {
PageHelper.startPage(page,pageSize);
List<VideosVO> list = videosMapperCustom.queryAllVideos();

PageInfo<VideosVO> pageList = new PageInfo<>(list);

PagedResult pagedResult = new PagedResult();
pagedResult.setPage(page);
pagedResult.setTotal(pageList.getPages());
pagedResult.setRows(list);
pagedResult.setRecords(pageList.getTotal());
return pagedResult;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Controller
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController extends BasicController {

@PostMapping(value = "/showAll")
public IMoocJSONResult showAll(Integer page) {
if(page == null){
page = 1;
}
PagedResult pagedResult = videoService.getAllVideos(page,PAGE_SIZE);
return IMoocJSONResult.ok(pagedResult);
}
}

首页分页联调

wx.getSystemInfoSync() - 获取系统信息同步接口

同步返回参数说明:

参数 说明
screenWidth 屏幕宽度
screenHeight 屏幕高度
windowWidth 可使用窗口宽度
windowHeight 可使用窗口高度
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
const app = getApp()
Page({
data: {
// 总页数
totalPage: 1,
// 当前页数
page: 1,
// 视频列表
videoList: [],
screenWidth: 350,
serverUrl: ""
},
onLoad: function(params) {
var me = this;
var screenWidth = wx.getSystemInfoSync().screenWidth;
me.setData({
screenWidth: screenWidth
});

// 获取当前的分页数
var page = me.data.page;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '视频加载中...',
});

wx.request({
url: serverUrl + '/video/showAll?page=' + page,
method:'POST',
success:function(res){
wx.hideLoading();
console.log(res.data);
// 获取数据
// 判断当前页page是否为第一页,第一页则清空videoList
if(page === 1){
me.setData({
videoList: []
});
}

var videoList = res.data.data.rows;
var newVideoList = me.data.videoList;

me.setData({
videoList: newVideoList.concat(videoList),
page:page,
totalPage: res.data.data.total,
serverUrl:serverUrl
});
}
})
}
})

首页——上拉分页

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
const app = getApp()
// 并且修改共用方法
Page({
data: {
// 总页数
totalPage: 1,
// 当前页数
page: 1,
// 视频列表
videoList: [],
screenWidth: 350,
serverUrl: ""
},
// 共用方法
getAllVideoList: function(page) {
var me = this;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '视频加载中...',
});
wx.request({
url: serverUrl + '/video/showAll?page=' + page,
method: 'POST',
success: function(res) {
wx.hideLoading();
console.log(res.data);
// 获取数据
// 判断当前页page是否为第一页,第一页则清空videoList
if (page === 1) {
me.setData({
videoList: []
});
}

var videoList = res.data.data.rows;
var newVideoList = me.data.videoList;

me.setData({
videoList: newVideoList.concat(videoList),
page: page,
totalPage: res.data.data.total,
serverUrl: serverUrl
});
}
});
}
// 上拉刷新
onReachBottom: function() {
var me = this;
var currentPage = me.data.page;
var totalPage = me.data.totalPage;
// 如果是最后一页,无须查询
if (currentPage === totalPage) {
wx.showToast({
title: '没有更多视频,请积极投稿',
icon: 'none'
});
return;
}
// 继续翻页
var page = currentPage + 1;
me.getAllVideoList(page);

},
// 具体的视频信息
showVideoInfo: function(e) {

}
})

首页——下拉刷新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const app = getApp()
Page({
// 共用方法
getAllVideoList: function(page) {
...
wx.request({
url: serverUrl + '/video/showAll?page=' + page,
method: 'POST',
success: function(res) {
// 隐藏刷新
wx.hideLoading();
wx.hideNavigationBarLoading();
wx.stopPullDownRefresh();
...
});
}
// 下拉刷新(需要在index.json启动事件)
onPullDownRefresh:function(){
wx.showNavigationBarLoading();
this.getAllVideoList(1);
}
})

视频组件与API

video 视频

属性名 类型 默认值 说明
src String 要播放视频的资源地址,支持云文件ID(2.2.3)
initial-time Number 指定视频初始播放位置
duration Number 指定视频时长
controls Boolean true 是否显示默认播放控件(播放/暂停按钮、播放进度、时间)
danmu-list Object Array 弹幕列表
danmu-btn Boolean false 是否显示弹幕按钮,只在初始化时有效,不能动态变更
enable-danmu Boolean false 是否展示弹幕,只在初始化时有效,不能动态变更
autoplay Boolean false 是否自动播放
loop Boolean false 是否循环播放
muted Boolean false 是否静音播放
page-gesture Boolean false 在非全屏模式下,是否开启亮度与音量调节手势
direction Number 设置全屏时视频的方向,不指定则根据宽高比自动判断。有效值为 0(正常竖向), 90(屏幕逆时针90度), -90(屏幕顺时针90度)
show-progress Boolean true 若不设置,宽度大于240时才会显示
show-fullscreen-btn Boolean true 是否显示全屏按钮
show-play-btn Boolean true 是否显示视频底部控制栏的播放按钮
show-center-play-btn Boolean true 是否显示视频中间的播放按钮
enable-progress-gesture Boolean true 是否开启控制进度的手势
wx.createVideoContext(videoId, this)

创建并返回 video 上下文 videoContext 对象。在自定义组件下,第二个参数传入组件实例this,以操作组件内 <video/>组件

videoContext

videoContext 通过 videoId 跟一个 video 组件绑定,通过它可以操作一个 video 组件


开源搜索视频组件

wsSearchView

该搜索框组件基于开源项目wxSearch 进行了改进,主要有以下几个修改点:

  • 增加了注释,修改了一些bug,项目可以跑起来。
  • 为了解决搜索框和输入法界面重叠的问题,将搜索组件作为一个独立的页面。
  • 修改了界面样式,更加美观。
  • 减少了暴露接口,复杂性更低。

搜索插件缓存

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
function search(inputValue) {
if (inputValue && inputValue.length > 0) {
// 添加历史记录
wxSearchAddHisKey(inputValue);
// 更新
var temData = __that.data.wxSearchData;
temData.value = inputValue;
__that.setData({
wxSearchData: temData
});
// 回调搜索
__searchFunction(inputValue);
}
}

// 添加本地缓存
function wxSearchAddHisKey(inputValue) {
if (!inputValue || inputValue.length == 0) {
return;
}
var value = wx.getStorageSync('wxSearchHisKeys');
if (value) {
if (value.indexOf(inputValue) < 0) {
value.unshift(inputValue);
}
wx.setStorage({
key: "wxSearchHisKeys",
data: value,
success: function() {
getHisKeys(__that);
}
})
} else {
value = [];
value.push(inputValue);
wx.setStorage({
key: "wxSearchHisKeys",
data: value,
success: function() {
getHisKeys(__that);
}
})
}
}

修改全局用户对象(使用缓存)

为了使用户每次打开小程序不需要重复的登录,根据官方文档的同步&异步方法的说明;

需要通过设置全局的用户对象,使用本地缓存的方式,解决这样的问题;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//app.js
App({
serverUrl: "ip",
userInfo: null,
setGlobalUserInfo:function(user){
wx.setStorageSync("userInfo", user);
},
getGlobalUserInfo: function () {
return wx.getStorageSync("userInfo");
}
})

// app.userInfo = res.data.data;
// fixme 修改原有全局对象为本地缓存
app.setGlobalUserInfo(res.data.data);
// fixme 修改原有全局对象为本地缓存
var userInfo = app.getGlobalUserInfo();
// 注销后,清空缓存
wx.removeStorageSync("userInfo");

查询接口完善 & 热搜词保存

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
// Service
package com.imooc.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.imooc.mapper.SearchRecordsMapper;
import com.imooc.mapper.VideosMapper;
import com.imooc.mapper.VideosMapperCustom;
import com.imooc.pojo.SearchRecords;
import com.imooc.pojo.Videos;
import com.imooc.pojo.vo.VideosVO;
import com.imooc.service.VideoService;
import com.imooc.utils.PagedResult;
import org.n3r.idworker.Sid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;
@Autowired
private VideosMapperCustom videosMapperCustom;
@Autowired
private SearchRecordsMapper searchRecordsMapper;
@Autowired
private Sid sid;

@Transactional(propagation = Propagation.REQUIRED)
@Override
public PagedResult getAllVideos(Videos video, Integer isSaveRecord, Integer page, Integer pageSize) {
// 保存热搜词记录
String desc = video.getVideoDesc();
if(isSaveRecord != null && isSaveRecord == 1){
SearchRecords record = new SearchRecords();
record.setId(sid.nextShort());
record.setContent(desc);
searchRecordsMapper.insert(record);
}
// 视频分页查询
PageHelper.startPage(page, pageSize);
List<VideosVO> list = videosMapperCustom.queryAllVideos(desc);

PageInfo<VideosVO> pageList = new PageInfo<>(list);

PagedResult pagedResult = new PagedResult();
pagedResult.setPage(page);
pagedResult.setTotal(pageList.getPages());
pagedResult.setRows(list);
pagedResult.setRecords(pageList.getTotal());
return pagedResult;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// VideosMapperCustom.xml
<select id="queryAllVideos" resultMap="BaseResultMap" parameterType="String">
select v.*,u.face_image as face_image,u.nickname as nickname from videos v
left join users u on u.id = v.user_id
where
1 = 1
<if test=" videoDesc != null and videoDesc != '' ">
and v.video_desc like '%${videoDesc}%'
</if>
and v.status = 1
order by v.create_time desc
</select>

热搜词查询接口

1
2
3
4
5
6
7
// SearchRecordsMapper.xml
<select id="getHotwords" resultType="String">
SELECT content
FROM search_records
GROUP BY content
ORDER BY count(content) DESC
</select>
1
2
3
4
5
6
7
8
9
10
11
12
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private SearchRecordsMapper searchRecordsMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public List<String> getHotwords() {
return searchRecordsMapper.getHotwords();;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// Controller
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController extends BasicController {
@Autowired
private VideoService videoService;

@PostMapping(value = "/hot")
public IMoocJSONResult hot() {
return IMoocJSONResult.ok(videoService.getHotwords());
}
}

热搜词联调

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
...  
// 2 搜索栏初始化(热搜词)
onLoad: function() {
var that = this;
// 查询热搜词
var serverUrl = app.serverUrl;
wx.request({
url: serverUrl+'/video/hot',
method:'POST',
success:function(res){
console.log(res);
var hotList = res.data.data;
// 搜索栏初始化
WxSearch.init(
that, // 本页面一个引用
// ['慕课网', "小程序", 'SpringBoot', 'linux'], 热点搜索推荐,[]表示不使用
hotList,
hotList, // 搜索匹配,[]表示不使用
that.mySearchFunction, // 提供一个搜索回调函数
that.myGobackFunction //提供一个返回回调函数
);
}
});
}...
})

Wechat Mini | Note-6

发表于 2018-08-20

微信小程序开发 Note-6

@ 2018年8月17日 22:51:33

上传短视频业务流程

用户选择视频(10s限制或即时拍摄);

BGM选择页面;

选择/不选择,并且输入描述(可为空);

上传(Controller);

保存视频截图(封面);

用户是否选择BGM;

​ -否——直接保存视频;

​ -是——合并视频和BGM成新视频,并保存;

用户选择视频

wx.chooseVideo(OBJECT)

拍摄视频或从手机相册中选视频,返回视频的临时文件路径。

OBJECT参数说明:

参数 类型 必填 说明
sourceType StringArray 否 album 从相册选视频,camera 使用相机拍摄,默认为:[‘album’, ‘camera’]
compressed Boolead 否 是否压缩所选的视频源文件,默认值为true,需要压缩
maxDuration Number 否 拍摄视频最长拍摄时间,单位秒。最长支持 60 秒
success Function 否 接口调用成功,返回视频文件的临时文件路径,详见返回参数说明
fail Function 否 接口调用失败的回调函数
complete 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
const app = getApp()
Page({
data: {
faceUrl: "../resource/images/noneface.png",
},
// 视频上传
uploadVideo: function() {
var me = this;
wx.chooseVideo({
sourceType: ['album'],
success: function(res) {
console.log(res);
var duration = res.duration;
var tempheight = res.height;
var tempwidth = res.width;
var tempVideoUrl = res.tempFilePath;
var tempCoverUrl = res.thumbTempFilePath;
if (duration > 11) {
wx.showToast({
title: '视频长度不能超过10秒',
icon: "none",
duration: 2500
});
} else if (duration < 3) {
wx.showToast({
title: '视频长度过短,请上传超过3秒以上的视频',
icon: "none",
duration: 2500
});
} else {
// TODO 打开BGM选择页面
}
}
});
}
})

后台BGM列表接口

1
2
3
4
5
6
7
8
9
10
11
12
// Service
@Service
public class BgmServiceImpl implements BgmService {
@Autowired
private BgmMapper bgmMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public List<Bgm> queryBgmList() {
return bgmMapper.selectAll();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Controller
@RestController
@Api(value = "BGM API", tags = {"BGM CONTROLLER"})
@RequestMapping("/bgm")
public class BgmController {
@Autowired
private BgmService bgmService;
@Autowired
private Sid sid;

@ApiOperation(value = "GET BGM LIST", notes = "GET BGM LIST API")
@PostMapping("/list")
public IMoocJSONResult list() {
return IMoocJSONResult.ok(bgmService.queryBgmList());
}
}

BGM页面联调 & 获取背景音乐列表

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
const app = getApp()
Page({
data: {
bgmList: [],
serverUrl: "",
},
onLoad: function() {
var me = this;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '加载背景音乐中...',
});
// BackEnd
wx.request({
url: serverUrl + '/bgm/list',
method: "POST",
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data);
wx.hideLoading();
if (res.data.status == 200) {
var bgmList = res.data.data;
me.setData({
bgmList: bgmList,
serverUrl: serverUrl
});
}
}
});
}
})
1
2
3
4
5
6
7
8
9
10
11
12
<view>
<form bindsubmit='upload'>
<radio-group name="bgmId">
<block wx:for="{{bgmList}}">
<view class='container'>
<audio name="{{item.name}}" author="{{item.author}}" src="{{serverUrl}}{{item.path}}" style="width:300px" id="{{item.id}}" controls loop></audio>
<radio style='margin-top:20px;' value=''></radio>
</view>
</block>
</radio-group>
</form>
</view>

开发上传短视频接口 & Swagger测试上传

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
// Controller
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController {

@ApiOperation(value = "UPLOAD VIDEO", notes = "UPLOAD VIDEO API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "bgmId", value = "背景音乐ID", dataType = "String", paramType = "query"),
@ApiImplicitParam(name = "videoSeconds", value = "视频长度", required = true, dataType = "double", paramType = "query"),
@ApiImplicitParam(name = "videoWidth", value = "视频宽度", required = true, dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "videoHeight", value = "视频高度", required = true, dataType = "int", paramType = "query"),
@ApiImplicitParam(name = "desc", value = "视频描述", dataType = "String", paramType = "query")
})
@PostMapping(value = "/upload",headers = "content-type=multipart/form-data")
public IMoocJSONResult upload(String userId, String bgmId,
double videoSeconds, int videoWidth, int videoHeight, String desc,
@ApiParam(value = "短视频",required = true) MultipartFile file) throws Exception {
if (StringUtils.isBlank(userId)) {
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
// 文件保存的命名空间
String fileSpace = "D:/xxx";
// 保存到数据库中的相对路径
String uploadPathDB = "/" + userId + "/video";

FileOutputStream fileOutputStream = null;
InputStream inputStream = null;

try {
if (file != null) {
String fileName = file.getOriginalFilename();
if (StringUtils.isNotBlank(fileName)) {
// 文件上传的最终保存绝对路径
String finalVideoPath = fileSpace + uploadPathDB + "/" + fileName;
// 设置数据库保存的路径
uploadPathDB += ("/" + fileName);

File outFile = new File(finalVideoPath);
if (outFile.getParentFile() != null || !outFile.getParentFile().isDirectory()) {
// 创建父文件夹
outFile.getParentFile().mkdirs();
}
fileOutputStream = new FileOutputStream(outFile);
inputStream = file.getInputStream();
IOUtils.copy(inputStream, fileOutputStream);
}
} else {
return IMoocJSONResult.errorMsg("视频上传错误");
}
} catch (Exception e) {
e.printStackTrace();
return IMoocJSONResult.errorMsg("视频上传错误");
} finally {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}
return IMoocJSONResult.ok();
}
}

视频临时参数传入下一个页面

1
2
3
4
5
6
7
8
// 打开BGM选择页面
wx.navigateTo({
url: '../chooseBgm/chooseBgm?duration=' + duration
+ "&tempHeight=" + tempHeight
+ "&tempWidth=" + tempWidth
+ "&tempVideoUrl=" + tempVideoUrl
+ "&tempCoverUrl=" + tempCoverUrl
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = getApp()
Page({
data: {
videoParams:{}
},
onLoad: function(params) {
console.log(params);
var me = this;
me.setData({
videoParams:params
});
},
// 在BGM选择页面,选择BGM后,上传视频
upload:function(e){
var me = this;
var bgmId = e.detail.value.bgmId;
var desc = e.detail.value.desc;
console.log(bgmId + "---" + desc);
}
})

小程序上传短视频联调

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
// Controler
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController {

@ApiOperation(value = "UPLOAD VIDEO", notes = "UPLOAD VIDEO API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "bgmId", value = "背景音乐ID", dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoSeconds", value = "视频长度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoWidth", value = "视频宽度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoHeight", value = "视频高度", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "desc", value = "视频描述", dataType = "String", paramType = "form")
})
@PostMapping(value = "/upload",headers = "content-type=multipart/form-data")
public IMoocJSONResult upload(String userId, String bgmId,
double videoSeconds, int videoWidth, int videoHeight, String desc,
@ApiParam(value = "短视频",required = true) MultipartFile file) throws Exception {
if (StringUtils.isBlank(userId)) {
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
// 文件保存的命名空间
String fileSpace = "D:/videos_dev_face";
// 保存到数据库中的相对路径
String uploadPathDB = "/" + userId + "/video";

FileOutputStream fileOutputStream = null;
InputStream inputStream = null;

try {
if (file != null) {
String fileName = file.getOriginalFilename();
if (StringUtils.isNotBlank(fileName)) {
// 文件上传的最终保存绝对路径
String finalVideoPath = fileSpace + uploadPathDB + "/" + fileName;
// 设置数据库保存的路径
uploadPathDB += ("/" + fileName);

File outFile = new File(finalVideoPath);
if (outFile.getParentFile() != null || !outFile.getParentFile().isDirectory()) {
// 创建父文件夹
outFile.getParentFile().mkdirs();
}
fileOutputStream = new FileOutputStream(outFile);
inputStream = file.getInputStream();
IOUtils.copy(inputStream, fileOutputStream);
}
} else {
return IMoocJSONResult.errorMsg("视频上传错误");
}
} catch (Exception e) {
e.printStackTrace();
return IMoocJSONResult.errorMsg("视频上传错误");
} finally {
if (fileOutputStream != null) {
fileOutputStream.flush();
fileOutputStream.close();
}
}

return IMoocJSONResult.ok();
}
}
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 app = getApp()

Page({
data: {
bgmList: [],
serverUrl: "",
videoParams: {}
},
// 在BGM选择页面,选择BGM后,上传视频
upload: function(e) {
var me = this;
var bgmId = e.detail.value.bgmId;
var desc = e.detail.value.desc;
console.log(bgmId + "---" + desc);

var duration = me.data.videoParams.duration;
var tmpHeight = me.data.videoParams.tmpHeight;
var tmpWidth = me.data.videoParams.tmpWidth;
var tmpVideoUrl = me.data.videoParams.tmpVideoUrl;
var tmpCoverUrl = me.data.videoParams.tmpCoverUrl;

console.log(tmpHeight + "," + tmpWidth);
console.log(tmpVideoUrl);
console.log(tmpCoverUrl);

wx.showLoading({
title: '视频上传中',
});
// 上传短视频
var serverUrl = app.serverUrl;
wx.uploadFile({
url: serverUrl + "/video/upload",
formData: {
userId: app.userInfo.id,
bgmId: bgmId,
videoSeconds: duration,
videoWidth: tmpHeight,
videoHeight: tmpWidth,
desc: desc
},
filePath: tmpVideoUrl,
name: 'file',
header: {
'content-type': 'application/json'
},
success: function(res) {
// var data = JSON.parse(res.data);
console.log(res);
wx.hideLoading();
if (res.status == 200) {
wx.showToast({
title: '视频上传成功',
icon: 'success',
duration: 3000
});
}
}
});
}
})

FFMPEG

视音频处理工具;跨平台的视音频处理解决方案;

播放器(暴风);转码工具(格式工厂);直播、视频加码、滤镜、水印、特效;

1
ffmpeg -i input.mp4 output.avi

JAVA & FFMPEG

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
public class FFMPEGTest {
private String ffmpegEXE;

public FFMPEGTest(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}

public void convertor(String videoInputPath, String videoOutputPath) throws IOException {
// cmd执行命令
List<String> command = new ArrayList<>();
command.add(ffmpegEXE);
command.add("-i");
command.add(videoInputPath);
command.add(videoOutputPath);
// 建立进程cmd
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
// 关闭流,避免资源浪费
InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);
String line = "";
while((line = br.readLine()) != null){
}
if(br != null){
br.close();
}
if(inputStreamReader != null){
inputStreamReader.close();
}
if(errorStream != null){
errorStream.close();
}
}
public static void main(String[] args) {
// 执行.exe的路径
FFMPEGTest ffmpeg = new FFMPEGTest("D:\\FFMEPG\\bin\\ffmpeg.exe");
try {
ffmpeg.convertor("D:\\test.mp4", "D:\\done.avi");
} catch (IOException e) {
e.printStackTrace();
}
}
}

FFMPEG操作视频 & BGM

1
ffmpeg.exe -i test.mp4 -i bgm.mp3 -t 10 -y done.mp4

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
// 在JAVA & FFMPEG的基础上,进行修改即可
public class MergeVideoMp3 {
private String ffmpegEXE;

public MergeVideoMp3(String ffmpegEXE) {
this.ffmpegEXE = ffmpegEXE;
}

public void convertor(String videoInputPath, String mp3InputPath, double seconds, String videoOutputPath) throws IOException {
// cmd执行命令 ffmpeg.exe -i test.mp4 -i bgm.mp3 -t 10 -y done.mp4
List<String> command = new ArrayList<>();
command.add(ffmpegEXE);
command.add("-i");
command.add(videoInputPath);
command.add("-i");
command.add(mp3InputPath);
command.add("-t");
command.add(String.valueOf(seconds));
command.add("-y");
command.add(videoOutputPath);
...
}

public static void main(String[] args) {
// 执行.exe的路径
MergeVideoMp3 ffmpeg = new MergeVideoMp3("D:\\FFMEPG\\bin\\ffmpeg.exe");
try {
ffmpeg.convertor("D:\\test.mp4", "D:\\bgm.mp3", 10, "D:\\done.mp4");
} catch (IOException e) {
e.printStackTrace();
}
}
}

保存视频信息 & 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;
@Autowired
private Sid sid;
@Transactional(propagation = Propagation.REQUIRED)
@Override
public String saveVideo(Videos video) {
video.setId(sid.nextShort());
videosMapper.insertSelective(video);
return video.getId();
}
}
1
2
3
4
// Controller
// 具体代码与头像上传和保存到数据库基本类似;
// 代码过长,不进行发表;
// 详情看github;

上传封面图 & 数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Service
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideosMapper videosMapper;

@Transactional(propagation = Propagation.REQUIRED)
@Override
public void updateVideo(String videoId, String coverPath) {
Videos video = new Videos();
video.setId(videoId);
video.setCoverPath(coverPath);
videosMapper.updateByPrimaryKeySelective(video);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Controller
// 具体代码与头像上传和保存到数据库基本类似;
@RestController
@Api(value = "VIDEO API", tags = {"VIDEO CONTROLLER"})
@RequestMapping("/video")
public class VideoController extends BasicController {

@Autowired
private VideoService videoService;
@ApiOperation(value = "UPLOAD COVER PIC", notes = "UPLOAD COVER PIC API")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "form"),
@ApiImplicitParam(name = "videoId", value = "视频ID", required = true, dataType = "String", paramType = "form")
})
@PostMapping(value = "/uploadCover", headers = "content-type=multipart/form-data")
public IMoocJSONResult uploadCover(String userId, String videoId, @ApiParam(value = "封面", required = true) MultipartFile file) throws Exception {
...
videoService.updateVideo(videoId,uploadPathDB);
return IMoocJSONResult.ok();
}
}

小程序上传视频业务流程联调

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
// 封面图上传 补充
const app = getApp()
Page({
data: {
bgmList: [],
serverUrl: "",
videoParams: {}
},
// 在BGM选择页面,选择BGM后,上传视频
upload: function(e) {

wx.showLoading({
title: '视频上传中',
});
// 上传短视频
...
success: function(res) {
var data = JSON.parse(res.data);
wx.hideLoading();
if (data.status == 200) {
var videoId = data.data;
// 封面图上传
wx.showLoading({
title: '封面图上传中',
});
wx.uploadFile({
url: serverUrl + "/video/uploadCover",
formData: {
userId: app.userInfo.id,
videoId: videoId
},
filePath: tmpCoverUrl,
name: 'file',
header: {
'content-type': 'application/json'
},
success: function(res) {
var data = JSON.parse(res.data);
wx.hideLoading();
if (data.status == 200) {
wx.showToast({
title: '上传成功',
icon: "success"
});
wx.navigateBack({
delta: 1,
});
} else {
wx.showToast({
title: '上传失败',
icon: "none"
});
}
}
});
} else {
wx.showToast({
title: '上传失败',
icon: "none"
});
}
}
});
}
})

联调手机端 & 坑

在使用手机端测试时,res所返回的数据与PC端返回的数据不同;

手机端所返回的数据中,不包含封面图的数据信息;

所以,才用FFMPEG生成截图的方式,生成封面图;

使用FFMPEG生成截图

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
public class FetchVideoCover {
// 视频路径
private String ffmpegEXE;

public void getCover(String videoInputPath, String coverOutputPath) throws IOException {
// ffmpeg.exe -ss 00:00:01 -i spring.mp4 -vframes 1 bb.jpg
List<String> command = new java.util.ArrayList<String>();
command.add(ffmpegEXE);

// 指定截取第1秒
command.add("-ss");
command.add("00:00:01");

command.add("-y");
command.add("-i");
command.add(videoInputPath);

command.add("-vframes");
command.add("1");

command.add(coverOutputPath);

ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();

InputStream errorStream = process.getErrorStream();
InputStreamReader inputStreamReader = new InputStreamReader(errorStream);
BufferedReader br = new BufferedReader(inputStreamReader);

String line = "";
while ((line = br.readLine()) != null) {
}

if (br != null) {
br.close();
}
if (inputStreamReader != null) {
inputStreamReader.close();
}
if (errorStream != null) {
errorStream.close();
}
}
}

上传视频流程 & 整合视频截图功能

上传流程联调

在上传视频的同时,制作封面图,并且将封面图的CoverPath存储到数据库即可;

就解决了PC和手机传回数据不同的问题,不再需要二次request的过程;

1
2
3
// 产生视频封面
FetchVideoCover videoInfo = new FetchVideoCover(FFMPEG_EXE);
videoInfo.getCover(finalVideoPath, FILE_SPACE + coverPathDB);

@ 2018年8月19日 19:47:47

Wechat Mini | Note-5

发表于 2018-08-19

微信小程序开发 Note-5

@ 2018年8月17日 15:15:32

注销接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@Api(value = "LOGIN & REGISTER API", tags = {"LOGIN & REGISTER CONTROLLER"})
public class RegistLoginController extends BasicController {

@ApiOperation(value = "LOGOUT", notes = "LOGOUT API")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query")
@PostMapping("/logout")
public IMoocJSONResult logout(String userId) throws Exception {
redis.del(USER_REDIS_SESSION + ":" + userId);
return IMoocJSONResult.errorMsg("注销成功");
}

}

注销联调

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
const app = getApp()
Page({
data: {
faceUrl: "../resource/images/noneface.png",
},
// 加载
onLoad: function(params) {
},
// 注销
logout: function(e) {
var user = app.userInfo;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '注销中',
});
// BackEnd
wx.request({
url: serverUrl + '/logout?userId=' + user.id,
method: "POST",
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data);
wx.hideLoading();
if (res.data.status == 200) {
wx.showToast({
title: '注销成功',
icon: 'success',
duration: 2000
});
app.userInfo = null;
// 页面跳转Login
wx.redirectTo({
url: '../userLogin/login',
});
}
}
});
}
})

用户头像接口

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
@RestController
@Api(value = "USER FUNCTION API", tags = {"USER FUNCTION CONTROLLER"})
@RequestMapping("/user")
public class UserController extends BasicController {
@ApiOperation(value = "USER UPLOAD FACE IMG", notes = "USER UPLOAD FACE IMG API")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query")
@PostMapping("/uploadFace")
public IMoocJSONResult uploadFace(String userId, @RequestParam("file") MultipartFile[] files) throws Exception {
// 文件保存的命名空间
String fileSpace = "D:/videos_dev_face";
// 保存到数据库中的相对路径
String uploadPathDB = "/" + userId + "/face";

FileOutputStream fileOutputStream = null;
InputStream inputStream = null;

try {
if (files != null && files.length > 0) {
String fileName = files[0].getOriginalFilename();
if (StringUtils.isNotBlank(fileName)) {
// 文件上传的最终保存绝对路径
String finalFacePath = fileSpace + uploadPathDB + "/" + fileName;
// 设置数据库保存的路径
uploadPathDB += ("/" + fileName);

File outFile = new File(finalFacePath);
if (outFile.getParentFile() != null || !outFile.getParentFile().isDirectory()) {
// 创建父文件夹
outFile.getParentFile().mkdirs();
}
fileOutputStream = new FileOutputStream(outFile);
inputStream = files[0].getInputStream();
IOUtils.copy(inputStream, fileOutputStream);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
if(fileOutputStream != null){
fileOutputStream.flush();
fileOutputStream.close();
}
}
return IMoocJSONResult.ok();
}

}

用户头像联调

wx.chooseImage(OBJECT)

从本地相册选择图片或使用相机拍照;

OBJECT参数说明:

参数 类型 必填 说明
count Number 否 最多可以选择的图片张数,默认9
sizeType StringArray 否 original 原图,compressed 压缩图,默认二者都有
sourceType StringArray 否 album 从相册选图,camera 使用相机,默认二者都有
success Function 是 成功则返回图片的本地文件路径列表 tempFilePaths
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)
wx.uploadFile(OBJECT)

将本地资源上传到开发者服务器,客户端发起一个 HTTPS POST 请求,其中 content-type 为 multipart/form-data ;

OBJECT参数说明:

参数 类型 必填 说明
url String 是 开发者服务器 url
filePath String 是 要上传文件资源的路径
name String 是 文件对应的 key , 开发者在服务器端通过这个 key 可以获取到文件二进制内容
header Object 否 HTTP 请求 Header, header 中不能设置 Referer
formData Object 否 HTTP 请求中其他额外的 form data
success Function 否 接口调用成功的回调函数
fail Function 否 接口调用失败的回调函数
complete 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
const app = getApp()
Page({
// 头像上传
changeFace: function(e) {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album'],
success: function(res) {
var tempFilePaths = res.tempFilePaths;
console.log(tempFilePaths);

wx.showLoading({
title: '上传中...',
});

// 上传
var serverUrl = app.serverUrl;
wx.uploadFile({
url: serverUrl + "/user/uploadFace?userId=" + app.userInfo.id,
filePath: tempFilePaths[0],
name: 'file',
header: {
'content-type': 'application/json'
},
success: function(res) {
var data = res.data
console.log(data);
wx.hideLoading();
wx.showToast({
title: '头像上传成功',
icon:'success',
duration:3000
})
}
});
}
});
}
})

用户头像更新到数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 添加Service
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UsersMapper userMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public void updateUserInfo(Users user) {
Example userExample = new Example(Users.class);
Example.Criteria criteria = userExample.createCriteria();
criteria.andEqualTo("id",user.getId());
userMapper.updateByExampleSelective(user,userExample);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 修改Controller
@RestController
@Api(value = "USER FUNCTION API", tags = {"USER FUNCTION CONTROLLER"})
@RequestMapping("/user")
public class UserController extends BasicController {
@Autowired
private UserService userService;

@ApiOperation(value = "USER UPLOAD FACE IMG", notes = "USER UPLOAD FACE IMG API")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query")
@PostMapping("/uploadFace")
public IMoocJSONResult uploadFace(String userId, @RequestParam("file") MultipartFile[] files) throws Exception {
if(StringUtils.isBlank(userId)){
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
...
// 上传成功,更新数据库头像信息
Users user = new Users();
user.setId(userId);
user.setFaceImage(uploadPathDB);
userService.updateUserInfo(user);
return IMoocJSONResult.ok();
}
}

SpringBoot静态资源配置,显示图片

通过虚拟目录的功能,创建一个配置类,继承WebMvcConfigurerAdapter,重写方法addResourceHandlers;

1
2
3
4
5
6
7
8
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("file:D:/xxx/");
}
}

启动项目后,即可通过访问http://localhost:8081/123/face/imooc.jpg的方式,获取到静态资源文件;

@问题,配置后,http://localhost:8081/swagger-ui.html 无法访问,报404;

需要重新配置;

1
2
3
4
5
6
7
8
9
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/META-INF/resources/")
.addResourceLocations("file:D:/xxx/");
}
}

小程序展示头像以及手机端调试

现在完成页面访问静态资源文件的功能,既可以完成小程序中展示头像的功能;

在上传完成文件后,并且更新数据库信息,需要给小程序返回相对路径的地址;

@success返回的data并不是JSON格式,而是String类型;

需要将data转换成JSON格式,在小程序前端实现格式化;

1
2
3
4
5
6
7
8
9
success: function(res) {
var data = JSON.parse(res.data);
...
// 获取返回的头像相对路径
var imageUrl = data.data;
me.setData({
faceUrl: serverUrl + imageUrl
});
}

使用预览模式,并且打开调试模式,即可完成效果;


查询用户信息接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Service
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UsersMapper userMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public Users queryUserInfo(String userId) {
Example userExample = new Example(Users.class);
Example.Criteria criteria = userExample.createCriteria();
criteria.andEqualTo("id",userId);
Users user = userMapper.selectOneByExample(userExample);
return user;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Controller
@RestController
@Api(value = "USER FUNCTION API", tags = {"USER FUNCTION CONTROLLER"})
@RequestMapping("/user")
public class UserController extends BasicController {
@Autowired
private UserService userService;

@ApiOperation(value = "QUERY USER INFORMATION", notes = "QUERY USER INFORMATION API")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "String", paramType = "query")
@PostMapping("/query")
public IMoocJSONResult query(String userId) {
if (StringUtils.isBlank(userId)) {
return IMoocJSONResult.errorMsg("用户不存在(无ID)");
}
Users userInfo = userService.queryUserInfo(userId);
UsersVO usersVO = new UsersVO();
BeanUtils.copyProperties(userInfo, usersVO);

return IMoocJSONResult.ok(usersVO);
}
}

用户信息展示联调

实现了后台的API后,对小程序进行数据的读取和设置;

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
const app = getApp()
Page({
data: {
faceUrl: "../resource/images/noneface.png",
},
// 加载
onLoad: function() {
var me = this;
var user = app.userInfo;
var serverUrl = app.serverUrl;
wx.showLoading({
title: '加载中...',
});
// BackEnd
wx.request({
url: serverUrl + '/user/query?userId=' + user.id,
method: "POST",
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data);
wx.hideLoading();
if (res.data.status == 200) {
var userInfo = res.data.data;
var faceUrl = "../resource/images/noneface.png";
// 头像判空
if (userInfo.faceImage != null && userInfo.faceImage != "" && userInfo.faceImage != undefined) {
faceUrl = serverUrl + userInfo.faceImage;
}
// 个人信息设置
me.setData({
faceUrl: faceUrl,
fansCounts: userInfo.fansCounts,
followCounts: userInfo.followCounts,
receiveLikeCounts: userInfo.receiveLikeCounts,
nickname: userInfo.nickname
});
}
}
});
}
})

@ 2018年8月17日 22:34:27

Wechat Mini | Note-4

发表于 2018-08-18

微信小程序开发 Note-4

@ 2018年8月16日 16:23:30


数据库设计

背景音乐表(bgm)

评论表(comments)

搜索记录(search_records)

用户表(users)

关注(粉丝)表(users_fans)

用户点赞视频表(users_like_videos)

用户举报表(users_report)

视频表(videos)


项目初始化

先进行前端小程序页面的设计,修改工程目录结构,保留必备文件即可;

映入静态资源文件 resource;

将后台项目工程,采用分层的目录结构,方便工程的后期维护和管理;


Mybatis逆向生成工具

这个工具,是生成表的实体对象,实体mapper,以及实体.xml文件的工具;

我觉得这个工具生成的文件和使用的方式,过于相似SSM的开发过程,不推荐使用,由于课程的技术,无法修改;

使用了SpringBoot开发框架的话,应该推荐使用JpaRepository的方式实现与数据持续层的访问;

在搭建分层工程结构的时候,遇到以下问题:

Q:JAR包冲突

A:删除对应JAR包、升级对应JAR包;

Q:由于是分层搭建工程,出现Caused by: java.lang.IllegalStateException: Found multiple @SpringBootConfiguration annotated classes [Generic bean: class [...]; scope=; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null;这样的错误

A:由于是分层工程,并且是父子关系的pom继承关系,存在@SpringBootTest的class路径的自动查询出现问题,需要在每个子工程的@SpringBootTest中指明明确的class路径;

例如

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@SpringBootTest
public class VideosDevServiceApplicationTests {
...
}
// 修改为
@RunWith(SpringRunner.class)
@SpringBootTest(classes = VideosDevServiceApplication.class)
public class VideosDevServiceApplicationTests {
...
}

用户注册接口

第一,我为上面说这是一个类似SSM框架不推荐的话,掌嘴;

第二,使用插件(idworker);

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
// RegisterLoginController
@RestController
public class RegisterLoginController {
@Autowired
private UserService userService;

@PostMapping("/regist")
public IMoocJSONResult regist(@RequestBody Users user) throws Exception {
// 1.判断用户名和密码不为空
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
return IMoocJSONResult.errorMsg("用户名和密码不能为空");
}
// 2.判断用户是否存在
boolean usernameIsExist = userService.queryUsernameIsExist(user.getUsername());
if(!usernameIsExist){
user.setNickname(user.getUsername());
user.setPassword(MD5Utils.getMD5Str(user.getPassword()));
user.setFansCounts(0);
user.setReceiveLikeCounts(0);
user.setFollowCounts(0);
userService.saveUser(user);
}else{
return IMoocJSONResult.errorMsg("用户名已存在");
}
// 3.保存用户,注册信息
return IMoocJSONResult.ok();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UsersMapper usersMapper;
@Autowired
private Sid sid;

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public boolean queryUsernameIsExist(String username) {
Users user = new Users();
user.setUsername(username);
Users result = usersMapper.selectOne(user);
return result != null;
}

@Override
@Transactional(propagation = Propagation.REQUIRED)
public void saveUser(Users user) {
user.setId(sid.nextShort());
usersMapper.insert(user);
}
}

Swagger2使用与restful接口测试

可以生成文档形式的api,并提供给不同的团队;

便于自我测试,也便于查阅;

无须过多冗余的word文档;

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
@Configuration
@EnableSwagger2
public class Swagger2 {
/**
* swagger2的配置文件,扫描的包等
* @return
*/
@Bean
public Docket createResApi(){
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.basePackage("top.rex.videos.controller"))
.paths(PathSelectors.any()).build();
}

/**
* 构建API文档的信息
* @return
*/
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Swagger API Document")
.contact(new Contact("REX", "https://reeeexchen.github.io/", "dy708484@163.com"))
.description("This Is About The Description")
.version("1.0.0").build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对于Controller的API注释
@RestController
@Api(value = "User Login & Register",tags = {"Login & Register Controller"})
public class RegisterLoginController {
@ApiOperation(value = "Register",notes = "Register")
@PostMapping("/regist")
public IMoocJSONResult regist(@RequestBody Users user) throws Exception {
...
}
}
// 对于Users实体的API注释
@ApiModel(value="Users Entity", description="This Is Users Obj")
public class Users {

@ApiModelProperty(hidden=true)
@Id
private String id;

@ApiModelProperty(value="Username", name="username", example="imoocuser", required=true)
private String username;

@ApiModelProperty(value="Password", name="password", example="123456", required=true)
private String password;
}

@太心累了,用Mybatis配置扫描包的操作,分层工程结构下的包路径,总是出错,无从下手;

@2018年8月17日 13:36:47

从昨晚开始,启动之后做测试,一直报出一个错误

[java.lang.IncompatibleClassChangeError: Implementing class]

我这个错误有点特殊,跟网上能找到的不一样,弄了4个小时之后,终于解决;

主要的问题依然是jar包;由于之前尝试运行一个含有druid的项目,一直也是类似的错误;

这次对机器上的JAVA进行了升级;

JRE和JDK从1.8.0_111升级到了1.8.0_181(JAVA1.8最新版本),问题解决;


用户注册联调

配置流程

服务器域名请在 小程序后台-设置-开发设置-服务器域名 中进行配置,配置时需要注意:

  • 域名只支持 https (request、uploadFile、downloadFile) 和 wss (connectSocket) 协议;
  • 域名不能使用 IP 地址或 localhost
  • 域名必须经过 ICP 备案;
  • 出于安全考虑,api.weixin.qq.com 不能被配置为服务器域名,相关API也不能在小程序内调用。开发者应将 appsecret 保存到后台服务器中,通过服务器使用 appsecret 获取 accesstoken,并调用相关 API。
  • 对于每个接口,分别可以配置最多 20 个域名

注意将微信开发工具中的,不进行证书校验打开

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
const app = getApp()
Page({
data: {
},
doRegist:function(e){
var formObject = e.detail.value;
var username = formObject.username;
var password = formObject.password;
// 简单的前端验证
if(username.length == 0 || password.length == 0){
wx.showToast({
title: '用户名或密码不能为空',
icon:'none',
duration:3000
})
}else{
var serverUrl = app.serverUrl;
wx.showLoading({
title: '提交中',
});
wx.request({
url: serverUrl + '/regist',
method:"POST",
data:{
username:username,
password:password
},
header:{
'content-type':'application/json'
},
success:function(res){
console.log(res.data);
wx.hideLoading();
var status = res.data.status;
if(status == 200){
wx.showToast({
title: '用户注册成功',
icon: 'success',
duration: 3000
}),
app.userInfo = res.data.data;
}else if(status == 500){
wx.showToast({
title: res.data.msg,
icon: 'none',
duration: 3000
})
}
}
})
}
}
})

用户登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UsersMapper userMapper;

@Transactional(propagation = Propagation.SUPPORTS)
@Override
public Users queryUserForLogin(String username, String password) {
Example userExample = new Example(Users.class);
Example.Criteria criteria = userExample.createCriteria();
criteria.andEqualTo("username",username);
criteria.andEqualTo("password",password);
Users result = userMapper.selectOneByExample(userExample);
return result;
}
}
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
@RestController
@Api(value = "LOGIN & REGISTER API", tags = {"LOGIN & REGISTER CONTROLLER"})
public class RegistLoginController {
@Autowired
private UserService userService;

@ApiOperation(value = "LOGIN", notes = "LOGIN API")
@PostMapping("/login")
public IMoocJSONResult login(@RequestBody Users user) throws Exception {
String username = user.getUsername();
String password = user.getPassword();
// 1.判断用户名和密码不为空
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
return IMoocJSONResult.errorMsg("用户名和密码不能为空");
}
// 2.判断用户是否存在
Users userResult = userService.queryUserForLogin(username,MD5Utils.getMD5Str(password));
// 3.返回用户信息
if(userResult != null){
userResult.setPassword("");
return IMoocJSONResult.ok(userResult);
}else{
return IMoocJSONResult.errorMsg("用户名或密码错误,请重试");
}
}
}
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
const app = getApp()
Page({
data: {
},
// 登录
doLogin: function(e) {
var me = this;
var formObject = e.detail.value;
var username = formObject.username;
var password = formObject.password;
// 简单的前端验证
if (username.length == 0 || password.length == 0) {
wx.showToast({
title: '用户名或密码不能为空',
icon: 'none',
duration: 3000
});
} else {
var serverUrl = app.serverUrl;
wx.showLoading({
title: '登录中',
});
// BackEnd
wx.request({
url: serverUrl + '/login',
method: "POST",
data: {
username: username,
password: password
},
header: {
'content-type': 'application/json'
},
success: function(res) {
console.log(res.data);
wx.hideLoading();
var status = res.data.status;
if (status == 200) {
wx.showToast({
title: '登录成功',
icon: 'success',
duration: 2000
});
app.userInfo = res.data.data;
// TODO 页面跳转
} else if (status == 500) {
wx.showToast({
title: res.data.msg,
icon: 'none',
duration: 3000
});
}
}
})
}
}
})

有状态会话与无状态会话基本概念

有状态session:每个用户访问都会产生一个session;

无状态session:app的用户访问服务时,不产生session;

redis-session:以通过无状态session,用redis的方式维护一个持续的会话状态;用户访问服务时,将用户的信息以JSON的形式保存到redis的缓存中;

redis-session好处

用户信息存储到redis缓存中,形成无状态会话;

便于扩展,当单体应用该扩展成集群会更方便;

便于权限认证;


redis

之前已经在我的阿里云服务器中搭建过redis-server,在此不再记录,详细参考网上的搭建教程;

基本命令

1
2
3
4
redis-cli.exe -h xxx.xxx.xxx.xxx -p 6379 -a xxx
ping
set key value
get key

开发用户redis-session

1
2
3
4
5
6
7
@RestController
public class BasicController {
@Autowired
public RedisOperator redis;

public static final String USER_REDIS_SESSION = "user-redis-session";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@Api(value = "LOGIN & REGISTER API", tags = {"LOGIN & REGISTER CONTROLLER"})
public class RegistLoginController extends BasicController{

@Autowired
private UserService userService;

/**
* 设置用户登录与注册的redis-session-token
* @param userModel
* @return
*/
public UsersVO setUserRedisSessionToken(Users userModel){
// REDIS SESSION
String uniqueToken = UUID.randomUUID().toString();
redis.set(USER_REDIS_SESSION + ":" + userModel.getId(),uniqueToken,1000 * 60 * 30);

UsersVO usersVO = new UsersVO();
BeanUtils.copyProperties(userModel,usersVO);
usersVO.setUserToken(uniqueToken);
return usersVO;
}
}

@ 2018年8月17日 15:15:10

Wechat Mini | Note-3

发表于 2018-08-17

微信小程序开发 Note-3

@ 2018年8月16日 11:23:54


button

button 按钮

属性名 类型 默认值 说明 生效时机 最低版本
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否禁用
loading Boolean false 名称前是否带 loading 图标
form-type String 用于 <form/> 组件,点击分别会触发 <form/> 组件的 submit/reset 事件
hover-class String button-hover 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-stop-propagation Boolean false 指定是否阻止本节点的祖先节点出现点击态

checkbox & label

checkbox 多选框

checkbox-group 多项选择器,内部由多个checkbox组成;

check双标签自带label ;

属性名 类型 默认值 说明
value String <checkbox/>标识,选中时触发<checkbox-group/>的 change 事件,并携带 <checkbox/> 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color Color checkbox的颜色,同css的color

label 标签

用来改进表单组件的可用性,使用for属性找到对应的id,或者将控件放在该标签下,当点击时,就会触发对应的控件;

目前可以绑定的控件有:<button/>, <checkbox/>, <radio/>, <switch/>;

属性名 类型 说明
for String 绑定控件的 id
1
2
3
4
5
6
7
8
9
10
<checkbox-group>
<label wx:for="{{array}}">
<checkbox id="{{item.id}}" value="{{item.value}}"
checked="{{item.checked}}" color="{{item.color}}" disabled="{{item.disabled}}">
{{item.name}}
</checkbox>
</label>
</checkbox-group>

<label for="1001">测试点击</label>
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
Page({
data: {
array: [{
id:"1001",
name: "中国",
value: "CHINA",
checked: true,
color: "red",
disabled: false
},
{
id: "1002",
name: "美国",
value: "USA",
checked: false,
color: "blue",
disabled: false
},
{
id: "1003",
name: "俄罗斯",
value: "RUS",
checked: true,
color: "pink",
disabled: false
}
]
}
})

submit & reset

form 表单

属性名 类型 说明
report-submit Boolean 是否返回 formId 用于发送模板消息
bindsubmit EventHandle 携带 form 中的数据触发 submit 事件,event.detail = {value : {‘name’: ‘value’} , formId: ‘’}
bindreset EventHandle 表单重置时会触发 reset 事件

input

input 输入框

属性名 类型 默认值 说明
value String 输入框的初始内容
type String text input 的类型
password Boolean false 是否是密码类型
placeholder String 输入框为空时占位符
placeholder-style String 指定 placeholder 的样式
placeholder-class String “input-placeholder” 指定 placeholder 的样式类
cursor-spacing Number 0 指定光标与键盘的距离,单位 px 。取 input 距离底部的距离和 cursor-spacing 指定的距离的最小值作为光标与键盘的距离
focus Boolean false 获取焦点
confirm-type String “done” 设置键盘右下角按钮的文字,仅在type=’text’时生效
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
selection-start Number -1 光标起始位置,自动聚集时有效,需与selection-end搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与selection-start搭配使用
adjust-position Boolean true 键盘弹起时,是否自动上推页面
bindinput EventHandle 键盘输入时触发,event.detail = {value, cursor, keyCode},keyCode 为键值,处理函数可以直接 return 一个字符串,将替换输入框的内容。
bindfocus EventHandle 输入框聚焦时触发,event.detail = { value, height },height 为键盘高度
bindblur EventHandle 输入框失去焦点时触发,event.detail = {value: value}
bindconfirm EventHandle 点击完成按钮时触发,event.detail = {value: value}

type 有效值:

值 说明
text 文本输入键盘
number 数字输入键盘
idcard 身份证输入键盘
digit 带小数点的数字键盘

confirm-type 有效值:

值 说明
send 右下角按钮为“发送”
search 右下角按钮为“搜索”
next 右下角按钮为“下一个”
go 右下角按钮为“前往”
done 右下角按钮为“完成”
1
2
3
4
5
6
7
8
<input value="学习小程序" maxlength='10' cursor-spacing='10' confirm-type="next" confirm-hold="true" bindinput='inputEvent' bindfocus='focusEvent' bindblur='blurEvent' bindconfirm='confirmEvent'></input>
<input value="number" type='number' focus='true' selection-start="2" selection-end="4" adjust-position="false"></input>
<input value="idcard" type='idcard'></input>
<input value="digit" type='digit'></input>
密码框:
<input value="" password="true"></input>
placeholder:
<input value="" placeholder="请输入你的用户名" placeholder-style="color:green" placeholder-class="placeholder"></input>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Page({
inputEvent: function() {
console.log("inputEvent");
return "imooc";
},
focusEvent: function () {
console.log("focusEvent");
},
blurEvent: function () {
console.log("blurEvent");
},
confirmEvent: function () {
console.log("confirmEvent");
},
})

picker

picker 选择器

从底部弹起的滚动选择器,现支持五种选择器,通过mode来区分,分别是普通选择器,多列选择器,时间选择器,日期选择器,省市区选择器,默认是普通选择器;

普通选择器:mode = selector
属性名 类型 默认值 说明
range Array / Object Array [] mode为 selector 或 multiSelector 时,range 有效
range-key String 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
bindcancel EventHandle 取消选择或点遮罩层收起 picker 时触发
bindchange EventHandle value 改变时触发 change 事件,event.detail = {value: value}
1
2
3
4
5
6
7
8
<picker mode="selector" range="{{array}}">
请选择数组
</picker>

<picker mode="selector" range="{{arrayObj}}" range-key="name"
bindcancel="cancelme" bindchange="changeme">
<view>{{showme}}</view>
</picker>
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
Page({
data:{
array:[1,2,3,4,5,6,7,8,9,0],
arrayObj:[
{id:"1001",name:"jack"},
{ id: "1002", name: "tom" },
{ id: "1003", name: "mary" },
{ id: "1004", name: "bob" },
{ id: "1005", name: "peter" }
],
showme:"请选择人名"
},
changeme:function(e){
var index = e.detail.value;
console.log("value is = " + index);
var id = this.data.arrayObj[index].id;
var name = this.data.arrayObj[index].name;
this.setData({
showme:id + name
})
},
cancelme: function (e) {
console.log("CANCEL");
}
})
多列选择器:mode = multiSelector
属性名 类型 默认值 说明
range 二维Array / 二维Object Array [] mode为 selector 或 multiSelector 时,range 有效。二维数组,长度表示多少列,数组的每项表示每列的数据,如[["a","b"], ["c","d"]]
range-key String 当 range 是一个 二维Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
bindchange EventHandle value 改变时触发 change 事件,event.detail = {value: value}
bindcolumnchange EventHandle 某一列的值改变时触发 columnchange 事件,event.detail = {column: column, value: value},column 的值表示改变了第几列(下标从0开始),value 的值表示变更值的下标
1
2
3
4
5
6
7
8
<view style='margin-top:150rpx;'>多列选择器</view>
<picker mode="multiSelector" range="{{arrayMulti}}">
请选择数组
</picker>
<picker mode="multiSelector" range="{{arrayObjMulti}}" range-key="id"
bindchange="changemeMulti" bindcolumnchange="columnchange">
<view>{{showme}}</view>
</picker>
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

Page({
data:{
arrayMulti:[
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0],
[1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
],
arrayObjMulti:[
[
{ id: "1001", name: "jack" },
{ id: "1002", name: "tom" },
{ id: "1003", name: "mary" },
{ id: "1004", name: "bob" },
{ id: "1005", name: "peter" }
], [
{ id: "1001", name: "jack" },
{ id: "1002", name: "tom" },
{ id: "1003", name: "mary" },
{ id: "1004", name: "bob" },
{ id: "1005", name: "peter" }
]
]
},
columnchange:function(e){
console.log(e.detail);
},
changemeMulti:function(e){
var indexs = e.detail.value;
var arrayObjMulti = this.data.arrayObjMulti;
for (var i = 0; i < indexs.length;i++){
var indexTmp = indexs[i];
var id = arrayObjMulti[i][indexTmp].id;
var name = arrayObjMulti[i][indexTmp].name;
console.log(id + " " + name);
}
}
})
时间选择器:mode = time
属性名 类型 默认值 说明
value String 表示选中的时间,格式为”hh:mm”
start String 表示有效时间范围的开始,字符串格式为”hh:mm”
end String 表示有效时间范围的结束,字符串格式为”hh:mm”
日期选择器:mode = date
属性名 类型 默认值 说明
value String 0 表示选中的日期,格式为”YYYY-MM-DD”
start String 表示有效日期范围的开始,字符串格式为”YYYY-MM-DD”
end String 表示有效日期范围的结束,字符串格式为”YYYY-MM-DD”
fields String day 有效值 year,month,day,表示选择器的粒度

fields 有效值:

值 说明
year 选择器粒度为年
month 选择器粒度为月份
day 选择器粒度为天
省市区选择器:mode = region
属性名 类型 默认值 说明
value Array [] 表示选中的省市区,默认选中每一列的第一个值
custom-item String 可为每一列的顶部添加一个自定义的项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<view style='margin-top:100rpx;'>时间选择器</view>
<picker mode="time" start="08:00" end="22:00" bindchange='changeTime'>
<view>当前选择:{{timeLabel}}</view>
</picker>

<view style='margin-top:100rpx;'>日期选择器</view>
<picker mode="date" start="2018-03-01" end="2018-09-01" bindchange='changeDate' fields="month">
<view>当前选择:{{dateLabel}}</view>
</picker>

<view style='margin-top:100rpx;'>城市选择器</view>
<picker mode="region" bindchange='changeCity'>
<view>当前选择:{{cityLabel}}</view>
</picker>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Page({
data:{
timeLabel:"null",
dateLabel:"null",
cityLabel:"null"
},
changeTime:function(e){
this.setData({
timeLabel: e.detail.value
})
},
changeDate: function (e) {
this.setData({
dateLabel: e.detail.value
})
},
changeCity: function (e) {
this.setData({
cityLabel: e.detail.value
})
}
})

pricker-view

picker-view 嵌入页面的滚动选择器

picker-view-column:仅可放置于中,其孩子节点的高度会自动设置成与picker-view的选中框的高度一致;

1
2
3
4
5
6
7
8
9
10
11
12
<picker-view style='width:100;height:250rpx;' bindchange="changeme">
<picker-view-column>
<view wx:for="{{year}}">{{item}}</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{month}}">{{item}}</view>
</picker-view-column>
<picker-view-column>
<view wx:for="{{day}}">{{item}}</view>
</picker-view-column>
</picker-view>
<view>"Current Value Is : "{{myvalue}}</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Page({
data: {
year: [1990, 1995, 2000, 2005, 2010, 2015, 2020],
month: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
day: [1, 5, 10, 15, 20, 25, 30],
myvalue: "null"
},
changeme: function(e) {
var indexs = e.detail.value;
var year = this.data.year[indexs[0]];
var month = this.data.month[indexs[1]];
var day = this.data.day[indexs[2]];
this.setData({
myvalue: year + " Y " + month + " M " + day + " D "
})
}
})

radio

radio 单选项

radio-group单项选择器,内部由多个<radio/>组成;

1
2
3
4
5
<radio-group class="radio-group" bindchange="radioChange">
<label class="radio" wx:for="{{items}}">
<radio value="{{item.name}}" checked="{{item.checked}}"/>{{item.value}}
</label>
</radio-group>
1
2
3
4
5
6
7
8
9
10
11
12
Page({
data: {
items: [
{name: 'USA', value: '美国'},
{name: 'CHN', value: '中国', checked: 'true'},
{name: 'BRA', value: '巴西'},
]
},
radioChange: function(e) {
console.log('radio发生change事件,携带value值为:', e.detail.value)
}
})

slider

slider 滑动选择器

属性名 类型 默认值 说明
min Number 0 最小值
max Number 100 最大值
step Number 1 步长,取值必须大于 0,并且可被(max - min)整除
color Color #e9e9e9 背景条的颜色(请使用 backgroundColor)
selected-color Color #1aad19 已选择的颜色(请使用 activeColor)
activeColor Color #1aad19 已选择的颜色
backgroundColor Color #e9e9e9 背景条的颜色
block-size Number 28 滑块的大小,取值范围为 12 - 28
block-color Color #ffffff 滑块的颜色
show-value Boolean false 是否显示当前 value
bindchange EventHandle 完成一次拖动后触发的事件,event.detail = {value: value}
bindchanging EventHandle 拖动过程中触发的事件,event.detail = {value: value}
1
2
3
4
<slider min='1' max='100'show-value="true" block-color='green' bindchanging='changing'>
</slider>
<view style='background-color:green;width:100%;height:{{myheight}};'>
</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
Page({
data: {
myheight:"500rpx" ,
staticHeight:500
},
changing:function(e){
var val = e.detail.value;
var newHeight = this.data.staticHeight * (val / 100);
this.setData({
myheight:newHeight + "rpx"
})
}
})

swich

switch 开关选择器

属性名 类型 默认值 说明
checked Boolean false 是否选中
type String switch 样式,有效值:switch, checkbox
bindchange EventHandle checked 改变时触发 change 事件,event.detail={ value:checked}
color Color switch 的颜色,同 css 的 color
1
2
3
4
5
<view class="body-view">
<switch checked bindchange="switchChange"/>
<switch checked bindchange="switchChange" type='checkbox'/>
<switch checked bindchange="switchChange" color='red'/>
</view>
1
2
3
4
5
Page({
switchChange: function(e) {
console.log('switch 发生 change 事件,携带值为', e.detail.value)
}
})

nav & value

navigator 页面链接

属性名 类型 默认值 说明
target String 在哪个目标上发生跳转,默认当前小程序
url String 当前小程序内的跳转链接
open-type String navigate 跳转方式
delta Number 当 open-type 为 ‘navigateBack’ 时有效,表示回退的层数
hover-stop-propagation Boolean false 指定是否阻止本节点的祖先节点出现点击态

open-type 有效值:

值 说明
navigate 对应 wx.navigateTo 或 wx.navigateToMiniProgram 的功能
redirect 对应 wx.redirectTo 的功能
switchTab 对应 wx.switchTab 的功能
reLaunch 对应 wx.reLaunch 的功能
navigateBack 对应 wx.navigateBack 的功能
exit 退出小程序,target=”miniProgram”时生效
1
2
3
4
5
6
7
8
9
10
11
12
13
// page1
<view>This is Page 1</view>
<navigator url='../page2/page2?id=1001&name=imooc'>跳转第2页</navigator>
<navigator url='../page3/page3'>跳转第3页</navigator>

// page2
<view>This is Page 2</view>
<navigator url='../page3/page3'>跳转第3页</navigator>
<navigator open-type='navigateBack'>返回</navigator>

// page3
<view>This is Page 3</view>
<navigator open-type='navigateBack' delta='2'>返回</navigator>
1
2
3
4
5
6
// page2
Page({
onLoad:function(params){
console.log(params);
}
})

image & video

image组件默认宽度300px、高度225px;

属性名 类型 默认值 说明
src String 图片资源地址
mode String ‘scaleToFill’ 图片裁剪、缩放的模式
lazy-load Boolean false 图片懒加载。只针对page与scroll-view下的image有效
bindload HandleEvent 当图片载入完毕时,发布到 AppService 的事件名,事件对象event.detail = {height:’图片高度px’, width:’图片宽度px’}

内网穿透

小程序不能直接访问后台接口;

通过内网穿透实现暴露到公网;

工具:ngrok(获取安全Token);

在ngrok目录下执行cmd,完成安装;

执行命令 ngrok http 8080 暴露内网端口到公网中;复制所给的Forwarding网址,即可完成对内网的穿透;


wx.request

通过wx.request(OBJECT)实现小程序和后台API的通信;

类似与HTML中的AJAX通信;

Bug & Tip

tip: content-type 默认为 ‘application/json’;

bug: 开发者工具 0.10.102800 版本,header 的 content-type 设置异常;

OBJECT参数说明:

参数名 类型 必填 默认值 说明
url String 是 开发者服务器接口地址
data Object/String/ArrayBuffer 否 请求的参数
header Object 否 设置请求的 header,header 中不能设置 Referer。
method String 否 GET (需大写)有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT
dataType String 否 json 如果设为json,会尝试对返回的数据做一次 JSON.parse
responseType String 否 text 设置响应的数据类型。合法值:text、arraybuffer
success Function 否 收到开发者服务成功返回的回调函数
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)

success返回参数说明:

参数 类型 说明
data Object/String/ArrayBuffer 开发者服务器返回的数据
statusCode Number 开发者服务器返回的 HTTP 状态码
header Object 开发者服务器返回的 HTTP Response Header
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Page({
clickme:function(){
wx.request({
url: '...',
data: {
id:1001,
name:"rex"
},
header: {
'content-type': 'application/json'
},
success: function (res) {
console.log(res.data)
}
})
}
})

附录

综合练习

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
<form bindsubmit='submitevent' style="font-size: 16px;">
<input name="username" placeholder='输入用户名'></input>
<input name="password" password='true' placeholder='输入密码'></input>

<checkbox-group name="skill" style="margin-top:30rpx">选择技能
<view wx:for="{{skills}}">
<checkbox id="{{item.id}}" value="{{item.value}}">{{item.name}}</checkbox>
</view>
</checkbox-group>

<picker name="birthday" mode="date" start="1990-01-01" end="2018-01-01" bindchange='changeDate' style='margin-top:30rpx'>
<view>出生日期为:{{dateLabel}}</view>
</picker>

<picker name="city" mode="region" bindchange='changeCity' style='margin-top:30rpx'>
<view>所在地区为:{{cityLabel}}</view>
</picker>

<radio-group name="sex" style='margin-top:30rpx'>
<radio value='男' checked='true'>男性</radio>
<radio value='女'>女性</radio>
</radio-group>

<view style='margin-top:30rpx'>
请选择年龄:
<slider name="age" value='18' min='18' max='65' show-value='true'></slider>
</view>

<switch name="isOK" checked='false' style='margin-top:30rpx'>
个人资料进行保密
</switch>

<textarea name="remark" placeholder='备注信息' style='margin-top:30rpx;border:1px;border-style:solid;width:90%;height:150rpx;'>
</textarea>

<view style='margin-top:30rpx'>
<button size='mini' form-type='submit' type='primary'>提交</button>
<button size='mini' form-type='reset' type='warn'style='margin-left:30rpx'>提交</button>
</view>
</form>
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
Page({
data: {
skills: [{
id: "1001",
name: "JAVA",
value: "java"
},
{
id: "1002",
name: "HTML",
value: "html"
},
{
id: "1003",
name: "SQL",
value: "sql"
},
{
id: "1004",
name: "DATA",
value: "data"
},
{
id: "1005",
name: "IOS",
value: "ios"
},
{
id: "1006",
name: "ANDROID",
value: "android"
}
],
dateLabel: "null",
cityLabel: "null"
},
changeDate: function (e) {
this.setData({
dateLabel: e.detail.value
})
},
changeCity: function (e) {
this.setData({
cityLabel: e.detail.value
})
},
submitevent:function(e){
console.log(e.detail.value);
}
})

@ 2018年8月16日 16:21:10

Wechat Mini | Note-2

发表于 2018-08-16

微信小程序开发 Note-2

@ 2018年8月15日 20:54:19


小程序的flex布局

小程序建议使用flex布局进行排版(响应式、自适应);

flex是一个盒装弹性布局(盒子中依然含有多个大小不一的盒子);

flex是一个容器,所有子元素都是他的成员;

定义布局:display:flex

flex容器的属性 flex-direction:排列方向,flex-wrap:换行规则 ,justify-content:对齐方式;


flex-direction

row:从左到右 row-reverse:从右到左

column:从上到下 column-reverse:从下到上

1
2
3
4
5
6
7
.container {
display: flex;
flex-direction: row;
flex-direction: row-reverse;
flex-direction: column;
flex-direction: row-lolumn-reverse;
}

flex-wrap

nowrap:默认不换行(根据所设置的长度进行压缩

wrap:换行(顶满则换行) wrap-reverse:倒序换行(从下开始换行)

1
2
3
4
5
6
.container {
display: flex;
flex-wrap: nowrap;
flex-wrap: wrap;
flex-wrap: wrap-reverse;
}

justify-content

决定内容的对齐方式;

flex-start:左对齐; flex-end:右对齐; center:居中对齐;

space-between:从左往右,平均分配,用空格分隔成员 ;

space-around:从左往右,平均分配,用空格包围成员;

1
2
3
4
5
6
7
8
.container {
display: flex;
justify-content: flex-start;
justify-content: flex-end;
justify-content: center;
justify-content: space-between;
justify-content: space-around;
}

flex成员元素的样式设置

order:成员之间的显示顺序;

flex:成员所占屏幕的比例(会导致width和height失效,并且将一行按比例分配给每个成员);

1
2
3
4
5
.a {
background-color: red;
order: 1;
flex: 3;
}

小程序的组件介绍

组件是视图层的基本组成单元,多个组件构成一张试图页面;

组件包含<开始标签></结束标签>;

每个组件都包含一些共有属性;

属性类型

Boolean Number String Array Object EventHandler 事件处理函数名 Any 任意属性

共同属性类型

id(String) class(String) style(String) hidden(Boolean) data-*(Any) bind*/catch*(EventHandler)


视图组件 view

视图容器 view

属性名 类型 默认值 说明
hover-class String none 指定按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-stop-propagation Boolean false 指定是否阻止本节点的祖先节点出现点击态
hover-start-time Number 50 按住后多久出现点击态,单位毫秒
hover-stay-time Number 400 手指松开后点击态保留时间,单位毫秒

scroll-view 可滚动视图

scroll-view 可滚动视图区域

属性名 类型 默认值 说明
scroll-x Boolean false 允许横向滚动
scroll-y Boolean false 允许纵向滚动
upper-threshold Number 50 距顶部/左边多远时(px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(px),触发 scrolltolower 事件
bindscrolltoupper EventHandle 滚动到顶部/左边,会触发 scrolltoupper 事件
bindscrolltolower EventHandle 滚动到底部/右边,会触发 scrolltolower 事件
scroll-top Number 设置竖向滚动条的默认位置
enable-back-to-top Boolean false iOS点击顶部状态栏/安卓双击标题栏时,滚动条返回顶部,只支持竖向
scroll-with-animation Boolean false 在设置滚动条位置时使用动画过渡
bindscroll EventHandle 滚动时触发,event.detail = {scrollLeft, scrollTop, scrollHeight, scrollWidth, deltaX, deltaY}
scroll-into-view String 值应为某子元素id(id不能以数字开头).设置哪个方向可滚动,则在哪个方向滚动到该元素
scroll-left Number 设置横向滚动条位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!scroll.wxml-->
<scroll-view scroll-y="true" style='height:300rpx' bindscrolltoupper="scrolltoupper" bindscrolltolower="scrolltolower" upper-threshold="0" lower-threshold="0"
enable-back-to-top="true" scroll-with-animation="true"
bindscroll="bindscroll" scroll-into-view="e">
<view id="a"class='a size'>a</view>
<view id="b"class='b size'>b</view>
<view id="c"class='c size'>c</view>
<view id="d"class='d size'>d</view>
<view id="e"class='e size'>e</view>
</scroll-view>

<scroll-view class="container" scroll-x="true" style='margin-top:250rpx' scroll-left="150">
<view id="a"class='a size'>a</view>
<view id="b"class='b size'>b</view>
<view id="c"class='c size'>c</view>
<view id="d"class='d size'>d</view>
<view id="e"class='e size'>e</view>
</scroll-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//scroll.js
Page({
data:{
},
scrolltolower:function(){
console.log("滚动到底部");
},
scrolltoupper:function(){
console.log("滚动到顶部");
},
bindscroll:function(){
console.log("滚动");
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**index.wxss**/

.container {
display: flex; white-space: nowrap;
}

.size {
width: 250rpx; height: 350rpx; display: inline-block;
}

.a {
background-color: red; order: 1; flex: 3;
}
.b c d e{
...
}

swiper 轮播图

swiper 滑块视图容器

属性名 类型 默认值 说明
indicator-dots Boolean false 是否显示面板指示点
autoplay Boolean false 是否自动切换
interval Number 5000 自动切换时间间隔
duration Number 500 滑动动画时长

movable-area/movable-view

movable-view 需要在movable-area成员之间;

属性名 类型 默认值 说明
direction String none movable-view的移动方向,属性值有all、vertical、horizontal、none
inertia Boolean false movable-view是否带有惯性
out-of-bounds Boolean false 超过可移动区域后,movable-view是否还可以移动
x/y Number / String 定义x/y轴方向的偏移,如果x/y的值不在可移动范围内,会自动移动到可移动范围;改变x/y的值会触发动画
damping Number 20 阻尼系数,用于控制x或y改变时的动画和过界回弹的动画,值越大移动越快
friction Number 2 摩擦系数,用于控制惯性滑动的动画,值越大摩擦力越大,滑动越快停止;必须大于0,否则会被设置成默认值
scale Boolean false 是否支持双指缩放,默认缩放手势生效区域是在movable-view内
scale-min/max Number 0.5 定义缩放倍数最小/大值
bindchange EventHandle 拖动过程中触发的事件,event.detail = {x: x, y: y, source: source},其中source表示产生移动的原因,值可为touch(拖动)、touch-out-of-bounds(超出移动范围)、out-of-bounds(超出移动范围后的回弹)、friction(惯性)和空字符串(setData)
1
2
3
4
<movable-area class="father-size">
<movable-view class='e size' direction='all' inertia='true' out-of-bounds='true' x='50' y='100' damping='100' friction='60' bindchange='doChange' scale='true' bindscale='doScale'>
</movable-view>
</movable-area>
1
2
3
4
5
6
7
8
Page({
doChange:function(){
console.log("Moving");
},
doScale:function(){
console.log("Scaling");
}
})

icon

icon 图标

属性名 类型 默认值 说明
type String icon的类型,有效值:success, success_no_circle, info, warn, waiting, cancel, download, search, clear
size Number 23 icon的大小,单位px
color Color icon的颜色,同css的color
1
2
3
4
5
6
7
8
9
10
<icon type='success' size='66'></icon>
<icon type='success' size='66' color='blue'></icon>
<icon type='success_no_circle' size='66'></icon>
<icon type='info' size='66'></icon>
<icon type='warn' size='66'></icon>
<icon type='waiting' size='66'></icon>
<icon type='cancel' size='66'></icon>
<icon type='download' size='66'></icon>
<icon type='search' size='66'></icon>
<icon type='clear' size='66'></icon>

text

属性名 类型 默认值 说明
selectable Boolean false 文本是否可选
space String false 显示连续空格
decode Boolean false 是否解码
space 有效值:
值 说明
ensp 中文字符空格一半大小
emsp 中文字符空格大小
nbsp 根据字体设置的空格大小
Tips
  • decode可以解析的有 &nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;
  • 各个操作系统的空格标准并不一致。
  • <text/> 组件内只支持 <text/> 嵌套。
  • 除了文本节点以外的其他节点都无法长按选中。
1
2
3
4
5
6
7
8
9
10
11
12
<view>
<view>this a view text</view>
<text selectable='true'>this is a text</text>
</view>

<view>
<text space='ensp'>this is a spac e text</text>
</view>

<view>
<text decode='true'>&nbsp; &lt; &gt; &amp; &apos; &ensp; &emsp;</text>
</view>

rich-text(富文本)

rich-text 富文本

nodes 属性推荐使用 Array 类型,由于组件会将 String 类型转换为 Array 类型,因而性能会有所下降;

属性 说明 类型 必填 备注
name 标签名 String 是 支持部分受信任的HTML节点
attrs 属性 Object 否 支持部分受信任的属性,遵循Pascal命名法
children 子节点列表 Array 否 结构和nodes一致
Bug & Tip

nodes 不推荐使用 String 类型,性能会有所下降;

rich-text 组件内屏蔽所有节点的事件;

attrs 属性不支持 id ,支持 class ;

name 属性大小写不敏感;

如果使用了不受信任的HTML节点,该节点及其所有子节点将会被移除;

img 标签仅支持网络图片;

如果在自定义组件中使用 rich-text 组件,那么仅自定义组件的 wxss 样式对 rich-text 中的 class 生效;

1
2
3
4
<!-- String -->
<rich-text nodes='{{mycontent}}'></rich-text>
<!-- Array -->
<rich-text nodes='{{mycontent2}}'></rich-text>
1
2
3
4
5
6
7
8
9
10
11
12
13
Page({
data: {
mycontent: '<img class="course-banner" src="//img3.mukewang.com/szimg/5a9ca4e80001786305400300.jpg">',

mycontent2: [{
name: "img",
attrs: {
class: "course-banner",
src: "//img3.mukewang.com/szimg/5a9ca4e80001786305400300.jpg"
}
}]
}
})

progress

progress 进度条

属性名 类型 默认值 说明
percent Float 无 百分比0~100
show-info Boolean false 在进度条右侧显示百分比
stroke-width Number 6 进度条线的宽度,单位px
color Color #09BB07 进度条颜色 (请使用 activeColor)
activeColor Color 已选择的进度条的颜色
backgroundColor Color 未选择的进度条的颜色
active Boolean false 进度条从左往右的动画
active-mode String backwards backwards: 动画从头播;forwards:动画从上次结束点接着播
1
2
<progress percent="{{percentdata}}" show-info='true' stroke-width="20" color="orange" backgroundColor='white' active='true'active-mode='forwards'/>
<view bindtap='addPercent'>addPercent</view>
1
2
3
4
5
6
7
8
9
10
11
Page({
data: {
percentdata: 15
},
addPercent: function () {
var newPercent = this.data.percentdata + 10;
this.setData({
percentdata: newPercent
})
}
})

@ 2018年8月16日 11:22:28

Spring Cloud | Note-9

发表于 2018-08-15

Spring Cloud微服务 | Note(9)

@ 2018年8月15日 10:19:40

微服务——熔断机制

当服务过载,流量激增,超过负荷时,掐断服务,保护服务的机制;

什么是服务的熔断机制

对于系统的防护机制,不会导致服务的不响应,返回默认值,依然保持服务的响应;

对该服务的调用执行熔断,对于后续的请求,不再继续调用该目标服务,而是直接返回默认值,从而快速释放资源;保护系统;


熔断的原理(服务熔断)

断路器:受保护的服务封装在断路器中,当故障达到阈值时,切断服务的响应,由断路器返回响应;

断路器模式:防止应用程序执行可能失败的操作,使应用程序检测故障是否解决;

Microsof Azure 断路器状态:关闭,打开,半打开;

Hystrix:服务正常,服务异常(Fallback);


熔断的意义

好处:保护系统,系统稳定,减少性能损耗,及时响应(简单的响应),阀值可定制;

功能:异常处理,日志记录,测试失败的操作,手动复位,并发,加速断路,重试失败请求;


熔断与降级的区别

相似:目的一致(可用性,可靠性,保护系统),表现类似(服务暂时不可达),粒度一致(服务级别,DAO)

区别:

​ ·触发条件不同——熔断由服务引起,降级由整体负荷引起;

​ ·管理目标层次不同——熔断管理整个框架级,每个服务都需要,降级管理业务层次;


如何集成Hystrix
1
2
3
4
// 依赖
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')
}
1
2
3
4
5
6
7
8
9
10
// 启用
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@EnableCircuitBreaker
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 服务过载时实现
@RestController
public class CityController {
@Autowired
private CityClient cityClient;

@GetMapping("/cities")
@HystrixCommand(fallbackMethod = "defaultCities")
public String listCity() {
// 通过Feign客户端来查找
String body = cityClient.listCity();
return body;
}
public String defaultCities(){
return "服务暂不可用(City Server Is Down)";
}
}

实现微服务的熔断机制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 具体服务中实现熔断
@FeignClient(name = "micro-weather-eureka-client-zuul",fallback = DataClientFallback.class)
public interface DataClient {...}

// Bean
@Component
public class DataClientFallback implements DataClient{
@Override
public List<City> listCity() throws Exception {
// 默认响应信息
...
}

@Override
public WeatherResponse getDataByCityId(String cityId) {
// 默认响应信息
return null;
}
}

// application.properties
spring.application.name=micro-weather-eureka-client-feign-gateway-hystrix
feign.hystrix.enabled=true

微服务——自动扩展

什么是自动扩展

垂直扩展(双核主机 –> 四核主机)硬件升级,瓶颈在于硬件水平的技术;

水平扩展(1台主机 –> x台主机)能力扩展,分布式系统解决计算能力;

自我注册和自我发现是自动扩展的前提条件;

服务注册表(服务注册中心),客户端,微服务实例;

客户端查询服务注册表,查询可用的服务;服务实例启动后,自我注册到服务注册表中,待客户端使用;

按需扩展:微服务实例分为使用中和保留,当需要更多实例时,再启动保留的服务实例,按照请求的需要扩展;


自动扩展的意义

解决应用程序为了支持长时间的运行,需要提前预留硬件、软件的功能,解决闲置资源的浪费问题;

按需扩展;

每个微服务实例需要独立的部署空间;

好处:

提供高可用性和容错能力:快速自动扩展使用新的服务实例,以满足服务的需要;

增加可伸缩性:水平扩展能力的增加,允许根据流量自动选择服务的规模;

具有最佳使用率,节约成本:按需扩展;

优先考虑服务或服务组:通过优先级分配资源;


自动扩展的常见模式

应用程序级别

扩展通过复制微服务实例实现,虚拟机或主机运行微服务;

更换服务实例;

基础架构级别

根据需求及时创建或消除;

保留的服务实例,被创建为具有预订意义的,服务实例虚拟机镜像,当有服务A的需求时,虚拟机将服务移动到激活的状态;

更换服务虚拟机,适用范围更广;

缺点:基于整个虚拟机扩展,体积更大,占用更多的系统资源;

解决:采用轻量级容器docker,减少资源的占用;

资源限制

监控程序:实时监控CPU的使用率,当使用率大于阀值时,启动新的服务实例;

特定时间段

根据特定时间段、季节性、业务高峰的服务需求,启动新的服务实例,减轻服务器的负载;

消息长度

监控程序监控每个服务实例的消息队列的长度,当队列长度大于设置的长度时,说明需要处理的消息超过服务可提供的能力,则新增服务实例;

业务参数

添加实例基于某些业务参数,解决业务事件时,通过先新增服务实例的方法,预防即将到来的大量业务交易请求;

根据预测

新型的自动扩展方式;

有多种信息事务模式的组合;

有助于解决硬编码和时间窗口;

根据历史信息,当前趋势进行预测(更精确,更有效的解决突发情况);


如何实现微服务的自动扩展

思考的问题

如何管理数千个容器?

如何监控容器?

在部署工件时,如何应用规则和约束?

如何利用容器获得资源效率?

如何确保至少有一定数量的最小实例正在运行?

如何确保依赖服务正常运行?

如何进行滚动升级和优雅的迁移?

如何回滚错误的部署?

所需功能

依赖两个关键功能:

1-一个容器抽象层,在许多物理或虚拟机上提供统一的抽象;

2-容器编排和初始化系统在集群抽象之上智能管理部署


容器编排

为开发以及架构团队,提供一个抽象层,来处理大规模集装箱式的部署;

共同功能:具备发现、资源管理、监控和部署等;

职责:集群管理(将虚拟机和物理机器的集群管理为一台大型机器 )

自动部署(能处理有大量及其的应用程序和容器的自动部署,支持多版本的应用程序容器,并且支持跨越大量集群机器的滚动升级和故障回滚)

可伸缩性(支持服务实例的自动和手动伸缩,以性能优化为主要目标)

运行状况的健康(管理集群、节点和应用程序的健康)

基础架构抽象(开发人员不需要担心集群和容量等问题)

资源优化(以有效的方式在可用机器上分配容器工作负载,从而减低成本,通过简单到复杂的算法有效提高利用率)

资源分配(基于应用程序开发人员设置的资源可用性和约束来分配服务器)

服务可用性(确保服务在集群中正常运行,机器故障的情况下,容器编排会自动通过在集群中的其他机器上重新启动这些服务来处理故障)

敏捷性(敏捷性工具能够快速分配工作负载到可用资源,或在资源需求发生变化时跨机器移动工作量)

隔离(提供资源隔离,即使应用程序不是容器化,也可以实现资源隔离)


资源分配常用算法

传播(Spread)工作负载平均分配到多台主机中;

装箱(Bin Packing)优先试图填满机器的最大负载,常用于按需付费的云服务;

随机(Random)工作负载随机分配到多台主机中;

常用的容器编排技术

Docker Swarm

Kubernetes

Apache Mesos


附录

@ 2018年8月15日 14:57:54

12345

REX CHEN

日常记录

47 日志
20 标签
© 2019 REX CHEN