木木

木木 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 my729.github.io/blog/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

木木 发布了文章 · 2019-06-19

http1.0 http1.1 http2.0特性及区别

原文地址

https://my729.github.io/blog/internetwork/http%E7%89%88%E6%9C%AC%E5%8C%BA%E5%88%AB.html

http1.0特性

  • 无状态:服务器不跟踪不记录请求过的状态
  • 无连接:浏览器每次请求都需要建立tcp连接

无状态

对于无状态的特性可以借助cookie/session机制来做身份认证和状态记录

无连接

无连接导致的性能缺陷有两种:

1. 无法复用连接
每次发送请求,都需要进行一次tcp连接(即3次握手4次挥手),使得网络的利用率非常低

2. 队头阻塞
http1.0规定在前一个请求响应到达之后下一个请求才能发送,如果前一个阻塞,后面的请求也给阻塞的

http1.1特性

为了解决http1.0的性能缺陷,http1.1出现了

http1.1特性:

  • 长连接:新增Connection字段,可以设置keep-alive值保持连接不断开
  • 管道化:基于上面长连接的基础,管道化可以不等第一个请求响应继续发送后面的请求,但响应的顺序还是按照请求的顺序返回
  • 缓存处理:新增字段cache-control
  • 断点传输

长连接

http1.1默认保持长连接,数据传输完成保持tcp连接不断开,继续用这个通道传输数据

管道化

基于长连接的基础,我们先看没有管道化请求响应:

tcp没有断开,用的同一个通道

请求1 > 响应1 --> 请求2 > 响应2 --> 请求3 > 响应3

管道化的请求响应:

请求1 --> 请求2 --> 请求3 > 响应1 --> 响应2 --> 响应3

即使服务器先准备好响应2,也是按照请求顺序先返回响应1

虽然管道化,可以一次发送多个请求,但是响应仍是顺序返回,仍然无法解决队头阻塞的问题

缓存处理

当浏览器请求资源时,先看是否有缓存的资源,如果有缓存,直接取,不会再发请求,如果没有缓存,则发送请求

通过设置字段cache-control来控制

断点传输

在上传/下载资源时,如果资源过大,将其分割为多个部分,分别上传/下载,如果遇到网络故障,可以从已经上传/下载好的地方继续请求,不用从头开始,提高效率

在 Header 里两个参数实现的,客户端发请求时对应的是 Range 服务器端响应时对应的是 Content-Range

http2.0特性

  • 二进制分帧
  • 多路复用: 在共享TCP链接的基础上同时发送请求和响应
  • 头部压缩
  • 服务器推送:服务器可以额外的向客户端推送资源,而无需客户端明确的请求

二进制分帧

将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码

多路复用

基于二进制分帧,在同一域名下所有访问都是从同一个tcp连接中走,http消息被分解为独立的帧,乱序发送,服务端根据标识符和首部将消息重新组装起来

区别

  1. http1.0 到http1.1的主要区别,就是从无连接到长连接
  2. http2.0对比1.X版本主要区别就是多路复用
查看原文

赞 4 收藏 2 评论 0

木木 发布了文章 · 2019-06-17

使用canvas一步步实现图片打码功能

使用canvas一步步实现图片打码功能

原文地址

https://github.com/MY729/front-common-funtion/blob/master/picture-code-demo/README.md

预览地址

https://my729.github.io/front-common-funtion/picture-code-demo/picture-code.html

准备工作

demo 基于 vue + elelment-ui

首先创建一个html文件, 并引入 vue 和 elelment-ui(注意还有样式文件)

<!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>Document</title>
  <!-- elelment-ui样式 -->
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
  
</body>
<!-- 引入vue -->
<script data-original="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- 引入element-ui -->
<script data-original="https://unpkg.com/element-ui/lib/index.js"></script>
</html>

接下来就可以写我们的打码功能啦

实现思路

  1. 创建canvas画布,并将要打码的图片绘制上去
  2. 监听鼠标在图片上的点击,移动、松开事件,在canvas画布上绘制要打码的区域
  3. 处理绘制的打码区域
  4. 保存打码后的图片

将要打码的图片绘制到canvas画布上

// 初始化 绘制图片
toCode (currentImg) {
  this.$nextTick(() => {
    // 获取将要绘制的canvas的父元素节点
    let parentId = document.getElementById('parentId')
    // 初始化图片
    let drawImg = new Image()
    drawImg.setAttribute('crossOrigin', 'anonymous')
    drawImg.crossOrigin = 'Anonymous'
    drawImg.src = currentImg
    // 创建canvas元素并添加到父节点中
    let addCanvas = document.createElement('canvas')
    parentId.appendChild(addCanvas)
    let canvas = parentId.lastElementChild
    canvas.id = 'imgCanvas'
    if (canvas.getContext) {
      let ctx = canvas.getContext('2d')
      // 绘制图片
      drawImg.onload = function () {
        canvas.width = 720
        canvas.height = 500
        ctx.drawImage(drawImg, 0, 0, 720, 500)
      }
    }
  })
}

点击打码按钮,绘制打码区域

思路:

  1. 鼠标点击,获取点击时的坐标,每次点击前可能会存在打过码的区域,先清除画布,重新绘制图片
  2. 鼠标移动,开始绘制打码的矩形,通过移动的坐标和上面点击的点坐标确定绘制的矩形坐标和宽高
  3. 将绘制的打码矩形,分割成一个个宽高15像素的小正方形,并给每个小正方形生产随机颜色
  4. 鼠标松开,停止绘制矩形
// 打码
dialogCode (img) {
  let parentId = document.getElementById('parentId')
  let canvas = document.getElementById('imgCanvas')
  if (canvas.getContext) {
    let ctx = canvas.getContext('2d')
    let drawImage = new Image()
    drawImage.crossOrigin = 'Anonymous'
    drawImage.src = img
    drawImage.onload = () => {
      ctx.drawImage(drawImage, 0, 0, 720, 500)
    }
    // 鼠标点击
    parentId.onmousedown = e => {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(drawImage, 0, 0, 720, 500)
      this.flag = true
      this.clickX = e.offsetX // 鼠标点击时的X
      this.clickY = e.offsetY // 鼠标点击时的Y
    }
    // 鼠标松开
    parentId.onmouseup = () => {
      this.flag = false
    }
    // 鼠标按下
    parentId.onmousemove = e => {
      if (this.flag) {
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        ctx.drawImage(drawImage, 0, 0, 720, 500)
        ctx.beginPath()
        let pixels = [] // 二维数组,每个子数组有5个值(绘制矩形左上角的X坐标、y坐标,矩形的宽、高,生成的4位随机数用于颜色值)
        for (let x = 0; x < (e.offsetX - this.clickX) / 15; x++) {
          for (let y = 0; y < (e.offsetY - this.clickY) / 15; y++) {
            pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
          }
          for (let y = 0; y > (e.offsetY - this.clickY) / 15; y--) {
            pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
          }
        }
        for (let x = 0; x > (e.offsetX - this.clickX) / 15; x--) {
          for (let y = 0; y > (e.offsetY - this.clickY) / 15; y--) {
            pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
          }
          for (let y = 0; y < (e.offsetY - this.clickY) / 15; y++) {
            pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
          }
        }
        // 遍历数组绘制小正方形块
        for (let i = 0; i < pixels.length; i++) {
          ctx.fillStyle = '#bf' + pixels[i][4]
          ctx.fillRect(pixels[i][0], pixels[i][1], pixels[i][2], pixels[i][3])
        }
        ctx.fill()
        ctx.closePath()
      }
    }
  }
}

保存

// 保存
dialogUpload () {
  let canvas = document.getElementById('imgCanvas')
  let tempImg = canvas.toDataURL('image/png')
  let imgURL = document.getElementById('imgURL')
  imgURL.crossOrigin = 'Anonymous'
  imgURL.src = tempImg
}

源码

复制到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>使用canvas一步步实现图片打码功能</title>
  <!-- elelment-ui样式 -->
  <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
  <style type="text/css">
    .rc-code__buttons {
      margin: 20px;
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="rc-code__buttons">
      <h1>vue项目中使用canvas一步步实现图片打码功能</h1>
      <el-button type="primary" @click="dialogCode(data.img_url)">打码</el-button>
      <el-button type="success" @click="dialogUpload()">保存</el-button>
    </div>
    <el-row>
      <el-col :span="12"><h3>点击打码按钮,在图片上绘制打码区域; 点击保存,生成打码后的图片</h3></el-col>
      <el-col :span="12"><h3>保存后的图片</h3></el-col>
      <el-col :span="12"><div id="parentId"></div></el-col>
      <el-col :span="12"><img id="imgURL"/></el-col>
    </el-row>
  </div>
</body>
<!-- 引入vue -->
<script data-original="https://cdn.jsdelivr.net/npm/vue"></script>
<!-- 引入element-ui -->
<script data-original="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
  el: '#app',
  data () {
    return {
      data: {
        img_url: 'https://avatars0.githubusercontent.com/u/26196557?s=460&v=4'
      },
      flag: false, // 是否绘制矩形
      clickX: '', // 开始绘制矩形时,鼠标点击时的x坐标
      clickY: '' // 开始绘制矩形时,鼠标点击时的y坐标
    }
  },
  mounted() {
    this.toCode(this.data.img_url)
  },
  methods: {
    // 初始化 绘制图片
    toCode (currentImg) {
      this.$nextTick(() => {
        let parentId = document.getElementById('parentId')
        let drawImg = new Image()
        drawImg.setAttribute('crossOrigin', 'anonymous')
        drawImg.crossOrigin = 'Anonymous'
        drawImg.src = currentImg
        let addCanvas = document.createElement('canvas')
        parentId.appendChild(addCanvas)
        let canvas = parentId.lastElementChild
        canvas.id = 'imgCanvas'
        if (canvas.getContext) {
          let ctx = canvas.getContext('2d')
          drawImg.onload = function () {
            canvas.width = 720
            canvas.height = 500
            ctx.drawImage(drawImg, 0, 0, 720, 500)
          }
        }
      })
    },
    // 打码
    dialogCode (img) {
      let parentId = document.getElementById('parentId')
      let canvas = document.getElementById('imgCanvas')
      if (canvas.getContext) {
        let ctx = canvas.getContext('2d')
        let drawImage = new Image()
        drawImage.crossOrigin = 'Anonymous'
        drawImage.src = img
        drawImage.onload = () => {
          ctx.drawImage(drawImage, 0, 0, 720, 500)
        }
        parentId.onmousedown = e => {
          ctx.clearRect(0, 0, canvas.width, canvas.height)
          ctx.drawImage(drawImage, 0, 0, 720, 500)
          this.flag = true
          this.clickX = e.offsetX // 鼠标点击时的X
          this.clickY = e.offsetY // 鼠标点击时的Y
        }
        parentId.onmouseup = () => {
          this.flag = false
        }
        parentId.onmousemove = e => {
          if (this.flag) {
            ctx.clearRect(0, 0, canvas.width, canvas.height)
            ctx.drawImage(drawImage, 0, 0, 720, 500)
            ctx.beginPath()
            let pixels = [] // 二维数组,每个子数组有5个值(绘制矩形左上角的X坐标、y坐标,矩形的宽、高,生成的4位随机数用于颜色值)
            for (let x = 0; x < (e.offsetX - this.clickX) / 15; x++) {
              for (let y = 0; y < (e.offsetY - this.clickY) / 15; y++) {
                pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
              }
              for (let y = 0; y > (e.offsetY - this.clickY) / 15; y--) {
                pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
              }
            }
            for (let x = 0; x > (e.offsetX - this.clickX) / 15; x--) {
              for (let y = 0; y > (e.offsetY - this.clickY) / 15; y--) {
                pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
              }
              for (let y = 0; y < (e.offsetY - this.clickY) / 15; y++) {
                pixels.push([(x * 15 + this.clickX), (y * 15 + this.clickY), 15, 15, Math.floor(Math.random() * 9999)])
              }
            }
            for (let i = 0; i < pixels.length; i++) {
              ctx.fillStyle = '#bf' + pixels[i][4]
              ctx.fillRect(pixels[i][0], pixels[i][1], pixels[i][2], pixels[i][3])
            }
            ctx.fill()
            ctx.closePath()
          }
        }
      }
    },
    // 保存
    dialogUpload () {
      let canvas = document.getElementById('imgCanvas')
      let tempImg = canvas.toDataURL('image/png')
      let imgURL = document.getElementById('imgURL')
      imgURL.crossOrigin = 'Anonymous'
      imgURL.src = tempImg
    }
  }
})
</script>
</html>
查看原文

赞 0 收藏 0 评论 0

木木 发布了文章 · 2019-06-06

vue项目中实现图片裁剪功能

原文地址

https://github.com/MY729/pict...

在vue项目中实现图片裁剪功能

演示地址

https://my729.github.io/picture-crop-demo/dist/#/

前言

使用 cropperjs插件 和 原生canvas 两种方式实现图片裁剪功能

使用cropperjs插件

安装cropperjs

yarn install cropperjs

初始化一个canvas元素,并在上面绘制图片

<canvas :id="data.src" ref="canvas"></canvas>
// 在canvas上绘制图片
drawImg () {
  this.$nextTick(() => {
    // 获取canvas节点
    let canvas = document.getElementById(this.data.src)
    if (canvas) {
      // 设置canvas的宽为canvas的父元素宽度,宽高比3:2
      let parentEle = canvas.parentElement
      canvas.width = parentEle.offsetWidth
      canvas.height = 2 * parentEle.offsetWidth / 3
      let ctx = canvas.getContext('2d')
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      let img = new Image()
      img.crossOrigin = 'Anonymous'
      img.src = this.data.src
      img.onload = function () {
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      }
    }
  })
}
如果遇到canvas跨域绘制图片报错,设置图片img.crossOrigin = 'Anonymous',并且服务器响应头设置Access-Control-Allow-Origin:*

创建cropperjs

// 引入
import Cropper from 'cropperjs'

// 显示裁剪框
initCropper () {
  let cropper = new Cropper(this.$refs.canvas, {
    checkCrossOrigin: true,
    viewMode: 2,
    aspectRatio: 3 / 2
  })
}
更多方法和属性,参考官网: https://github.com/fengyuanchen/cropperjs

具体实现,可以查看源码的cropper.vuecropper.one.vue组件:

cropper.vue组件:https://github.com/MY729/pict...
cropper.one.vue组件:https://github.com/MY729/pict...

使用canvas实现图片裁剪

支持鼠标绘制裁剪框,并移动裁剪框

思路:

  • 在canvas上绘制图片为背景
  • 监听鼠标点击、移动、松开事件
canvas的isPointInPath()方法:
如果给定的点的坐标位于路径之内的话(包括路径的边),否则返回 false

具体实现可查看源码cropper.canvas.vue组件: https://github.com/MY729/pict...

cropImg () {
  let canvas = document.getElementById(this.data.img_url)
  let ctx = canvas.getContext('2d')
  let img = new Image()
  img.onload = function () {
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
  }
  img.src = this.data.src
  let drag = false // 是否拖动矩形
  let flag = false // 是否绘制矩形
  let rectWidth = 0 // 绘制矩形的宽
  let rectHeight = 0 // 绘制矩形的高
  let clickX = 0 // 矩形开始绘制X坐标
  let clickY = 0 // 矩形开始绘制Y坐标
  let dragX = 0 // 当要拖动矩形点击时X坐标
  let dragY = 0 // 当要拖动矩形点击时Y坐标
  let newRectX = 0 // 拖动变化后矩形开始绘制的X坐标
  let newRectY = 0 // 拖动变化后矩形开始绘制的Y坐标
  // 鼠标按下
  canvas.onmousedown = e => {
    // 每次点击前如果有绘制好的矩形框,通过路径绘制出来,用于下面的判断
    ctx.beginPath()
    ctx.setLineDash([6, 6])
    ctx.moveTo(newRectX, newRectY)
    ctx.lineTo(newRectX + rectWidth, newRectY)
    ctx.lineTo(newRectX + rectWidth, newRectY + rectHeight)
    ctx.lineTo(newRectX, newRectY + rectHeight)
    ctx.lineTo(newRectX, newRectY)
    ctx.strokeStyle = 'green'
    ctx.stroke()
    // 每次点击,通过判断鼠标点击的点在矩形框内还是外,来决定重新绘制还是移动矩形框
    if (ctx.isPointInPath(e.offsetX, e.offsetY)) {
      drag = true
      dragX = e.offsetX
      dragY = e.offsetY
      clickX = newRectX
      clickY = newRectY
    } else {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      flag = true
      clickX = e.offsetX
      clickY = e.offsetY
      newRectX = e.offsetX
      newRectY = e.offsetY
    }
  }
  // 鼠标抬起
  canvas.onmouseup = () => {
    if (flag) {
      flag = false
      this.sureCrop(clickX, clickY, rectWidth, rectHeight)
    }
    if (drag) {
      drag = false
      this.sureCrop(newRectX, newRectY, rectWidth, rectHeight)
    }
  }
  // 鼠标移动
  canvas.onmousemove = (e) => {
    if (flag) {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      rectWidth = e.offsetX - clickX
      rectHeight = e.offsetY - clickY

      ctx.beginPath()
      ctx.strokeStyle = '#FF0000'
      ctx.strokeRect(clickX, clickY, rectWidth, rectHeight)
      ctx.closePath()
    }
    if (drag) {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
      ctx.beginPath()
      newRectX = clickX + e.offsetX - dragX
      newRectY = clickY + e.offsetY - dragY
      ctx.strokeStyle = 'yellow'
      ctx.strokeRect(newRectX, newRectY, rectWidth, rectHeight)
      ctx.closePath()
    }
  }
},
// 拿到裁剪后的参数,可自行处理图片
sureCrop (x, y, width, height) {
  let canvas = document.getElementById(this.data.img_url + 'after')
  // 设置canvas的宽为canvas的父元素宽度,宽高比3:2
  let parentEle = canvas.parentElement
  canvas.width = parentEle.offsetWidth
  canvas.height = 2 * parentEle.offsetWidth / 3
  let ctx = canvas.getContext('2d')
  let img = new Image()
  img.src = this.data.src
  img.onload = function () {
    ctx.beginPath()
    ctx.moveTo(x, y)
    ctx.lineTo(x + width, y)
    ctx.lineTo(x + width, y + height)
    ctx.lineTo(x, y + height)
    ctx.clip()
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
  }
  ctx.stroke()
}

源码地址

https://github.com/MY729/pict...

可以直接clone项目,本地运行查看代码和效果

查看原文

赞 2 收藏 1 评论 0

木木 赞了文章 · 2019-05-24

二叉树遍历

前言

本篇文章是在二叉排序树的基础上进行遍历、查找、与删除结点。

那么首先来看一下什么是二叉排序树?

二叉排序树

定义

二叉排序树,又称二叉查找树、二叉搜索树。

  • 若左子树不为空,左子树上所有结点均小于它的根结点的值;
  • 若右子树不为空,右子树上所有结点均大于它的根结点的值;
  • 左右子树也分别为二叉排序树。
插入算法

我们知道了什么是二叉排序树,现在来看下它的具体算法实现。

// 构建二叉树
function BinaryTree() {
    // 定义结点
    let Node = function(key) {
        this.key = key
        this.left = left
        this.right = right
    }
    
    // 定义根结点
    let root = null
    
    // 获得整棵树
    this.getRoot = function() {
        return this.root
    }
    // 定义插入结点算法
    let insertNode = function(node, newNode) {
        // 比较要插入结点与当前结点值的大小,若大于当前结点,插入左子树,反之,插入右子树
        if(newNode.key < node.key) {
            if(node.left === null) {
                node.left = newNode
            } else {
                insertNode(node.left, newNode)
            }
        } else {
            if(node.right === null) {
                node.right = newNode
            } else {
                insertNode(node.right, newNode)
            }
        }
    }
    
    // 定义二叉排序树插入算法
    this.insert = function(key) {
        let newNode = new Node(key)
        if(root === null) {
            root = newNode
        } else {
            insertNode(root, newNode)
        }
    }
}

let nodes = [8,3,30,1,6,14,4,7,13]
// 创建树实例
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
console.log("创建的二叉树是:", tree.getRoot())

至此,一棵二叉排序树就构造完啦。接下来我们根据构造的这颗二叉树进行相应遍历、查找与删除操作。

遍历二叉树

二叉树的遍历分为深度优先遍历广度优先遍历

深度优先遍历

深度优先遍历(Depth First Search)是指沿着树的深度进行遍历树的结点。其中深度优先遍历又分为三种:前序遍历、中序遍历、后序遍历。

这里前序、中序、后序是根据根结点的顺序命名的。

1、前序遍历

定义

前序遍历也叫做先根遍历、先序遍历、前序周游,记做 根左右

  • 先访问根结点;
  • 前序遍历左子树;
  • 前序遍历右子树。

前序遍历的作用是可以复制已有的二叉树,且比重新构造的二叉树的效率高。

下面我们来看它的算法实现。分为递归与非递归两种。

方法一 递归实现
function BinaryTree() {
    // 这里省略了二叉排序树的构建方法
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            callback(node.key) // 先访问当前根结点
            preOrderTraverseNode(node.left, callback) // 访问左子树
            preOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
       preOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) // 8 3 1 6  4 7 10 14 13
方法二 非递归实现
function BinaryTree() {
    // ...
    
    // 定义前序遍历算法
    let preOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node !== null) {
            stack.push(node)
        }
        while(stack.length) {
            let temp = stack.pop()
            callback(temp.key)
            // 这里先放右边再放左边是因为取出来的顺序相反
            if(temp.right !== null) {
                stack.push(temp.right)
            }
            if(temp.left !== null) {
                stack.push(temp.left)
            }
        }
    }
    
    // 定义前序遍历方法
    this.preOrderTraverse = function(callback) {
        preOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.preOrderTraverse(callback) //8 3 1 6  4 7 10 14 13

2、中序遍历

定义

中序遍历也叫做中根遍历、中序周游,记做 左根右

  • 若左子树不为空,则先中序遍历左子树;
  • 访问根结点;
  • 若右子树不为空,则中序遍历右子树。
中序遍历二叉排序树,得到的数组是有序的且是升序的。

下面我们来看中序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
function BinaryTree() {
    // 省略二叉排序树的创建
    
    // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            inOrderTraverseNode(node.left, callback) // 先访问左子树
            callback(node.key) // 再访问当前根结点
            inOrderTraverseNode(node.right, callback) // 访问右子树
        }
    }
    
    // 定义中序遍历方法
    this.inOrderTraverse = function(callback) {
       inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14
方法二 非递归实现

借助于栈,先将左子树全部放进栈中,之后输出,最后处理右子树。

function BinaryTree() {
    // 省略二叉排序树的构建方法
    
     // 定义中序遍历算法
    let inOrderTraverseNode = function(node, callback) {
        let stack = []
        while(true) {
            // 将当前结点的左子树推入栈
            while(node !== null) {
                stack.push(node)
                node = node.left
            }

            // 定义终止条件
            if(stack.length === 0) {
                break
            }
            let temp = stack.pop()
            callback(temp.key)
            node = temp.right
        }
    }
    this.inOrderTraverse = function(callback) {
        inOrderTraverseNode(root, callback) 
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key) // 构造二叉树
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}
tree.inOrderTraverse(callback) // 1 3 4 6 7 8 10 13 14

3、后序遍历

定义

后序遍历也叫做后根遍历、后序周游,记做 左右根

  • 若左子树不为空,后序遍历左子树;
  • 若右子树不为空,后序遍历右子树;
  • 访问根结点。
后序遍历的作用用于文件系统路径中,或将正常表达式变成逆波兰表达式。

下面我们来看后序遍历算法的实现。分为递归和非递归两种。

方法一 递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        if(node !== null) {
            postOrderTraverseNode(node.left, callback) // 遍历左子树
            postOrderTraverseNode(node.right, callback) // 再遍历右子树
            callback(node.key) // 访问根结点
        }
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8
方法二 非递归实现
// 先构造一棵二叉树
function BinaryTree() {
    // 省略二叉排序树的构建方法

    // 定义后序遍历算法
    let postOrderTraverseNode = function(node, callback) {
        let stack = []
        let res = []
        stack.push(node)
        while(stack.length) {
            let temp = stack.pop()
            res.push(temp.key)
            if(temp.left !== null) {
                stack.push(temp.left)
            }
            if(temp.right !== null) {
                stack.push(temp.right)
            }
        }
        callback(res.reverse())
    }
    
    // 定义后序遍历方法
    this.postOrderTraverse = function(callback) {
        postOrderTraverseNode(root, callback)
    }
}
let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key){
    tree.insert(key)
})

// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.postOrderTraverse(callback) // 1 4 7 6 3 13 14 10 8

广度优先遍历

广度优先遍历(Breadth First Search),又叫做宽度优先遍历、层次遍历,是指从根结点沿着树的宽度搜索遍历。

下面来看它的实现原理

方法一 递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(root) {
        let stack = [root] // 先将要遍历的树压入栈

        return function bfs(callback) {
            let node = stack.shift()
            if(node) {
                callback(node.key)
                if(node.left) stack.push(node.left);
                if(node.right) stack.push(node.right);
                bfs(callback)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root)(callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法二 非递归

使用栈实现,未访问的元素入栈,访问后则出栈,并将其leve左右元素入栈,直到叶子元素结束。

function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            // 每一层的结点数
            let level = stack.length
            // 遍历每一层元素
            for(let i = 0; i < level; i++) {
                // 当前访问的结点出栈
                let temp = stack.shift()
                
                // 出栈结点的孩子入栈
                temp.left ? queue.push(temp.left) : ''
                temp.right ? queue.push(temp.right) : ''
                callback(temp.key)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13
方法三 非递归
function BinaryTree() {
    // 省略二叉排序树的构建
    
    let wideOrderTraverseNode = function(node, callback) {
        let stack = []
        if(node === null) {
            return []
        }
        stack.push(node)
        while(stack.length) {
            let temp = stack.shift()
            callback(temp.key)
            if(temp.left) {
                stack.push(temp.left)
            }
            if(temp.right) {
                stack.push(temp.right)
            }
        }
    }
    
     this.wideOrderTraverse = function(callback) {
        wideOrderTraverseNode(root, callback)
    }
}

let nodes = [8,3,10,1,6,14,4,7,13]
let tree = new BinaryTree()
nodes.forEach(function(key) {
    tree.insert(key)
})
// 定义回调函数
let callback = function(key) {
    console.log(key)
}

tree.wideOrderTraverse(callback) // 8,3,10,1,6,14,4,7,13

鉴于篇幅过长,二叉树结点的查找和删除会在下一篇文章内~

查看原文

赞 3 收藏 0 评论 0

木木 关注了专栏 · 2019-05-17

前端工匠公众号

我是浪里行舟,每周分享至少两篇前端文章,致力于打造一系列能够帮助初中级工程师提高的优质文章

关注 7043

木木 收藏了文章 · 2019-03-12

入门 Webpack,看这篇就够了

2018年8月25日更新,目前 webpack 已经更新值 4.17.1 ,本文所用到的各种库或多或少有些过时,跟着代码操作下来可能会遇到各种问题,不过 webpack 的主体思想没变,所以还是希望本文对新学 webpack 的你,有所帮助。此外用基于 webpack 4.17.1 写了一个简单的demo,如果遇到啥问题,可以参考,之后应该会逐步来完善这个demo,如果有啥通用的想实现的功能,也可以在里面提 issue。

2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本文涉及到的所有代码的示例,如果你在学习过程中出错了,可点击此处参考(有些过时了,不要再 fork 了)

写在前面的话

阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前的我一样,对很多选项存在着疑惑,那花一段时间慢慢阅读本文,你的疑惑一定一个一个都会消失;如果你以前没怎么接触过Webpack,而你又你对webpack感兴趣,那么动手跟着本文中那个贯穿始终的例子写一次,写完以后你会发现你已明明白白的走进了Webpack的大门。
// 一个常见的`webpack`配置文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地服务器所加载的页面所在的目录
            historyApiFallback: true, //不跳转
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};

什么是WebPack,为什么要使用它?

为什要使用WebPack

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

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

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

什么是Webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Grunt和Gulp的工作流程

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

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

开始使用Webpack

初步了解了Webpack工作方式后,我们一步步的开始学习使用Webpack。

安装

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack sample project),在终端中转到该文件夹后执行下述指令就可以完成安装。

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

正式使用Webpack前的准备

  1. 在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件
npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。

  1. package.json文件已经就绪,我们在本项目中安装Webpack作为依赖包
// 安装Webpack
npm install --save-dev webpack
  1. 回到之前的空文件夹,并在里面创建两个文件夹,app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:
  • index.html --放在public文件夹中;
  • Greeter.js-- 放在app文件夹中;
  • main.js-- 放在app文件夹中;

此时项目结构如下图所示
项目结构

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script data-original="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

正式使用Webpack

webpack可以在终端中使用,在基本的使用方法如下:

# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}

指定入口文件后,webpack将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules中的地址,继续上面的例子,在终端中输入如下命令

# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js

结果如下

使用命令行打包

可以看出webpack同时编译了main.jsGreeter,js,现在打开index.html,可以看到如下结果
htmlResult1

有没有很激动,已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

通过配置文件来使用Webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}
:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:

配合配置文件进行打包

又学会了一种使用Webpack的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack(非全局安装需使用node_modules/.bin/webpack)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build,我们在命令行中输入npm start试试,输出结果如下:

使用npm start 打包代码

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项配置结果
source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}
cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

npm install --save-dev webpack-dev-server

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

devserver的配置选项功能描述
contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
port设置默认监听端口,如果省略,默认为”8080“
inline设置为true,当源文件改变时会自动刷新页面
historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open"
  },

在终端中输入npm run server即可在本地的8080端口查看结果

开启本地服务器

Loaders

鼎鼎大名的Loaders登场了!

Loaderswebpack提供的最激动人心的功能之一了。通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

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

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

不过在配置loader之前,我们把Greeter.js里的问候消息放在一个单独的JSON文件里,并通过合适的配置使Greeter.js可以读取该JSON文件的值,各文件修改后的代码如下:

在app文件夹中创建带有问候信息的JSON文件(命名为config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
由于webpack3.*/webpack2.*已经内置可处理JSON文件,这里我们无需再添加webpack1.*需要的json-loader。在看如何具体使用loader之前我们先看看Babel是什么?

Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

localhost:8080

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那那些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
请注意这里对同一个文件引入多个loader的方法。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));
通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

应用了css module后的样式

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')
    ],
};

通过这个插件,打包后的JS文件显示如下

版权所有,翻版必究

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

安装

npm install --save-dev html-webpack-plugin

这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:

  1. 移除public文件夹,利用此插件,index.html文件会自动生成,此外CSS已经通过前面的操作打包到JS中了。
  2. 在app目录下,创建一个index.tmpl.html文件模板,这个模板包含title等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html中的模板源代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3.更新webpack的配置文件,方法同上,新建一个build文件夹用来存放最终的输出文件

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        })
    ],
};

再次执行npm start你会发现,build文件夹下面生成了bundle.jsindex.html

build文件夹

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};
   

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}
注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".谢谢评论区简友提醒。

优化插件

webpack提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack社区,可以通过npm安装,通过以下插件可以完成产品发布阶段所需的功能

  • OccurenceOrderPlugin :为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID
  • UglifyJsPlugin:压缩JS代码;
  • ExtractTextPlugin:分离CSS和JS文件

我们继续用例子来看看如何添加它们,OccurenceOrder 和 UglifyJS plugins 都是内置插件,你需要做的只是安装其它非内置插件

npm install --save-dev extract-text-webpack-plugin

在配置文件的plugins后引用它们

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'none',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ],
};

此时执行npm run build可以看见代码是被压缩后的

压缩后的代码

缓存

缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)

webpack可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash])到输出文件名前

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
..
    output: {
        path: __dirname + "/build",
        filename: "bundle-[hash].js"
    },
   ...
};

现在用户会有合理的缓存了。

带hash值的js名

去除build文件中的残余文件

添加了hash之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多,因此这里介绍另外一个很好用的插件clean-webpack-plugin

安装
cnpm install clean-webpack-plugin --save-dev

使用

引入clean-webpack-plugin插件后在配置文件的plugins中做相应配置即可:

const CleanWebpackPlugin = require("clean-webpack-plugin");
  plugins: [
    ...// 这里是之前配置的其它各种插件
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
  ]

关于clean-webpack-plugin的详细使用可参考这里

总结

其实这是一年前的文章了,趁周末重新运行和修改了一下,现在所有的代码都可以正常运行,所用webpack基于最新的webpack3.5.3。希望依旧能对你有帮助。

这是一篇好长的文章,谢谢你的耐心,能仔细看到了这里,大概半个月前我第一次自己一步步配置项目所需的Webpack后就一直想写一篇笔记做总结,几次动笔都不能让自己满意,总觉得写不清楚。其实关于Webpack本文讲述得仍不完全,不过相信你看完后已经进入Webpack的大门,能够更好的探索其它的关于Webpack的知识了。

欢迎大家在文后发表自己的观点讨论。

更新说明

2017-12-11更新,修改css module部分代码及示例图片,css module真的非常好用,希望大家都能用上。

2017年9月18日更新,添加了一个使用webpack配置多页应用的demo,可以点击此处查看

2017年8月13日更新,本文依据webpack3.5.3将文章涉及代码完全重写,所有代码都在Mac上正常运行过。希望依旧对你学习webpack有帮助。

2017年8月16号更新:
最近在Gitchat上将发起了一场关于webpack的分享,目的在于一起花最短的时间理解和学会webpack,感兴趣的童鞋可以微信扫描注册哈。
webpack从入门到工程实践

查看原文

木木 收藏了文章 · 2019-03-03

你们真的了解JS的继承嘛?

前言

我们学JAVA的时候说到继承就是一个extends ClassName的事情,但是在JS的世界里继承和我们在JAVA所认识的继承实现方法又有一些不同,你们真的了解JS的继承吗?就当你们很了解了,毕竟是基础知识,我就简单说说

原型链继承

简言之就是把被继承的对象赋值给继承者的原型对象

function Super() {
    this.name = 'mirok';
}
Super.prototype.showName = function () {
    console.log(this.name);
}
function Sub() {
    this.name = 'july';
}
Sub.prototype = new Super();
const obj = new Sub();
obj.showName(); //输出july

原型实现继承虽然可以,但是也有相应的弊端,例如new Super()构建多个实例,继承里面的方法被其中一个实例重写,就会影响其他实例,也就是说原型里的是所有实例所共享的,这是我们不愿看到的,因此就有以下的方法。

借用构造函数

简言之就是在继承者的构造函数中去调用被继承者的构造函数(即使用apply()/call()实现)

function Super() {
    this.supername = 'mirok';
}
function Sub() {
    Super.call(this)
    this.name = 'july';
}
Sub.prototype = new Super();
const obj = new Sub();
obj.name; //july
obj.supername; //mirok

这种方式实现的继承相对于之前的来说不仅解决了之前的问题还能向被继承者传参数,但是也有一定的弊端,即容易覆盖本身的属性,解决方法就是在调用被继承者的构造函数再对自己添加属性,也就是说上面的Super.call要在this.name定义属性之前。另一个弊端就是继承的是无法共享的

组合继承

这个就是组合前面的原型链继承和借用构造函数继承两者之长,也就是说既能在继承后的实例都有一份属性又能共用

function Super() {
    this.name = 'mirok';
}
Super.prototype.showName = function () {
    console.log(this.name);
}
function Sub1() {
    Super.call(this);
    this.name = 'july';
}
function Sub2() {
    Super.call(this);
    this.name = 'deny';
}
Sub1.prototype = new Super();
Sub2.prototype = new Super();
const obj1 = new Sub1();
const obj2 = new Sub2();
obj1.showName(); // july
obj2.showName(); // deny

原型式继承

这个比较特殊一点,就是在一个函数里去做原型链继承的事情

function object(obj) {
    function fun() {};
    fun.prototype = obj;
    return new fun();
}

ES5规范了这类写法,就是Object.create(),但是弊端和第一种类似,因为不是我们理想的继承这里就不详细介绍

寄生式继承

这个也比较特殊,就是把继承的事情放在一个函数里去做,再把对象返回

function object(obj) {
    function fun() {};
    fun.prototype = obj;
    return new fun();
}
function factory() {
    const person = {name:'mirok', age: 22};
    const obj = object(person);
    obj.show = function() {console.log(this.name)}
    return obj;
}
factory().show(); //'mirok'

至于弊端可见而知,不能实现共享

寄生组合式继承

组合继承有个弊端就是会调用两次被继承者的构造函数,解决方法就是使用寄生组合式继承。这又是什么呢?这个相对之前的比较复杂,但是高效的一点是只调用一次被继承者构造函数,原理就是通过寄生方式创建一个被继承者的副本,副本和被继承者共用一个prototype,这样就解决了之前的问题

function object(obj) {
    function fun() {};
    fun.prototype = obj;
    return new fun();
}
function factory(Sub, Super) {
    var proto = object(Super.prototype); //返回Super的一个副本
    proto.constructer = Sub; //设置constructor指向, 因为新副本的原型对象被重写
    Sub.prototype = proto; //副本作为sub的原型对象
}
function Super () {
    this.name = 'july';
}
Super.prototype.show = function () {
    console.log(this.name);
}
function Sub1 () {
    Super.call(this);
    this.name = 'mirok'
}
function Sub2 () {
    Super.call(this);
    this.name = 'deny'
}
factory(Sub1, Super);
factory(Sub2, Super);
var obj1 = new Sub1();
var obj2 = new Sub2();
obj1.show(); // mirok
obj2.show(); // deny
查看原文

木木 收藏了文章 · 2019-02-25

读书笔记:《图解HTTP》第一章 网络基础

原文地址
博客积累地址

HTTP的诞生及发展

HTTP是为了知识共享而诞生,最初的设计理念是让远隔两地的研究者们共享知识,借助多文档之间相互关联形成的超文本,连成可相互参阅的 WWW(World Wide Web,万维网)

3项WWW 构建技术

  • 作为页面的文本标记语言 HTML(超文本标记语言)
  • 作为文档传递协议的 HTTP
  • 指定文档所在地址的 URL(统一资源定位符)

现在我们都知道在网页浏览器的地址栏中输入URL可以呈现Web页面

当然Web页面不能凭空显示出来,它是根据浏览器输入的URl,浏览器从web服务器获取文件资源等信息,从而显示出web页面

像这种通过发送请求获取服务资源的web浏览器器等,都称为客户端(client)

An image

从客户端到服务器端获取信息的一系列运作流程,需要双方规定一些规则,以此来告知双方自己需要什么消息,我们可以将这些规则理解为HTTP协议,即HTTP协议就是双方通信的规范。

TCP/IP协议

计算机与网络设备要相互通信,双方就必须基于相同的方法。

早期的计算机网络,都是由各厂商自己规定一套通信协议,互不兼容,带来不同协议之间无法通信的弊端

为了把世界所有不同类型的计算机连接起来,规定了一套全球通用的协议

因为互联网协议包括上百种协议标准,但是最重要的两个协议是TCP和IP协议,所以大家把互联网的协议简称TCP和IP协议

HTTP 属于TCP/IP协议内部的一个子集、一个协议标准

TCP/IP协议重要的一个概念是分层,我们称为TCP/IP模型

分层的好处是,如果某个地方有变动,只需改变某层内部,只需把各层的之间的接口部分规划好,每个层次的内部设计可以随意改动

TCP/IP模型

分层内容
应用层向用户提供应用服务时通信的活动;FTP(文件传输协议)、DNS服务、HTTP协议等
传输层提供处于网络连接中计算机之间的数据传输;TCP(传输控制协议)、UDP(用户数据报协议)
网络层规定传输路线,传输数据包(数据包是网络传输最小的数据单位)
链路层处理连接网络的硬件部分

TCP/IP通信传输流

TCP/IP协议会通过分层顺序与对方进行通信

  1. 客户端在应用层发出HTTP请求
  2. 传输层对从应用层收到的数据(HTTP请求报文)进行分割,在各个报文上打上标记序号、端口号后转发给网络层
  3. 网络层,增加作为通信目的地MAC地址转发给链路层
  4. 服务器在链路层收到消息,按层往上发送,直到应用层,最终收到客户端的HTTP请求

发送端在层与层之间传输数据时,每经过一层时必定会被打上一个该 层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层 时会把对应的首部消去

An image

IP、TCP 和 DNS

IP、TCP 和 DNS 是与HTTP密不可分的三个协议,接下来一一说明

负责传输的IP协议

IP协议位于网络层,它的作用是把各种数据包传送给对方

而要保证确实传送到对方那里,需要满足各类条件;其中两个重要的条件是IP地址和MAC地址

IP地址指明节点被分配的地址,MAC地址指网卡所属的固定地址
IP地址可以和MAC地址配对,IP地址可变换,MAC地址基本不会更改

确保可靠性的TCP协议

TCP协议位于传输层,提供可靠的字节流服务

字节流服务
字节流服务是指,为了方便传输,将大块数据分割成报文段为单位的数据包进行管理,而TCP协议能够确认数据最终是否发送到对方

为了保证通信的可靠性,TCP协议采用了三次握手策略建立连接,TCP协议发送数据包后,会向对方确认是否发送成功

TCP三次握手

我们将发送一次消息称为一次握手

TCP三次握手使用了TCP的标志:SYN 和 ACK

  1. 发送端首先发送一个带有SYN标志的数据包给对方,确认对方是否可以收到消息
  2. 对方收到后,返回带有SYN/ACK标志的数据包表示收到消息,你可以跟我通信啦
  3. 发送端最后发送带ACK标志的数据包,会话结束

若在握手的某个阶段莫名中断,TCP协议会以相同的顺序发送相同的数据包

An image

负责域名解析的DNS服务

DNS服务和HTTP协议一样位于应用层,DNS提供域名到IP地址之间的解析服务

计算机被赋予IP地址,通过IP地址可以访问页面,与IP地址的一组数字相比,域名(字母数字的组合:比如www.baidu.com)更符合人们的记忆习惯,但是计算机擅长处理数字,为了解决这一问题,DNS 服务应运而生

An image

TCP、IP和DNS在使用HTTP协议通信过程中各自发挥的作用

An image

查看原文

木木 收藏了文章 · 2019-02-25

读书笔记:《图解HTTP》第二章 HTTP协议

原文地址
博客积累地址

HTTP协议用途

HTTP协议用于客户端和服务器端之间的通信

  • 客户端:请求访问文本或图像等资源的一端
  • 服务器端: 提供资源响应的一端

在两台计算机之间,必有一端担任客户端角色,另一端担任服务器端角色,有时角色可能会互换,HTTP协议能够明确区分哪端是客户端,哪端是服务器端

通过请求和响应的交换达成通信

由客户端发出请求报文,服务器端回复响应报文

请求报文

请求报文的构成

  • 请求方法
  • 请求URI(请求访问的资源对象)
  • 请求首部字段【可选】
  • 内容实体【可选】

请求报文的构成

响应报文

响应报文的构成

  • 协议版本
  • 状态码
  • 解释状态码的原因短语
  • 响应首部字段【可选】
  • 内容实体【可选】

响应报文的构成

注意:请求和响应报文的内容实体前都空一行

HTTP协议是无状态协议

HTTP协议自身不具备保存之前发送过的请求和响应的功能

为了解决一些需要保存状态的业务场景(比如保存登录状态),HTTP/1.1引入了Cookie技术

请求URI定位资源

HTTP协议使用URI让客户端定位到资源

客户端发送请求时,需要将URI信息包含在请求报文内

https://www.google.cn/index.html为例,指定URI的方式有以下两种:

  • 完整的URI
GET https://www.google.cn/index.html HTTP/1/1
  • 在首部字段Host中写明网络域名或IP地址
GET /index.html HTTP/1/1
Host: www.google.cn

HTTP请求方法

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

方法描述
GET【获取资源】请求指定的页面信息,并返回实体主体。
HEAD【获得报文首部】类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
POST【传输实体主体】向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
PUT【传输文件】从客户端向服务器传送的数据取代指定的文档的内容。
DELETE【删除文件】请求服务器删除指定的页面。
CONNECT【要求用隧道协议连接代理】HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
OPTIONS【询问支持的方法】允许客户端查看服务器的性能。
TRACE【追踪路径】回显服务器收到的请求,主要用于测试或诊断。

持久连接和管线化

持久连接

当建立一个TCP连接后,可以发送多个请求/响应。除非任意一端提出断开连接,否则一直保持TCP连接状态

管线化

管线化是指客户端不用等待响应可直接发送下一个请求,这样能够同时并行发送多个请求

查看原文

木木 收藏了文章 · 2019-02-25

读书笔记:《图解HTTP》第三章 HTTP报文

原文地址
博客积累地址

HTTP报文的作用

HTTP报文时是HTTP进行请求和响应时用来交换信息的,可以理解它为搬东西的包裹,来搬运交换的信息

报文流

  • HTTP报文在HTTP应用程序(客户端、服务器、代理)之间发送数据块,这些数据块以文本形式描述了报文的内容和含义。
  • 这些报文在客户端、服务器、代理之间流动,称之报文流,用流入、流出、上游和下游这些术语描述报文流动的方向

报文如何流动

报文是由客户端流入服务器的

由客户端流入源端服务器,服务器处理完之后,流回(流出)用户的Agent代理中

报文流入源端服务器示意图:
报文流入源端服务器示意图

所有报文都会向下游流动

所有报文的发送者都在接收者的上游
对于请求报文来说,客户端是发送者,对于响应报文来说,服务器端是发送者

报文向下游流动示意图:
报文向下游流动

报文的组成部分

报文语法

HTTP报文分为两类:

  • 请求报文:客户端向web服务器请求一个动作
  • 响应报文: 服务器将请求结果返回给客户端

请求和响应报文的基本报文结构相同,都是由三部分组成:

  1. 起始行:对报文进行描述
  2. 首部:一些属性
  3. 主体:数据【可选】

请求报文和响应报文只有起始行的语法不同

  • 请求报文结构
<method> <request-url> <version>
<header>

<entity-body>
  • 响应报文结构
<version> <status-code> <reason-phrase>
<header>

<entity-body>

下面是对各部分的简要说明:

字段名描述
方法(method)HTTP请求方法
请求url (request-url)请求资源的路径
版本(version)使用的HTTP版本,格式为: HTTP/<主要版本号>.<次要版本号>
状态码(status-code)数字HTTP状态码,用于描述请求过程的状态,比如成功、失败
原因短语(reason-phrase)上面数字状态码的简短文本描述
首部(header)可以有0或多个首部。格式为: <名字>:<值>,以空行结束
主体(entity-body)包含由任意数据组成的数据块,并不是所有报文都有主体

请求和响应报文示例:
请求和响应报文示例

起始行

上面我们知道HTTP报文分为请求报文和响应报文两类,而且只有起始行不同

这里我们是请求报文的起始行为请求行,响应报文的起始行为响应行

HTTP报文-首部部分

HTTP报文的首部字段向请求和响应报文中添加一些附加信息,本质上来说,它们只是一些<名字>/<值>对的列表

首部分类

HTTP规范定义了几种首部字段,应用程序也可以随意发明自己所用的首部

  • 通用首部

既可以出现在请求报文中,也可以出现在响应报文中

字段描述
Cache-Control通过指定首部字段Cache-Control的指令,就能操作缓存的工作机制。
Connection控制不在转发给代理的首部字段;管理持久连接。
Data表明创建HTTP报文的时间和日期。
Pragma只用在客户端发送的请求中,所有的中间服务器不返回缓存的资源。
Trailer事先说明报文主体后记录了哪些首部字段。同样可以用在分块传输编码时。
Transfer-Encoding规定了传输报文主体时采用的编码方式。
Upgrade用于检测HTTP协议及其他协议是否可以使用更高的版本进行通信。
Via为了追踪客户端与服务器之间的请求和响应报文的传输路径。
Warning通常会告知用户一些与缓存相关的问题的警告
  • 请求首部

提供请求相关的信息

字段描述
Accept该字段可通知服务器,用户代理能够处理的媒体类型及媒体类型的相对优先级。
Accept-Charset用来通知服务器用户代理支持的字符集及字符集的相对优先顺序,可一次性指定多种字符集。
Accept-Encoding用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序。
Accept-Language用来告知服务器用户代理嫩巩固处理的自然语言集(中文或英文等),以及自然语言集的相对优先级。
Authorization用来告知服务器,用户代理的认证信息。
Expect客户端使用首部字段Except来告知服务器,期望出现的某种指定行为。
From用来告知服务器使用用户代理的用户的电子邮件地址。
Host告知服务器,请求的资源所处的互联网主机名和端口号。Host首部字段在HTTP/1.1规范内是唯一一个必须包含在请求内的首部字段。
If-Match类似于If-xxx这样的请求首部,可以称为条件请求。
If-Modified-Since告知服务器若该字段值早于资源的更新时间,则希望能处理该请求。
If-None-Match该字段值得实体标记值与请求资源的ETag不一致时,它就告知服务器处理该请求。
If-Range它告知服务器若指定的If-Range字段值和请求资源的ETag值或时间相一致时,则作为范围请求处理。反之则返回全体资源。
If-Unmodified-Since告知服务器,指定的请求资源只有在字段值内指定的日期时间之后,未发生更新的情况下,才能处理请求。
Max-Forwards通过TRACE方法或OPTIONS方法,发送包含首部字段Max-Forwards的请求时,该字段以十进制整数形式指定可经过的服务器最大数目。当服务器接收到Max-Forwards值为0的请求时,则不再进行转发,而是直接返回响应。
Proxy-Authorization客户端会发送包含首部字段Proxy-Authorization的请求,以告知服务器认证所需要的信息。
Range告知服务器资源的指定范围。
TE告知服务器客户端能够处理响应的传输编码方式及相对优先级。
User-Agent将创建请求的浏览器用户代理名称等信息传达给服务器
  • 响应首部

提供响应相关的信息

字段描述
Accept-Ranges用来告知客户端服务器是否能够处理范围请求,以指定获取服务器端某个部分的资源。
Age告知客户端,源服务器在多久前创建了响应。单位秒。
ETag告知客户端实体标识,它是一种可将资源以字符串形式做唯一标识的方式。
Location可以将响应接收方引导至某个与请求URI位置不同的资源。
Proxy-Authenticate把由代理服务器所要求的认证信息发送给客户端。
Retry-After告知客户端应该在多久之后再次发送请求。
Server告知客户端当前服务器上安装的HTTP服务器应用程序的信息。
Vary可对缓存进行控制,源服务器回向代理服务器传达关于本地缓存使用方法的命令。
WWW-Authenticate用于HTTP访问认证
  • 实体首部

描述主体的长度和内容,或者资源自身

字段描述
Allow用于通知客户端能够支持Request-URI指定资源的所有HTTP方法。
Content-Encoding告知客户端服务器对实体的主体部分选用的内容编码方式。(gzip/compress/deflate/identity)
Content-Language告知客户端,实体主体使用的自然语言。(中文或英文等语言)
Content-Length表明了实体主体部分的大小。
Content-Location给出与报文主体返回资源对应的URI。
Content-MD5是一串由MD5算法生成的值,其目的在于检查报文主体在传输过程中是否保持完整,以及确认传输到达。
Content-Range针对范围请求,返回响应时使用的首部字段,能告知客户端作为相应返回的实体的哪个部分符合范围请求。
Content-Type说明了实体主体内对象的媒体类型,该字段用type/subtype形式赋值。
Expires会将资源失效的日期告知客户端。
Last-Modified指明资源最终修改的时间
  • 拓展首部

规范中没有定义的新首部

HTTP状态码

HTTP状态码被分为五大类:

  • 1xx: 信息性状态码,表示接收到请求并且继续处理
  • 2xx: 成功状态码,表示动作被成功接收、理解和接受
  • 3xx: 重定向状态码,为了完成指定的动作,必须接受进一步处理
  • 4xx: 客户端错误状态码,请求包含错误语法或不能正确执行
  • 5xx: 服务器错误状态码,服务器不能正确执行一个正确的请求

常用状态码

状态码短语描述描述
100Continue继续,一般在发送post请求时,已发送了http header之后服务端将返回此信息,表示确认,之后发送具体参数信息
200OK正常返回信息
201Created请求成功并且服务器创建了新的资源
202Accepted服务器已接受请求,但尚未处理
301Moved Permanently请求的网页已永久移动到新位置。
302Found临时性重定向。
303See Other临时性重定向,且总是使用 GET 请求新的 URI。
304Not Modified自从上次请求后,请求的网页未修改过。
400Bad Request服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。
401Unauthorized请求未授权。
403Forbidden禁止访问。
404Not Found找不到如何与 URI 相匹配的资源。
500Internal Server Error最常见的服务器端错误。
503Service Unavailable服务器端暂时无法处理请求(可能是过载或维护)

完整版状态码

状态码描述
100客户必须继续发出请求
101客户要求服务器根据请求转换HTTP协议版本
200表明该请求被成功地完成,所请求的资源发送回客户端
201提示知道新文件的URL
202接受和处理、但处理未完成
203返回信息不确定或不完整
204请求收到,但返回信息为空
205服务器完成了请求,用户代理必须复位当前已经浏览过的文件
206服务器已经完成了部分用户的GET请求
300请求的资源可在多处得到
301本网页被永久性转移到另一个URL
302请求的网页被转移到一个新的地址,但客户访问仍继续通过原始URL地址,重定向,新的URL会在response中的Location中返回,浏览器将会使用新的URL发出新的Request。
303建议客户访问其他URL或访问方式
304自从上次请求后,请求的网页未修改过,服务器返回此响应时,不会返回网页内容,代表上次的文档已经被缓存了,还可以继续使用
305请求的资源必须从服务器指定的地址得到
306前一版本HTTP中使用的代码,现行版本中不再使用
307申明请求的资源临时性删除
400客户端请求有语法错误,不能被服务器所理解
401请求未经授权
402保留有效ChargeTo头响应
403禁止访问,服务器收到请求,但是拒绝提供服务
404一个404错误表明可连接服务器,但服务器无法取得所请求的网页,请求资源不存在。eg:输入了错误的URL
405用户在Request-Line字段定义的方法不允许
406根据用户发送的Accept拖,请求资源不可访问
407类似401,用户必须首先在代理服务器上得到授权
408客户端没有在用户指定的饿时间内完成请求
409对当前资源状态,请求不能完成
410服务器上不再有此资源且无进一步的参考地址
411服务器拒绝用户定义的Content-Length属性请求
412一个或多个请求头字段在当前请求中错误
413请求的资源大于服务器允许的大小
414请求的资源URL长于服务器允许的长度
415请求资源不支持请求项目格式
416请求中包含Range请求头字段,在当前请求资源范围内没有range指示值,请求也不包含If-Range请求头字段
417服务器不满足请求Expect头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求长
500服务器遇到错误,无法完成请求
502网关错误
503由于超载或停机维护,服务器目前无法使用,一段时间后可能恢复正常
查看原文

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-08-30
个人主页被 348 人浏览