站如果是完全禁止右键(复制、另存为等)操作,对用户来说体验感会降低,但是又不希望自己的原创内容直接被copy,今天飞飞和你们分享几行复制转载提醒弹窗Html代码。
效果展示:
复制以下代码,将其放在网站footer.php或者header.php任意底部位置即可。
<!-- 复制提醒开始 -->
<link rel="stylesheet" href="https://cdn.bootcss.com/sweetalert/1.1.3/sweetalert.min.css" />
<script type="text/javascript" src="https://cdn.bootcss.com/sweetalert/1.1.3/sweetalert.min.js"></script>
<script>
document.body.oncopy = function() {
swal("复制成功!", "若要转载请保留原文链接,感谢支持!", "success");
};
</script>
<!-- 复制提醒结束 -->
通过这几行代码,我们可以在保护原创内容的同时,不影响用户体验。当然,除了添加复制转载提醒弹窗之外,我们还可以通过其他方式来保护原创内容,例如添加水印、限制转载等等。总之,在权衡用户体验和版权保护时,我们需要寻找一个平衡点,既能够保护自己的权益,又不会影响用户的使用体验。
感谢您的阅读,服务器大本营助您成为更专业的服务器管理员!
今年国庆假期终于可以憋在家里了不用出门了,不用出去看后脑了,真的是一种享受。这么好的光阴怎么浪费,睡觉、吃饭、打豆豆这怎么可能(耍多了也烦),完全不符合我们程序员的作风,赶紧起来把文章写完。
这篇文章比较基础,在国庆期间的业余时间写的,这几天又完善了下,力求把更多的前端所涉及到的关于文件上传的各种场景和应用都涵盖了,若有疏漏和问题还请留言斧正和补充。
以下是本文所涉及到的知识点,break or continue ?
原理很简单,就是根据 http 协议的规范和定义,完成请求消息体的封装和消息体的解析,然后将二进制内容保存到文件。
我们都知道如果要上传一个文件,需要把 form 标签的enctype设置为multipart/form-data,同时method必须为post方法。
那么multipart/form-data表示什么呢?
multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件,具体的定义可以参考RFC 7578。
multipart/form-data 结构
看下 http 请求的消息体
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次请求要上传文件,其中boundary表示分隔符,如果要上传多个表单项,就要使用boundary分割,每个表单项由———XXX开始,以———XXX结尾。
每一个表单项又由Content-Type和Content-Disposition组成。
Content-Disposition: form-data 为固定值,表示一个表单元素,name 表示表单元素的 名称,回车换行后面就是name的值,如果是上传文件就是文件的二进制内容。
Content-Type:表示当前的内容的 MIME 类型,是图片还是文本还是二进制数据。
解析
客户端发送请求到服务器后,服务器会收到请求的消息体,然后对消息体进行解析,解析出哪是普通表单哪些是附件。
可能大家马上能想到通过正则或者字符串处理分割出内容,不过这样是行不通的,二进制buffer转化为string,对字符串进行截取后,其索引和字符串是不一致的,所以结果就不会正确,除非上传的就是字符串。
不过一般情况下不需要自行解析,目前已经有很成熟的三方库可以使用。
至于如何解析,这个也会占用很大篇幅,后面的文章在详细说。
使用 form 表单上传文件
在 ie时代,如果实现一个无刷新的文件上传那可是费老劲了,大部分都是用 iframe 来实现局部刷新或者使用 flash 插件来搞定,在那个时代 ie 就是最好用的浏览器(别无选择)。
DEMO
这种方式上传文件,不需要 js ,而且没有兼容问题,所有浏览器都支持,就是体验很差,导致页面刷新,页面其他数据丢失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
选择文件:
<input type="file" name="f1"/> input 必须设置 name 属性,否则数据无法发送<br/>
<br/>
标题:<input type="text" name="title"/><br/><br/><br/>
<button type="submit" id="btn-0">上 传</button>
</form>
复制代码
服务端文件的保存基于现有的库koa-body结合 koa2实现服务端文件的保存和数据的返回。
在项目开发中,文件上传本身和业务无关,代码基本上都可通用。
在这里我们使用koa-body库来实现解析和文件的保存。
koa-body 会自动保存文件到系统临时目录下,也可以指定保存的文件路径。
然后在后续中间件内得到已保存的文件的信息,再做二次处理。
NODE
/**
* 服务入口
*/
var http = require('http');
var koaStatic = require('koa-static');
var path = require('path');
var koaBody = require('koa-body');//文件保存库
var fs = require('fs');
var Koa = require('koa2');
var app = new Koa();
var port = process.env.PORT || '8100';
var uploadHost= `http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//设置文件的默认保存目录,不设置则保存在系统临时目录下 os
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 开启文件上传,默认是关闭
}));
//开启静态文件访问
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//文件二次处理,修改名称
app.use((ctx) => {
var file = ctx.request.files.f1;//得道文件对象
var path = file.path;
var fname = file.name;//原文件名称
var nextPath = path+fname;
if(file.size>0 && path){
//得到扩展名
var extArr = fname.split('.');
var ext = extArr[extArr.length-1];
var nextPath = path+'.'+ext;
//重命名文件
fs.renameSync(path, nextPath);
}
//以 json 形式输出上传文件地址
ctx.body = `{
"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
}`;
});
/**
* http server
*/
var server = http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ...... ');
复制代码
CODE
https://github.com/Bigerfe/fe-learn-code/
本文将叙述web端消息推送是什么,它的原理、项目实战、遇到的问题以及解决方法。
web端消息推送用一句话解释就是:服务端向浏览器客户端发送了一条消息,客户端收到推送消息后以通知的形式展示出来,用户点击消息之后能进行一系列后续的操作。这个能力让我们可以从服务端向用户推送各类消息并引导用户触发相应交互,例如:
从1中可知,消息推送包括两部分的功能:消息推送与通知提醒,整个流程有些复杂,因此,在进入具体技术细节之前,我们先了解一下整个流程与相关概念
client:就是我们的浏览器客户端
Push Service:专门的Push服务,可以认为是一个第三方服务,目前chrome与firefox都有自己的Push Service Service。理论上只要浏览器支持,可以使用任意的Push Service
application server:指我们自己的后端服务
subscribe阶段:
在订阅阶段,首先浏览器会询问用户是否允许通知,只有在用户允许后,才能进行后面的操作。这一步不在上图的流程中,这其实是浏览器中的策略。客户端如果愿意接收服务端的推送,会利用service worker的的接口向 push service 发起一个订阅。push service是提供中间人服务的服务器,由浏览器负责,在使用时对普通开发者基本是透明的。
push service 返回 subscription
客户端发起subscribe后,要收到返回的subscription对象才算成功。得到的subscription会被接着传给应用服务器
向服务器发送 subscription
客户端得到的 subscription 要发给服务器。这样服务器在推送消息时,才能向 push service 证明自己和客户端是有建立合法推送约定的
服务器向客户端推送消息
服务器是不能直接向客户端发送消息的,它只是将客户端给的subscription对象和要推送的消息一起发给 push service,然后由push service 确认了 它 与客户端的有效订阅关系后,由 push service 代为推送
push serivce 推送
push serivce 再验证过服务器和客户端的订阅关系后,会将服务器的消息推送给客户端。客户端接收到消息后,以通知的形式展示出来。
以下分别具体介绍获取通知授权、订阅消息、推送消息、接受消息并展示通知的实现
在订阅消息之前,浏览器需要得到用户授权,同意后才能使用消息推送服务。通知授权类似如下图:
显示以上对话框有两种方式:
(1)在订阅之前先获取用户授权,使用Notification.requestPermission方法
Notification.requestPermission().then((permission) => {
console.log("permission=", permission);
if (permission === "granted") {
//do something
} else if (permission === "denied") {
//do something
} else {
//do something
}
});
Notification.requestPermission()方法执行会返回授权结果,主要有granted(已授权)、denied(被拒绝)、default(被关闭)这3中状态。只有当授权结果为granted时,当前网页才能进行后面订阅推送服务和通知消息这两个步骤。
(2)如果不选择使用方法(1),在正式订阅时浏览器也会自动弹出,对于开发者而言不需要显式调用
值得注意的是,当用户允许或者拒绝授权后,后续都不会重复询问。想要更改这个设置,在 Chrome 地址栏左侧网站信息中如下手动修改:
订阅消息
订阅消息的具体实现步骤如下:
(1)注册 Service Worker
(2)使用 pushManager 添加订阅,浏览器向推送服务发送请求,其中传递参数对象包含两个属性:
(3)得到推送服务成功响应后,浏览器将推送服务返回的 subscribe,向后端服务器发送这个subscribe并存储
代码参考如下:
// 注册Service Worker
if ("serviceWorker" in navigator && "PushManager" in window) {
navigator.serviceWorker
.register("./service-worker.js")
.then(function (reg) {});
navigator.serviceWorker.ready.then(function (reg) {
subscribe(reg);
});
}
// 发起订阅
function subscribe(serviceWorkerReg) {
serviceWorkerReg.pushManager
.subscribe({ userVisibleOnly: true, applicationServerKey: "xxxx" })
.then(function (subscribe) {
//获取到的subscribe发送推送给后端存储起来
sendToServer(subscribe);
})
.catch(function () {
// 用户拒绝了订阅请求
if (Notification.permission === "denied") {
//do something
}
});
}
插播一个生成公钥私钥的方法:可以用web push的Node包生成:
(1)npm install web-push --save
(2)每运行一次就会生成一对新的密钥对,公钥私钥只要能配套就好,公钥在浏览器端使用,用来生成subscribe,私钥在服务端使用,用来发Push,代码如下:
const webpush = require('web-push');
//VAPID keys should only be generated only once.
const vapidKeys = webpush.generateVAPIDKeys();
console.log(vapidKeys.publicKey, vapidKeys.privateKey);
推送消息
当服务器想推送消息给用户时,可以用FCM提供的web push的库发送推送,它支持多种语言,包括Node.js/PHP等版本。用Node.js可以这样发Push:
const webpush = require("web-push");
// 从数据库取出用户的subsciption,例如取出的subsciption如下
const pushSubscription = {
endpoint: "xxx",
expirationTime: null,
keys: {
p256dh: "xxxx",
auth: "xxxx",
},
};
// push的数据
const payload = {
title: "消息标题",
body: "点开看看吧",
icon: "xxx.png",
data: { url: "https://www.xxx.com" },
};
webpush.sendNotification(pushSubscription, JSON.stringify(payload));
推送服务接收到了服务器的调用请求,向设备推送消息。
要想在浏览器中接收推送信息,只需在Service Worker中监听push事件即可,接收到消息之后调用通知api展示通知:
this.addEventListener("push", function (event) {
console.log("[Service Worker] Push Received.");
console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);
let notificationData = event.data.json();
const title = notificationData.title;
// 弹通知消息框
event.waitUntil(self.registration.showNotification(title, notificationData));
});
三、项目实战 |
本次项目使用的是firebase云消息服务,firebase对一系列api进行了封装,在我们项目中的流程为:
原理大家都懂了,下面关结合代码介绍关键步骤:
在最开始,需要在firebase上进行云消息项目的配置,配置完成后,就可以拿到公钥和私钥、项目id等信息,用于初始化firebase以及后端发送推送时需要的一些私钥等。
来到步骤2,进行消息推送相关api浏览器支持情况检查,若检查到浏览器不支持,则进行不支持回调,该浏览器无法使用消息推送功能:
//检查是否支持service worker、notification、pushManager
function checkFirebaseSupport() {
return (
checkNotificationSupport() &&
checkServiceworkerSupport() &&
checkPushMessageSupport()
);
}
//Notification支持判断
function checkNotificationSupport() {
return "Notification" in window;
}
//service worker支持判断
function checkServiceworkerSupport() {
return "serviceWorker" in window.navigator;
}
//PushManager支持判断
function checkPushMessageSupport() {
return "PushManager" in window;
}
3、初始化firebase
确认支持之后初始化firebase,带上创建项目时的参数,具体看如下代码注释解析:
import * as firebase from "firebase";
//firebase项目初始化
function firebaseInit() {
firebase.initializeApp({
messagingSenderId: FCM_APP_ID,
projectId: PROJECT_ID,
apiKey: API_KEY,
appId: APP_ID,
});
this.messaging = firebase.messaging();
window.messaging = this.messaging;
htmlOnMessageInit();
}
//当页面聚焦时,监听推送事件,必须初始化,否则收不到推送
function htmlOnMessageInit() {
window.addEventListener("load", function () {
//手动更新service worker
navigator.serviceWorker
.getRegistration("/firebase-cloud-messaging-push-scope")
.then((registration) => {
registration && registration.update();
});
if (window.messaging) {
//firebase封装的方法,监听service worker的postMessage事件
//当收到推送且用户当前浏览器页面处于该service worker的作用域范围时,能拿到推送的数据payload
//考虑到用户已经处于我们平台的页面了,此时不显示通知,实际上是可以调用notification的api去显示通知的
window.messaging.onMessage(function (payload) {
console.log("Message received:", payload);
});
}
});
}
4、通知授权
当用户触发页面授权的操作时,会弹出询问是否允许授权接收通知弹窗,通知有3种状态,能监听到状态并进行行为上报
//调起授权
function getRequestPermission(grantedCallback, deniedCallback, closeCallback) {
//调起授权询问
Notification.requestPermission().then(async (permission) => {
switch (permission) {
case "granted": {
//此处可加上允许授权事件上报
//调用获取subscribe方法
let token = await _this.getToken();
if (token) {
grantedCallback(token);
sendTokenToServer(token);
}
break;
}
case "denied": {
//此处可加上拒绝授权事件上报
deniedCallback();
break;
}
default: {
//此处可加上默认处理、关闭通知授权事件上报
closeCallback();
break;
}
}
});
}
若授权了,则能够调用firebase封装好的getToken()方法获取到subscribe,唯一标识此浏览器,并将此标识发送给后端存储起来
//获取firebase token
async function getToken() {
try {
//firebase封装好的getToken()方法获取到subscribe
let token = await this.messaging.getToken();
return token;
} catch (e) {
console.log("get token error", e);
return false;
}
}
6、推送消息
后端使用FCM提供的web push的库发送推送,带上私钥和subscribe
from firebase_admin import messaging
import sys
import json
cred = credentials.Certificate('xxxx.json')
default_app = firebase_admin.initialize_app(cred)
message = messaging.Message(
data={
'data_title': 'test title',
'data_body': 'hahahaha',
'data_icon': 'xxx',
'jump_url': 'xxx',
'send_id' : '123123',
'isShow': 'true',
},
token='xxx'
)
response = messaging.send(message)
7、接收推送并展示通知
FCM验证过后,发送push给浏览器,service worker监听push事件,根据所在页面不同状态而调用不同的处理方法并进行行为统计上报
a.当浏览器位于前台时,考虑到用户已经在当前活动页面了就不显示通知了(实际上也是可以调用notification的方法显示通知的),但是能监听到消息
if (window.messaging) {
//此处是与service worker进行通信
window.messaging.onMessage(function (payload) {
console.log("Message received:", payload);
});
}
b.当浏览器位于后台时,收到并显示通知,在service worker文件处理:
//当页面位于后台时,调用的是此方法
messaging.setBackgroundMessageHandler(function (payload) {
console.log(
"[firebase-messaging-sw.js] Received background message ",
payload
);
if (payload.data.isShow === "true") {
//payload.data可获取到消息的内容
const notification = payload.data;
//设置消息的标题、内容、图标、点击需要跳转的地址、该消息的标识
const notificationTitle = notification.data_title;
const notificationOptions = {
body: notification.data_body,
icon: notification.data_icon,
data: {
linkUrl: notification.jump_url,
sendId: notification.send_id,
},
};
//向后端发送收到消息统计
//service worker限制无法使用XMLHttpRequest,可使用fetch api发起请求
var request = new Request(
`https://xxx.xxx.com/game_message/show_times?SEND_ID=${notification.send_id}`,
{
method: "GET",
mode: "no-cors",
redirect: "follow",
headers: new Headers({
"Content-Type": "text/plain",
}),
}
);
fetch(request)
.then(function () {
console.log("send show notification statistics succ");
})
.catch(function (err) {
console.log("send show notification statistics fail");
});
//设置显示通知
return self.registration.showNotification(
notificationTitle,
notificationOptions
);
}
});
四、问题&解决
1、如何监控用户收到消息后的行为?
firebase消息分2种类型,分别为通知消息和数据消息
但是不管哪一种方式,页面都必须要监听messaging.onMessage()方法,否则无法收到推送(踩过的坑)
2、如何更新service worker文件?
(1)每次进入页面,代码手动更新servie worker文件,当service worker文件有更新的时候,会再次进行安装,但是安装完成之后处于waiting状态,还是旧的service worker控制页面,需要关闭页面之后再打开页面,新的service worker才能控制页面,于是需要配合(2)的使用,安装完成之后新service worker就能控制页面了(网上说service worker每24小时会自动更新一次,测试结果来看是没有的)
navigator.serviceWorker
.getRegistration("/firebase-cloud-messaging-push-scope")
.then((registration) => {
registration && registration.update();
});
2)在service worker的install事件中,调用self.skipWaiting()跳过waiting状态直接进入activate激活状态,新service worker控制页面
self.addEventListener("install", function (event) {
self.skipWaiting();
})
3、是否需要频繁授权通知,影响用户体验?(通知授权逻辑是如何的呢?)
文档没说明,那就手动测试一下吧:
4、如何清除无效token?
背景:目前发送失败一次我们才知道这条token是无效的,浪费了发送时间,且当后续收集到很多token时,查询和发送效率会有一定影响。
解决:与后端协商推送消息时带上isShow参数,isShow为true时,才显示消息,isShow为false则用来检查token是否清除,不显示消息,不会对用户造成影响,这个可以在服务端空闲的时候进行定期的清除工作,不影响正常推送过程。
收益:
(1)提高查询token的效率
(2)提高发送消息,减少无效发送
messaging.setBackgroundMessageHandler(function (payload) {
//payload.data.isShow === 'true'才显示消息,否则是token有效性检查,不显示通知
if (payload.data.isShow === "true") {
const notification = payload.data;
const notificationTitle = notification.data_title;
const notificationOptions = {
body: notification.data_body,
icon: notification.data_icon,
data: {
linkUrl: notification.jump_url,
sendId: notification.send_id,
},
};
return self.registration.showNotification(
notificationTitle,
notificationOptions
);
}
});
五、通知其他玩法
1、通知多选形式:https://www.jianshu.com/p/f9480c35e32d
2、更多的通知属性配置参考
https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
来源-微信公众号:三七互娱技术团队
出处:https://mp.weixin.qq.com/s/bqfPma5E0PfZ0c3XgpkSCQ
*请认真填写需求信息,我们会在24小时内与您取得联系。