小熊苗苗

小熊苗苗 查看完整档案

杭州编辑杭州电子科技大学  |  数学 编辑  |  填写所在公司/组织填写个人主网站
编辑

如果你的才华还实现不了你的野心,那就静下心来,埋头苦干

个人动态

小熊苗苗 赞了文章 · 2019-12-25

「圣诞特辑」纯前端实现人脸识别自动佩戴圣诞帽

在线体验地址:hksite.cn/prjs/christmashat

源码地址:https://github.com/hk029/christmas-hat

写在开头

叮叮当,叮叮当,吊儿个郎当,一年一度的圣诞节到咯,我不由的回想起了前两年票圈被圣诞帽支配的恐惧。打开票圈全是各种@官方求帽子的:

Untitled/Untitled.png?imageView&thumbnail=400x400

票圈头像也瞬间被圣诞帽攻陷:

Untitled/Untitled%201.png?imageView&thumbnail=400x400

在那段时间,你没一顶圣诞帽还真不好意思发票圈

Untitled/Untitled%202.png?imageView&thumbnail=400x400

各种戴帽子的软件也如雨后春笋般浮现出来,不管是小程序还是美图软件无一例外的都增加了戴圣诞帽的功能。但是对于懒人的我来说,自己调整一个圣诞帽子佩戴还是太麻烦了。于是我就想了,有没有什么办法能让我的头像自动佩戴上圣诞帽呢?

还真给我想到了,这就是今天的主题,用纯前端的方式给你做一个自动佩戴圣诞帽的网站。

有了这个网站,你就能顺利在票圈装 13 了,不仅如此,你还可能因此邂逅一段完美的爱情!试想一下,当你发了这个网站在票圈后,女神看到了就会为你的技术所折服,然后主动把照片给你,让你帮她给头像戴上圣诞帽,然后你就顺利的得到了和女神搭讪的机会,然后赢取白富美,走向人生巅峰,想想还有点小激动呢。

Untitled/Untitled%203.png?imageView&thumbnail=400x400

给头像戴上圣诞帽需要几步

给头像佩戴上圣诞帽需要几个步骤呢?很简单,跟大象装进冰箱一样,只需要三个步骤:

  • 打开头像
  • 戴上圣诞帽
  • 下载图片

Untitled/Untitled%204.png?imageView&thumbnail=400x400

其中第一步和最后一步看起来好像都不是什么难事,关键是这第二点,如何给头像戴上圣诞帽?

首先你必须要懂的,当我在说:戴上圣诞帽的时候,我在说什么?让我来翻译以下:

将圣诞帽的图片素材绘制在头像图片的合适位置,使之变成一张图片

所以我们今天的重点来了:如何能确定圣诞帽的位置,并将它和头像合成为一张图片。

首先让我们来聊聊如何确定圣诞帽的位置。

确定圣诞帽的位置

通过手动的方式,我们是很容易确定圣诞帽应该放在什么位置的,那机器如何能确定呢?有人可能想到了那不就是人脸识别技术?是的,这就是我们今天需要用到的技术。

早在 2017 年之前,纯前端说想实现人脸识别还有点天方夜谭的感觉,但是 Tensorflow.js 的出现让这一切成为了可能:

Untitled/Untitled%205.png?imageView&thumbnail=400x400

它是 Google 推出的第一个基于 TensorFlow 的前端深度学习框架。它允许你在浏览器上训练模型,或以推断模式运行预训练的模型。TensorFlow.js 不仅可以提供低级的机器学习构建模块,还可以提供高级的类似 Keras 的 API 来构建神经网络。

Tensorflow.js 是我第一个想到的可以应用的库,但是当我打开官方文档,看到如 Tensors (张量)、Layers (图层)、Optimizers (优化器)……各种陌生概念扑面而来,砸的人生疼,现学感觉是来不及了,那有什么办法能在我不了解各种概念的情况下也能快速上手人脸识别呢?

答案当然有,那就是:face-api.js

face-api.js

face-api.js 是大神 Vincent Mühler 的最新力作,他为人所知的开源项目还有 opencv4nodejsface-recognize(NodeJs 的人脸识别包,不过现在 face-api.js 已经支持 Node 端了,他推荐直接使用 face-api)

face-api.js 是一个建立在 Tensorflow.js 内核上的 Javascript 模块,它实现了三种卷积神经网络(CNN)架构,用于完成人脸检测、识别和特征点检测任务。简而言之,借助它,前端也能很轻松的完成人脸识别的工作。

原理简析

想看实现的童鞋请直接略过这一段,直接开始上手操作。

我们知道机器学习有几个基本要素:数据,模型,算法。他们之间的关系如下:

Untitled/Untitled%206.png?imageView&thumbnail=400x400

  • 训练数据: 训练数据就是一系列打过标签的数据,比如一系列人脸和不是人脸的图片数据。
  • 模型(这里我们主要指监督学习模型): 模型你可以简单理解为是一个预测函数(f(x) = y),简单来说就是根据输入的数据,能给出结果。
  • 算法: 算法就是教机器如何获得最优的模型(损失最小)。比如当机器通过当前模型识别到一张训练图片为人脸,但是标签是「非人脸」,此时就需要根据算法对模型进行调整。常见的算法有例如:梯度下降法(Gradient Descent),共轭梯度法(Conjugate Gradient),牛顿法和拟牛顿法,模拟退火法(Simulated Annealing)……

所以,我们可以这么说,只要有了一个训练好的预测模型,我们就可以对未知数据进行分析预测了。

face-api 的原理

首先,为了在图片中识别出人脸,我们需要告诉机器什么样的脸是人脸,因此我们需要大量的人脸照片,并且标明里面的各种脸部特征数据,让机器去学习:

Untitled/Untitled%207.png?imageView&thumbnail=400x400

face-api.js 针对人脸检测工作实现了一个 SSD(Single Shot Multibox Detector)算法,它本质上是一个基于 MobileNetV1 的卷积神经网络(CNN),同时在网络的顶层加入了一些人脸边框预测层。

然后 face-api.js 会通过该算法让机器不断的学习并优化,从而训练出模型,通过该模型可以识别出所有的人脸边界框

光识别出人脸还远远不够,我们的很多应用都需要找到人脸的特征点(眉毛,鼻子,嘴巴这些的)。因此 face-api.js 会从图片中抽取出每个边界框中的人脸居中的图像,接着将其再次作为输入传给人脸识别网络,让其学习。

为了实现特征点识别这个目标,face-api.js 又实现了一个简单的卷积神经网络,它可以返回给定图像的 68 个人脸特征点:

通过该算法,face-api.js 训练了一系列的模型,通过使用这些已经训练好的模型,我们可以快速实现我们想要的功能。

Untitled/Untitled%208.png?imageView&thumbnail=400x400

face-api.js 的使用方法

引入方式

如果你不使用打包工具的话,可以直接导入 face-api.js 的脚本:dist/face-api.js 获得最新的版本,或者从 dist/face-api.min.js 获得缩减版,并且导入脚本:

<script data-original="face-api.js"></script>

如果你使用 npm 包管理工具,可以输入如下指令:

npm i face-api.js

初始化

我们之前说过,face-api 它实现了一系列的卷积神经网络,并针对网络和移动设备进行了优化。所有的神经网络实例在 faceapi.nets中获取到

var nets = {
    ssdMobilenetv1: new SsdMobilenetv1(), // ssdMobilenetv1 目标检测
    tinyFaceDetector: new TinyFaceDetector(),  // 人脸识别(精简版)
    tinyYolov2: new TinyYolov2(),   // Yolov2 目标检测(精简版)
    mtcnn: new Mtcnn(),   // MTCNN
    faceLandmark68Net: new FaceLandmark68Net(),  // 面部 68 点特征识别
    faceLandmark68TinyNet: new FaceLandmark68TinyNet(), // 面部 68 点特征识别(精简版)
    faceRecognitionNet: new FaceRecognitionNet(),  // 面部识别
    faceExpressionNet: new FaceExpressionNet(),  //  表情识别
    ageGenderNet: new AgeGenderNet()  // 年龄识别
};

其中 MobileNetsyolov2 是业内比较有名的目标检测算法,有兴趣的可以点击链接去看论文(我是看不懂),这篇文章 简要介绍了这些算法,大概就是说他们的检测速度和检测效率都不错。这里你可以根据自己的需要选择不同的算法,加载不同的模型。

官方推荐使用ssdMobilenetv1,因为它的识别精度比较高,但是检索速度相对较慢,如果是实时检测的场景,它的检索速度可能会成为问题,因此,今年下半年作者把 MTCNN 算法也引入了,如果想用实时人脸检测的场景,可以试试 MTCNN。(可以看看作者 这篇文章

模型加载

通过之前的介绍我们也可以知道,模型才是重中之重,有了训练好的模型,我们就可以跳过训练的阶段,直接使用来做人脸识别了。

这也就是国外一个机器学习的布道者 Dan Shiffman 在 视频 中一直所强调的:并不是所有的机器学习入门都应该从学习算法入手,毕竟术业有专攻,目前已经有很多人建立了很多成熟的模型(图形检测,文本识别,图像分类……),我们可以站在巨人的肩膀上去做更多有意思的事情。

face-api 本身也提供了一系列的模型数据(/weights),可以开箱即用:

await faceapi.nets.ssdMobilenetv1.load('/weights')

其中 /weights 是放了 manifest.json 和 shard 文件的目录,建议把官方的 weights 目录直接拷贝下来,因为经常你需要几个模型同时使用。

识别

face-api 提供了很多高级的方法可以使用,其中最方便的就是detectAllFaces / detectSingleFace(input, options) , 注意:它默认是使用SSD Mobilenet V1 ,如果要使用Tiny FaceDetector,需要手动指定:

const detections1 = await faceapi.detectAllFaces(input, new faceapi.SsdMobilenetv1Options())
const detections2 = await faceapi.detectAllFaces(input, new faceapi.TinyFaceDetectorOptions())

其中 detect 系方法都支持链式调用,因此你可以这样用:

await faceapi.detectAllFaces(input)
await faceapi.detectAllFaces(input).withFaceExpressions()
await faceapi.detectAllFaces(input).withFaceLandmarks()
await faceapi.detectAllFaces(input).withFaceLandmarks().withFaceExpressions()

获取识别数据

进行识别操作后,返回的数据是什么样的呢?

如果你是进行的全脸识别,那么数据是一个数组,其中 detection 是默认会有的属性,它提供了一个人脸部的盒子信息

[{detection:{
    box: {
        x: 156.22306283064717
        y: 76.60605907440186
        width: 163.41096172182577
        height: 182.21931457519534
        left: 156.22306283064717
        top: 76.60605907440186
        right: 319.63402455247297
        bottom: 258.82537364959717
        area: 29776.633439024576
        topLeft: Point
        topRight: Point
        bottomLeft: Point
        bottomRight: Point
    }
    ……
}]

如果你进行了链式操作,比如 withFaceLandmarks() 那这个对象会增加一个landmarks的属性,以此类推。

[{detection, landmarks, ……}]

其中landmarks提供了获取脸部各种特征点的方法:

const landmarkPositions = landmarks.positions  // 获取全部 68 个点
const jawOutline = landmarks.getJawOutline()  // 下巴轮廓
const nose = landmarks.getNose()  // 鼻子
const mouth = landmarks.getMouth()  // 嘴巴
const leftEye = landmarks.getLeftEye()  // 左眼
const rightEye = landmarks.getRightEye()  // 右眼
const leftEyeBbrow = landmarks.getLeftEyeBrow()  // 左眉毛
const rightEyeBrow = landmarks.getRightEyeBrow()  // 右眉毛

处理识别数据

要知道,你拿到的数据是根据图片的真实数据来处理的,但我们在网页展示的图片通常不会是 1:1 的实际图片,也就是说图片会进行缩放/扩大处理。比如一张图片是 1000x1000 的,图片上的人脸嘴巴可能在(600,500)这个位置,但是我们实际展示的是 600x600 的图片,如果根据(600,500)这个坐标去画,那早就在画布外了。

因此如果我想要在图片上做一点事情,我们需要把当前的数据进行一个转换,让它的数据匹配特定的大小,这里,可以用它提供的 matchDimensions(canvas, displaySize)resizeResults(result, displaySize) 方法:

// 把 canvas 固定到 displaySize 的大小
faceapi.matchDimensions(canvas, displaySize) 
// 把数据根据 displaySize 做转换 
const resizedResults = faceapi.resizeResults(detectionsWithLandmarks, displaySize)

其中 displaySize 是一个拥有{ width, height }的对象,所以你也可以直接传入带 width 和 height 的 DOM 元素,如 <canvas />, <img />

根据数据绘制图形

光拿到数据可没用,我们主要目的是为了绘制图形,在绘制这一块 face-api 也是提供了一系列高级方法,比如:

faceapi.draw.drawDetections(canvas, resizedDetections)  // 直接在识别区域画个框
faceapi.draw.drawFaceLandmarks(canvas, resizedResults)  // 直接画出识别的的特征点

Untitled/Untitled%209.png?imageView&thumbnail=400x400

(以下测试图片均是采用从百度搜「女生头像」搜到的小姐姐,如有侵权,请告知)

当然你还可以在特定位置画个框或文字,具体用法可以参考:DrawBoxDrawTextField

const drawOptions = {
  label: 'Hello I am a box!',
  lineWidth: 2
}
const drawBox = new faceapi.draw.DrawBox(resizedResults[0].detection.box, drawOptions)
drawBox.draw(canvas)

Untitled/Untitled%2010.png?imageView&thumbnail=400x400

圣诞帽的绘制

说了这么多,突然发现还没到我们的主题,画圣诞帽!让我们赶紧回来。

确定圣诞帽的位置

现在假定我现在拥有了所有的面部数据,我应该如何确定圣诞帽的正确位置?首先,我们必须明确一点,圣诞帽应该是要戴在头顶的,应该没有人把圣诞帽戴在眉毛上吧?(好吧当我没说)

Untitled/Untitled%2011.png?imageView&thumbnail=400x400

但是人脸识别的数据中一般是不包含头顶的,这可咋办?还好我小学一年级学过半个学期的素描,在素描中有个很重要的概念叫三庭五眼

Untitled/Untitled%2012.png?imageView&thumbnail=400x400

也是说正常人的发际线到眉骨的距离是眉骨到下颌距离的一半(作为程序猿的我表示,该规则可能已经不适用了)。

因此我们可以通过获取眉毛的坐标和下颌的坐标来计算出头顶的位置:

/**
 * 获取头顶的坐标
 * @param {*} midPos 眉心点坐标
 * @param {*} jawPos 下巴底点坐标
 */
const getHeadPos = (midPos, jawPos) => {
  // 获取线的 k 值
  const k = getK(midPos, jawPos);
  // 获取眉心到下颌的距离
  const distanceOfEye2Jaw = getDistance(midPos, jawPos);
  return getPos(k, distanceOfEye2Jaw / 2, midPos);
};

 在这里让我们复习几个解析几何的公式:

  • 两点之间距离公式:

Untitled/Untitled%2013.png?imageView&thumbnail=400x400

  • 根据两点确定斜率:

    Untitled/Untitled%2014.png?imageView&thumbnail=400x400

  • 点到直线的距离公式:

Untitled/Untitled%2015.png?imageView&thumbnail=400x400

  • 相互垂直的直线,斜率之积为-1

Untitled/Untitled%2016.png?imageView&thumbnail=400x400

要特别注意的是,由于 Canvas 默认的坐标系的结构和我们之前数学课上学的不太一样,我们把它逆时针旋转 90 度可以发现它的 x,y 轴跟我们认识的是反的,因此为了方便,我们通常在代入公式计算的时候把 x,y 进行一下调换。

Untitled/Untitled%2017.png?imageView&thumbnail=400x400

/**
 * 获取 K 值
 * @param {*} a
 * @param {*} b
 */
const getK = (a, b) => (a.x - b.x) / (a.y - b.y)

/**
 * 获取两点之间距离
 * @param {*} a
 * @param {*} b
 */
const getDistance = (a, b) => Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));

因此目前已知眉心的坐标,下颌坐标,可以计算出这条直线的斜率,然后根据眉心到头顶的距离(眉心到下巴的一半),可以算出头顶的坐标:

/**
 * 已知 K,d, 点,求另一个点
 * @param {*} k 值
 * @param {*} d 距离
 * @param {*} point 一个基础点
 */
const getPos = (k, d, point) => {
  // 取 y 变小的那一边
  let y = -Math.sqrt((d * d) / (1 + k * k)) + point.y;
  let x = k * (y - point.y) + point.x;
  return { x, y };
};

图片合成

当我们已经知道了圣诞帽子的位置了,那接下的问题就是如何把圣诞帽合成到头像上去了,这里我们采用 Canvas 来实现 ,原理很简单:首先把头像绘制到 Canvas 上,然后再继续绘制圣诞帽就行了。

由于图片中可能不止一个面部数据,可能需要绘制多个帽子:

/**
 * 获取图片
 * @param {*} src 图片地址
 * @param {*} callback
 */
function getImg(src, callback) {
  const img = new Image();
  img.setAttribute('crossOrigin', 'anonymous');
  img.src = src;
  img.onload = () => callback(img);
}
/**
 * 绘制主流程
 * @param {*} canvas 
 * @param {*} options 
 */
function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg' } = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上(由于图片中可能不止一个面部数据,可能需要绘制多个帽子)
    for(let i = 0, len=info.length;i < len;i++) {
        const { headPos } = info[i];
        getImg('images/hat.png', img => ctx.drawImage(img, headPos.x, headPos.y, 200, 120));
    }
}

我们可以看到帽子已经绘制上去了,但是位置有点奇怪:

Untitled/Untitled%2018.png?imageView&thumbnail=400x400

这是因为绘制图片的时候是按图片的左上角为原点绘制的,因此我们在实际绘制帽子的时候需要对坐标进行一个偏移:

/**
 * 根据我当前的圣诞帽元素进行一些偏移(我的图片大小是 200*130), 圣诞帽可佩戴部分的中心 (60,60)
 * @param {*} x
 * @param {*} y
 */
const translateHat = (x, y) => {
    return {
        x: x - 60,
        y: y - 60,
    };
};

function drawing(canvas, options) {
    ……
    const {x, y} = translateHat(headPos.x, headPos.y);
    getImg('images/hat.png', img => ctx.drawImage(img, x, y, 200, 120));
    ……
}

Untitled/Untitled%2019.png?imageView&thumbnail=400x400

这么看起来好多了,感觉自己棒棒哒!

Untitled/Untitled%2020.png?imageView&thumbnail=400x400

优化

但是有小伙伴可以会发现,这样的结果还是有点问题:

  • 帽子大小是固定的,但是头像的上的面孔可大可小,大脸放小帽子显然有点不合适。
  • 帽子的朝向是固定的,如果有人的头像是偏着的呢,帽子是不是也应该偏过来?

因此我们还需要继续做优化:

圣诞帽的大小

圣诞帽的大小我们可以根据识别出的脸的大小来确定,通常来说,帽子可戴的宽度等于脸宽就行了(考虑到展示效果可以略大),我这里强调可戴宽度是因为一个圣诞帽的图片中只有一部分是可戴区域

Untitled/Untitled%2021.png?imageView&thumbnail=400x400

// 0.7 为可戴区域占总区域的比重(为了让帽子更大一点,选择 0.6),0.65 是图片的宽高比
const picSize = { width: faceWidth / 0.6, height: (faceWidth * 0.65) / 0.6 };

而面部的大小可以通过 jawOutlinePoints的起始点距离来计算

/**
 * 获取脸的宽度(即帽子宽度)
 * @param {*} outlinePoints
 */
const getFaceWith = outlinePoints => getDistance(outlinePoints[0], outlinePoints[outlinePoints.length - 1])

圣诞帽的角度

圣诞帽的角度该如何计算呢?其实也很简单,知道头的偏转角度就行了,而头的偏转角度,直接用脸中线(眉心到下颌)和 y 轴的夹角就行了(直接用 atan2 算出来的是补角,需要用 180 度减),这里考虑到后续使用方便直接用的弧度。

/**
 * 获取脸的倾斜弧度
 * @param {*} jawPos
 * @param {*} midPointOfEyebrows
 */
const getFaceRadian = (jawPos, midPointOfEyebrows) =>
    Math.PI - Math.atan2(jawPos.x - midPointOfEyebrows.x, jawPos.y - midPointOfEyebrows.y); //弧度  0.9272952180016122

Untitled/Untitled%2022.png?imageView&thumbnail=400x400

Canvas 中图片旋转

注意,在 Canvas 中没办法直接旋转图片,只能旋转画布,而且画布是按照原点旋转的,这点会特别坑。

Untitled/Untitled%2023.png?imageView&thumbnail=400x400

这里,我们只想让图片按照中心旋转怎么办?我们可以先让 Canvas 把原点平移到帽子的位置,然后再通过帽子的内部偏移使得帽子中心刚好在原点,此时再旋转画布把帽子画上就只影响这一个帽子图片了。

Untitled/Untitled%2024.png?imageView&thumbnail=400x400

/**
 * 绘制帽子 
 * @param {*} ctx 画布实例
 * @param {{}} config 配置
 */
function drawHat(ctx, config) {
    const { headPos, angle, faceWidth } = config;
    getImg('images/hat.png?imageView&thumbnail=400x400', img => {
        // 保存画布
        ctx.save();
        // 画布原点移动到画帽子的地方
        ctx.translate(headPos.x, headPos.y);
        // 旋转画布到特定角度
        ctx.rotate(angle);
        // 偏移图片,使帽子中心刚好在原点
        const { x, y, width, height } = translateHat(faceWidth, 0, 0);
        // 我的圣诞帽子实际佩戴部分长度只有 0.75 倍整个图片长度
        ctx.drawImage(img, x, y, width, height);
        // 还原画布
        ctx.restore();
    });
}

这样整个绘制的主流程大概就是这样:

function drawing(canvas, options) {
    const { info, width = 200, height = 200, imgSrc = 'images/default.jpg'} = options;
    const ctx = canvas.getContext('2d');
    // 重置
    ctx.clearRect(0, 0, width, height);
    // 先把图片绘制上去
    getImg(imgSrc, img => ctx.drawImage(img, 0, 0, width, height));
    // 循环把帽子画到对应的点上
    for (let i = 0, len = info.length; i < len; i++) {
        drawHat(ctx, info[i]);  
    }
}

成品展示

Untitled/Untitled%2025.png?imageView&thumbnail=400x400

Untitled/Untitled%2026.png?imageView&thumbnail=400x400

Untitled/Untitled%2027.png?imageView&thumbnail=400x400

Untitled/Untitled%2028.png?imageView&thumbnail=400x400

image-20191223201537510

是不是感觉已经很完美了?目前已经能实现对各种大小的脸,不同朝向的脸进行适配了。甚至连狗狗的头像也能识别出来,这都得利于 face-api.js 提供的模型中也有狗狗的脸训练数据。

当然,这个例子实际上还是很不完善,因为当你测试的图片多的时候就会发现,对侧脸头像的识别还是有问题:

image-20191223201619850

这是因为脸的大小用之前的方法计算的其实是不准确的,实际上还应该根据双边眼睛的比例知道用户现在是侧脸还是正面,从而继续调整帽子的大小和位置。但是这里就不继续优化了,感兴趣的小伙伴可以自己去琢磨琢磨如何修改。

写在最后

通过上面这个小例子,我们可以发现前端利用机器学习也可以实现很多不可思议的有趣玩意,再结合 VRARwebRTC,我们甚至可以做一个纯前端的互动小游戏。

当然就算在这个例子上,你也可以进行扩展,比如绘制圣诞胡须,化妆……

如果你想继续深入学习机器学习的相关知识,可以去看看 Coursea 的 机器学习入门课程,如果你想深入学习一下 Tensorflow,可以去看看 Tensorflow.js 的 官方文档

虽然之前有吐槽 Tensorflow.js 知识点太多的问题,但是不得不说 Google 的文档写的还是不错的,提供了很多案例,手把手教你如何实现一些简单的功能:手写数字识别,预测,图片分类器……所以对 Tensorflow.js 感兴趣的童鞋不妨去它的官方文档中逛逛。

不过毕竟 Tensorflow.js 还是很底层的库,如果你只是想用机器学习做一些有趣的事情,不妨尝试一下 ml5.js,这里有一套学习视频

最后,祝大家圣诞快乐!

参考

https://github.com/justadudewhohacks/face-api.js

face-api.js — JavaScript API for Face Recognition in the Browser with tensorflow.js

https://tensorflow.google.cn/js/tutorials

https://www.youtube.com/watch?v=jmznx0Q1fP0

https://www.cnblogs.com/suyuanli/p/8279244.html

https://learn.ml5js.org/docs/#/

本文发布自 网易云音乐前端团队,文章未经授权禁止任何形式的转载。我们一直在招人,如果你恰好准备换工作,又恰好喜欢云音乐,那就 加入我们
查看原文

赞 63 收藏 33 评论 0

小熊苗苗 赞了回答 · 2018-06-20

react中 怎么在componentWillUnmount中使用removeEventListener

export default class A extends React.Component {

    constructor(props) {
        super(props);
        this.scroll1 = this.scroll1.bind(this)
    }
    scroll1(){}

    componentDidMount() {
        window.addEventListener('scroll', this.scroll1);
    }
    
    componentWillUnmount(){
        window.removeEventListener('scroll', this.scroll1);

    }

    render() {
    };

}

因为你绑定的是this.scroll1.bind(this),而不是this.scroll1,所以需要将this.scroll1.bind(this)指到同一个变量
另外airbnb的react guide也是建议用这种写法
https://github.com/JasonBoy/javascript/tree/master/react#methods-%E5%87%BD%E6%95%B0

当在 render() 里使用事件处理方法时,提前在构造函数里把 this 绑定上去.
为什么? 在每次 render 过程中, 再调用 bind 都会新建一个新的函数,浪费资源.

关注 3 回答 1

小熊苗苗 回答了问题 · 2018-06-07

重置查询条件后分页的样式不会改变。

分页器组件 有一个 current 属性,用于控制当前页
在搜索之后成功的回调中将 current 属性置为 1,就会回到第一页了

关注 3 回答 2

小熊苗苗 关注了用户 · 2018-04-28

名一 @jchenn

关注 490

小熊苗苗 发布了文章 · 2017-10-12

《慕课React入门》总结

React 入门与实战

react组件

虚拟DOM的概念 这是React性能高效的核心算法

React为此引入了虚拟DOMVirtual DOM)的机制。基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。尽管每一次都需要构造完整的虚拟DOM树,但是因为虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是Diff部分,因而能达到提高性能的目的。


React 组件,理解什么是组件化

像插入普通 HTML 标签一样,在网页中插入这个组件

所谓组件,即封装起来的具有独立功能的UI部件

  • React推荐以组件的方式去重新思考UI构成,将UI上每一个功能相对独立的模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建。
  • 对于React而言,则完全是一个新的思路,开发者从功能的角度出发,将UI分成不同的组件,每个组件都独立封装。

React一个组件应该具有如下特征:

  1. 可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件.
  2. 可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景
  3. 可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护

语法

  • 组件名称一定要大写
  • 组件只能包含一个顶层标签
// Header组件 
// export 导出供外部使用
export default class ComponentHeader extends React.Component{
    //render方法:用于解析本身类的输出
    render(){
        return (<header><h1>这里是头部</h1></header>)
    }
}



// Index组件
import  ComponentHeader from './component/header';
class Index extends React.Component{
    render(){
        // 插入普通 HTML 标签一样
        return <ComponentHeader/>
    }
}

// 将模版转化为HTML语言,并插入到指定节点
// 相当于程序的入口

ReactDOM.render(<Index/>,document.getElementById('one')) 

React 多组件嵌套

各个组件之间相互独立,利于维护,重用。在Index文件中进行调用,简明。
需要变更时,我们只需要改变相应的组件,所有引用该组件的的页面就都发生的变更,很好维护。

import  ComponentHeader from './component/header';
import  BodyIndex from './component/body';
import  ComponentFooter from './component/footer';

class Index extends React.Component{
    render(){
        //将组件赋值给一个变量,这样方便做判断
        var component;
        if(/*条件*/){
             component = <ComponentLoginedHeader/>;
        }else{
             component = <ComponentHeader/>;
        }
        
        return (
            <div>
                { component}
                <BodyIndex/>
                <ComponentFooter/>
            </div>
        )
    }
}

JXS内置表达式

  • HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTMLJavaScript 的混写。
  • JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析
  • JSX 中的注释 {/*注释*/}
export default class BodyIndex extends React.Component{
   render(){
   
    var userName = 'xiaoxiong';
    var boolInput;
    var html1 = 'Mooc&nbsp;Lesson';
    //对 &nbsp; 进行Unicode转码
    var html2 = 'Mooc\u0020Lesson'
       return (
           <div>
               <h2>页面主体内容</h2>
                // 三元表达式的使用
               <p>{userName = '' ?'用户还没有登录':'用户名:' +userName }</p>
               // 使用{boolInput}进行值的绑定
               <input type='button' value='按钮' disable={boolInput}>
               //解析html
               <p>{html1}</p>  //'Mooc&nbsp;Lesson' &nbsp;不会被解析为空格
               <p>{html2}</p>  //'Mooc Lesson'
               <p dangerouslySetInnerHTML={{__html:html1}}></p> //'Mooc Lesson' 此方法可能会存在xss攻击 
               
           </div>
       )
   }
}

声明周期,纵观整个React的生命周期

在ES6中,一个React组件是用一个class来表示的

过程描述

React中有4中途径可以触发render

  • 首次渲染Initial Render
constructor --> componentWillMount -->  render -->conponentDidMount 
  • 调用this.setState (并不是一次setState会触发一次renderReact可能会合并操作,再一次性进行render
shouldComponentUpdate --> componentWillUpdate --> render --> componentDidUpdate
  • 父组件发生更新(一般就是props发生改变,但是就算props没有改变或者父子组件之间没有数据交换也会触发render
componentWillReceiveProps --> shouldComponentUpdate --> componentWillUpdate --> render --> componentDidUpdate
  • 调用this.forceUpdate
componentWillUpdate --> render --> componentDidUpdate

总结

  • constructorcomponentWillMountcomponentDidMount只有第一次渲染时候会被调用
  • componentWillUpdatecomponentDidUpdateshouldComponentUpdate 在以后的每次更新渲染之后都会被调用

图片描述
图片描述
图片描述


考虑到性能的问题,如果有些属性的变化,不需要重新刷新页面,我们是使用 componentShouldUpdate() 进行控制。


官方文档
https://facebook.github.io/re...

参考文献
http://www.jianshu.com/p/4784...


react属性与事件

state 属性控制React的一切

组件自身的状态,props为外部传入的状态

state对组件做了更新之后,会马上反应到虚拟DOM上,最后更新到DOM上。这个过程是自动完成的。

图片描述

组件免不了要与用户互动,React 的一大创新,就是将组件看成是一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染 UI .

export default class BodyIndex extends React.Component{
   // 初始化
   constructor(){
       super();//调用基类的所有初始化方法
       //state的作用域是当前组件,不会污染其他模块
       this.state = {
           username:"xiaoxiong"
       };
       
   };
   
   //修改state
   setTimeOut(()=>{
       this.setState({username:"miaomiao"})
   },4000);
   
   render(){
       return (
           <div>
               <h2>页面主体内容</h2>
               // 引用state值
               <p>{this.state.username}</p>
           </div>
       )
   }
}

Props属性

其他组件传递参数,对于本模块来说,属于外来属性。

图片描述

//使用组件时,传入参数
< BodyIndex userid="123" username={xiaoxiong}/>


export default class BodyIndex extends React.Component{
  
    render(){
        return (
            <div>
                <h2>页面主体内容</h2>
                // 接收参数
                <p>{this.props.userid}</p>
                <p>{this.props.username}</p>
            </div>
        )
    }
}

添加组件属性,有一个地方需要注意,就是 class 属性需要写成 classNamefor 属性需要写成 htmlFor ,这是因为 classforJavaScript 的保留字。


事件和数据的双向绑定,包含了父子页面之间的参数互传

  • 父组件给子组件传递参数,使用props

事件绑定

export default class BodyIndex extends React.Component{
    constructor(){
        super();
        this.state={
            username="xiaoxiong",
            age:20
        }
    };
    
    changeUserInfo(){
        this.setState({age:50})
    };
  
    render(){
        return (
            <div>
                <h2>页面主体内容</h2>
                <p>{this.props.username} {this.props.age }</p>
                //事件绑定 ES6写法 
                //ES5写法 onClick=this.chanchangeUserInfo
                <input type="button" value="提交" onClick=this.chanchangeUserInfo.bind(this)/>
            </div>
        )
    }
}
  • 子组件为父组件传参使用事件
  • 在子页面中通过调用父页面传递过来的事件 props 进行组件间的参数传递

就是让子页面的变动体现在父页面上,而页面状态的改变由state控制,因此我们让父页面通过props将函数传递给子页面,此函数可以取得子页面的值,并改变父页面的state

//子组件
export default class BodyChild extends React.Component{
         
    render(){
        return (
            <div>
                // 调用父组件传过来的函数
                <p>子页面输入:<input type="text" onChange={this.props.handler}/></p>
            </div>
        )
    }
}

//父组件
import BodyChild from "./component/bodychild";

export default class Body extends React.Component{

    constructor(){
        super();
        this.state={
            age:20
        }
    }

    // 父页面的函数,可以操控父页面的 state
    handler(e){
        this.setState({age:e.target.value})
    }
   
    render(){
        return (
            <div>
                <p>{this.state.age}<p>
                <BodyChild handler={this.handler.bind(this)}/>
            </div>
        )
    }
}

可复用组件,真正让React开发快速、高效的地方

使用组件时,传递props,在组件定义的文件中可使用这些props

export default class BodyIndex extends React.Component{
   
    render(){
        return (
            <div>
                <p>{this.props.userid}<p>
                <p>{this.props.username}<p>
            </div>
        )
    }
}

// 对传递过来的 props 的类型进行约束
BodyIndex.propTypes = {
    // userid为number类型且必须传递
    userid:React.propTypes.number.isRuquired
};

// 设置默认值,使用组件时,如果不传递参数,将显示默认值
BodyIndex.defaultProps = {
    username:"xiaoxiong"
};

组件多层嵌套时,传递参数的简便方法

export default class Body extends React.Component{
   
    render(){
        return (
            <div>
                //在父页面定义props
               <BodySon username="xiaoxiong" age="25"/>
            </div>
        )
    }
}


export default class BodySon extends React.Component{
   
    render(){
        return (
            <div>
                <p>{this.props.username}</p>
                //取得父页面(BodySon)的所有props属性
               <Bodygrandson ...this.props />
            </div>
        )
    }
}



export default class Bodygrandson extends React.Component{
   
    render(){
        return (
            <div>
               // 使用传递过来的props
               <p>{this.props.username}{this.props.age}</p>
            </div>
        )
    }
}


组件的Refs
React中多数情况下,是通过state的变化,来重新刷新页面。但有时也需要取得html节点,比如对input进行focus等;下面我们来看下,怎么取得原生的html节点,并对其进行操作。

export default class BodyChild extends React.Component{

    handler(){
        //第一种方式
        var mySubmitBotton = document.getElementById("submitButton");
        //这样也是可以的
        mySubmitBotton.style.color="red";
        ReactDOM.findDOMNode(mySubmitBotton).style.color = 'red';
        
        //第二种方式
        this.refs.submitBotton.style.color = 'red';
        
    }
         
    render(){
        return (
            <div>
                <input id="submitButton" ref="submitButton"type="button" onClick={this.handler.bind(this)}/>
            </div>
        )
    }
}
  • refs是访问到组件内部DOM节点的唯一可靠方式
  • refs会自动销毁对子组件的引用
  • 不要在RenderRender之前对Refs进行调用,因为Refs获取的是真实的DOM节点,要在插入真实DOM节点之后调用。
  • 不要滥用Refs
  • 不必须使用真实DOM时,用方法1即可

独立组件间共享Mixins

在所有的组件见共享一些方法

ES6中使用mixin需要插件支持

npm install react-mixin --save

使用

//mixin.js
const MixinLog = {
    //有自身的生命周期
    componentDidMount(){
        console.log('MixinLog ComponentDidMount');
    },
    log(){
        console.log('Mixins');
    }
};

//暴露供外部使用
export default MixinLog;

//body.js
//导入MixinLog 
import MixinLog from './component/mixin.js';
import ReactMixin from 'react-mixin';
export default class Body extends React.Component{

     handler(){
        //调用MixinLog中方法
        MixinLog.log();
     }
   
    render(){
        return (
            <div>
               <p>{this.props.username}</p>
            </div>
        )
    }
}

ReactMixin(Body.prototype,MixinLog);

react样式

内联样式

原生中用 - 连接的样式属性,在这里要采用驼峰写法或加引号"",属性值一律加引号"" ,这样的书写方式,实际上就是加入了内联样式,结构和样式混在一起,不是很好的做法。并且这种写法,不能使用伪类、动画。

export default class Header extends React.Component{
    
    render(){
    //将样式定义为一个变量
    //注意样式属性的书写
    const styleComponentHeader = {
        header:{
            backgroundColor:"#333",
            color:"#fff",
            "padding-top":"15px",
            paddingBottom:"15px",
        },
        //还可以定义其他样式
    };

        return (
            //使用样式,这样写,实际上就是内联样式
            <header style={styleComponentHeader. header}>
                <h1>头部</h1>
            </header>
        )
    }
}

也可以在index.html文件中,引入css文件,并在需要使用样式的地方加入类名(className),但这种写法会污染全局。

//index.html
<header>
    <link href="../css.js">
</header>
<div id="one"></div>

//组件
export default class Header extends React.Component{
    
    render(){
        return (
            //使用类名,加入样式
            <header className="header">
                <h1>头部</h1>
            </header>
        )
    }
}

内联样式中的表达式

根据state的值,控制样式

export default class Header extends React.Component{
    
    constructor(){
        super();
        this.state={
            miniHeader:false
        }
    }
    //点击头部,样式发生变化
    swithHeader(){
        this.setState({
           miniHeader:!this.state.miniHeader
        })
    }
    
    render(){
    const styleComponentHeader = {
        header:{
            backgroundColor:"#333",
            color:"#fff",
            "padding-top":"15px",
            //根据state的值,变化 paddingBottom的值
            //样式中可以使用表达式
            paddingBottom: (this.state.miniHeader)?"3px":"15px"
        },
    };

        return (
        
            <header style={styleComponentHeader. header} onClick={this.swithHeader.bind(this)}>
                <h1>头部</h1>
            </header>
        )
    }
}

CSS模块化,学习如何使用require进行样式的引用
问题:

  • 怎样实现css的按需加载,使用这个模块时,只加载这个模块的样式。
  • 模块之间的样式是相互独立的,即使使用相同的类名,也不会相互影响。

-

使用webpack,安装相关插件

//使用require导入css文件
npm install css-loader --save
//计算之后的样式加入页面中
npm install style-loader --save

webpack.config.js文件中进行配置
css文件导入相应的组件后,会生成一个对应关系

footer:_footer_minifooter_vxs08s

_footer_minifooter_vxs08s是按照一定的规则生成一个唯一的类名,当我们为当前组件应用 .footer 类的时候,就会按照这个对应关系去样式文件中找,然后应用响应的样式。
实际上是改变了样式文件中类的名称,使其唯一。
这样即使将所有文件打包到一起,也不会引起冲突。
不同的文件可以 使用相同的类名,不会冲突,因为模块化之后,类名都进行了转换。

module:{
        loaders:[
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
                query: {
                    presets: ['es2015','react']
                }
            },
            {
                test: /\.css$/,
                //modules 模块化配置
                loader: 'style-loader!css-loader?modules'
            },
        ]
    },

使用

//footer.css

.footer{
    background-color:#333;
}


//Footer组件
//导入css样式,这样这个样式文件只作用于这个组件
var  footerCss = require('./css/footer.css');

export default class Footer extends React.Component{
    
    render(){
        return (
            <footer className="footerCss.footer">
                <h1>底部</h1>
            </footer>
        )
    }
}

总结

为什么要css模块化

  • 可以避免全局污染、命名混乱

模块化的优点

  • 所有的样式都是local的,解决的命名冲突和全局污染的问题
  • class名生成规则配置灵活,可以以此来压缩class名(在webpack.config.js文件中配置)
  • 使用某个组件时,只要import组件,再使用即可,无需管理样式。
  • 不需要像书写内部样式一样,属性的名称需要驼峰写法。只需在webpack.config.js中进行配置,书写时,还是我们熟悉的css

JSX样式与CSS样式互转

线上转换工具
http://staxmanade.com/CssToRe...


react-router

Router 概念


控制页面之间的层级关系
底层机制
通过状态的改变,导致组件从新渲染,从而改变页面显示

React: state/props -> Component ->UI

通过改变url,导致Router变化,从而改变页面显示

React:location(hasj) -> Router ->UI

hashHistory && browserHistory

慕课老师的demo使用的是hashHistory,而另一种方式则是使用browserHistory

如果希望使用browserHistory达到hashHistory的效果,则需要做2件事情:

1、服务器支持。如果开发服务器使用的是webpack-dev-server,加上--history-api-fallback参数就可以了。
2、作为项目入口的HTML文件中,相关外链的资源路径都要改为绝对路径,即以"/"根路径开头。


安装

// 版本 2.8.1
npm install react-router

使用

component指定组件
path 指定路由的匹配规则

router可以进行嵌套
ComponentDetails嵌套在Index页面中,我们要在Index中进行展示。

//index.js
export default class Index extends React.Component{

    render(){
        return ( 
            <div>
                //此处展示的是ComponentDetails页面
                {this.props.children} 
            </div>
        )
    }
}
import React from 'react';
import ReactDOM from 'react-dom';
import Index from './index';
import { Router, Route, hashHistory} from 'react-router';
import ComponentList from './components/list';
import ComponentHeader from './components/header';
import ComponentDetails from './components/details';

export  default class Root extends React.Component{
    render(){
        //这里替换了之前的index,变成了程序的入口 (注意修改webpack.conf.js中的入口文件为root.js)
        return (
            <Router history={hashHistory}>
                <Route component={Index} path="/">
                    <Route component={ComponentDetails} path="details"></Route>
                </Route>
                <Route component={ComponentList} path="list/:id"></Route>
                <Route component={ComponentHeader} path="header"></Route>
            </Router> 
        );
    };
}


ReactDOM.render(
    <Root/>,
    document.getElementById('example')
);

有了Router之后,用Link进行跳转

<Link to={`/`}>首页</Link>
<Link to={`/details`}>嵌套的详情页面</Link>

Router 参数传递

//在Router中定义参数名称
<Route component={ComponentList} path="list/:id"></Route>

//在Link中传入参数
<Link to={`/list`}></Link>

//在list组件页面中读取传入的参数
render(){
    <div>{this.props.params.id}</div>
}

使用NPM配置React环境

//初始化  建立初始化文件
npm init

package.json文件

npmstart是一个特殊的脚本名称,它的特殊性表现在,在命令行中使用npm start就可以执行相关命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build

{
  "name": "reactconf",//项目名称
  "version": "1.0.0",//项目版本
  "description": "",//项目描述
  "main": "root.js",//入口文件
  //自定义的脚本任务
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --inline --content-base ."
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "antd": "^2.10.1",
    "babel-preset-es2015": "^6.24.1",
    "babel-preset-react": "^6.24.1",
    "babelify": "^7.3.0",
    "css-loader": "^0.25.0",
    "react": "^15.5.4",
    "react-dom": "^15.5.4",
    "react-mixin": "^2.0.2",
    "react-router": "^2.8.1",
    "style-loader": "^0.13.1",
    "webpack": "^1.13.2",
    "webpack-dev-server": "^1.16.1"
  },
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-loader": "^7.0.0"
  }
}

安装依赖

babelify
Babel其实是一个编译JavaScript的平台,它的强大之处表现在可以通过编译帮你达到以下目的:

  • 下一代的JavaScript标准(ES6ES7),这些标准目前并未被当前的浏览器完全的支持
  • 使用基于JavaScript进行了拓展的语言,比如ReactJSX

babel-preset-reactreact转码规则
babel-preset-es2015ES2015转码规则


npm install react react-dom babelify --save

npm install  babel-preset-react babel-preset-es2015 --save

使用webpack打包

安装相关的包

npm install webpack -g 
npm install webpack-dev-server -g
npm install webpack  --save
npm install webpack-dev-server --save
//webpack.config.js

// 引用webpack相关的包
var webpack = require('webpack');
var path = require('path');
var WebpackDevServer = require("webpack-dev-server");

module.exports = {
    //入口文件  __dirname 项目根目录
    //“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录
    context:  __dirname + "/src",
    entry:"./js/root.js",
    
    //loaders
    module:{
        loaders:[
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: 'babel',
                query: {
                    presets: ['es2015','react']
                }
            },
            //下面是使用 ant-design 的配置文件 不再使用 ?modules 因为样式是全局的 不再需要局部样式
            {
                test: /\.css$/,
                //modules  模块化配置
                loader: 'style-loader!css-loader?modules'
            },
        ]
    },
    
    //出口文件
    output: {
        path:   __dirname + '/src/',
        filename: 'bundle.js'
    },
};

打包命令

// 正常情况下
webpack

//配置热加载情况下
webpack-dev-server --inline

使用 --watch 可是自动打包,但不会自动刷新

可以用content-base设定 webpack-dev-server 伺服的 directory (就是指管这个路径下文件的变动),如果不进行设定的话,默认是在当前目录下

webpack-dev-server --content-base -src --inline

关于webpack

为什么使用webpack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;类似于TypeScript这种在JavaScript基础上拓展的开发语言,使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能能装换为JavaScript文件使浏览器可以识别.
  • ScsslessCSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。


webpack并不强制你使用某种模块化方案,而是通过兼容所有模块化方案让你无痛接入项目,当然这也是webpack牛逼的地方


webpackgulp的区别

gulp是工具链、构建工具,可以配合各种插件做js压缩,css压缩,less编译 替代手工实现自动化工作

1.构建工具

2.自动化

3.提高效率用

webpack是文件打包工具,可以把项目的各种js文、css文件等打包合并成一个或多个文件,主要用于模块化方案,预编译模块的方案

1.打包工具

2.模块化识别

3.编译模块代码方案
  • 虽然都是前端自动化构建工具,但看他们的定位就知道不是对等的。
  • gulp严格上讲,模块化不是他强调的东西,他旨在规范前端开发流程。
  • webpack更是明显强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。

webpack工作方式

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个浏览器可识别的JavaScript文件。


Loaders

通过使用不同的loaderwebpack通过调用外部的脚本或工具可以对各种各样的格式的文件进行处理,比如说分析JSON文件并把它转换为JavaScript文件,或者说把下一代的JS文件(ES6ES7)转换为现代浏览器可以识别的JS文件。或者说对React的开发而言,合适的Loaders可以把ReactJSX文件转换为JS文件。

Loaders需要单独安装并且需要在webpack.config.js下的modules关键字下进行配置,Loaders的配置选项包括以下几方面:

test:一个匹配loaders所处理的文件的拓展名的正则表达式(必须)
loaderloader的名称(必须)
include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
query:为loaders提供额外的设置选项(可选)


依赖模块

// MediaQuery 进行移动端适配
var MediaQuery = require('react-responsive');
<MediaQuery query='(min-device-width:1224px)'>
fetch 向后台请求数据

响应式 以1224px为分界


查看原文

赞 2 收藏 10 评论 0

小熊苗苗 赞了文章 · 2017-10-12

基于html2canvas实现网页保存为图片及图片清晰度优化

2019.12 更新:《高质量前端快照方案:来自页面的「自拍」
2019.12 更新:《高质量前端快照方案:来自页面的「自拍」
2019.12 更新:《高质量前端快照方案:来自页面的「自拍」

新作见上面哦~ 👆👆👆

本次技术调研来源于H5项目中的一个重要功能需求:实现微信长按网页保存为截图

这里有个栗子(请用微信打开,长按图片即可保存):3分钟探索你的知识边界

将整个网页保存为图片是一个十分有趣的功能,常见于H5活动页的结尾页分享。以下则是项目中调研和踩坑的一些小结和汇总。


一、实现HTML页面保存为图片

1.1 已知可行方案

现有已知能够实现网页保存为图片的方案包括:

  • 方案1:将DOM改写为canvas,然后利用canvas的toDataURL方法实现将DOM输出为包含图片展示的data URI
  • 方案2:使用html2canvas.js实现(可选搭配Canvas2Image.js实现网页保存为图片)
  • 方案3:使用rasterizeHTML.js实现

1.2 解决方案的选择

  • 方案1:需要手动计算每个DOM元素的Computed Style,然后需要计算好元素在canvas的大小位置等属性。

    方案1难点

    1. 相当于完全重写了整个页面的布局样式,增加了工作量。
    2. 由于canvas中没有的对象概念,对于元素丰富、布局复杂的页面,不易重构。
    3. 所有DOM元素改写进canvas会带来一些困难,例如:难以支持响应式,图片元素清晰度不佳和文字点击区域识别问题等。
  • 方案2:该类功能中Github上stars最多(至今仍在维护),Stack Overflow亦有丰富的讨论。只需简单调用html2canvas方法并设定配置项即可。
  • 方案3:该方案的限制较多,目前仅支持3类可转为canvas的目标格式: 页面url,html字符串和document对象。

小结: html2canvas是目前实现网页保存为图片功能的综合最佳选择。

1.3 html2canvas的使用方法

官方GitHub:https://github.com/niklasvh/h...

以下描述针对html2canvas版本是0.5.0-beta4

1.3.1 实现保存为图片的第一步:html转为canvas

基于html2canvas.js可将一个元素渲染为canvas,只需要简单的调用html2canvas(element[, options]);即可。下列html2canvas方法会返回一个包含有<canvas>元素的promise

html2canvas(document.body).then(function(canvas) {
    document.body.appendChild(canvas);
});

1.3.2 实现保存为图片的第二步:canvas转image

上一步生成的canvas即为包含目标元素的<canvas>元素对象。实现保存图片的目标只需要将canvas转image即可。

这里的转换方案有2种

  • 方案1:基于原生canvas的toDataURL方法将canvas输出为data: URI类型的图片地址,再将该图片地址赋值给<image>元素的src属性即可
  • 方案2:使用第三方库Canvas2Image.js,调用其convertToImage方法即可(GitHub

实际上,Canvas2Image.js也是基于canvas.toDataURL的封装,相比原生的canvas API对于转为图片的功能上考虑更为具体(未压缩的包大小为7.4KB),适合项目使用。

二、生成图片的清晰度优化方案

2.1 基础的清晰度优化方案

最终图片的清晰度取决于第一步中html转换成的canvas的清晰度。

现有解决方案参考;

基本原理为:
canvas的属性widthheight属性放大为2倍(或者设置为devicePixelRatio倍),最后将canvas的CSS样式width和height设置为原先1倍的大小。

例如:希望在html中实际显示的<canvas>宽高分别为160px,90px则可作如下设置

<canvas width="320" height="180" style="width:160px;height:90px;"></canvas>

参考上述文档具体的使用案例如下;

convert2canvas() {

    var shareContent = YourTargetElem; 
    var width = shareContent.offsetWidth; 
    var height = shareContent.offsetHeight; 
    var canvas = document.createElement("canvas"); 
    var scale = 2; 

    canvas.width = width * scale; 
    canvas.height = height * scale; 
    canvas.getContext("2d").scale(scale, scale); 

    var opts = {
        scale: scale, 
        canvas: canvas, 
        logging: true, 
        width: width, 
        height: height 
    };
    html2canvas(shareContent, opts).then(function (canvas) {
        var context = canvas.getContext('2d');

        var img = Canvas2Image.convertToImage(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);
        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        })
    });
}

2.2 进阶的清晰度优化方案

上述设置可以解决通常情况下图片不清晰的问题,不过探索并没有结束。

实际在我们的项目中,即使作出2.1节的设置后,大果粒一般的渲染结果依然尴尬。

下面直接给出3条进一步的优化策略:

  1. 更改百分比布局px布局(如果原先是百分比布局的话)
  2. 关闭canvas默认的抗锯齿设
  3. 设置模糊元素的widthheight为素材原有宽高,然后通过transform: scale进行缩放。这里scale的数值由具体需求决定。
基本原理
  1. 如果原来使用百分比设置元素宽高,请更改为px为单位的宽高,避免样式二次计算导致的模糊
  2. 默认情况下,canvas的抗锯齿是开启的,需要关闭抗锯齿来实现图像的锐化(MDN: imageSmoothingEnabled )
  3. 除了canvas可以通过扩大2倍宽高然后缩放至原有宽高来提高清晰度,对于DOM中其他的元素也可以使用css样式scale来实现同样的缩放
例: html2canvas配置
convert2canvas() {

    var cntElem = $('#j-sec-end')[0];

    var shareContent = cntElem;//需要截图的包裹的(原生的)DOM 对象
    var width = shareContent.offsetWidth; //获取dom 宽度
    var height = shareContent.offsetHeight; //获取dom 高度
    var canvas = document.createElement("canvas"); //创建一个canvas节点
    var scale = 2; //定义任意放大倍数 支持小数
    canvas.width = width * scale; //定义canvas 宽度 * 缩放
    canvas.height = height * scale; //定义canvas高度 *缩放
    canvas.getContext("2d").scale(scale, scale); //获取context,设置scale 
    var opts = {
        scale: scale, // 添加的scale 参数
        canvas: canvas, //自定义 canvas
        // logging: true, //日志开关,便于查看html2canvas的内部执行流程
        width: width, //dom 原始宽度
        height: height,
        useCORS: true // 【重要】开启跨域配置
    };

    html2canvas(shareContent, opts).then(function (canvas) {

        var context = canvas.getContext('2d');
        // 【重要】关闭抗锯齿
        context.mozImageSmoothingEnabled = false;
        context.webkitImageSmoothingEnabled = false;
        context.msImageSmoothingEnabled = false;
        context.imageSmoothingEnabled = false;
        
        // 【重要】默认转化的格式为png,也可设置为其他格式
        var img = Canvas2Image.convertToJPEG(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);

        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        }).addClass('f-full');

    });
}
例: DOM元素样式:

.targetElem {width: 54px;height: 142px;margin-top:2px;margin-left:17px;transform: scale(0.5)}

三、含有跨域图片的配置

由于canvas对于图片资源的同源限制,如果画布中包含跨域的图片资源则会污染画布,造成生成图片样式混乱或者html2canvas方法不执行等问题。

以下主要解决两类跨域的图片资源:包括已配置过CORS的CDN中的图片资源和微信用户头像图片资源。

3.1 针对CDN中的图片的配置

  1. 要求CDN的图片配置好CORSCDN配置好后,通过chrome开发者工具可以看到响应头中应含有Access-Control-Allow-Origin的字段。
  2. 开启html2canvasuseCORS配置项。即作如下设置:

var opts = {useCORS: true};

html2canvas(element, opts);

注意
如果没有开启html2canvasuseCORS配置项,html2canvas会正常执行且不会报错,但是不会输出对应的CDN图片
(已测试同时包含CDN的图片本地图片的资源的页面,但是只有本地图片能够被正常渲染出来)

3.2 针对微信用户头像的配置

如果需要将微信平台中的用户头像一并保存为图片,3.1的方案无能为力。可通过配置服务端代理转发(forward)实现,此处不赘述。

其他注意事项

1. margin的遮挡问题

微信中,唤出长按保存图片的菜单要求长按的对象直接是<image>元素,如果<image>元素上方存在遮挡,则不会唤出菜单。
而事实上,引发遮挡的并不只是非<image>元素,还可能是margin属性。例如:若在页面底部,对一个绝对定位的元素设置了数值很大的margin-top,则margin-top所涉及的区域,均无法长按唤出菜单。解决方案:将margin-top改用为top即可。

2. 安卓版微信保存图片失败的问题

canvas2img默认保存图片的格式为png,而在安卓版微信中所生成的图片尽管能长按唤出保存图片的菜单,但是无法正确保存到本地相册解决方案:设置canvas2img的生成图片格式配置项为jpeg即可。

3. JPEG的黑屏问题

设置canvas2img输出格式为jpeg,会有一定几率导致生成的图片包含大量的黑色块。可能的解决方案:缩减部分图片元素的体积和尺寸大小。

4. 不能保留动效

在图片的转化前,必须停止或者删除动效后才能正确渲染出图片,否则生成的图片是破裂的。

参考文献

查看原文

赞 259 收藏 422 评论 23

小熊苗苗 关注了专栏 · 2017-08-01

Jensen

http://www.jianshu.com/u/7ae8671ca7f4

关注 282

小熊苗苗 关注了用户 · 2017-08-01

Jensen121 @jensenczx

连续创业者。
更多技术干货博客:https://juejin.im/user/59a387...
生活记录:https://www.jianshu.com/u/7ae...

关注 244

小熊苗苗 发布了文章 · 2017-07-28

cookie、sessionStorage、localStorage 详解及应用场景

cookie、sessionStorage、localStorage 详解及应用场景

Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在,而Web Storage仅仅是为了在本地“存储”数据而生


cookie

Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在

了解cookie

  1. 要表示唯一的一个cookie值需要:namedomainpath

  2. 一个cookie就是一个小型的文本文件

  3. 虽然cookie保存在浏览器端,但是一般是在服务器端设置的。

  4. 可以在HTTP返回体里,通过设置Set-Cookie来告诉浏览器端所要存储的cookie

  5. 用来保存客户浏览器请求服务器页面的请求信息


cookie相关字段的说明

  1. 名称:一个唯一确定cookie的名称。cookie名称是不区分大小写的。cookie的名称必须是经过URL编码的。

  2. 值:储存在cookie中的字符串值。值必须被URL编码。

  3. 域:cookie对于哪个域是有效的。所有向该域发送的请求中都会包含这个cookie信息。如果没有明确设定,那么这个域会被认作来自设置cookie的那个域。

  4. 路径:对于指定域中的那个路径,应该向服务器发送cookie。例如,你可以指定cookie只有从http://www.wrox.com/books/中才能访问,那么http://www.wrox.com的页面就不会发送cookie信息,即使请求都是来自同一个域的。

  5. 失效时间:表示cookie何时应该被删除的时间戳(也就是,何时应该停止向服务器发送这个cookie)。默认情况下,浏览器会话结束时即将所有cookie删除;不过也可以自己设置删除时间。这个值是个GMT格式的日期(Wdy,DD-Mon-YYYY HH:MM:SSGMT),用于指定应该删除cookie的准确时间。因此,cookie可在浏览器关闭后依然保存在用户的机器上。如果你设置的失效日期是个以前的时间,则cookie会被立刻删除。

  6. 安全标志:指定后,cookie只有在使用SSL连接的时候才发送到服务器。例如,cookie信息只能发送给 https://www.wrox.com,而http://www.wrox.com的请求则不能发送 cookie


cookie的应用场景

  • 简单来说,Cookie就是服务器暂存放在你的电脑里的资料(.txt格式的文本文件),好让服务器用来辨认你的计算机。当你在浏览网站的时候,Web服务器会先送一小小资料放在你的计算机上,Cookie 会把你在网站上所打的文字或是一些选择都记录下来。当下次你再访问同一个网站,Web服务器会先看看有没有它上次留下的Cookie资料,有的话,就会依据Cookie里的内容来判断使用者,送出特定的网页内容给你。

  • 网站可以利用cookie跟踪统计用户访问该网站的习惯,比如什么时间访问,访问了哪些页面,在每个网页的停留时间等。利用这些信息,一方面是可以为用户提供个性化的服务,另一方面,也可以作为了解所有用户行为的工具,对于网站经营策略的改进有一定参考价值。

  • 目前Cookie最广泛的是记录用户登录信息,这样下次访问时可以不需要输入自己的用户名、密码了——当然这种方便也存在用户信息泄密的问题,尤其在多个用户共用一台电脑时很容易出现这样的问题。


设置/修改 cookie

cookie的原生的API,需要我们自己进行封装

//直接复制 【直接复制不是覆盖,而是追加】
document.cookie = 'name=value;'

//封装setCookie方法
//setCookie 首先对name和value进行编码
function setCookie(name,value,expires,path,domain,secure){

    var cookie = encodeURIComponent(name)+ '=' +encodeURIComponent(value);
    
    //注意分号后面要有空格
    //后面的4个参数是可选的,所以用if判断并追加
     
    if(expires){
        cookie +='; expires='+expires.toGMTString();
    }
    if(path){
        cookie += '; path='+path;
    }
    if(domain){
        cookie += '; domain='+domain;
    }
    if(secure){
        cookie += '; secure='+secure;
    }
    document.cookie = cookie;
}

删除cookie

输入参数为namepathdomain3个是唯一标识cookie的,将max-age设置为0,就可以立即删除了.

function remove(name,domain,path){
    document.cookie = 'name='+name
                    +'; domain='+domain
                    +'; path='+path
                    +'; max-age=0';
}

cookie缺点

  • Cookie数量和长度的限制。IE6或更低版本每个domian下最多20cookieIE7和之后的版本最多可以有 50cookieFirefox最多50cookiechromeSafari没有做硬性限制,每个cookie长度不能超过4KB,否则会被截掉。

  • IEOpera清理近期最少使用的cookieFirefox会随机清理cookie。这就导致不能永久储存信息。

  • 安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。

  • 并且每次你请求一个新的页面的时候,cookie只要满足作用域和作用路径,Cookie都会被发送过去,这样无形中浪费了带宽


本地储存

Web Storage是为了在本地“存储”数据而生。html5中的 Web Storage 包括了两种存储方式:sessionStoragelocalStorage

localStorage && sessionStorage

只要有效期和作用域,浏览器每次访问的时候都会将Storage载入到内存里

  • localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。

  • sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。也就是说只要这个浏览器窗口没有关闭,即使刷新页面或进入同源另一页面,数据仍然存在。关闭窗口后,sessionStorage即被销毁

  • localStorage也受同源策略的限制。

  • localStoragesessionStorage都具有相同的操作方法,如setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookiegetCookie


localStorage应用场景

localStorage可以用于存储该浏览器对该页面的访问次数,当然,如果换个浏览器,这个次数就重新开始计数了。还可以用来存储一些固定不变的页面信息,这样就不需要每次都重新加载了,这个值也可以进行覆盖。

访问这个页面的时候,script 脚本会自动运行,localStorage.pagecount就会 ++ 了,从而达到统计页面访问次数的目的。

<!DOCTYPE HTML>
<html>
<body>

<script type="text/javascript">

if (localStorage.pagecount){
    localStorage.pagecount=Number(localStorage.pagecount) +1;
}
else{
    localStorage.pagecount=1;
}

document.write("Visits: " + localStorage.pagecount + " time(s).");

</script> 

<p>刷新页面会看到计数器在增长。</p>

<p>请关闭浏览器窗口,然后再试一次,计数器会继续计数。</p>

</body>
</html>

sessionStorage应用场景

使用 sessionStorage 进行页面传值

//有时会有这样的需求,我们从A页面获取的数据,需要在B页面发送给后端,这时就需要我们将数据从A页面传递到B页面。

//A页面
//首先检测Storage
if (typeof(Storage) !== "undefined") {
    sessionStorage.'name'=value;
} else {
    sessionStorage.name = '';
}


//B页面
if (typeof(Storage) !== "undefined") {
    var B_name = sessionStorage.name;
    }
//注意,如果要储存的数据对象、数组
//那么在储存之前,用JSON.stringify将数据转换为字符串
//读取之后,再用JSON.parse转换为对象、数组

//存储
var obj = {name:"xiaoxiong",age:25};
var arr = [1,2,3,4];
window.sessionStorage.obj = JSON.stringify(obj);
window.sessionStorage.arr = JSON.stringify(arr);

//读取
var OBJ = window.sessionStorage.obj;//"{"name":"xiaoxiong","age":25}"
JSON.parse(OBJ);//Object {name: "xiaoxiong", age: 25}

var ARR = window.sessionStorage.arr;//"[1,2,3,4]"
JSON.parse(ARR);//(4) [1, 2, 3, 4]

总结一下cookie数量和长度都有限制,Web Storage解决了这样的限制,且localStorage做到了永久储存。但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生。

查看原文

赞 22 收藏 88 评论 2

小熊苗苗 发布了文章 · 2017-07-24

浏览器同源政策以及JS跨域

浏览器同源政策以及JS跨域

同源是指协议相同、域名相同、端口相同。同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

同源策略主要限制下面三种情况

  • Cookie 无法读取

  • DOM 无法获得

  • AJAX 请求不能发送

同源策略的本意是为了保证用户的信息安全。但有时也会带来不便,下面我们来看一下怎样规避同源的限制。


Cookie

是服务器写入浏览器的一小段信息,只有同源的网页才能共享。

当两个网页的一级域名相同,只是二级域名不同的时候,我们可以通过设置document.domain来共享cookie
具体操作如下:

// 这两个网页的一级域名是相同的 
http://h1.test.com
http://h2.test.com

//为两个页面设置相同的 document.domain
document.domain = "test.com"

// 这样两个网页就能共享`Cookie`

document.domain 不能随意设置,只能把document.domain设置成自身或更高一级的父域。


跨域文档通信

如果两个网页不同源,就无法拿到对方的DOM,也无法进行通信。典型的例子是iframe窗口和window.open方法打开的窗口,它们与父窗口无法通信。

如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain属性,就可以规避同源政策,拿到DOM

关于通信,我们来看一下两种解决方案:

片段识别符

片段标识符(fragment identifier)指的是,URL#号后面的部分,如果只是改变片段标识符,页面不会重新刷新。

//父窗口可以把信息,写入子窗口的片段标识符
var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

//子窗口通过监听hashchange事件得到通知
window.onhashchange = checkMessage;

function checkMessage() {
  var message = window.location.hash;
  // ...
}


//同样的,子窗口也可以改变父窗口的片段标识符
parent.location.href= target + "#" + hash;

window.postMessage

window.postMessage 是HTML5为了解决这个问题,引入了一个全新的API,无论两个窗口是否同源,都允许一个窗口向另一个窗口发送数据。

语法:

// otherWindow 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象

//message 将要发送到其他 window的数据

//targetOrigin 接收消息的窗口的源(origin)

otherWindow.postMessage(message, targetOrigin)

其他window可以监听message

//监听 message 事件

window.addEventListener("message", receiveMessage, false);

//事件对象有一些常用的属性

//data 从其他 window 中传递过来的对象

//origin 消息发送方窗口的 origin

//source 对发送消息的窗口对象的引用

function receiveMessage(event){
    var origin = event.origin;
    
    //对发送消息的源进行验证
    
   if (origin !== "http://example.org:8080")
      return;

  // ...
}

实例:

窗口A : http://xiaoxiong.com
窗口B : http://miaomiao.com
显然这两个窗口不同源,不能通信,现在我们用postMessage进行通信。

//Awindow、Bwindow分别表示对 A B 窗口对象的引用
//B窗口向A窗口发消息
//如果A窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,消息就不能发送成功
//注意是用 Awindow 调用 postMessage 方法
 Awindow.postMessage("hello!","http://xiaoxiong.com");
 
 //在A中设置监听事件
 window.addEventListener("message", receiveMessage, false);
 
 function receiveMessage(event){
    console.log(event.origin);//http://miaomiao.com
    console.log(event.source);// Bwindow
    console.log(event.data);// hello!
    
} 

AJAX

AJAX请求是我们经常用到的异步请求方法,但是AJAX请求是不能跨域的。

下面我们看一下常见的AJAX跨域方法

JSONP

基本思想是,网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

function addScript(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script, body.firstChild);
}

window.onload = function () {
  //请求的查询字符串有一个callback参数,用来指定回调函数的名字,这对于JSONP是必需的
  addScript('http://test.com/a?callback=handler');
}

//请求回来数据作为回调函数的参数
//作为参数的JSON数据被视为JavaScript对象 不用进行转换
function handler(data) {
  console.log(data);
};

JSONP的优点:不受同源策略的限制;它的兼容性更好,在更加古老的浏览器中都可以运行;并且在请求完毕后可以通过调用callback的方式回传结果。

JSONP的缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。


WebSocket

WebSocket是一种通信协议,使用ws://(非加密)和wss://(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

实例:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

上面代码中,有一个字段是Origin,表示该请求的请求源(origin),即发自哪个域名。
正是因为有了Origin这个字段,所以WebSocket才没有实行同源政策。因为服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应。


CORS(Cross-origin resource sharing)

跨域资源共享 是官方的跨域解决方案。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

基本思想
CORS定义了必须在访问跨域资源时,浏览器与服务器应该如何沟通。CORS背后的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

基本流程

浏览器发现是跨源AJAX请求,浏览器就直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://xiaoxiong.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://xiaoxiong.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。


CORSJSONP对比,更为先进、方便和可靠。

1、 JSONP只能实现GET请求,而CORS支持所有类型的HTTP请求。

2、 使用CORS,开发者可以使用普通的XMLHttpRequest发起请求和获得数据,比起JSONP有更好的错误处理。

3、 JSONP主要被老的浏览器支持,它们往往不支持CORS,而绝大多数现代浏览器都已经支持了CORS)。

4、 jsonp在调用失败的时候不会返回各种HTTP状态码。

5、在请求完毕后可以通过调用callback的方式回传结果。将回调方法的权限给了调用方。这个就相当于将controller层和view层终于分开了。我提供的jsonp服务只提供纯服务的数据,至于提供服务以后的页面渲染和后续view操作都由调用者来自己定义就好了。如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,逻辑都可以使用同 一个jsonp服务。

参考文献

查看原文

赞 1 收藏 7 评论 0

认证与成就

  • 获得 72 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-21
个人主页被 788 人浏览