首页

实现一个Vue自定义指令懒加载

seo达人

什么是图片懒加载

当我们向下滚动的时候图片资源才被请求到,这也就是我们本次要实现的效果,进入页面的时候,只请求可视区域的图片资源这也就是懒加载。


比如我们加载一个页面,这个页面很长很长,长到我们的浏览器可视区域装不下,那么懒加载就是优先加载可视区域的内容,其他部分等进入了可视区域在加载。


这个功能非常常见,你打开淘宝的首页,向下滚动,就会看到会有图片不断的加载;你在百度中搜索图片,结果肯定成千上万条,不可能所有的都一下子加载出来的,很重要的原因就是会有性能问题。你可以在Network中查看,在页面滚动的时候,会看到图片一张张加载出来。


lazyLoad


为什么要做图片懒加载

懒加载是一种网页性能优化的方式,它能极大的提升用户体验。就比如说图片,图片一直是影响网页性能的主要元凶,现在一张图片超过几兆已经是很经常的事了。如果每次进入页面就请求所有的图片资源,那么可能等图片加载出来用户也早就走了。所以,我们需要懒加载,进入页面的时候,只请求可视区域的图片资源。


总结出来就两个点:


1.全部加载的话会影响用户体验


2.浪费用户的流量,有些用户并不像全部看完,全部加载会耗费大量流量。


懒加载原理

图片的标签是 img标签,图片的来源主要是 src属性,浏览器是否发起加载图片的请求是根据是否有src属性决定的。


所以可以从 img标签的 src属性入手,在没进到可视区域的时候,就先不给 img 标签的 src属性赋值。


懒加载实现

实现效果图:


imgLazyLoad


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <meta name="viewport" content="width=device-width, initial-scale=1.0">

   <title>Document</title>

   <style>

       div {

           display: flex;

           flex-direction: column;

       }

       img {

           width: 100%;

           height: 300px;

       }

   </style>

</head>

<body>

   <div>

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657907683.jpeg">

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657913523.jpeg">

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657925550.jpeg">

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657930289.jpeg">

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657934750.jpeg">

       <img data-src="https://cdn.suisuijiang.com/ImageMessage/5adad39555703565e79040fa_1590657918315.jpeg">

   </div>

</body>


</html>

监听 scroll 事件判断元素是否进入视口

const imgs = [...document.getElementsByTagName('img')];

let n = 0;


lazyload();


function throttle(fn, wait) {

   let timer = null;

   return function(...args) {

       if(!timer) {

           timer = setTimeout(() => {

               timer = null;

               fn.apply(this, args)

           }, wait)

       }

   }

}

 

function lazyload() {

   var innerHeight = window.innerHeight;

   var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;

   for(let i = n; i < imgs.length; i++) {

       if(imgs[i].offsetTop < innerHeight + scrollTop) {

           imgs[i].src = imgs[i].getAttribute("data-src");

           n = i + 1;

       }

       

   }

}

window.addEventListener('scroll', throttle(lazyload, 200));

可能会存在下面几个问题:


每次滑动都要执行一次循环,如果有1000多个图片,性能会很差

每次读取 scrollTop 都会引起回流

scrollTop跟DOM的嵌套关系有关,应该根据getboundingclientrect获取

滑到最后的时候刷新,会看到所有的图片都加载了

IntersectionObserver

Intersection Observer API提供了一种异步观察目标元素与祖先元素或文档viewport的交集中的变化的方法。


创建一个 IntersectionObserver对象,并传入相应参数和回调用函数,该回调函数将会在目标(target)元素和根(root)元素的交集大小超过阈值(threshold)规定的大小时候被执行。


var observer = new IntersectionObserver(callback, options);

IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数(即目标元素出现在root选项指定的元素中可见时,回调函数将会被执行),option是配置对象(该参数可选)。


返回的 observer是一个观察器实例。实例的 observe 方法可以指定观察哪个DOM节点。


具体的用法可以 查看 MDN文档


const imgs = [...document.getElementsByTagName('img')];

// 当监听的元素进入可视范围内的会触发回调

if(IntersectionObserver) {

    // 创建一个 intersection observer

    let lazyImageObserver = new IntersectionObserver((entries, observer) => {

        entries.forEach((entry, index) => {

            let lazyImage = entry.target;

            // 相交率,默认是相对于浏览器视窗

            if(entry.intersectionRatio > 0) {

               lazyImage.src = lazyImage.getAttribute('data-src');

               // 当前图片加载完之后需要去掉监听

                lazyImageObserver.unobserve(lazyImage);

            }


        })

    })

    for(let i = 0; i < imgs.length; i++) {

       lazyImageObserver.observe(imgs[i]);

    }

}

源码地址-codePen点击预览

vue自定义指令-懒加载

Vue自定义指令

下面的api来自官网自定义指令:


钩子函数

bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind: 只调用一次,指令与元素解绑时调用。

钩子函数参数

指令钩子函数会被传入以下参数:


el:指令所绑定的元素,可以用来直接操作 DOM。

binding:一个对象,包含以下 property:


name:指令名,不包括 v- 前缀。

value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。

expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。

arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。

modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。

vnode:Vue 编译生成的虚拟节点。

oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。

实现 v-lazyload 指令

<!DOCTYPE html>

<html lang="en">

   <head>

       <meta charset="UTF-8" />

       <meta name="viewport" content="width=device-width, initial-scale=1.0" />

       <title>Document</title>

       <style>

           img {

               width: 100%;

               height: 300px;

           }

       </style>

   </head>

   <body>

       <div id="app">

           <p v-for="item in imgs" :key="item">

               <img v-lazyload="item">

           </p>

       </div>

   </body>

   <script src="https://cdn.jsdelivr.net/npm/vue"></script>

   <script>

       Vue.directive("lazyload", {

           // 指令的定义

           bind: function(el, binding) {

2020年这些

seo达人

火车车次

/^[GCDZTSPKXLY1-9]\d{1,4}$/

手机机身码(IMEI)

/^\d{15,17}$/

必须带端口号的网址(或ip)

/^((ht|f)tps?:\/\/)?[\w-]+(\.[\w-]+)+:\d{1,5}\/?$/

网址(url,支持端口和"?+参数"和"#+参数)

/^(((ht|f)tps?):\/\/)?[\w-]+(\.[\w-]+)+([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?$/

统一社会信用代码

/^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/

迅雷链接

/^thunderx?:\/\/[a-zA-Z\d]+=$/

ed2k链接(宽松匹配)

/^ed2k:\/\/\|file\|.+\|\/$/

磁力链接(宽松匹配)

/^magnet:\?xt=urn:btih:[0-9a-fA-F]{40,}.*$/

子网掩码

/^(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])(?:\.(?:\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){3}$/

linux"隐藏文件"路径

/^\/(?:[^\/]+\/)*\.[^\/]*/

linux文件夹路径

/^\/(?:[^\/]+\/)*$/

linux文件路径

/^\/(?:[^\/]+\/)*[^\/]+$/

window"文件夹"路径

/^[a-zA-Z]:\\(?:\w+\\?)*$/

window下"文件"路径

/^[a-zA-Z]:\\(?:\w+\\)*\w+\.\w+$/

股票代码(A股)

/^(s[hz]|S[HZ])(000[\d]{3}|002[\d]{3}|300[\d]{3}|600[\d]{3}|60[\d]{4})$/

大于等于0, 小于等于150, 支持小数位出现5, 如145.5, 用于判断考卷分数

/^150$|^(?:\d|[1-9]\d|1[0-4]\d)(?:.5)?$/

html注释

/^<!--[\s\S]*?-->$/

md5格式(32位)

/^([a-f\d]{32}|[A-F\d]{32})$/

版本号(version)格式必须为X.Y.Z

/^\d+(?:\.\d+){2}$/

视频(video)链接地址(视频格式可按需增删)

/^https?:\/\/(.+\/)+.+(\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i

图片(image)链接地址(图片格式可按需增删)

/^https?:\/\/(.+\/)+.+(\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i

24小时制时间(HH:mm:ss)

/^(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/

12小时制时间(hh:mm:ss)

/^(?:1[0-2]|0?[1-9]):[0-5]\d:[0-5]\d$/

base64格式

/^\s*data:(?:[a-z]+\/[a-z0-9-+.]+(?:;[a-z-]+=[a-z0-9-]+)?)?(?:;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*?)\s*$/i

数字/货币金额(支持负数、千分位分隔符)

/^-?\d+(,\d{3})*(\.\d{1,2})?$/

数字/货币金额 (只支持正数、不支持校验千分位分隔符)

/(?:^[1-9]([0-9]+)?(?:\.[0-9]{1,2})?$)|(?:^(?:0){1}$)|(?:^[0-9]\.[0-9](?:[0-9])?$)/

银行卡号(10到30位, 覆盖对公/私账户, 参考微信支付)

/^[1-9]\d{9,29}$/

中文姓名

/^(?:[\u4e00-\u9fa5·]{2,16})$/

英文姓名

/(^[a-zA-Z]{1}[a-zA-Z\s]{0,20}[a-zA-Z]{1}$)/

车牌号(新能源)

/[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(([0-9]{5}[DF])|([DF][A-HJ-NP-Z0-9][0-9]{4}))$/

车牌号(非新能源)

/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/

车牌号(新能源+非新能源)

/^(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-HJ-NP-Z]{1}(?:(?:[0-9]{5}[DF])|(?:[DF](?:[A-HJ-NP-Z0-9])[0-9]{4})))|(?:[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领 A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9 挂学警港澳]{1})$/

手机号(mobile phone)中国(严谨), 根据工信部2019年公布的手机号段

/^(?:(?:\+|00)86)?1(?:(?:3[\d])|(?:4[5-7|9])|(?:5[0-3|5-9])|(?:6[5-7])|(?:7[0-8])|(?:8[\d])|(?:9[1|8|9]))\d{8}$/

手机号(mobile phone)中国(宽松), 只要是13,14,15,16,17,18,19开头即可

/^(?:(?:\+|00)86)?1[3-9]\d{9}$/

手机号(mobile phone)中国(最宽松), 只要是1开头即可, 如果你的手机号是用来接收短信, 优先建议选择这一条

/^(?:(?:\+|00)86)?1\d{10}$/

date(日期)

/^\d{4}(-)(1[0-2]|0?\d)\1([0-2]\d|\d|30|31)$/

email(邮箱)

/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/

座机(tel phone)电话(国内),如: 0341-86091234

/^\d{3}-\d{8}$|^\d{4}-\d{7}$/

身份证号(1代,15位数字)

/^[1-9]\d{7}(?:0\d|10|11|12)(?:0[1-9]|[1-2][\d]|30|31)\d{3}$/

身份证号(2代,18位数字),最后一位是校验位,可能为数字或字符X

/^[1-9]\d{5}(?:18|19|20)\d{2}(?:0\d|10|11|12)(?:0[1-9]|[1-2]\d|30|31)\d{3}[\dXx]$/

身份证号, 支持1/2代(15位/18位数字)

/(^\d{8}(0\d|10|11|12)([0-2]\d|30|31)\d{3}$)|(^\d{6}(18|19|20)\d{2}(0\d|10|11|12)([0-2]\d|30|31)\d{3}(\d|X|x)$)/

护照(包含香港、澳门)

/(^[EeKkGgDdSsPpHh]\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\d{7}$)/

帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线组合

/^[a-zA-Z]\w{4,15}$/

中文/汉字

/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/

小数

/^\d+\.\d+$/

数字

/^\d{1,}$/

html标签(宽松匹配)

/<(\w+)[^>]*>(.*?<\/\1>)?/

qq号格式正确

/^[1-9][0-9]{4,10}$/

数字和字母组成

/^[A-Za-z0-9]+$/

英文字母

/^[a-zA-Z]+$/

小写英文字母组成

/^[a-z]+$/

大写英文字母

/^[A-Z]+$/

密码强度校验,最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符

/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Z])(?=\S*[a-z])(?=\S*[!@#$%^&*? ])\S*$/

用户名校验,4到16位(字母,数字,下划线,减号)

/^[a-zA-Z0-9_-]{4,16}$/

ip-v4

/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/

ip-v6

/^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$/i

16进制颜色

/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/

微信号(wx),6至20位,以字母开头,字母,数字,减号,下划线

/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/

邮政编码(中国)

/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\d{4}$/

中文和数字

/^((?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])|(\d))+$/

不能包含字母

/^[^A-Za-z]*$/

java包名

/^([a-zA-Z_][a-zA-Z0-9_]*)+([.][a-zA-Z_][a-zA-Z0-9_]*)+$/

mac地址

/^((([a-f0-9]{2}:){5})|(([a-f0-9]{2}-){5}))[a-f0-9]{2}$/i

vue-router 导航守卫中 next 控制实现

seo达人

使用 vue-router 的导航守卫钩子函数,某些钩子函数可以让开发者根据业务逻辑,控制是否进行下一步,或者进入到指定的路由。


例如,后台管理页面,会在进入路由前,进行必要登录、权限判断,来决定去往哪个路由,以下是伪代码:


// 全局导航守卫

router.beforEach((to, from, next) => {

 if('no login'){

   next('/login')

 }else if('admin') {

   next('/admin')

 }else {

   next()

 }

})


// 路由配置钩子函数

{

 path: '',

 component: component,

 beforeEnter: (to, from, next) => {

   next()

 }

}


// 组件中配置钩子函数

{

 template: '',

 beforeRouteEnter(to, from, next) {

   next()

 }

}

调用 next,意味着继续进行下面的流程;不调用,则直接终止,导致路由中设置的组件无法渲染,会出现页面一片空白的现象。


钩子函数有不同的作用,例如 beforEach,afterEach,beforeEnter,beforeRouteEnter,beforeRouteUpdate,beforeRouteLeave,针对这些注册的钩子函数,要依次进行执行,并且在必要环节有控制权决定是否继续进入到下一个钩子函数中。


以下分析下源码中实现的方式,而源码中处理的边界情况比较多,需要抓住核心点,去掉冗余代码,精简出便于理解的实现。


精简源码核心功能

总结下核心点:钩子函数注册的回调函数,能顺序执行,同时会将控制权交给开发者。


先来一个能够注册回调函数的类:


class VueRouter {

 constructor(){

   this.beforeHooks = []

   this.beforeEnterHooks = []


   this.afterHooks = []

 }


 beforEach(callback){

   return registerHook(this.beforeHooks, callback)

 }

 beforeEnter(callback){

   return registerHook(this.beforeEnterHooks, callback)

 }

 afterEach(callback){

   return registerHook(this.afterHooks, callback)

 }

}

function registerHook (list, fn) {

 list.push(fn)

 return () => {

   const i = list.indexOf(fn)

   if (i > -1) list.splice(i, 1)

 }

}

声明的类,提供了 beforEach 、beforeEnter 和 afterEach 来注册必要的回调函数。


抽象出一个 registerHook 公共方法,作用:


注册回调函数

返回的函数,可以取消注册的回调函数

使用一下:


const router = new VueRouter()


const beforEach = router.beforEach((to, from, next) => {

 console.log('beforEach');

 next()

})

// 取消注册的函数

beforEach()

以上的回调函数会被取消,意味着不会执行了。



router.beforEach((to, from, next) => {

 console.log('beforEach');

 next()

})


router.beforeEnter((to, from, next) => {

 console.log('beforeEnter');

 next()

})


router.afterEach(() => {

 console.log('afterEach');

})

以上注册的钩子函数会依次执行。beforEach 和 beforeEnter 的回调接收内部传来的参数,同时通过调用 next 可继续走下面的回调函数,如果不调用,则直接被终止了。

最后一个 afterEach 在上面的回调函数都执行后,才被执行,且不接收任何参数。


先来实现依次执行,这是最简单的方式,在类中增加 run 方法,手动调用:



class VueRouter {

 // ... 其他省略,增加 run 函数


 run(){

   // 把需要依次执行的回调存放在一个队列中

   let queue = [].concat(

     this.beforeHooks,

     this.afterHooks

   )

   

   for(let i = 0; i < queue.length; i++){

     if(queue(i)) {

       queue(i)('to', 'from', () => {})

     }

   }

 }

}


// 手动调用


router.run()

打印:


'beforEach'

'beforeEnter'

上面把要依次执行的回调函数聚合在一个队列中执行,并传入必要的参数,但这样开发者不能控制是否进行下一步,即便不执行 next 函数,依然会依次执行完队列的函数。


改进一下:


class VueRouter {

 // ... 其他省略,增加 run 函数


 run(){

   // 把需要依次执行的回调存放在一个队列中

   let queue = [].concat(

     this.beforeHooks,

     this.afterHooks

   )

   queue[0]('to', 'from', () => {

     queue[1]('to', 'from', () => {

      console.log('调用结束');

     })

   })

 }

}


router.beforEach((to, from, next) => {

 console.log('beforEach');

 // next()

})


router.beforeEnter((to, from, next) => {

 console.log('beforeEnter');

 next()

})

传入的 next 函数会有调用下一个回调函数的行为,把控制权交给了开发者,调用了 next 函数会继续执行下一个回调函数;不调用 next 函数,则终止了队列的执行,所以打印结果是:


'beforEach'

上面实现有个弊端,代码不够灵活,手动一个个调用,在真实场景中无法确定注册了多少个回调函数,所以需要继续抽象成一个功能更强的方法:


function runQueue (queue, fn, cb) {

 const step = index => {

   // 队列执行结束了

   if (index >= queue.length) {

     cb()

   } else {

     // 队列有值

     if (queue[index]) {

       // 传入队列中回调,做一些必要的操作,第二个参数是为了进行下一个回调函数

       fn(queue[index], () => {

         step(index + 1)

       })

     } else {

       step(index + 1)

     }

   }

 }

 // 初次调用,从第一个开始

 step(0)

}

runQueue 就是执行队列的通用方法。


第一个参数为回调函数队列, 会依次取出来;

第二个参数是函数,它接受队列中的函数,进行一些其他处理;并能进行下个回调函数的执行;

第三个参数是队列执行结束后调用。

知道了这个函数的含义,来使用一下:



class VueRouter {

 // ... 其他省略,增加 run 函数


 run(){

   // 把需要依次执行的回调存放在一个队列中

   let queue = [].concat(

     this.beforeHooks,

     this.beforeEnterHooks

   )


   // 接收回到函数,和进行下一个的执行函数

   const iterator = (hook, next) => {

     // 传给回调函数的参数,第三个参数是函数,交给开发者调用,调用后进行下一个

     hook('to', 'from', () => {

       console.log('执行下一个回调时,处理一些相关信息');

       next()

     })

   }


   runQueue(queue, iterator, () => {


     console.log('执行结束');

     // 执行 afterEach 中的回调函数

     this.afterHooks.forEach((fn) => {

       fn()

     })

   })

 }

}

// 注册

router.beforEach((to, from, next) => {

 console.log('beforEach');

 next()

})


router.beforeEnter((to, from, next) => {

 console.log('beforeEnter');

 next()

})


router.afterEach(() => {

 console.log('afterEach');

})


router.run();

从上面代码可以看出来,每次把队列 queue 中的回调函数传给 iterator , 用 hook 接收,并调用。

传给 hook 必要的参数,尤其是第三个参数,开发者在注册的回调函数中调用,来控制进行下一步。

在队列执行完毕后,依次执行 afterHooks 的回调函数,不传入任何参数。


所以打印结果为:


beforEach

执行下一个回调时,处理一些相关信息

beforeEnter

执行下一个回调时,处理一些相关信息

执行结束

afterEach

以上实现的非常巧妙,再看 Vue-router 源码这块的实现方式,相信你会豁然开朗。

JavaScript 对象可以做到的三件事

seo达人

1. 访问内部属性

JavaScript 对象无法以常规方式访问的内部属性。内部属性名由双方括号[[]]包围,在创建对象时可用。


内部属性不能动态地添加到现有对象。


内部属性可以在某些内置 JavaScript 对象中使用,它们存储ECMAScript规范指定的内部状态。


有两种内部属性,一种操作对象的方法,另一种是存储数据的方法。例如:


[[Prototype]] — 对象的原型,可以为null或对象

[[Extensible]] — 表示是否允许在对象中动态添加新的属性

[[PrivateFieldValues]] — 用于管理私有类字段

2. 属性描述符对象

数据属性包含了一个数据值的位置,在这个位置可以读取和写入值。也就是说,数据属性可以通过 对象.属性 访问,就是我么平常接触的用户赋什么值,它们就返回什么,不会做额外的事情。


数据属性有4个描述其行为的特性(为了表示内部值,把属性放在两对方括号中),称为描述符对象。


属性 解释 默认值

[[Configurable]] 能否通过delete删除属性从而重新定义属性;

能否修改属性的特性;

能否把属性修改为访问器属性 true

[[Enumerable]] 能否通过for-in循环返回属性 true

[[Writable]] 能否修改属性的值 true

[[Value]] 包含这个属性的数据值 undefined

value 描述符是属性的数据值,例如,我们有以下对象 :


let foo = {

 a: 1

}

那么,a 的value属性描述符为1。


writable是指该属性的值是否可以更改。 默认值为true,表示属性是可写的。 但是,我们可以通过多种方式将其设置为不可写。


configurable 的意思是可以删除对象的属性还是可以更改其属性描述符。 默认值为true,这意味着它是可配置的。


enumerable 意味着它可以被for ... in循环遍历。 默认值为true,说明能通过for-in循环返回属性


将属性键添加到返回的数组之前,Object.keys方法还检查enumerable 描述符。 但是,Reflect.ownKeys方法不会检查此属性描述符,而是返回所有自己的属性键。


Prototype描述符有其他方法,get和set分别用于获取和设置值。


在创建新对象, 我们可以使用Object.defineProperty方法设置的描述符,如下所示:


let foo = {

 a: 1

}

Object.defineProperty(foo, 'b', {

 value: 2,

 writable: true,

 enumerable: true,

 configurable: true,

});

这样得到foo的新值是{a: 1, b: 2}。


我们还可以使用defineProperty更改现有属性的描述符。 例如:


let foo = {

 a: 1

}

Object.defineProperty(foo, 'a', {

 value: 2,

 writable: false,

 enumerable: true,

 configurable: true,

});

这样当我们尝试给 foo.a 赋值时,如:


foo.a = 2;

如果关闭了严格模式,浏览器将忽略,否则将抛出一个错误,因为我们将 writable 设置为 false, 表示该属性不可写。


我们还可以使用defineProperty将属性转换为getter,如下所示:


'use strict'

let foo = {

 a: 1

}


Object.defineProperty(foo, 'b', {

 get() {

   return 1;

 }

})

当我们这样写的时候:


foo.b = 2;

因为b属性是getter属性,所以当使用严格模式时,我们会得到一个错误:Getter 属性不能重新赋值。


3.无法分配继承的只读属性

继承的只读属性不能再赋值。这是有道理的,因为我们这样设置它,它是继承的,所以它应该传播到继承属性的对象。


我们可以使用Object.create创建一个从原型对象继承属性的对象,如下所示:


const proto = Object.defineProperties({}, {

 a: {

   value: 1,

   writable: false

 }

})


const foo = Object.create(proto)

在上面的代码中,我们将proto.a的 writable 描述符设置为false,因此我们无法为其分配其他值。


如果我们这样写:


foo.a = 2;

在严格模式下,我们会收到错误消息。


总结

我们可以用 JavaScript 对象做很多我们可能不知道的事情。


首先,某些 JavaScript 对象(例如内置浏览器对象)具有内部属性,这些属性由双方括号包围,它们具有内部状态,对象创建无法动态添加。


JavaScript对象属性还具有属性描述符,该属性描述符使我们可以控制其值以及可以设置它们的值,还是可以更改其属性描述符等。


我们可以使用defineProperty更改属性的属性描述符,它还用于添加新属性及其属性描述符。


最后,继承的只读属性保持只读状态,这是有道理的,因为它是从父原型对象继承而来的。

Web安全之CSRF实例解析

seo达人

前言

文章首次发表在 个人博客


之前写过一篇 web安全之XSS实例解析,是通过举的几个简单例子讲解的,同样通过简单得例子来理解和学习CSRF,有小伙伴问实际开发中有没有遇到过XSS和CSRF,答案是有遇到过,不过被测试同学发现了,还有安全扫描发现了可能的问题,这两篇文章就是简化了一下当时实际遇到的问题。


CSRF

跨站请求伪造(Cross Site Request Forgery),是指黑客诱导用户打开黑客的网站,在黑客的网站中,利用用户的登陆状态发起的跨站请求。CSRF攻击就是利用了用户的登陆状态,并通过第三方的站点来做一个坏事。


要完成一次CSRF攻击,受害者依次完成两个步骤:


登录受信任网站A,并在本地生成Cookie

在不登出A的情况,访问危险网站B

CSRF攻击


在a.com登陆后种下cookie, 然后有个支付的页面,支付页面有个诱导点击的按钮或者图片,第三方网站域名为 b.com,中的页面请求 a.com的接口,b.com 其实拿不到cookie,请求 a.com会把Cookie自动带上(因为Cookie种在 a.com域下)。这就是为什么在服务端要判断请求的来源,及限制跨域(只允许信任的域名访问),然后除了这些还有一些方法来防止 CSRF 攻击,下面会通过几个简单的例子来详细介绍 CSRF 攻击的表现及如何防御。


下面会通过一个例子来讲解 CSRF 攻击的表现是什么样子的。

实现的例子:

在前后端同域的情况下,前后端的域名都为 http://127.0.0.1:3200, 第三方网站的域名为 http://127.0.0.1:3100,钓鱼网站页面为 http://127.0.0.1:3100/bad.html。


平时自己写例子中会用到下面这两个工具,非常方便好用:

http-server: 是基于node.js的HTTP 服务器,它最大的好处就是:可以使用任意一个目录成为服务器的目录,完全抛开后端的沉重工程,直接运行想要的js代码;

nodemon: nodemon是一种工具,通过在检测到目录中的文件更改时自动重新启动节点应用程序来帮助开发基于node.js的应用程序

前端页面: client.html


<!DOCTYPE html>

<html lang="en">


<head>

   <meta charset="UTF-8">

   <meta name="viewport" content="width=device-width, initial-scale=1.0">

   <meta http-equiv="X-UA-Compatible" content="ie=edge">

   <title>CSRF-demo</title>

   <style>

       .wrap {

           height: 500px;

           width: 300px;

           border: 1px solid #ccc;

           padding: 20px;

           margin-bottom: 20px;

       }

       input {

           width: 300px;

       }

       .payInfo {

           display: none;

       }

       .money {

           font-size: 16px;

       }

   </style>

</head>


<body>

   <div class="wrap">

       <div class="loginInfo">

           <h3>登陆</h3>

           <input type="text" placeholder="用户名" class="userName">

           <br>

           <input type="password" placeholder="密码" class="password">

           <br>

           <br>

           <button class="btn">登陆</button>

       </div>

       

       

       <div class="payInfo">

           <h3>转账信息</h3>

           <p >当前账户余额为 <span class="money">0</span>元</p>

           <!-- <input type="text" placeholder="收款方" class="account"> -->

           <button class="pay">支付10元</button>

           <br>

           <br>

           <a href="http://127.0.0.1:3100/bad.html" target="_blank">

               听说点击这个链接的人都赚大钱了,你还不来看一下么

           </a>

       </div>

   </div>

</body>

<script>

   const btn = document.querySelector('.btn');

   const loginInfo = document.querySelector('.loginInfo');

   const payInfo = document.querySelector('.payInfo');

   const money = document.querySelector('.money');

   let currentName = '';

   // 第一次进入判断是否已经登陆

   Fetch('http://127.0.0.1:3200/isLogin', 'POST', {})

   .then((res) => {

       if(res.data) {

           payInfo.style.display = "block"

           loginInfo.style.display = 'none';

           Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 0})

           .then((res) => {

               money.innerHTML = res.data.money;

           })

       } else {

           payInfo.style.display = "none"

           loginInfo.style.display = 'block';

       }

       

   })

   // 点击登陆

   btn.onclick = function () {

       var userName = document.querySelector('.userName').value;

       currentName = userName;

       var password = document.querySelector('.password').value;

       Fetch('http://127.0.0.1:3200/login', 'POST', {userName, password})

       .then((res) => {

           payInfo.style.display = "block";

           loginInfo.style.display = 'none';

           money.innerHTML = res.data.money;

       })

   }

   // 点击支付10元

   const pay = document.querySelector('.pay');

   pay.onclick = function () {

       Fetch('http://127.0.0.1:3200/pay', 'POST', {userName: currentName, money: 10})

       .then((res) => {

           console.log(res);

           money.innerHTML = res.data.money;

       })

   }

   // 封装的请求方法

   function Fetch(url, method = 'POST', data) {

       return new Promise((resolve, reject) => {

           let options = {};

           if (method !== 'GET') {

               options = {

                   headers: {

                       'Content-Type': 'application/json',

                   },

                   body: JSON.stringify(data),

               }

           }

           fetch(url, {

               mode: 'cors', // no-cors, cors, *same-origin

               method,

               ...options,

               credentials: 'include',

           }).then((res) => {

               return res.json();

           }).then(res => {

               resolve(res);

           }).catch(err => {

               reject(err);

           });

       })

   }

   

</script>


</html>

实现一个简单的支付功能:


会首先判断有没有登录,如果已经登陆过,就直接展示转账信息,未登录,展示登陆信息

登陆完成之后,会展示转账信息,点击支付,可以实现金额的扣减

后端服务: server.js


const Koa = require("koa");

const app = new Koa();

const route = require('koa-route');

const bodyParser = require('koa-bodyparser');

const cors = require('@koa/cors');

const KoaStatic = require('koa-static');


let currentUserName = '';


// 使用  koa-static  使得前后端都在同一个服务下

app.use(KoaStatic(__dirname));


app.use(bodyParser()); // 处理post请求的参数


// 初始金额为 1000

let money = 1000;


// 调用登陆的接口

const login = ctx => {

   const req = ctx.request.body;

   const userName = req.userName;

   currentUserName = userName;

   // 简单设置一个cookie

   ctx.cookies.set(

       'name',

       userName,

       {

         domain: '127.0.0.1', // 写cookie所在的域名

         path: '/',       // 写cookie所在的路径

         maxAge: 10 * 60 * 1000, // cookie有效时长

         expires: new Date('2021-02-15'),  // cookie失效时间

         overwrite: false,  // 是否允许重写

         SameSite: 'None',

       }

     )

   ctx.response.body = {

       data: {

           money,

       },

       msg: '登陆成功'

   };

}

// 调用支付的接口

const pay = ctx => {

   if(ctx.method === 'GET') {

       money = money - Number(ctx.request.query.money);

   } else {

       money = money - Number(ctx.request.body.money);

   }

   ctx.set('Access-Control-Allow-Credentials', 'true');

   // 根据有没有 cookie 来简单判断是否登录

   if(ctx.cookies.get('name')){

       ctx.response.body = {

           data: {

               money: money,

           },

           msg: '支付成功'

       };

   }else{

       ctx.body = '未登录';

   }

}


// 判断是否登陆

const isLogin = ctx => {

   ctx.set('Access-Control-Allow-Credentials', 'true');


   if(ctx.cookies.get('name')){

       ctx.response.body = {

           data: true,

           msg: '登陆成功'

       };


   }else{

       ctx.response.body = {

           data: false,

           msg: '未登录'

       };

   }

}

// 处理 options 请求

app.use((ctx, next)=> {

   const headers = ctx.request.headers;

   if(ctx.method === 'OPTIONS') {

       ctx.set('Access-Control-Allow-Origin', headers.origin);

       ctx.set('Access-Control-Allow-Headers', 'Content-Type');

       ctx.set('Access-Control-Allow-Credentials', 'true');

       ctx.status = 204;

   } else {

       next();

   }

})


app.use(cors());

app.use(route.post('/login', login));

app.use(route.post('/pay', pay));

app.use(route.get('/pay', pay));

app.use(route.post('/isLogin', isLogin));


app.listen(3200, () => {

   console.log('启动成功');

});

执行 nodemon server.js,访问页面 http://127.0.0.1:3200/client.html


CSRF-demo


登陆完成之后,可以看到Cookie是种到 http://127.0.0.1:3200 这个域下面的。


第三方页面 bad.html


<!DOCTYPE html>

<html lang="en">

<head>

   <meta charset="UTF-8">

   <meta name="viewport" content="width=device-width, initial-scale=1.0">

   <title>第三方网站</title>

</head>

<body>

   <div>

       哈哈,小样儿,哪有赚大钱的方法,还是踏实努力工作吧!

       <!-- form 表单的提交会伴随着跳转到action中指定 的url 链接,为了阻止这一行为,可以通过设置一个隐藏的iframe 页面,并将form 的target 属性指向这个iframe,当前页面iframe则不会刷新页面 -->

       <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr" style="display: none">

           <input type="text" name="userName" value="xiaoming">

           <input type="text" name="money" value="100">

       </form>

       <iframe name="targetIfr" style="display:none"></iframe>

   </div>

</body>

<script>

   document.querySelector('.form').submit();

</script>

</html>

使用 HTTP-server 起一个 本地端口为 3100的服务,就可以通过 http://127.0.0.1:3100/bad.html 这个链接来访问,CSRF攻击需要做的就是在正常的页面上诱导用户点击链接进入这个页面

CSRF-DEMO


点击诱导链接,跳转到第三方的页面,第三方页面自动发了一个扣款的请求,所以在回到正常页面的时候,刷新,发现钱变少了。

我们可以看到在第三方页面调用 http://127.0.0.1:3200/pay 这个接口的时候,Cookie自动加在了请求头上,这就是为什么 http://127.0.0.1:3100/bad.html 这个页面拿不到 Cookie,但是却能正常请求 http://127.0.0.1:3200/pay 这个接口的原因。


CSRF攻击大致可以分为三种情况,自动发起Get请求, 自动发起POST请求,引导用户点击链接。下面会分别对上面例子进行简单的改造来说明这三种情况


自动发起Get请求

在上面的 bad.html中,我们把代码改成下面这样


<!DOCTYPE html>

<html>

 <body>

   <img src="http://127.0.0.1:3200/payMoney?money=1000">

 </body>

</html>

当用户访问含有这个img的页面后,浏览器会自动向自动发起 img 的资源请求,如果服务器没有对该请求做判断的话,那么会认为这是一个正常的链接。


自动发起POST请求

上面例子中演示的就是这种情况。


<body>

   <div>

       哈哈,小样儿,哪有赚大钱的方法,还是踏实努力工作吧!

       <!-- form 表单的提交会伴随着跳转到action中指定 的url 链接,为了阻止这一行为,可以通过设置一个隐藏的iframe 页面,并将form 的target 属性指向这个iframe,当前页面iframe则不会刷新页面 -->

       <form action="http://127.0.0.1:3200/pay" method="POST" class="form" target="targetIfr">

           <input type="text" name="userName" value="xiaoming">

           <input type="text" name="money" value="100">

       </form>

       <iframe name="targetIfr" style="display:none"></iframe>

   </div>

</body>

<script>

   document.querySelector('.form').submit();

</script>

上面这段代码中构建了一个隐藏的表单,表单的内容就是自动发起支付的接口请求。当用户打开该页面时,这个表单会被自动执行提交。当表单被提交之后,服务器就会执行转账操作。因此使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交。


引导用户点击链接

诱惑用户点击链接跳转到黑客自己的网站,示例代码如图所示


<a href="http://127.0.0.1:3100/bad.html">听说点击这个链接的人都赚大钱了,你还不来看一下么</a>

用户点击这个地址就会跳到黑客的网站,黑客的网站可能会自动发送一些请求,比如上面提到的自动发起Get或Post请求。


如何防御CSRF

利用cookie的SameSite

SameSite有3个值: Strict, Lax和None


Strict。浏览器会完全禁止第三方cookie。比如a.com的页面中访问 b.com 的资源,那么a.com中的cookie不会被发送到 b.com服务器,只有从b.com的站点去请求b.com的资源,才会带上这些Cookie

Lax。相对宽松一些,在跨站点的情况下,从第三方站点链接打开和从第三方站点提交 Get方式的表单这两种方式都会携带Cookie。但如果在第三方站点中使用POST方法或者通过 img、Iframe等标签加载的URL,这些场景都不会携带Cookie。

None。任何情况下都会发送 Cookie数据

我们可以根据实际情况将一些关键的Cookie设置 Stirct或者 Lax模式,这样在跨站点请求的时候,这些关键的Cookie就不会被发送到服务器,从而使得CSRF攻击失败。


验证请求的来源点

由于CSRF攻击大多来自第三方站点,可以在服务器端验证请求来源的站点,禁止第三方站点的请求。

可以通过HTTP请求头中的 Referer和Origin属性。


HTTP请求头


但是这种 Referer和Origin属性是可以被伪造的,碰上黑客高手,这种判断就是不安全的了。


CSRF Token

最开始浏览器向服务器发起请求时,服务器生成一个CSRF Token。CSRF Token其实就是服务器生成的字符串,然后将该字符串种植到返回的页面中(可以通过Cookie)

浏览器之后再发起请求的时候,需要带上页面中的 CSRF Token(在request中要带上之前获取到的Token,比如 x-csrf-token:xxxx), 然后服务器会验证该Token是否合法。第三方网站发出去的请求是无法获取到 CSRF Token的值的。

其他知识点补充

1. 第三方cookie

Cookie是种在服务端的域名下的,比如客户端域名是 a.com,服务端的域名是 b.com, Cookie是种在 b.com域名下的,在 Chrome的 Application下是看到的是 a.com下面的Cookie,是没有的,之后,在a.com下发送b.com的接口请求会自动带上Cookie(因为Cookie是种在b.com下的)


2. 简单请求和复杂请求

复杂请求需要处理option请求。


之前写过一篇特别详细的文章 CORS原理及@koa/cors源码解析,有空可以看一下。


3. Fetch的 credentials 参数

如果没有配置credential 这个参数,fetch是不会发送Cookie的


credential的参数如下


include:不论是不是跨域的请求,总是发送请求资源域在本地的Cookies、HTTP Basic anthentication等验证信息

same-origin:只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息

omit: 从不发送cookies.

平常写一些简单的例子,从很多细节问题上也能补充自己的一些知识盲点。

写一个脚本,将所有js文件后缀批量改成ts后缀

seo达人

做项目的时候准备把js项目重构成ts项目,需要把文件后缀改成ts,一个bat脚本搞定,命令如下:

@echo off

rem 正在搜索...

for /f "delims=" %%i in ('dir /b /a-d /s "*.js"') do ren "%%i" "%%~ni.ts" rem 搜索完毕 @pause

把脚本放到根目录下,双击运行完就可以了

六个好用的程序员开发在线工具

seo达人

网上可以找到前端开发社区贡献的大量工具,这篇文章列出了我最喜欢的一些工具,这些工具给我的工作带来了许多便利。


1. EnjoyCSS


老实说,虽然我做过许多前端开发,但我并不擅长 CSS。当我陷入困境时,EnjoyCSS 是我的大救星。EnjoyCSS 提供了一个简单的交互界面,帮助我设计元素,然后自动输出相应的 CSS 代码。




EnjoyCSS 可以输出 CSS、LESS、SCSS 代码,并支持指定需要支持哪些浏览器及其版本。开发简单页面时用起来比较方便,但不太适合复杂一点的前端项目(这类项目往往需要引入 CSS 框架)。

2. Prettier Playground


Prettier 是一个代码格式化工具,支持格式化 JavaScript 代码(包括 ES2017、JSX、Angular、Vue、Flow、TypeScript 等)。Prettier 会移除代码原本的样式,替换为遵循最佳实践的标准化、一致的样式。IDE 大多支持 Prettier 工具,不过 Prettier 也有在线版本,让你可以在浏览器里格式化代码。




如果工作电脑不在手边,使用移动端设备或者临时借用别人的电脑查看代码时,Prettier Playground 非常好用。相比在 IDE 或编辑器下使用 Prettier,个人更推荐通过 git pre-commit hook 配置 Prettier:hook 可以保证整个团队使用统一的配置,免去各自分别配置 IDE 或编辑器的麻烦。如果是老项目,hook 还可以设置只格式化有改动的单个文件甚至有改动的代码段,避免在 IDE 或编辑器下使用 Prettier 时不小心格式了大量代码,淹没了 commit 的主要改动,让 review 代码变得十分痛苦。

3. Postman


Postman 一直在我的开发工具箱里,测试后端 API 接口时非常好用。GET、POST、DELETE、OPTIONS、PUT 这些方法都支持。毫无疑问,你应该使用这个工具。




Postman 之外,Insomnia 也是很流行的 REST API 测试工具,亮点是支持 GraphQL。不过 Postman 从 去年夏天发布的 v7.2 起也支持了 GraphQL。

4. StackBlitz


Chidume Nnamdi 盛赞这是每个用户最喜欢的在线 IDE。StackBlitz 将大家最喜欢、最常用的 IDE Visual Studio Code 搬进了浏览器。


StackBlitz 支持一键配置 Angular、React、Ionic、TypeScript、RxJS、Svelte 等 JavaScript 框架,也就是说,只需几秒你就可以开始写代码了。


我觉得这个在线 IDE 很有用,特别是可以在线尝试一些样例代码或者库,否则仅仅尝试一些新特性就需要花很多时间在新项目初始化配置上。有了 StackBlitz,无需在本地从头搭建环境,花上几分钟就可以试用一个 NPM 包。很棒,不是吗?




微软官方其实也提供了在线版本的 VSCode,可以在浏览器内使用 VSCode,并且支持开发 Node.js 项目(基于 Azure)。不过 StackBlitz 更专注于优化前端开发体验,界面更加直观一点,也推出了 beta 版本的 Node.js 支持(基于 GCP,需要填表申请)。

5. Bit.dev


软件开发的基本原则之一就是代码复用。代码复用减少了开发量,让你不用从头开发组件。


这正是 Bit.dev 做的事,分享可重用的组件和片段,降低开发量,加速开发进程。


除了公开分享,它还支持在团队分享,让团队协作更方便。


正如 Bit.dev 的口号「组件即设计体系。协同开发更好的组件。」所言,Bit.dev 可以用来创建设计体系,允许团队内的开发者和设计师一起协作,从头搭建一套设计体系。


Bit.dev 目前支持 React、Vue、Angular、Node 及其他 JavaScript 框架。




在 Bit.dev 上不仅可以搜索组件,还可以直接查看组件的依赖,浏览组件的代码,甚至在线编辑代码并查看预览效果!选好组件后可以通过 Bit.dev 的命令行工具 bit 在本地项目引入组件,也可以通过 npm、yarn 引入组件。

6. CanIUse


CanIUse是非常好用的在线工具,可以方便地查看各大浏览器对某个特性的支持程度。


我过去经常碰到自己开发的应用的一些功能在其他浏览器下不支持的情况。比如我的作品集项目使用的某个特性在 Safari 下不支持,直到项目上线几个月后我才意识到。这些经验教训让我意识到需要检查浏览器兼容性。


我们来看一个例子吧。哪些浏览器支持 WebP 图像格式?




如你所见,Safari 和 IE 目前不支持 WebP。这意味着需要为不兼容的浏览器提供回退选项,比如:


<picture>

CanIUse 还可以在命令行下使用,例如,在命令行下查看 WebP 图像格式的浏览器兼容性:caniuse webp(运行命令前需要事先通过 npm install -g caniuse-cmd安装命令行工具。


10 个超有用的 JavaScript 技巧

seo达人

方法参数的验证

JavaScript 允许你设置参数的默认值。通过这种方法,可以通过一个巧妙的技巧来验证你的方法参数。


const isRequired = () => { throw new Error('param is required'); };

const print = (num = isRequired()) => { console.log(`printing ${num}`) };

print(2);//printing 2

print()// error

print(null)//printing null

非常整洁,不是吗?


格式化 json 代码

你可能对 JSON.stringify 非常熟悉。但是你是否知道可以用 stringify 进行格式化输出?实际上这很简单。


stringify 方法需要三个输入。 value,replacer 和 space。后两个是可选参数。这就是为什么我们以前没有注意过它们。要对 json 进行缩进,必须使用 space 参数。


console.log(JSON.stringify({name:"John",Age:23},null,'\t'));

>>>

{

"name": "John",

"Age": 23

}

从数组中获取唯一值

要从数组中获取唯一值,我们需要使用 filter 方法来过滤出重复值。但是有了新的 Set 对象,事情就变得非常顺利和容易了。


let uniqueArray = [...new Set([1, 2, 3, 3, 3, "school", "school", 'ball', false, false, true, true])];

>>> [1, 2, 3, "school", "ball", false, true]

从数组中删除虚值(Falsy Value)

在某些情况下,你可能想从数组中删除虚值。虚值是 JavaScript 的 Boolean 上下文中被认定为为 false 的值。 JavaScript 中只有六个虚值,它们是:


undefined

null

NaN

0

"" (空字符串)

false

滤除这些虚值的最简单方法是使用以下函数。


myArray.filter(Boolean);

如果要对数组进行一些修改,然后过滤新数组,可以尝试这样的操作。请记住,原始的 myArray 会保持不变。


myArray

   .map(item => {

       // Do your changes and return the new item

   })

   .filter(Boolean);

合并多个对象

假设我有几个需要合并的对象,那么这是我的首选方法。


const user = {

    name: 'John Ludwig',

    gender: 'Male'

};

const college = {

    primary: 'Mani Primary School',

    secondary: 'Lass Secondary School'

};

const skills = {

   programming: 'Extreme',

   swimming: 'Average',

   sleeping: 'Pro'

};

const summary = {...user, ...college, ...skills};

这三个点在 JavaScript 中也称为展开运算符。你可以在这里学习更多用法。


对数字数组进行排序

JavaScript 数组有内置的 sort 方法。默认情况下 sort 方法把数组元素转换为字符串,并对其进行字典排序。在对数字数组进行排序时,这有可能会导致一些问题。所以下面是解决这类问题的简单解决方案。


[0,10,4,9,123,54,1].sort((a,b) => a-b);

>>> [0, 1, 4, 9, 10, 54, 123]

这里提供了一个将数字数组中的两个元素与 sort 方法进行比较的函数。这个函数可帮助我们接收正确的输出。


Disable Right Click

禁用右键

你可能想要阻止用户在你的网页上单击鼠标右键。


<body oncontextmenu="return false">

   <div></div>

</body>

这段简单的代码将为你的用户禁用右键单击。


使用别名进行解构

解构赋值语法是一种 JavaScript 表达式,可以将数组中的值或对象的值或属性分配给变量。解构赋值能让我们用更简短的语法进行多个变量的赋值。


const object = { number: 10 };


// Grabbing number

const { number } = object;


// Grabbing number and renaming it as otherNumber

const { number: otherNumber } = object;

console.log(otherNumber); //10

获取数组中的最后一项

可以通过对 splice 方法的参数传入负整数,来数获取组末尾的元素。


let array = [0, 1, 2, 3, 4, 5, 6, 7]

console.log(array.slice(-1));

>>>[7]

console.log(array.slice(-2));

>>>[6, 7]

console.log(array.slice(-3));

>>>[5, 6, 7]

等待 Promise 完成

在某些情况下,你可能会需要等待多个 promise 结束。可以用 Promise.all 来并行运行我们的 promise。


const PromiseArray = [

   Promise.resolve(100),

   Promise.reject(null),

   Promise.resolve("Data release"),

   Promise.reject(new Error('Something went wrong'))];


Promise.all(PromiseArray)

 .then(data => console.log('all resolved! here are the resolve values:', data))

 .catch(err => console.log('got rejected! reason:', err))

关于 Promise.all 的主要注意事项是,当一个 Promise 拒绝时,该方法将引发错误。这意味着你的代码不会等到你所有的 promise 都完成。


如果你想等到所有 promise 都完成后,无论它们被拒绝还是被解决,都可以使用 Promise.allSettled。此方法在 ES2020 的最终版本得到支持。


const PromiseArray = [

   Promise.resolve(100),

   Promise.reject(null),

   Promise.resolve("Data release"),

   Promise.reject(new Error('Something went wrong'))];


Promise.allSettled(PromiseArray).then(res =>{

console.log(res);

}).catch(err => console.log(err));


//[

//{status: "fulfilled", value: 100},

//{status: "rejected", reason: null},

//{status: "fulfilled", value: "Data release"},

//{status: "rejected", reason: Error: Something went wrong ...}

//]

即使某些 promise 被拒绝,Promise.allSettled 也会从你所有的 promise 中返回结果。

修复一个因为 scrollbar 占据空间导致的 bug

seo达人

背景

这一个因为滚动条占据空间引起的bug, 查了一下资料, 最后也解决了,顺便研究一下这个属性, 做一下总结,分享给大家看看。


正文

昨天, 测试提了个问题, 现象是一个输入框的聚焦提示偏了, 让我修一下, 如下图:


image.png


起初认为是红框提示位置不对, 就去找代码看:


<Input

 // ...

 onFocus={() => setFocusedInputName('guidePrice')}

 onBlur={() => setFocusedInputName('')}

/>


<Table

 data-focused-column={focusedInputName}

 // ...

/>

代码上没有什么问题, 不是手动设置的,而且, 在我和另一个同事, 还有PM的PC上都是OK的:


image.png


初步判断是,红框位置结算有差异, 差异大小大概是17px, 但是这个差异是怎么产生的呢?


就去测试小哥的PC上看, 注意到一个细节, 在我PC上, 滚动条是悬浮的:

image.png


在他PC上, 滚动条是占空间的:


image.png


在他电脑上, 手动把原本的 overscroll-y: scroll 改成 overscroll-y: overlay 问题就结局了。


由此判定是: 滚动条占据空间 引起的bug。


overscroll-y: overlay

CSS属性 overflow, 定义当一个元素的内容太大而无法适应块级格式化上下文的时候该做什么。它是 overflow-x 和overflow-y的 简写属性 。

/* 默认值。内容不会被修剪,会呈现在元素框之外 */

overflow: visible;


/* 内容会被修剪,并且其余内容不可见 */

overflow: hidden;


/* 内容会被修剪,浏览器会显示滚动条以便查看其余内容 */

overflow: scroll;


/* 由浏览器定夺,如果内容被修剪,就会显示滚动条 */

overflow: auto;


/* 规定从父元素继承overflow属性的值 */

overflow: inherit;

官方描述:

overlay  行为与 auto 相同,但滚动条绘制在内容之上而不是占用空间。 仅在基于 WebKit(例如,Safari)和基于Blink的(例如,Chrome或Opera)浏览器中受支持。

表现:

html {

 overflow-y: overlay;

}

兼容性

没有在caniuse上找到这个属性的兼容性, 也有人提这个问题:


image.png


问题场景以及解决办法

1. 外部容器的滚动条

这里的外部容器指的是html, 直接加在最外层:


html {

 overflow-y: scroll;

}

手动加上这个特性, 不论什么时候都有滚动宽度占据空间。


缺点: 没有滚动的时候也会有个滚动条, 不太美观。


优点: 方便, 没有兼容性的问题。


2. 外部容器绝对定位法

用绝对定位,保证了body的宽度一直保持完整空间:


html {

 overflow-y: scroll; // 兼容ie8,不支持:root, vw

}


:root {

 overflow-y: auto;

 overflow-x: hidden;

}


:root body {

 position: absolute;

}


body {

 width: 100vw;

 overflow: hidden;

}

3. 内部容器做兼容


.wrapper {

   overflow-y: scroll; // fallback

   overflow-y: overlay;

}

总结

个人推荐还是用 overlay, 然后使用scroll 做为兜底。


内容就这么多, 希望对大家有所启发。


文章如有错误, 请在留言区指正, 谢谢。

将 Gatsby 项目迁移到 TypeScript

seo达人

之前花了些时间将gatsby-theme-gitbook迁移到 Typescript,以获得在 VSCode 中更好的编程体验.

整体差不多已经完成迁移,剩下将 Gatsby 的 API 文件也迁移到 TS,这里可以看到 gatsby#21995 官方也在将核心代码库迁移到 Typescript,准备等待官方将核心代码库迁移完成,在迁移 API 文件.


这篇文章用XYShaoKang/gatsby-project-config,演示如何将 gatsby 迁移到 TypeScript,希望能帮到同样想要在 Gatsby 中使用 TS 的同学.


迁移步骤:


TS 配置

配置 ESLint 支持 TS

完善 GraphQL 类型提示

初始化项目

gatsby new gatsby-migrate-to-typescript XYShaoKang/gatsby-project-config

cd gatsby-migrate-to-typescript

yarn develop

TS 配置

安装typescript

添加typescript.json配置文件

修改 js 文件为 tsx

补全 TS 声明定义

安装typescript

yarn add -D typescript

添加配置文件tsconfig.json

// https://www.typescriptlang.org/v2/docs/handbook/tsconfig-json.html

{

 "compilerOptions": {

   "target": "esnext", // 编译生成的目标 es 版本,可以根据需要设置

   "module": "esnext", // 编译生成的目标模块系统

   "lib": ["dom", "es2015", "es2017"], // 配置需要包含的运行环境的类型定义

   "jsx": "react", // 配置 .tsx 文件的输出模式

   "strict": true, // 开启严格模式

   "esModuleInterop": true, // 兼容 CommonJS 和 ES Module

   "moduleResolution": "node", // 配置模块的解析规则,支持 node 模块解析规则

   "noUnusedLocals": true, // 报告未使用的局部变量的错误

   "noUnusedParameters": true, // 报告有关函数中未使用参数的错误

   "experimentalDecorators": true, // 启用装饰器

   "emitDecoratorMetadata": true, // 支持装饰器上生成元数据,用来进行反射之类的操作

   "noEmit": true, // 不输出 js,源映射或声明之类的文件,单纯用来检查错误

   "skipLibCheck": true // 跳过声明文件的类型检查,只会检查已引用的部分

 },

 "exclude": ["./node_modules", "./public", "./.cache"], // 解析时,应该跳过的路晋

 "include": ["src"] // 定义包含的路径,定义在其中的声明文件都会被解析进 vscode 的智能提示

}

将index.js改成index.tsx,重新启动服务,查看效果.


其实 Gatsby 内置了支持 TS,不用其他配置,只要把index.js改成index.tsx就可以直接运行.添加 TS 依赖是为了显示管理 TS,而tsconfig.json也是这个目的,当我们有需要新的特性以及自定义配置时,可以手动添加.

补全 TS 声明定义

打开index.tsx,VSCode 会报两个错误,一个是找不到styled-components的声明文件,这个可以通过安装@types/styled-components来解决.

另外一个错误绑定元素“data”隐式具有“any”类型。,这个错误是因为我们在tsconfig.json中指定了"strict": true,这会开启严格的类型检查,可以通过关闭这个选项来解决,只是我们用 TS 就是要用它的类型检查的,所以正确的做法是给data定义类型.

下面来一一修复错误.


安装styled-components的声明文件


yarn add -D @types/styled-components

修改index.tsx


import React, { FC } from 'react'

import styled from 'styled-components'

import { graphql } from 'gatsby'

import { HomeQuery } from './__generated__/HomeQuery'


const Title = styled.h1`

 font-size: 1.5em;

 margin: 0;

 padding: 0.5em 0;

 color: palevioletred;

 background: papayawhip;

`


const Content = styled.div`

 margin-top: 0.5em;

`


interface PageQuery {

 data: {

   allMarkdownRemark: {

     edges: Array<{

       node: {

         frontmatter: {

           title: string

         }

         excerpt: string

       }

     }>

   }

 }

}


const Home: FC<PageQuery> = ({ data }) => {

 const node = data.allMarkdownRemark.edges[0].node


 const title = node.frontmatter?.title

 const excerpt = node.excerpt


 return (

   <>

     <Title>{title}</Title>

     <Content>{excerpt}</Content>

   </>

 )

}


export default Home


export const query = graphql`

 query HomeQuery {

   allMarkdownRemark {

     edges {

       node {

         frontmatter {

           title

         }

         excerpt

       }

     }

   }

 }

`

这时候会出现一个新的错误,在excerpt: string处提示Parsing error: Unexpected token,这是因为 ESLint 还无法识别 TS 的语法,下面来配置 ESLint 支持 TS.


配置 ESLint 支持 TypeScript

安装依赖


yarn add -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置.eslintrc.js


module.exports = {

 parser: `@typescript-eslint/parser`, // 将解析器从`babel-eslint`替换成`@typescript-eslint/parser`,用以解析 TS 代码

 extends: [

   `google`,

   `eslint:recommended`,

   `plugin:@typescript-eslint/recommended`, // 使用 @typescript-eslint/eslint-plugin 推荐配置

   `plugin:react/recommended`,

   `prettier/@typescript-eslint`, // 禁用 @typescript-eslint/eslint-plugin 中与 prettier 冲突的规则

   `plugin:prettier/recommended`,

 ],

 plugins: [

   `@typescript-eslint`, // 处理 TS 语法规则

   `react`,

   `filenames`,

 ],

 // ...

}

在.vscode/settings.json中添加配置,让VSCode使用ESLint扩展格式化ts和tsx文件


// .vscode/settings.json

{

 "eslint.format.enable": true,

 "[javascript]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[javascriptreact]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[typescript]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 },

 "[typescriptreact]": {

   "editor.defaultFormatter": "dbaeumer.vscode-eslint"

 }

}

完善 GraphQL 类型提示

// index.tsx

import React, { FC } from 'react'

// ...

interface PageQuery {

 data: {

   allMarkdownRemark: {

     edges: Array<{

       node: {

         frontmatter: {

           title: string

         }

         excerpt: string

       }

     }>

   }

 }

}


const Home: FC<PageQuery> = ({ data }) => {

 // ...

}


export default Home


export const query = graphql`

 query HomeQuery {

   allMarkdownRemark {

     edges {

       node {

         frontmatter {

           title

         }

         excerpt

       }

     }

   }

 }

`

我们看看index.tsx文件,会发现PropTypes和query结构非常类似,在Gatsby运行时,会把query查询的结果作为组件prop.data传入组件,而PropTypes是用来约束prop存在的.所以其实PropTypes就是根据query写出来的.


如果有依据query自动生成PropTypes的功能就太棒了.

另外一个问题是在query中编写GraphQL查询时,并没有类型约束,也没有智能提示.


总结以下需要完善的体验包括:


GraphQL 查询编写时的智能提示,以及错误检查

能够从 GraphQL 查询生成对应的 TypeScript 类型.这样能保证类型的唯一事实来源,并消除 TS 中冗余的类型声明.毕竟如果经常需要手动更新两处类型,会更容易出错,而且也并不能保证手动定义类型的正确性.

实现方式:


通过生成架构文件,配合Apollo GraphQL for VS Code插件,实现智能提示,以及错误检查

通过graphql-code-generator或者apollo生成 TS 类型定义文件

如果自己去配置的话,是挺耗费时间的,需要去了解graphql-code-generator的使用,以及Apollo的架构等知识.

不过好在社区中已经有对应的 Gatsby 插件集成了上述工具可以直接使用,能让我们不用去深究对应知识的情况下,达到优化 GraphQL 编程的体验.

尝试过以下两个插件能解决上述问题,可以任选其一使用


gatsby-plugin-codegen

gatsby-plugin-typegen

另外还有一款插件gatsby-plugin-graphql-codegen也可以生成 TS 类型,不过配置略麻烦,并且上述两个插件都可以满足我现在的需求,所以没有去尝试,感兴趣的可以尝试一下.


注意点:


Apollo不支持匿名查询,需要使用命名查询

第一次生成,需要运行Gatsby之后才能生成类型文件

整个项目内不能有相同命名的查询,不然会因为名字有冲突而生成失败

下面是具体操作


安装vscode-apollo扩展

在 VSCode 中按 Ctrl + P ( MAC 下: Cmd + P) 输入以下命令,按回车安装


ext install apollographql.vscode-apollo

方式一: 使用gatsby-plugin-codegen

gatsby-plugin-codegen默认会生成apollo.config.js和schema.json,配合vscode-apollo扩展,可以提供GraphQL的类型约束和智能提示.

另外会自动根据query中的GraphQL查询,生成 TS 类型,放在对应的tsx文件同级目录下的__generated__文件夹,使用时只需要引入即可.

如果需要在运行时自动生成 TS 类型,需要添加watch: true配置.


安装gatsby-plugin-codegen


yarn add gatsby-plugin-codegen

配置gatsby-config.js


// gatsby-config.js

module.exports = {

 plugins: [

   // ...

   {

     resolve: `gatsby-plugin-codegen`,

     options: {

       watch: true,

     },

   },

 ],

}

重新运行开发服务生成类型文件


yarn develop

如果出现以下错误,一般是因为没有为查询命名的缘故,给查询添加命名即可,另外配置正确的话,打开对应的文件,有匿名查询,编辑器会有错误提示.


fix-anonymous-operations.png


这个命名之后会作为生成的类型名.


修改index.tsx以使用生成的类型


gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.


import { HomeQuery } from './__generated__/HomeQuery' // 引入自动生成的类型

// ...


// interface PageQuery {

//   data: {

//     allMarkdownRemark: {

//       edges: Array<{

//         node: {

//           frontmatter: {

//             title: string

//           }

//           excerpt: string

//         }

//       }>

//     }

//   }

// }


interface PageQuery {

 data: HomeQuery // 替换之前手写的类型

}


// ...

将自动生成的文件添加到.gitignore中


apollo.config.js,schema.json,__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.

# Generated types by gatsby-plugin-codegen

__generated__

apollo.config.js

schema.json

方式二: 使用gatsby-plugin-typegen

gatsby-plugin-typegen通过配置生成gatsby-schema.graphql和gatsby-plugin-documents.graphql配合手动创建的apollo.config.js提供GraphQL的类型约束和智能提示.

根据GraphQL查询生成gatsby-types.d.ts,生成的类型放在命名空间GatsbyTypes下,使用时通过GatsbyTypes.HomeQueryQuery来引入,HomeQueryQuery是由对应的命名查询生成


安装gatsby-plugin-typegen


yarn add gatsby-plugin-typegen

配置


// gatsby-config.js

module.exports = {

 plugins: [

   // ...

   {

     resolve: `gatsby-plugin-typegen`,

     options: {

       outputPath: `src/__generated__/gatsby-types.d.ts`,

       emitSchema: {

         'src/__generated__/gatsby-schema.graphql': true,

       },

       emitPluginDocuments: {

         'src/__generated__/gatsby-plugin-documents.graphql': true,

       },

     },

   },

 ],

}

//apollo.config.js

module.exports = {

 client: {

   tagName: `graphql`,

   includes: [

     `./src/**/*.{ts,tsx}`,

     `./src/__generated__/gatsby-plugin-documents.graphql`,

   ],

   service: {

     name: `GatsbyJS`,

     localSchemaFile: `./src/__generated__/gatsby-schema.graphql`,

   },

 },

}

重新运行开发服务生成类型文件


yarn develop

修改index.tsx以使用生成的类型


gatsby-plugin-codegen插件会更具查询生成对应的查询名称的类型,保存在对应tsx文件同级的__generated__目录下.


// ...


// interface PageQuery {

//   data: {

//     allMarkdownRemark: {

//       edges: Array<{

//         node: {

//           frontmatter: {

//             title: string

//           }

//           excerpt: string

//         }

//       }>

//     }

//   }

// }


interface PageQuery {

 data: GatsbyTypes.HomeQueryQuery // 替换之前手写的类型

}


// ...

将自动生成的文件添加到.gitignore中


__generated__能通过运行时生成,所以可以添加到.gitignore中,不用提交到 git 中.当然如果有需要也可以选择提交到 git 中.

# Generated types by gatsby-plugin-codegen

__generated__

日历

链接

blogger

蓝蓝 http://www.lanlanwork.com

存档