帖子主要解决的是使用share.js分享时无法自定义图片的问题,最后通过引入了微信的SDK解决了这个问题(下面放了share.js和微信sdk的官方地址)。如果没有不引入微信SDK、加域名也是可以实现分享的,但是无法实现小图片和分享描述自定义。lz开发时的环境:Vue + share.js + 微信SDK + 域名 + Java。所以要求开发的时候需要有微信公众号,里面需要提供AppId和Secret。lz使用Java的原因是因为前端相对经验不足,对接微信时出现了各种各样的问题,所以我提供了一个接口给他获取微信相关的access_token等其他信息(实际可以不使用java,前端直接访问微信sdk)。最终实现的分享效果如图和界面图
- 分享效果图
- 界面效果图
相关资源链接:
github上的share.js地址:overtrue/share.js
微信分享sdk地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141115
参考链接:微信分享SDK接入——Java
微信分享功能(分享给好友/分享到朋友圈-java版本)
后端代码
后端代码分享:后端主要内容有三个类Sign,WeiXinEntity、WeiXinUtil,代码如下,lz特地加了很多的注解,可以自己看看。其中需要修改的地方只有两个将AppId和Secret换成自己微信工作号上获取的即可,至于跟微信相关的开发操作我把它放在代码后面
-
类关系图
-
Sign.class
package com.luntek.certificate.utils.wxshare;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
class Sign {
public static void main(String[] args) {
String jsapi_ticket = "jsapi_ticket";
// 注意 URL 一定要动态获取,不能 hardcode
String url = "http://example.com";
Map<String, String> ret = sign(jsapi_ticket, url);
for (Map.Entry entry : ret.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
}
;
/**
* 获取微信上返回的签名信息
*
* @param jsapi_ticket 指定算法生成的
* @return 包含微信客户端需要的信息
* @author Czw
* @date 2020/9/4 0004 下午 5:45
*/
public static Map<String, String> sign(String jsapi_ticket, String url) {
Map<String, String> ret = new HashMap<String, String>();
String nonce_str = create_nonce_str();
String timestamp = create_timestamp();
String string1;
String signature = "";
//注意这里参数名必须全部小写,且必须有序
string1 = "jsapi_ticket=" + jsapi_ticket +
"&noncestr=" + nonce_str +
"×tamp=" + timestamp +
"&url=" + url;
System.err.println("string1=" + string1);
try {
MessageDigest crypt = MessageDigest.getInstance("SHA-1");
crypt.reset();
crypt.update(string1.getBytes(StandardCharsets.UTF_8));
signature = byteToHex(crypt.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
ret.put("url", url);
ret.put("jsapi_ticket", jsapi_ticket);
ret.put("nonceStr", nonce_str);
ret.put("timestamp", timestamp);
ret.put("app_id", WeiXinUtil.AppId);
ret.put("signature", signature);
return ret;
}
private static String byteToHex(final byte[] hash) {
Formatter formatter = new Formatter();
for (byte b : hash) {
formatter.format("%02x", b);
}
String result = formatter.toString();
formatter.close();
return result;
}
private static String create_nonce_str() {
return UUID.randomUUID().toString();
}
private static String create_timestamp() {
return Long.toString(System.currentTimeMillis() / 1000);
}
}
- WeiXinUtil
package com.luntek.certificate.utils.wxshare;
import net.sf.json.JSONObject;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* @description: 微信工具类,获取access_token、ticket
* @author: Czw
* @create: 2020-09-03 10:38
**/
public class WeiXinUtil {
//获取access_token填写client_credential,不需要修改
public static String grant_type = "client_credential";
//微信公众号上获取的用户唯一凭证,换成自己的
public static String AppId = "XXXXXXXXX";
//第三方用户唯一凭证密钥,即appsecret,换成自己的
public static String secret = "XXXXXXXX";
/**
* 获取需要分享的链接信息
*
* @param url 被分享的网址链接中需要没有中文字符,必须是公网链接
* @return WeiXinEntity 返回微信分享需要的参数信息对象
* @author Czw
* @date 2020/9/4 0004 下午 5:39
*/
public static WeiXinEntity getWinXinEntity(String url) {
WeiXinEntity wx = new WeiXinEntity();
String access_token = getAccessToken();
String ticket = getTicket(access_token);
Map<String, String> ret = Sign.sign(ticket, url);
wx.setTicket(ret.get("jsapi_ticket"));
wx.setSignature(ret.get("signature"));
wx.setNonceStr(ret.get("nonceStr"));
wx.setTimestamp(ret.get("timestamp"));
wx.setAppId(ret.get("app_id"));
wx.setAccessToken(access_token);
return wx;
}
/**
* 从微信公众号平台上获取accessToken
*
* @return accessToken
* @author Czw
* @date 2020/9/4 0004 下午 5:42
*/
private static String getAccessToken() {
String access_token = "";
//这个url链接地址和参数皆不能变
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=" + grant_type + "&appid=" + AppId + "&secret=" + secret; //访问链接
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
http.connect();
InputStream is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes, StandardCharsets.UTF_8);
JSONObject demoJson = JSONObject.fromObject(message);
access_token = demoJson.getString("access_token");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return access_token;
}
/**
* 根据微信公众号获取的access_token获取标记ticket
*
* @param access_token 微信公众号获取获取的token
* @return ticket 标记
*/
private static String getTicket(String access_token) {
String ticket = null;
String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + access_token + "&type=jsapi";//这个url链接和参数不能变
try {
URL urlGet = new URL(url);
HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
http.setRequestMethod("GET"); // 必须是get方式请求
http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
http.setDoOutput(true);
http.setDoInput(true);
System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
http.connect();
InputStream is = http.getInputStream();
int size = is.available();
byte[] jsonBytes = new byte[size];
is.read(jsonBytes);
String message = new String(jsonBytes, StandardCharsets.UTF_8);
JSONObject demoJson = JSONObject.fromObject(message);
ticket = demoJson.getString("ticket");
is.close();
} catch (Exception e) {
e.printStackTrace();
}
return ticket;
}
}
- WeiXinEntity
package com.luntek.certificate.utils.wxshare;
import lombok.Data;
/**
* 微信分享实体类
*
* @author: Czw
* @create: 2020-09-03 10:39
**/
@Data
public class WeiXinEntity {
private String appId;
private String accessToken;
private String ticket;
private String nonceStr;
private String timestamp;
private String str;
private String signature;
}
- WxShareController
package com.luntek.certificate.api;
import com.github.pagehelper.util.StringUtil;
import com.luntek.certificate.enums.CommonExEnum;
import com.luntek.certificate.exception.BusinessException;
import com.luntek.certificate.utils.wxshare.WeiXinEntity;
import com.luntek.certificate.utils.wxshare.WeiXinUtil;
import io.swagger.annotations.Api;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 微信分享对应接口
*
* @author Czw
* @Date 2020/2/25 0025 下午 12:54
*/
@Validated
@Api(tags = "微信分享controller")
@RestController
@RequestMapping("/wxShare")
public class WxShareController {
/**
* 修改文章是否开启状态
*
* @param shareUrl 被分享的网址链接中需要没有中文字符,必须是公网链接
* @return ResponseResult
* @auther Czw
* @date 2020/5/9 0009 上午 10:13
*/
@GetMapping("/getShareMessage")
public WeiXinEntity changeState(@RequestParam("weixinShareUrl") String shareUrl) {
if (StringUtil.isEmpty(shareUrl)) {
throw new BusinessException(CommonExEnum.SHARE_URL_EMPTY);
}
WeiXinEntity weiXinEntity = WeiXinUtil.getWinXinEntity(shareUrl);
return weiXinEntity;
}
}
相关的操作步骤:
再发一次文档链接:微信SDK地址
大致步骤:
首先要有一个公众号和已经备案好的域名!自行准备
1. 获取appId和secret、放入MP_verify_0HU5eN6Tzfwovxxx.txt文件到项目根目录下、添加项目访问域名地址、配置IP白名单地址
-
获取appId和secret
先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”,所谓的安全域名就是要分享的网页所在的域名,填写完域名后在“基本配置”可以看到开发者ID(AppID)和开发者密码(AppSecret,也就是secret)
-
下载txt文件(MP_verify_0HU5eN6Tzfwovxxx.txt),放到项目根目录下。lz放在ng的HTML下,如果没有使用ng可以放在项目的index同级下
- 添加项目访问域名地址,lz下面添加的方式是错误的,需要在前面加上www
- 配置IP白名单地址
2. 复制签名函数
上面的Sign类为获取签名
3. 获取access_token、ticket
建立一个WeinXinUtil类,里面的getAccessToken方法和getTicket方法分别获取access_token、ticket
4. 获取需要分享的url,并进行签名
url的获取要特别注意,如果是使用的histry路由则url中会有#需要去除掉,如果不是此路由方式则无需处理。lz需要分享的url由前端作为参数传递过来,所以相对简单。微信在分享时会在原来的url上加上一些&from=singlemessage、&from=…的,所以url必须要动态获取,网上有些用ajax,在网页通过“location.href.split(‘#’)“ 获取url,在用ajax传给后台,这种方法可行,但是不推荐,一方面用ajax返回,就要访问分享的逻辑,这样后台分享的逻辑增加复杂度,带来不便,是代码不易于维护,可读性低!另一方面分享是返回页面,而ajax是返回json,又增加了复杂度。所以,也可以用HttpRequest的方法,不管微信加多少后缀,都可以获取到完整的当前url
例如,以下代码仅供参考:
public String share(HttpServletRequest request) {
...
String strUrl = "http://www.xxxxx.com" //换成安全域名
+ request.getContextPath() //项目名称
+ request.getServletPath() //请求页面或其他地址
+ "?" + (request.getQueryString()); //参数
...
}
sign类里面有一个签名的方法public static Map<String, String> sign(String jsapi_ticket, String url)
传入ticket和url即可。也就是WeinXinUtil的getWinXinEntity方法,并将返回的map的信息读取存入WinXinEntity 中。在调试时,把sign返回的map打印出来,主要看看生成的signature。然后,把jsapi_ticket、noncestr、timestamp、url 复制到微信提供的”微信 JS 接口签名校验工具“:校验前面是否正确的微信官方工具,比较代码签名生成的signature与校验工具生成的签名signature是否一致,如果一致,说明前面的步骤都是正确的,如果不一致,仔细检查!
5. 集成进java web
我们把微信分享分解成3个工具类,现在在处理分享的controller,只要两句话就可以调用微信分享,一句获取url,一句获取WinXinEntity,代码为WxShareController中的代码
前端
需要使用share.js(具体使用自行百度)安装sdk (npm install --save weixin-js-sdk
)
<!-- 论坛文章/提问内容详情 -->
<template>
<div v-wechat-title="$route.meta.title=post.title" class="post-wrapper">
<transition name="el-fade-in-linear">
<div class="post-content">
<div class="suspended-bar">
<el-badge :value="post.likeCount" class="icon-badge" type="primary">
<div class="icon-background" @click="handleLike">
<svg-icon icon-class="like" class="icon" :class="{
'is-like':post.like}"/>
</div>
</el-badge>
<div class="icon-background" @click="handleScrollTo">
<svg-icon icon-class="comment" class="icon"/>
</div>
<div v-if="post.type !== '2'" class="icon-background" @click="handleCollection">
<svg-icon icon-class="collection" class="icon" :class="{
'is-like':post.collect}"/>
</div>
<div class="icon-background" @click="handleReset">
<svg-icon icon-class="report" class="icon"/>
</div>
<!-- <el-tooltip effect="dark" content="返回" placement="bottom">-->
<!-- <div class="icon-background" @click="$router.go(-1)"><svg-icon icon-class="back" class="icon" /></div>-->
<!-- </el-tooltip>-->
</div>
<transition name="el-fade-in-linear">
<div class="post-detail">
<div v-show="!isLoading">
<div class="article-top">
<div class="avatar-wrapper">
<img :src="post.icon" class="el-avatar--circle author-avatar" alt=""
@click="handleRedirectUserCenter(post.createBy)"/>
</div>
<div class="author-article-info">
<div class="author-name" @click="handleRedirectUserCenter(post.createBy)">{
{
post.nickName }}
</div>
<div class="article-time">{
{ post.createTime | parseTime('{y}年{m}月{d}日') }} · {
{
post.sortName }} · 阅读 {
{ post.lookCount }}
</div>
</div>
</div>
<!--封面图片-->
<img v-if="post.thumbnailPath && post.thumbnailPath.length > 0"
:src="post.thumbnailPath" class="article-thumb">
<div class="article-content">
<h2 class="post_title">{
{ post.title }}</h2>
<div v-html="post.content"></div>
<el-tag v-for="(item,index) in labelList" :key="index" class="article-tag"
type="info">{
{ item }}
</el-tag>
<share v-if="post.title" :config="config"/>
<!-- <forum-post-share v-if="post.title"/>-->
</div>
</div>
</div>
</transition>
<forum-comment-list id="anchor" :article="post" @commentFinish="commentFinish"/>
</div>
</transition>
<div class="action-box">
<div class="icon-wrapper split" @click="handleLike">
<svg-icon icon-class="like" class="icon" :class="{
'is-like':post.like}"/>
{
{ post.likeCount }}
</div>
<div v-if="post.type !== '2'" class="icon-wrapper split" @click="handleCollection">
<svg-icon icon-class="collection" class="icon" :class="{
'is-like':post.collect}"/>
收藏
</div>
<div class="icon-wrapper" @click="handleReset">
<svg-icon icon-class="report" class="icon"/>
举报
</div>
</div>
<forum-report :report-dialog-visible.sync="reportDialogVisible"
:reported-id="$route.params.id"/>
<back-to-top
class="back-to-top"
:custom-style="myBackToTopStyle"
:visibility-height="300"
transition-name="fade"
/>
</div>
</template>
<script>
import ForumCommentList from '../Forum/ForumCommentList';
import {
addPostCollect,
getArticleInfo,
getWeixinShare,
likeOrUnlikePost
} from '../../api/forum';
import {
parseTime } from '@/utils';
import ForumReport from './ForumReport';
import {
getToken } from '../../utils/auth';
import BackToTop from '../BackToTop/index';
// import ForumPostShare from './ForumPostShare';
import wx from 'weixin-js-sdk';
export default {
name: 'ForumPost',
components: {
// ForumPostShare,
ForumReport,
ForumCommentList,
BackToTop
},
filters: {
parseTime
},
data() {
return {
post: {
},
temp: {
reportedId: null,
message: null,
detail: null,
},
labelList: [],
isLoading: true,
reportDialogVisible: false,
reportTheme: ['政治相关', '内容涉黄', '内容抄袭', '内容侵权', '侮辱谩骂', '其他问题'],
config: {
title: '朗迅芯云学院',
image: '',
sites: ['qzone', 'qq', 'weibo', 'wechat'],
url: '',
wechatQrcodeTitle: '微信扫一扫:分享',
wechatQrcodeHelper: '<p>微信扫一下二维码</p><p>点右上角省略号便可分享至朋友圈</p>'
},
myBackToTopStyle: {
right: '40px',
bottom: '40px',
width: '40px',
height: '40px',
'border-radius': '50%',
'line-height': '38px',
background: '#FF9607',
color: '#fff',
zIndex: 800
}
};
},
created() {
this.isLoading = true;
this.getPost();
},
mounted() {
if (this.$route.params.anchor) {
setTimeout(() => {
this.handleScrollTo();
}, 800);
}
this.shareWeixin();
},
methods: {
handleRedirectUserCenter(id) {
this.$router.push({
name: 'ForumUser',
params: {
userId: id }
});
},
getPost() {
getArticleInfo(this.$route.params.id)
.then(res => {
this.config.title = res.data.title;
this.config.image = res.data.thumbnailPath;
this.config.url = window.location.href;
this.post = res.data;
this.$emit('returnSortId', this.post.sortId);
this.labelList = res.data.labels.split(',');
this.isLoading = false;
});
},
judgeLogin() {
if (!getToken()) {
this.$message({
type: 'error',
message: '请登录后操作!'
});
this.$router.push({
path: '/login',
query: {
path: `forum/post/${
this.$route.params.id}` }
});
return true;
}
},
handleChooseTheme(index) {
this.$set(this.temp, 'theme', this.reportTheme[index]);
this.temp.theme = this.reportTheme[index];
},
handleLike() {
if (this.judgeLogin()) {
return;
}
if (this.post.like === true) {
this.post.likeCount--;
this.post.like = false;
} else {
this.post.likeCount++;
this.post.like = true;
}
likeOrUnlikePost({
postId: this.$route.params.id });
},
commentFinish() {
this.getPost();
},
handleScrollTo() {
document.getElementById('anchor')
.scrollIntoView();
},
handleReset() {
this.reportDialogVisible = true;
},
handleCollection() {
if (this.judgeLogin()) {
return;
}
this.post.collect = this.post.collect !== true;
addPostCollect(this.$route.params.id);
},
shareWeixin() {
getWeixinShare(window.location.href)
.then(res => {
// debugger
this.infoList = res.data;
wx.config({
debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: this.infoList.appId, // 必填,公众号的唯一标识
timestamp: this.infoList.timestamp, // 必填,生成签名的时间戳
nonceStr: this.infoList.nonceStr, // 必填,生成签名的随机串
signature: this.infoList.signature, // 必填,签名
jsApiList: [
'onMenuShareTimeline',
'onMenuShareAppMessage'
] // 必填,需要使用的JS接口列表
});
wx.ready(() => {
getArticleInfo(this.$route.params.id).then(response=>{
wx.onMenuShareAppMessage({
title: response.data.title,
desc: '朗讯,用“芯”创造美好未来!',
link: window.location.href + '?from=groupmessage',
imgUrl: response.data.thumbnailPath,
success: function success(res) {
console.log('已分享');
},
cancel: function cancel(res) {
console.log('已取消');
},
fail: function fail(res) {
alert(JSON.stringify(res));
}
});
// 2.2 监听“分享到朋友圈”按钮点击、自定义分享内容及分享结果接口
wx.onMenuShareTimeline({
title: response.data.title,
desc: '朗讯,用“芯”创造美好未来!',
link: window.location.href + '?from=groupmessage',
imgUrl: response.data.thumbnailPath,
// success: function success(res) {
// console.log('已分享');
// },
// cancel: function cancel(res) {
// console.log('已取消');
// },
// fail: function fail(res) {
// alert(JSON.stringify(res));
// }
});
})
});
wx.error((res) => {
alert('微信验证失败');
});
});
}
}
};
</script>
<style lang="scss" scoped>
@import '~@/assets/style/global';
.post-wrapper {
width: 100%;
flex: 1;
list-style: none;
min-height: 750px;
.post-content {
background-color: white;
box-shadow: 0 2px 4px rgba(26, 26, 26, .1);
.post-detail {
min-height: 500px;
.article-top {
display: flex;
padding: 24px 0 12px 24px;
.avatar-wrapper {
width: 55px;
height: 55px;
.author-avatar {
width: 100%;
cursor: pointer;
}
}
.author-article-info {
position: relative;
flex: 1;
padding-left: 18px;
.author-name {
font-weight: bold;
padding: 4px 0;
cursor: pointer;
font-size: 1rem;
}
.article-time {
bottom: 4px;
font-size: 14px;
color: #909090;
}
}
}
.article-thumb {
width: 100%;
padding: 12px 24px;
}
.article-content {
position: relative;
max-width: 680px;
width: 100%;
padding: 12px 12px;
word-wrap: break-word;
/deep/ img, p, span {
width: 100%;
}
/deep/ pre {
overflow-x: auto;
}
.article-tag {
width: auto;
margin: 4px 7px 12px 7px;
}
}
}
.suspended-bar {
position: fixed;
margin-left: -60px;
margin-top: 250px;
.icon-background {
width: 34px;
height: 34px;
margin-bottom: 6px;
border-radius: 50%;
background-color: white;
text-align: center;
vertical-align: center;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .04);
cursor: pointer;
.icon {
font-size: 18px;
margin-top: 8px;
color: #aaaaaa;
&.is-like {
color: $forum-blue;
}
&.is-like:hover {
color: $forum-blue;
}
&:hover {
color: #777777;
}
}
}
}
}
.action-box {
@media screen and (min-width: 960px) {
display: none;
}
position: fixed;
z-index: 1;
bottom: 0;
width: 100%;
height: 48px;
display: flex;
border-top: 1px solid #ebebeb;
border-bottom: 1px solid #ebebeb;
background: #fff;
.icon-wrapper {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
cursor: pointer;
user-select: none;
&.split {
&:after {
content: "";
position: absolute;
top: 50%;
right: 0;
margin-top: -1rem;
width: 1px;
height: 2rem;
background-color: #ebebeb;
}
}
.icon {
font-size: 19px;
margin-right: 5px;
color: #aaaaaa;
&.is-like {
color: $forum-blue;
}
}
}
}
.help-btn-wrapper {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
.btn {
margin: 0 0 5px 0;
}
}
.back-to-top {
@media (max-width: 400px) {
display: none;
}
}
.post_title {
font-weight: bold;
margin: 0px 0px 30px 0px;
}
}
</style>
- 前端核心代码截图,大家自己凑合着用吧
有什么问题可以私聊我,或者加我QQ:1142937352
写了好几个小时,点个赞再走吧。也许不一定和你想做的一模一样,但肯定有你想要的地方
余生还长,切勿惆怅