JavaScript
使用localStorage¶
Cookie存数据影响访问速度(每次请求都需要带上Cookie),使用localStorage存储有更大容量,还不易丢失
建议将用户的大段输入随时存储到localStorage中
高级应用可以是把js等代码文件这样缓存到本地,安全性讨论见https://imququ.com/post/enhance-security-for-ls-code.html
//写入
var storage=window.localStorage;
storage["a"]=1;
//清空
window.localStorage.clear();
使用phantomjs爬取网页¶
有些时候我们用Python的requests并不能很完美地渲染好网页,例如人家用酷炫的js作图了,我就想得到这张图,这时候用phantomjs就好啦
爬取目标:
这个网页的右边有一张Tumor Types with AKT1 Mutations的图
代码:
代码的细节:
-
打开页面之前为了截图方便需要先设置浏览器的大小,这里设置为了1920*1080
-
不要一打开页面就截图,而是等到页面加载好了最后一个请求(从Chrome开发人员工具查看最后的请求是啥)后,再等待5s后执行截图、导出HTML并退出
-
为了防止无限等待,设置最长2min后timeout退出
-
为了方便批量化处理,从命令行参数读取需要爬取的基因名称
-
在运行的时候有设置代理和不要载入图片的参数,具体见官方文档
jQuery劫持show事件¶
我的需求:用户登录的div需要点击Login后显示(toggle),此时浏览器已经自动帮用户填上了用户名和密码,用户需要手动点击登录按钮才会触发登录请求;现在我想加入快速登录功能,在显示登录div后自动提交登录请求,如果为空或密码错误再交给用户输入
我的解决方案:加入下述扩展jQuery的代码后,对#login绑定beforeShow事件,处理函数先根据全局变量是否存在来判断是否执行过(防止死循环),如果没有执行过则执行登录函数clicklogin并设置全局变量
效果:如果浏览器自动填入了正确的用户名密码,则用户点击Login后快速闪过登录输入框即完成登录;如果浏览器没有自动填入用户名密码,clicklogin函数直接return,用户没有感知;如果浏览器填入的密码是错的,用户会看到密码错误提示,1s后再次toggle登录的div要求用户输入
From: http://stackoverflow.com/questions/1225102/jquery-event-to-trigger-action-when-a-div-is-made-visible
引入jQuery后,修改jQuery自身的show函数以扩展bind:
jQuery(function($) {
var _oldShow = $.fn.show;
$.fn.show = function(speed, oldCallback) {
return $(this).each(function() {
var obj = $(this),
newCallback = function() {
if ($.isFunction(oldCallback)) {
oldCallback.apply(obj);
}
obj.trigger('afterShow');
};
// you can trigger a before show if you want
obj.trigger('beforeShow');
// now use the old function to show the element passing the new callback
_oldShow.apply(obj, [speed, newCallback]);
});
}
});
然后就可以使用bind注册beforeShow
,afterShow
咯:
jQuery(function($) {
$('#test')
.bind('beforeShow', function() {
alert('beforeShow');
})
.bind('afterShow', function() {
alert('afterShow');
})
.show(1000, function() {
alert('in show callback');
})
.show();
});
读取GET参数¶
有些时候对GET参数的处理交给了前端,后端的PHP可以$_GET[“parameter”],前端JS咋办呢?
From: http://stackoverflow.com/questions/979975/how-to-get-the-value-from-the-get-parameters
var QueryString = function () {
// This function is anonymous, is executed immediately and
// the return value is assigned to QueryString!
var query_string = {};
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
// If first entry with this name
if (typeof query_string[pair[0]] === "undefined") {
query_string[pair[0]] = decodeURIComponent(pair[1]);
// If second entry with this name
} else if (typeof query_string[pair[0]] === "string") {
var arr = [ query_string[pair[0]],decodeURIComponent(pair[1]) ];
query_string[pair[0]] = arr;
// If third or later entry with this name
} else {
query_string[pair[0]].push(decodeURIComponent(pair[1]));
}
}
return query_string;
}();
执行后就可以这么使用:
if (typeof(QueryString.parameter)!="undefined") {
alert(QueryString.parameter);//do something with the parameter
}
使用 Github Issue 作为博客评论区¶
人家大佬的项目:http://github.com/wzpan/comment.js,中文文档
如果觉得cloudflare加载速度不佳,可以把所有js打包成一个文件
效果如本博客页面底部评论区所示,为了偷懒就没有为每个md文件单独开issue了,整个blog共用一个issue
history.replaceState修改历史记录¶
如v2ex按照是否:visited来区分点开过和没点开过的帖子,其实现是url带上#reply回复数量
但如果帖子页面有多种进入方式(自动跳转到页尾、发起了回复等),那么url并不一定与需要的一致
我们可以使用history API来修改历史记录,从而保证带上#reply回复数量
的url一定被认为访问过;而且自动改回去用户无感知(否则刷新后会打开不一样的页面)
代码如下:
<script>
setTimeout( function(){
var oldurl = location.href;
history.replaceState(null, null, '/t/{{topic["id"]}}#reply{{topic["replyCount"]}}');
history.replaceState(null, null, oldurl);
}, 1000);
</script>
记住一个checkbox的状态(用localStorage)¶
查询是否勾选用.is(":checked")
, 改变勾选状态用.prop("checked",true)
<script>
function checkbox_onclick(){
var checked = $("#thecheckbox").is(":checked");
if(checked) localStorage.setItem("status_thecheckbox","1");
else localStorage.setItem("status_thecheckbox","0");
}
</script>
<input type="checkbox" id="thecheckbox" onclick='checkbox_onclick();'>
<script>
var status_thecheckbox = localStorage.getItem("status_thecheckbox");
if(status_thecheckbox!=null && status_thecheckbox=="1") $("#thecheckbox").prop("checked",true);
</script>
NodeJS¶
用Docker执行npm¶
例如安装canvas和gifencoder包:
PACKAGES="canvas gifencoder"
docker run --rm --volume="`pwd`:/app" -w /app -it node:10 npm install ${PACKAGES} --registry=https://registry.npm.taobao.org
使用InstantClick踩坑¶
快速使用¶
http://instantclick.io/v3.1.0/instantclick.min.js
一定要在页面底部 </body>
之前才能引入:
<script src="instantclick.min.js" data-no-instant></script>
<script data-no-instant>InstantClick.init('mousedown');</script>
被预加载的页面不能让后端返回302¶
否则会显示跳转之前的URL
这种情况下可以对这个链接禁止预加载(不过更应该考虑这种链接改为post请求) 在a标签加上data-no-instant
注意默认配置下后端将被频繁请求 频率限制需要放宽¶
官网给出的代码使用InstantClick.init()
,意味着鼠标移动上去就会触发加载(不是只触发一次),鼠标反复移动会导致大量的请求
如果后端做了请求频率限制 需要放宽限制
还是改为用mousedown
来初始化 只有用户确实点击了才开始加载 据说也能有很好的效果
InstantClick引入一些副作用 对页面js要进行修改¶
js无法取得正确的referrer¶
页面加载的请求是js执行的 document.referrer不会被设置为上一页
document.addEventListener 重复触发¶
例如绑定paste事件 你可能这么写:
document.addEventListener('paste', handlepaste);
在切换页面后 这个事件会多次绑定 导致多次触发
我的做法是先判断一个变量是否存在 不存在才设置:
if(typeof paste_registered == "undefined"){
document.addEventListener('paste', handlepaste);
paste_registered = true;
}
你也可以把这一部分不能重复执行的代码放入<script data-no-instant>
中,但如果前一页没有这一块代码(也就是这个代码是当前页面才有的,需要执行一次),进入当前页面是不会触发的
返回上一页重复执行页面添加元素的js 导致元素重复出现¶
现在的方法是对js动态添加的元素加个class 然后用jQuery的remove方法先通通删掉再添加
页面ready事件不会触发¶
需要加入InstantClick.on('change', callback);
加到Init后即可
但是似乎这个事件触发在页面图片加载完成之前Orz 不够完美
超链接的#hash定位功能也需要自己实现¶
预加载的页面总是定位到顶部,忽视地址栏中的#end
这种定位hash
我的做法是这样写上述onchange的callback函数implement_hashjump
:
function has_hashjump(){ // if there is a #hash present for jumpping, return true
var hash = document.location.hash.replace("#","");
if(!hash) return false;
if(document.getElementById(hash) || document.getElementsByName(hash).length>0) return true;
else return false;
}
function implement_hashjump() {
if ( has_hashjump() ) {
var hash = document.location.hash.replace("#","");
if(document.getElementById(hash)) {
document.documentElement.scrollTop = $("#"+hash).offset().top;
}
else{
document.documentElement.scrollTop = $("[name='"+hash+"']").offset().top;
}
}
}
用原生Javascript操作DOM节点 The Basics of DOM Manipulation in Vanilla JavaScript¶
https://www.sitepoint.com/dom-manipulation-vanilla-javascript-no-jquery/
选择元素¶
const myElement = document.querySelector('#foo > div.bar')
myElement.matches('div.bar') === true
注意querySelector是立即执行 而getElementsByTagName是取值的时候执行效率更高
对元素列表遍历应该这么写:
[].forEach.call(myElements, doSomethingWithEachElement)
myElement.children
,myElement.firstElementChild
只会有tag,而myElement.childNodes
,myElement.firstChild
会有文本节点
如:myElement.firstChild.nodeType === 3 // this would be a text node
修改class和属性¶
myElement.classList.add('foo')
myElement.classList.remove('bar')
myElement.classList.toggle('baz')
// Set multiple properties using Object.assign()
Object.assign(myElement, {
value: 'foo',
id: 'bar'
})
// Remove an attribute
myElement.value = null
除了直接赋值,还有这些方法.getAttibute(), .setAttribute() and .removeAttribute()
但他们会直接修改HTML 导致重绘 只有没有对应属性的时候如colspan
才应该这么干
修改CSS¶
myElement.style.marginLeft = '2em'
//获得计算出来的CSS属性
getComputedStyle(myElement).getPropertyValue('margin-left')
修改DOM¶
const myNewElement = document.createElement('div')
const myNewTextNode = document.createTextNode('some text')
// Append element1 as the last child of element2
element1.appendChild(element2)
// Insert element2 as child of element 1, right before element3
element1.insertBefore(element2, element3)
// Create a clone
const myElementClone = myElement.cloneNode()
myParentElement.appendChild(myElementClone)
// 删除一个节点
myElement.parentNode.removeChild(myElement)
当需要把多个元素appendChild到一个已经在页面上的元素时,每次append都会重绘 这时候就应该用DocumentFragment
const fragment = document.createDocumentFragment()
fragment.appendChild(text)
fragment.appendChild(hr)
myElement.appendChild(fragment)
监听事件¶
事件event里面有target
指向谁触发的事件
const myForm = document.forms[0]
const myInputElements = myForm.querySelectorAll('input')
Array.from(myInputElements).forEach(el => {
el.addEventListener('change', function (event) {
console.log(event.target.value)
})
})
阻止默认行为¶
.preventDefault()
.stopPropagation()
子节点click不会再冒泡触发父节点onclick
Event delegation¶
对表单每个input修改时执行,直接对form添加change的事件 不需要对每个input添加,这样也自动支持动态新添加的input
myForm.addEventListener('change', function (event) {
const target = event.target
if (target.matches('input')) {
console.log(target.value)
}
})
动画¶
需要高性能时 不要用setTimeout 而使用requestAnimationFrame
const start = window.performance.now()
const duration = 2000
window.requestAnimationFrame(function fadeIn (now)) {
const progress = now - start
myElement.style.opacity = progress / duration
if (progress < duration) {
window.requestAnimationFrame(fadeIn)
}
}
劫持动态图片加载 修改src属性¶
React网站应用底层用的是createElement方法(svg等对象用createElementNS),可以通过劫持document所属类原型的createElement方法来实现图片路径重定向
但是没有考虑使用innerHTML直接赋值的操作,如果目标站点确实用了这种技术,大不了再加个定时器遍历即可
var dc = HTMLDocument.prototype.createElement;
HTMLDocument.prototype.createElement = function (tag, options) {
var r = dc.call(document, tag, options);
if(tag=="img"||tag=='a') {
var x=r.setAttribute;
r.setAttribute=function(a,b){
if(a=="src"||a=="href"){
if(b[0]=="/") b=b.replace("/", window.ROOT);
else{
b = b.replace("http://","/web/0/http/0/");
b = b.replace("https://","/web/0/https/0/");
}
}
return x.call(r,a,b);
}
}
return r;
}
上述代码会将/开头的src和href属性的第一个/替换为window.ROOT
劫持Ajax和fetch¶
需要将fetch使用xhr实现,然后Hook Ajax即可
参见完整的RVPN劫持代码 jshook_preload.js
背景知识参见:RVPN网页版介绍 https://www.cc98.org/topic/4816921/
多个Ajax请求等待全部完成¶
方法就是把jQuery的ajax函数返回值放到数组里面,然后用$.when.apply(null, 数组).done
即可
实例:CC98发米机
function apiget(url, callback){
return $.get("/98api_cache/"+url, null, callback, "json");
}
var async_request=[check_permission(topic.boardId)];
var i;
for(i=from_; i>=from_-80;i-=20){
if(i<0) break;
async_request.push(apiget("Topic/"+topicid+"/post?from="+i+"&size=20", function(data){
lastfloors.push.apply(lastfloors,data);
}))
}
$.when.apply(null, async_request).done( function(){ alert("all done")} )
等待图片加载完成后 缩小过大的图片¶
首先等待DOM节点就绪,找到所有的img,等待图片加载完成后判断图片高度是否大于窗口高度的80%,如果太长就设置max-width:80vh
,可点击展开,再次点击则折叠并跳至图片开始的地方
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){var b="waitForImages",c=function(a){return a.srcset&&a.sizes}(new Image);a.waitForImages={hasImageProperties:["backgroundImage","listStyleImage","borderImage","borderCornerImage","cursor"],hasImageAttributes:["srcset"]},a.expr.pseudos["has-src"]=function(b){return a(b).is('img[src][src!=""]')},a.expr.pseudos.uncached=function(b){return!!a(b).is(":has-src")&&!b.complete},a.fn.waitForImages=function(){var d,e,f,g=0,h=0,i=a.Deferred(),j=this,k=[],l=a.waitForImages.hasImageProperties||[],m=a.waitForImages.hasImageAttributes||[],n=/url\(\s*(['"]?)(.*?)\1\s*\)/g;if(a.isPlainObject(arguments[0])?(f=arguments[0].waitForAll,e=arguments[0].each,d=arguments[0].finished):1===arguments.length&&"boolean"===a.type(arguments[0])?f=arguments[0]:(d=arguments[0],e=arguments[1],f=arguments[2]),d=d||a.noop,e=e||a.noop,f=!!f,!a.isFunction(d)||!a.isFunction(e))throw new TypeError("An invalid callback was supplied.");return this.each(function(){var b=a(this);f?b.find("*").addBack().each(function(){var b=a(this);b.is("img:has-src")&&!b.is("[srcset]")&&k.push({src:b.attr("src"),element:b[0]}),a.each(l,function(a,c){var d,e=b.css(c);if(!e)return!0;for(;d=n.exec(e);)k.push({src:d[2],element:b[0]})}),a.each(m,function(a,c){var d=b.attr(c);return!d||void k.push({src:b.attr("src"),srcset:b.attr("srcset"),element:b[0]})})}):b.find("img:has-src").each(function(){k.push({src:this.src,element:this})})}),g=k.length,h=0,0===g&&(d.call(j),i.resolveWith(j)),a.each(k,function(f,k){var l=new Image,m="load."+b+" error."+b;a(l).one(m,function b(c){var f=[h,g,"load"==c.type];if(h++,e.apply(k.element,f),i.notifyWith(k.element,f),a(this).off(m,b),h==g)return d.call(j[0]),i.resolveWith(j[0]),!1}),c&&k.srcset&&(l.srcset=k.srcset,l.sizes=k.sizes),l.src=k.src}),i.promise()}});
$(function(){$("img").waitForImages(function(){
( $("img").filter(function(){return $(this).height()>document.documentElement.clientHeight * 0.8}) )
.css("max-height","80vh")
.css("cursor","pointer")
.on("click", function(){
if($(this).css("max-height")!="100%"){
$(this).css("max-height","100%");
}else {
$(this).css("max-height","80vh");
$("html,body").animate({scrollTop:$(this).position().top},"fast")
}
});
})});
CSS inline模糊预览图片¶
参考: https://css-tricks.com/the-blur-up-technique-for-loading-background-images/
使用一张很大的图片作为背景的时候,可能需要一张inline到css中的模糊背景图,完整方案参见上述链接
这里介绍从一张图片怎么变成模糊预览的inline CSS:
- 首先把图片变成40x22大小,这个直接用Windows自带的画图工具即可完成
- 然后丢给tinyjpg.com再压缩一下
- 用
base64 -w0 < x.jpg
获取图片的base64文本 - 放入下述svg中,再交给这个svg encoder: https://codepen.io/yoksel/details/JDqvs/
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="1500" height="823"
viewBox="0 0 1500 823">
<filter id="blur" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feGaussianBlur stdDeviation="20 20" edgeMode="duplicate" />
<feComponentTransfer>
<feFuncA type="discrete" tableValues="1 1" />
</feComponentTransfer>
</filter>
<image filter="url(#blur)"
xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJ ...[truncated]..."
x="0" y="0"
height="100%" width="100%"/>
</svg>
注意encoder输出的内容还要加上charset
,最终效果:
background-image: url(data:image/svg+xml;charset=utf-8,%3Csvg...);
a链接改用POST请求 jQuery¶
参考OneIndex,用POST方法表示来自文件列表的点击可以显示网页,默认的GET请求则下载文件
$('.file a').each(function(){
$(this).on('click', function () {
var form = $('<form target=_blank method=post></form>').attr('action', $(this).attr('href')).get(0);
$(document.body).append(form);
form.submit();
$(form).remove();
return false;
});
});
创建一个文件下载 Blob¶
参考OneIndex的downall
方法
Blob文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
还可以看看这篇:https://juejin.im/post/59e35d0e6fb9a045030f1f35
let blob = new Blob(["文档内容"], {
type: 'text/plain'
}); // 构造Blob对象
let a = document.createElement('a'); // 伪造一个a对象
a.href = window.URL.createObjectURL(blob); // 构造href属性为Blob对象生成的链接
a.download = "666.txt"; // 文件名称,你可以根据你的需要构造
a.click() // 模拟点击
a.remove();
爬取微信小程序 朵朵校友圈¶
- 在分身空间中安装微信,使用HttpCanary抓到wxapkg的url
- 用https://gist.githubusercontent.com/Integ/bcac5c21de5ea35b63b3db2c725f07ad/raw/a4d5f24f4d0102ce864008a86fdcc6e7888205c0/unwxapkg.py 这个工具对小程序解包
- 搜索
duo_session
关键词,找到对应的util.js的addSign,用chrome开发人员工具格式化代码 - 看了看整段代码 挺复杂的,懒得用python改写,就保留原样js使用nodejs调用吧
- burpsuite验证确实可行
假设你已经有了addSign方法 那么就提供个http服务给python爬虫调用吧:
var http = require("http");
function start(port) {
function onRequest(request, response) {
var postData = "";
request.setEncoding("utf8");
request.addListener("data", function(postDataChunk) {
postData += postDataChunk;
});
request.addListener("end", function() {
//console.log(postData);
console.log("["+new Date().toLocaleString()+"]", request.connection.remoteAddress);
var data = JSON.parse(postData);
addSign(data)
response.writeHead(200, {"Content-Type": "text/html"});
response.write(JSON.stringify(data));
response.end();
});
}
http.createServer(onRequest).listen(port);
console.log("Server has started.");
}
start(8888);
保持特定元素相对于窗口的位置不变¶
考虑这样一个场景:一个列表,每一项都可以点击来展开详情div,点击时同时隐藏其他的详情(同一时刻只显示一个)
发现一个bug:特定情况下(不明原因),用户点击后页面位置发生了变化:前面的一个比较长的div隐藏后,当前的位置跳到了很下面的地方,需要手动翻回去,用户体验极差
总而言之,进行一些页面DOM操作后,我们想保持特定元素相对于窗口的位置不变
解决方案:在处理点击event时,先记录event.target相对于window的top位置,在详情div隐藏以及显示后再次记录top位置,这两个位置之间的差值就是需要滚动页面的多少
当这个差值很小的时候,可以理解为允许的误差,实际没有可见的变化 无需操作
HTML:
onclick="handle_click(event)"
JS:
function fix_position(et, oldt){
var newt = et.getBoundingClientRect().top;
if(Math.abs(oldt-newt)<2) return;
$(window).scrollTop($(window).scrollTop()+newt-oldt);
}
function handle_click(event){
var et = event.target;
var oldt = et.getBoundingClientRect().top;
//...code for hide and show divs...
fix_position(et, oldt); //also include this line to callback function if using ajax
}
Tampermonkey自动填充用户名密码表单,并通过前端的表单检查¶
感谢@CoolSpring的解决方案: https://v2ex.com/t/701749
现代化的前端做了表单检查,直接对input赋值不能通过检查,需要调用被重载的setter函数:
function mytype(input, value){
var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
nativeInputValueSetter.call(input, value);
input.dispatchEvent(new Event('input', {bubbles: true}));
}
用法:
mytype(document.querySelectorAll("input")[0], USERNAME);
mytype(document.querySelectorAll("input")[1], PASSWORD);
使用browserify将npm包打包成浏览器能用的js文件¶
浏览器不支持require,怎么在浏览器里使用一个npm包呢? browserify
示例:我想在tampermonkey里使用user-event这个npm包, 用来完整地模拟用户的交互,这个其实是上一个问题的笨重版本的解决方案
# 先安装目标库、browserify和terser
cnpm install @testing-library/user-event @testing-library/dom --save-dev
cnpm i -g browserify terser
# 写一个main.js导入这个库,导出到window里
var userEvent = require('@testing-library/user-event');
window.userEvent = userEvent.default;
# 执行打包
browserify main.js | terser --compress --mangle > bundle.js
# 在tampermonkey里使用
// @require 上传到cdn后的js地址
userEvent.type(document.querySelectorAll("input")[0], USERNAME);
Ubuntu安装nodejs¶
curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /usr/share/keyrings/yarnkey.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | tee /etc/apt/sources.list.d/yarn.list
apt update && apt install -y nodejs yarn
yarn config set registry https://registry.npm.taobao.org -g
yarn config set disturl https://npm.taobao.org/dist -g
yarn config set electron_mirror https://npm.taobao.org/mirrors/electron/ -g
yarn config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/ -g
yarn config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/ -g
yarn config set chromedriver_cdnurl https://cdn.npm.taobao.org/dist/chromedriver -g
yarn config set operadriver_cdnurl https://cdn.npm.taobao.org/dist/operadriver -g
yarn config set fse_binary_host_mirror https://npm.taobao.org/mirrors/fsevents -g