ireeoo

ireeoo 查看完整档案

苏州编辑  |  填写毕业院校同程网  |  前端工程师 编辑 reeoo.me 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

ireeoo 收藏了文章 · 2018-11-24

深入理解vue中的slot与slot-scope

作者/云荒杯倾

写在前面

vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。

实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。

进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示怎样显示

由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块

单个插槽 | 默认插槽 | 匿名插槽

首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。

单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。

下面通过一个例子来展示。

父组件:

<template>
    <div class="father">
        <h3>这里是父组件</h3>
        <child>
            <div class="tmpl">
              <span>菜单1</span>
              <span>菜单2</span>
              <span>菜单3</span>
              <span>菜单4</span>
              <span>菜单5</span>
              <span>菜单6</span>
            </div>
        </child>
    </div>
</template>

子组件:

<template>
    <div class="child">
        <h3>这里是子组件</h3>
        <slot></slot>
    </div>
</template>

在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。

<div class="tmpl">
  <span>菜单1</span>
  <span>菜单2</span>
  <span>菜单3</span>
  <span>菜单4</span>
  <span>菜单5</span>
  <span>菜单6</span>
</div>

最终的渲染结果如图所示:


注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。

具名插槽

匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。

父组件:

<template>
  <div class="father">
    <h3>这里是父组件</h3>
    <child>
      <div class="tmpl" slot="up">
        <span>菜单1</span>
        <span>菜单2</span>
        <span>菜单3</span>
        <span>菜单4</span>
        <span>菜单5</span>
        <span>菜单6</span>
      </div>
      <div class="tmpl" slot="down">
        <span>菜单-1</span>
        <span>菜单-2</span>
        <span>菜单-3</span>
        <span>菜单-4</span>
        <span>菜单-5</span>
        <span>菜单-6</span>
      </div>
      <div class="tmpl">
        <span>菜单->1</span>
        <span>菜单->2</span>
        <span>菜单->3</span>
        <span>菜单->4</span>
        <span>菜单->5</span>
        <span>菜单->6</span>
      </div>
    </child>
  </div>
</template>

子组件:

<template>
  <div class="child">
    // 具名插槽
    <slot name="up"></slot>
    <h3>这里是子组件</h3>
    // 具名插槽
    <slot name="down"></slot>
    // 匿名插槽
    <slot></slot>
  </div>
</template>

显示结果如图:

可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。

作用域插槽 | 带数据的插槽

最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写

匿名插槽
<slot></slot>
具名插槽
<slot name="up"></slot>

但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。

<slot name="up" :data="data"></slot>
 export default {
    data: function(){
      return {
        data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
      }
    },
}

我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。

<child>
   html模板
</child>

写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?

正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。

我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。

下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。

父组件:

<template>
  <div class="father">
    <h3>这里是父组件</h3>
    <!--第一次使用:用flex展示数据-->
    <child>
      <template slot-scope="user">
        <div class="tmpl">
          <span v-for="item in user.data">{{item}}</span>
        </div>
      </template>

    </child>

    <!--第二次使用:用列表展示数据-->
    <child>
      <template slot-scope="user">
        <ul>
          <li v-for="item in user.data">{{item}}</li>
        </ul>
      </template>

    </child>

    <!--第三次使用:直接显示数据-->
    <child>
      <template slot-scope="user">
       {{user.data}}
      </template>

    </child>

    <!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
    <child>
      我就是模板
    </child>
  </div>
</template>

子组件:

<template>
  <div class="child">

    <h3>这里是子组件</h3>
    // 作用域插槽
    <slot  :data="data"></slot>
  </div>
</template>

 export default {
    data: function(){
      return {
        data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
      }
    }
}

结果如图所示:

github

以上三个demo就放在GitHub了,有需要的可以去取。使用非常方便,是基于vue-cli搭建工程。

地址点这里

最后

如果本文对你理解slot和scope-slot有帮助,请不要吝啬手中的点赞哟。
编程贵在实践,赶紧行动起来吧!

关于作者

技术博客 || GitHub || 掘金主页

转载请注明链接。

查看原文

ireeoo 评论了文章 · 2018-11-13

结合Vue实现344分割手机号码

这一个小需求,断断续续解决了好久,中间一直存在各种bug,现在基本上已经完全解决,因此,打算从头到尾记录一下,方便以后查询。

需求

开始的时候,还是把需求简单的说下:

移动端中,弹出系统的数字键盘,实现344分割手机号

需求明确之后,下面就是分析需求,然后才能确定实现方式:

  1. 明确说明是移动端,那么也就是说,兼容问题不需要考虑太多
  2. 如果要弹出系统的数字键盘,那么符合条件的只有一种,那就是type="phone"的input输入框
  3. 在输入的时候,实现344分割手机号,这个时候就要监视输入框中内容的变化,这个可以结合Vue的watch来实现
  4. 手机号344分割,那么输入框中输入的最大长度应该就是13了

经过分析可以看出,实际需要使用的知识点并不是很多。分析完成之后,下面就要来具体实现这个需求了。

实现

搭建结构

首先,在写真正的程序之前,要把结构搭建完成,后续写代码直接在结构中写就可以了。

  1. 首先来搭建html5的代码结构,然后自定义input的样式(方便后续查看)
  2. 定义input输入框,并设置typemaxlength
  3. 通过v-model实现数据的双向绑定
  4. 引入vue.js并搭建基本的代码结构
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style type="text/css">
    /* 自定义input输入框的样式 */
    #app input { width: 100%; height: 50px; border: 1px solid red; font-size: 30px; outline: none; }
  </style>
</head>
<body>
  <div id="app">
    <!-- 设置type和maxlength,并实现数据双向绑定 -->
    <input v-model="phone" type="phone" placeholder="请输入手机号" maxlength="13">
  </div>
</body>
</html>

<script type="text/javascript" data-original="./vue2.4.2.js"></script>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    data() {
      return {
        phone: ''
      }
    },
    watch: {
      phone(newValue, oldValue) {
        // 具体的代码在这里实现
      }
    }
  })
</script>

分析输入和输出

在具体实现之前,首先要明白,输入和输出的分别触发什么样的操作。
下面,我们把watch中的newValueoldValue的值输出,看一下规律。

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        console.group('< === 是否是输入 === >')
        console.log("%c%s", "color:red", `${newValue.length > oldValue.length}`)
        console.groupEnd('< === 是否是输入 === >')
      }

当在输入框中输入内容的时候:

输入

当在输入框中删除内容的时候:

输出

从分析可以看出,在输入的时候,长度是增加的,删除的时候,长度是减少的。

删除的具体实现

在删除的时候,会出现两个转折点,就是在删除第10个元素或者第5个元素的时候,要自动删除空格。

很明显,这里出现了两个条件,下标为4的时候和下标为9的时候,要去除字符串的空格。

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        if (newValue.length > oldValue.length) {
          // ...
        } else {
          if (newValue.length === 9 || newValue.length === 4) {
            this.phone = this.phone.trim()
          }
        }
      }

代码写出了之后,看着有些不舒服,有没有优化方案呢?

再次经过分析会发现,既然在下标为4或9的时候,我们做的事是去除空格,那么就可以根据去空格的特性(有空格去除空格,无空格不进行处理),直接只做去除空格的操作,不进行判断了。

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        if (newValue.length > oldValue.length) {
          // ...
        } else {
          this.phone = this.phone.trim()
        }
      }

现在,删除的功能实现了,并且代码也进行了优化,下面开始实现输入的操作。

输入的具体实现

下面来分析一下输入的问题:

在输入的时候,当值的长度小于3的时候,肯定是原样返回的;当值的长度大于等于3且小于7的时候,要增加一个空格;当长度大于等于7的时候,要增加两个空格。这个时候就要牵涉到条件判断了。

另外,在判断值的时候,肯定是判断总数字的个数,因此是去除空格的。

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        if (newValue.length > oldValue.length) {
          if (newValue.replace(/\s/g, '').length < 3) {
            this.phone = newValue.replace(/\s/g, '')
          } else if (newValue.replace(/\s/g, '').length >= 3 && newValue.replace(/\s/g, '').length < 7) {
            this.phone = newValue.replace(/\s/g, '').replace(/(\d{3})/, '$1 ')
          } else if (newValue.replace(/\s/g, '').length >= 7) {
            this.phone = newValue.replace(/\s/g, '').replace(/(\d{3})(\d{4})/, '$1 $2 ')
          }
        } else {
          this.phone = this.phone.trim()
        }
      }

我的天哪,这个代码怎么看着那么乱,应该可以优化的。由于正则表达式是一个强大的工具,因此,可以结合正则来优化。

首先,我们可以把空格作为分割点,那么空格之前的内容属于一组,所以共分三组。第一组的长度为3个数字,第二组和第三组的长度都是0-4个数字,因此,通过正则可以简化代码:

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        if (newValue.length > oldValue.length) {
          this.phone = newValue.replace(/\s/g, '').replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1 $2 $3')
        } else {
          this.phone = this.phone.trim()
        }
      }

简化代码

上面的代码已经基本算精简了,可是,如果使用三目运算符,还可以更加精简:

      phone(newValue, oldValue) {
        // 具体的代码在这里实现
        this.phone = newValue.length > oldValue.length ? newValue.replace(/\s/g, '').replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1 $2 $3') : this.phone.trim()
      }

至此,该需求已经完成了,最后实际的代码只有一句。

总结

其实这个需求很简单,需要总结的东西不多,因此,在这里把具体的代码实现贴出来,方便后续使用:

<div id="app">
  <!-- 设置type和maxlength,并实现数据双向绑定 -->
  <input v-model="phone" type="phone" placeholder="请输入手机号" maxlength="13">
</div>


<script type="text/javascript" data-original="./vue2.4.2.js"></script>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    data() {
      return {
        phone: '' // 双向绑定的数据
      }
    },
    watch: {
      phone(newValue, oldValue) { // 监听
        this.phone = newValue.length > oldValue.length ? newValue.replace(/\s/g, '').replace(/(\d{3})(\d{0,4})(\d{0,4})/, '$1 $2 $3') : this.phone.trim()
      }
    }
  })
</script>
查看原文

ireeoo 赞了文章 · 2018-06-07

Web 前端中的增强现实(AR)开发技术

本文作者 GeekPlux,博客地址:http://geekplux.com/2018/01/18/augmented-reality-development-tech-in-web-frontend.html。注明作者和地址即可转载。

增强现实(以下简称 AR)浪潮正滚滚而来,Web 浏览器作为人们最唾手可得的人机交互终端,正在大力发展 AR 技术。很多 Web 前端工程师也在寻求职业转型,于是我把我近半年的相关调研结果在此汇结成文。本文力求把目前前端方向的 AR 技术都罗列一遍,细节不赘述(保证文章篇幅不过长),只做概括和科普(因此文章中的链接很多),零零散散写了一个多月(拖延症),欢迎已经在从事该领域或研究的道友前来纠正补充。

Web AR 初音未来

AR 可以简单的理解为一种实时将虚拟图像叠加在现实场景中的技术,且能交互[1]。我个人觉得 AR 比 VR 要有前景,主要因为:

AR 的优势在于把目之所及的现实场景变成了背景,并将现实世界和数字世界无缝连接。

当然这个“无缝”目前还谈不上,不过一直在进步。在谈 Web 前端如何做 AR 前,有必要先了解一下 AR 实现的 2 种主要方式和其关键技术:

AR 实现的方式和关键技术

AR 的主要实现方式有 2 种[2][3]:光学透视式 (Optical see-through) 和视频透视式 (Video see-through)。目前,市面上的头戴式设备通常采用 2 种方式中的 1 种或 2 种都采用,而手持设备(手机、平板等)通常采用视频透视式。光学透视式是将电脑生成的数字图像显示在眼前的一层半透明镜片上,这样就可以使现实场景和虚拟信息同时出现在视网膜上。而视频透视式技术是将现实场景首先通过相机录入电脑,经过和虚拟对象整合、压缩,再统一呈现在用户眼前。两者各有优劣[4]:光学透视式中的现实场景因为没有经过电脑处理,因此显示得更自然、直接;虽然它实现简单,但是也存在定位精度不高、匹配不准确、显示有延迟等问题。而视频透视式因为经过整合,所以匹配准确,最终显示效果同步程度高,还能对生成的显示结果根据用户需求进行进一步处理;但是它实现难度较高,且丢失了一部分真实感。目前(2017 年底) Web 前端要想实现 AR,都是靠的视频透视式技术

另外,计算机视觉技术在 AR 中起着至关重要的作用。因为实现 AR 最核心的是识别与追踪。首先,相机要先识别基准标志、关键点、光学图片等;然后再根据特征检测、边缘检测或其他图像处理方法来实时追踪;最后将虚拟图像叠加到真实场景中。根据 2008 年的统计结果显示,近十年著名的 AR 会议 ISMAR 中有关追踪技术的论文占到了 20%以上[3].

Web AR

根据上一节的阐述,我们可以得出结论:要实现 AR 需要识别、追踪和渲染三步,在浏览器中也不外如是。另外,还可以结合传感器来提供更多的交互或让 AR 渲染得更准确、通过网络连接云端来加速计算或交换更多数据等。如下图所示,这是我自己整理出的一个 Web AR 流程图。Web AR 或者说移动 AR 在某些方面如便携性、传感器丰富、自带网络等还是有很大优势的,在此我就不多说了。

Web AR 流程图

WebVR 规范

首先,Web AR 目前还是一项前沿技术,没有标准也没有成熟的库供使用,不过已经有大公司和一部分开发者正在积极推进。2017 年 10 月 2 日 W3C 的 WebVR 组 发布了 WebVR 规范 1.1 版的初稿,2.0 版还在热火朝天地修订当中。

WebVR 是一个开放标准,使您可以在浏览器中体验 VR。我们的目标是让每个人都可以更轻松地体验 VR,无论您拥有什么设备。 -  webvr.info

为什么本文的题目是 Web AR,这里却提到 WebVR 呢?因为 WebVR 规范中的部分 API 对 Web AR 也同样适用。比如 VRDevicePose 可以获取摄像头位置。这是目前唯一接近 Web AR 的标准,有了标准我们就可以只针对规范的接口做开发,从而适应绝大多数的设备。扩展阅读:WebVR 于增强现实针对智能手机 AR 的 WebVR API 扩展

WebARonARKit, WebARonARCore

ARKit 和 ARCore 分别是苹果和谷歌两大巨头出品的移动 AR SDK,提供的功能也类似:运动追踪、环境感知和光线感应,我相信很多对 AR 感兴趣的开发者对这两个 SDK 都不陌生。但这两个都是移动 AR 的 SDK,于是谷歌的 AR 团队提供了 WebARonARKitWebARonARCore 两个库,以便开发者能用 Web 技术来基于 ARKit 和 ARCore 开发,从而实现 WebAR。目前这两个库都还在试验阶段,想吃螃蟹的人赶紧去试试。其实现原理都是结合特定系统(iOS 和 Android)扩展了 WebVR API。Google AR 团队封装了一个 three.ar.js 的库,提供了一些实用的 AR API,包括 ARView, ARReticle, ARPerspectiveCamera, ARDebug 和 ARUtils 等。

AR.js

2017 年 SIGGRAPH(图形学顶级会议)上 AR.js 可谓大放异彩,有人做了 Web AR 相关的 session 就是用了 AR.js 来讲解。AR.js 是 Jerome Etienne 开发的一款 Web AR 库,可以用十行 HTML 就实现 AR,并有 60 FPS 的帧率。但其实 AR.js 做的事很简单,它主要封装了以下几个库:

  • WebRTC。下文会详细讲解,主要是获取视频流。
  • JSARToolKitARToolKit 可以说是第一个开源的 AR 框架,在 1999 年发布,一直更新至今。虽然历史悠久但目前仍被广泛应用(官方网站的风格一点也没有历史感)。它主要提供了识别和追踪 marker 的功能,本文附录中还有补充。
  • Three.js, Babylon.js, A-Frame。这几个都是基于 WebGL 的渲染库,用于渲染要在 AR 环境中显示的东西,下文会扩充。

由此观之,AR.js 像是一个把所有轮子都拼起来的瑞士军刀,简单易用。作者在 GitHub 和 Twitter 上都很活跃,有什么问题可以直接问他。

WebRTC 获取视频流

前三节我们提到了一个正在成形的标准和两个框架,是目前 Web AR 的最新进展了。指望标准发布肯定黄花菜都凉了,但我们可以自己动手丰衣足食。

刚才我们说到 AR 首先要识别,那就要用到 WebRTC 技术。WebRTC(Web 实时通信,Web Real-Time Communication),顾名思义是一个支持网页浏览器进行实时语音对话或视频对话的技术。它其中有个很重要的 API:getUserMedia() 可以实时获取摄像头的视频流,这是视频透视式的 AR 实现的前提(目前 iOS 11 刚刚支持这个 API,Android 是很早就能用)。有了视频流我们就可以分析其中的特征点,运用计算机视觉的算法识别和追踪视频流中的事物。这里有 2 个要点也要提一下:一是 getUserMedia 默认获取的是前置摄像头,如果想获取后置摄像头的视频流,需要用 navigator.mediaDevices.enumerateDevices() 将设备的音频、视频设备遍历得到,具体参照 demo;二是要用 https 打开网页才能访问摄像头。

Tracking.js, JSFeat, ConvNetJS, deeplearn.js, keras.js 识别与追踪

获取到视频流之后的工作就是识别和追踪了。视频流你可以看作是一帧一帧的图像,所以处理视频流的过程可以理解为图像处理的过程。但这里其实还涉及到一个如何传输视频流的问题,一般有两种方式:

1. 在前端直接处理视频流

在前端直接进行图像处理,可以用 Tracking.jsJSFeat。这两个库类似,都是在前端做计算机视觉的,包括提取特征点、人脸识别等。把 WebRTC 拿到的视频流直接传给它们并调用 API 就能得到自己想要的效果。对于一些成熟的算法,如人脸识别,可以直接拿到识别结果,如果自己要识别的物体比较复杂你也可以自己进行特征点的计算,但这可能在前端会算力不足,关于性能的问题下文再论述。

提到计算机视觉,不得不提深度学习,毕竟现在很多图像处理算法被深度学习吊打。ConvNetJS,是斯坦福大学开源的一个前端深度学习框架,可以让你在前端完成深度神经网络的训练。deeplearn.js 则是 Google Brain 团队搞的,功能和 ConvNetJS 类似。现在 ConvNetJS 好像不怎么维护了,deeplearn.js 还在频繁更新中,感兴趣的同学可以试用一下。另外一个紧锣密鼓开发的深度学习库 keras.js 则是让你可以在浏览器中运行已经训练好的 Keras 模型(Kears 是著名的深度学习开发框架),并支持 WebGL 2。

这些框架都在主页上提供了丰富的 Demo,非常有趣,把玩一下说不定激发你的灵感。

2. 前端传输视频流给后端,后端处理完毕返回结果到前端

另一种处理视频流的方法就是传到后端去处理,后端处理方式的选择就数不胜数了,现在实现 AR 大多数用的都是 SLAM 算法,后端处理完返回前端结果即可。那么如何传输成了我们前端同学的难题,一般有这两种方法:

  • 传图片信息给后端。Canvas 提供了两个 API,一个是 toDataURL,它可以生成图片的 base64 字符串;另一个是 toBlob,这个方法是异步的,可以将图片转换成 Blob 文件对象,因为其是二进制的,所以更方便传给后端。具体使用来看,后者比前者的效率更高一点。
  • 传像素信息给后端。WebGL 的 readPixels 方法,可以获取 framebuffer 中的像素值。

除此之外应该还有其他方法,总之目标是将前端的图像信息传给后端,传输方式可以用 AJAX,也可以用 WebSocket,具体根据场景来定。

这一节主要讲了识别和追踪,其实除了单纯的对图像、视频流处理,我们还能通过移动端设备的各种传感器数据获取到更多的距离、深度、光照等信息,从而使识别追踪更准确。

A-Frame, Three.js, Babylon.js, Pixi.js, WebGL 渲染与交互

讲完识别和追踪,终于该聊聊渲染了。A-Frame 是 Mozilla 团队在 2015 年开源的一款做 WebVR 的框架,但日前 A-Frame 团队发布的 aframe-xr 其中包括了一些 Web AR 组件。一开始我们也说过 VR 和 AR 中有部分实现是重合的,所以用 A-Frame 的各种组件可以让你用很少的代码构建出 AR 所需要的 3D 立体世界。提到 3D,不得不提 WebGL。WebGL 是 OpenGL ES 在浏览器端的实现,你可以理解其为 OpenGL 的子集。用 WebGL 你可以操作前端的每一个像素点,懂一点图形学的同学一定知道它的强大,而且它能调用 GPU,所以前端涉及到 GPU 的地方也缺不了它。WebGL 虽然强大,但写起来异常复杂,学习成本也很高,而前端最著名的 3D 库 Three.js 将繁琐的 WebGL API 进行了封装和优化,让你可以用可读性更好的代码在前端书写 WebGL。Pixi.js 和 Three.js 做了类似的事情,但它只支持 2D 渲染,不过它还是很好用的,如果你只是想用 WebGL 来做复杂的渲染但没涉及到 3D 场景,不妨试试它。Babylon.js 就更牛了,它是一款游戏引擎,也是封装了 WebGL 在前端做高性能的渲染,但它和 Three.js 的关注点不一样,如果你对渲染的精细程度非常有要求,比如光线、阴影等,那么你可以考虑下 babylon.js,毕竟这是款由微软前员工开发的游戏引擎啊……

这些基于 WebGL 的渲染方法,有一个共性的难题是如何交互,比如 hover, click 效果如何实现。其实在 Web AR 中交互非常局限:如果是桌面设备即电脑,和浏览网页的交互差不多,有 hover, click, drag 拖拽等;如果用的是移动设备,即手机、平板,则可能有 zoom 的交互(这里多嘴一句,其实移动 AR 中,应该尽量避免手指去 zoom 的交互,而应该引导用户用移近或移远设备来进行放大缩小)。这些实现起来要依赖于 光线投射算法 Ray casting 方法。Three.js 直接提供了 Raycaster 类供实现 ray casting 算法。其实原理很简单,就是摄像头(这里的摄像头不是指手机的摄像头,而是你渲染时的 Camera,可以参考 Three.js 中的 Camera)视作视点,与你在屏幕上触碰的点坐标连城一条射线,看这条射线与你视图中哪些物体相交。

Ray casting 算法

这一节主要讲了渲染与交互,事实上在实现 AR 的时候,识别追踪和渲染交互是同时进行的,如何给用户更好、更流畅的体验是现在 Web AR 的又一大难题。

性能

性能是很多人关心的问题。目前浏览器的算力确实还不足以与客户端去媲美,但较之前也有了巨大的提升。识别和追踪本质上是像素级的计算,对算力的要求都很高,因此 maker-based 的 AR 定位效率通常比 makerless 的要高很多。此外,计算机视觉算法的效率对性能影响也很大,比如人脸识别目前较其他识别要成熟很多,所以人脸识别的算法在 Web 前端运行还算流畅。

提升性能的方法有很多种,大家一般会先想到用 WebGL 调用 GPU 加速,其次会想到用 Web Worker,WebAssembly。前两者我都试过,把纯计算的代码移到 WebGL 的 shader 或 Web Worker 里,这两者虽然都是加速计算,但适用场景不同。shader 可以用于加速只和渲染(重绘)有关的代码,无关渲染的代码放入 shader 中反而会造成重复计算。Web Worker 适用于事先计算或实时性要求不高的代码,如布局算法。WebAssembly 我还没在做 AR 的时候用过,还有一个库 gpu.js也没试过,希望有大神试过之后告诉我有什么效果。

还有一种变相“提升”性能的方法是用滤波算法(比如卡尔曼滤波)将卡顿降到更小,让用户从视觉感受上似乎更流畅。

结尾

现在 Web AR 大潮刚刚开始,有很多高地需要人去攻克,比如光照估计、性能优化等,希望有兴趣的同学可以积极参与进来。而且 Web 前端无论是技术还是人口都发展迅速,充满了无限可能,有限的只是你的想象力。我很久之前做了个人脸识别 + AR 的小 demo,在 GitHub 上 https://github.com/geekplux/AR-AI-VIS-demo,大家可以玩玩,其实就几行代码。下一篇可能会写写 Web 前端做人脸识别相关的文章,感觉又给自己挖了个大坑,希望我的拖延症早日治好。

附录:AR 开发技术

参考文献 [2] 中曾总结了当时所有的 AR 开发技术,如下表:

AR 开发技术

这张表将 AR 开发工具分成了四类,分别罗列出来。其实目前大多的 AR 开发都是用 Unity 去做的,很多第三方 SDK 也都是先集成到 Unity 上,再由 Unity 输出到对应设备所需的格式。表中的 Vuforia 据我观察是目前用的最多的第三方 SDK。ARToolKit 则在 Web 前端和移动端用的很多,它的开源版是基于标记的 (Marker-based),也提供机器学习的训练方法,让你可以将任意图片训练成 Marker。另外由于这张表是 2015 年的,当时苹果公司的 ARKit 和谷歌的 ARCore 这 2 个 SDK 还没有横空出世,可以将其归到表中的第三行。

参考文献

  • [1] Azuma R T. A survey of augmented reality[J]. Presence Teleoperators & Virtual Environments, 1997, 6(4): 355-385
  • [2] Billinghurst M, Clark A, Lee G. A survey of augmented reality[J]. Foundations and Trends in Human-Computer Interaction, 2015, 8(2-3): 73-272
  • [3] Zhou F, Duh B L, Billinghurst M. Trends in augmented reality tracking, interaction and display: a review of ten years of ISMAR[C] //Proceedings of the 7th IEEE/ACM International Symposium on Mixed and Augmented Reality. Washington: IEEE Computer Society Press, 2008: 193-202
  • [4] Rolland J P, Fuchs H. Optical versus video see-through head-mounted displays in medical visualization[M]. Cambridge: MIT Press, 2000, 9: 287-309
查看原文

赞 71 收藏 180 评论 0

ireeoo 收藏了文章 · 2018-03-26

你真的会使用XMLHttpRequest吗?

看到标题时,有些同学可能会想:“我已经用xhr成功地发过很多个Ajax请求了,对它的基本操作已经算挺熟练了。” 我之前的想法和你们一样,直到最近我使用xhr时踩了不少坑儿,我才突然发现其实自己并不够了解xhr,我知道的只是最最基本的使用。
于是我决定好好地研究一番xhr的真面目,可拜读了不少博客后都不甚满意,于是我决定认真阅读一遍W3C的XMLHttpRequest标准。看完标准后我如同醍醐灌顶一般,感觉到了从未有过的清澈。这篇文章就是参考W3C的XMLHttpRequest标准和结合一些实践验证总结而来的。

AjaxXMLHttpRequest

我们通常将Ajax等同于XMLHttpRequest,但细究起来它们两个是属于不同维度的2个概念。

以下是我认为对Ajax较为准确的解释:(摘自what is Ajax
AJAX stands for Asynchronous JavaScript and XML. AJAX is a new technique for creating better, faster, and more interactive web applications with the help of XML, HTML, CSS, and Java Script.

AJAX is based on the following open standards:

  • Browser-based presentation using HTML and Cascading Style Sheets (CSS).

  • Data is stored in XML format and fetched from the server.

  • Behind-the-scenes data fetches using XMLHttpRequest objects in the browser.

  • JavaScript to make everything happen.

从上面的解释中可以知道:ajax是一种技术方案,但并不是一种新技术。它依赖的是现有的CSS/HTML/Javascript,而其中最核心的依赖是浏览器提供的XMLHttpRequest对象,是这个对象使得浏览器可以发出HTTP请求与接收HTTP响应。

所以我用一句话来总结两者的关系:我们使用XMLHttpRequest对象来发送一个Ajax请求。

XMLHttpRequest的发展历程

XMLHttpRequest一开始只是微软浏览器提供的一个接口,后来各大浏览器纷纷效仿也提供了这个接口,再后来W3C对它进行了标准化,提出了XMLHttpRequest标准XMLHttpRequest标准又分为Level 1Level 2
XMLHttpRequest Level 1主要存在以下缺点:

  • 受同源策略的限制,不能发送跨域请求;

  • 不能发送二进制文件(如图片、视频、音频等),只能发送纯文本数据;

  • 在发送和获取数据的过程中,无法实时获取进度信息,只能判断是否完成;

那么Level 2Level 1 进行了改进,XMLHttpRequest Level 2中新增了以下功能:

  • 可以发送跨域请求,在服务端允许的情况下;

  • 支持发送和接收二进制数据;

  • 新增formData对象,支持发送表单数据;

  • 发送和获取数据时,可以获取进度信息;

  • 可以设置请求的超时时间;

当然更详细的对比介绍,可以参考阮老师的这篇文章,文章中对新增的功能都有具体代码示例。

XMLHttpRequest兼容性

关于xhr的浏览器兼容性,大家可以直接查看“Can I use”这个网站提供的结果XMLHttpRequest兼容性,下面提供一个截图。

clipboard.png

从图中可以看到:

  • IE8/IE9、Opera Mini 完全不支持xhr对象

  • IE10/IE11部分支持,不支持 xhr.responseTypejson

  • 部分浏览器不支持设置请求超时,即无法使用xhr.timeout

  • 部分浏览器不支持xhr.responseTypeblob

细说XMLHttpRequest如何使用

先来看一段使用XMLHttpRequest发送Ajax请求的简单示例代码。

function sendAjax() {
  //构造表单数据
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);
  //创建xhr对象 
  var xhr = new XMLHttpRequest();
  //设置xhr请求的超时时间
  xhr.timeout = 3000;
  //设置响应返回的数据格式
  xhr.responseType = "text";
  //创建一个 post 请求,采用异步
  xhr.open('POST', '/server', true);
  //注册相关事件回调处理函数
  xhr.onload = function(e) { 
    if(this.status == 200||this.status == 304){
        alert(this.responseText);
    }
  };
  xhr.ontimeout = function(e) { ... };
  xhr.onerror = function(e) { ... };
  xhr.upload.onprogress = function(e) { ... };
  
  //发送数据
  xhr.send(formData);
}

上面是一个使用xhr发送表单数据的示例,整个流程可以参考注释。


接下来我将站在使用者的角度,以问题的形式介绍xhr的基本使用。
我对每一个问题涉及到的知识点都会进行比较细致地介绍,有些知识点可能是你平时忽略关注的。

如何设置request header

在发送Ajax请求(实质是一个HTTP请求)时,我们可能需要设置一些请求头部信息,比如content-typeconnectioncookieaccept-xxx等。xhr提供了setRequestHeader来允许我们修改请求 header。

void setRequestHeader(DOMString header, DOMString value);

注意点

  • 方法的第一个参数 header 大小写不敏感,即可以写成content-type,也可以写成Content-Type,甚至写成content-Type;

  • Content-Type的默认值与具体发送的数据类型有关,请参考本文【可以发送什么类型的数据】一节;

  • setRequestHeader必须在open()方法之后,send()方法之前调用,否则会抛错;

  • setRequestHeader可以调用多次,最终的值不会采用覆盖override的方式,而是采用追加append的方式。下面是一个示例代码:

var client = new XMLHttpRequest();
client.open('GET', 'demo.cgi');
client.setRequestHeader('X-Test', 'one');
client.setRequestHeader('X-Test', 'two');
// 最终request header中"X-Test"为: one, two
client.send();

如何获取response header

xhr提供了2个用来获取响应头部的方法:getAllResponseHeadersgetResponseHeader。前者是获取 response 中的所有header 字段,后者只是获取某个指定 header 字段的值。另外,getResponseHeader(header)header参数不区分大小写。

DOMString getAllResponseHeaders();
DOMString getResponseHeader(DOMString header);

这2个方法看起来简单,但却处处是坑儿。

你是否遇到过下面的坑儿?——反正我是遇到了。。。

  1. 使用getAllResponseHeaders()看到的所有response header与实际在控制台 Network 中看到的 response header 不一样

  2. 使用getResponseHeader()获取某个 header 的值时,浏览器抛错Refused to get unsafe header "XXX"

经过一番寻找最终在 Stack Overflow找到了答案

"simple response header"包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;
"Access-Control-Expose-Headers":首先得注意是"Access-Control-Expose-Headers"进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。

所以getAllResponseHeaders()只能拿到限制以外(即被视为safe)的header字段,而不是全部字段;而调用getResponseHeader(header)方法时,header参数必须是限制以外的header字段,否则调用就会报Refused to get unsafe header的错误。

如何指定xhr.response的数据类型

有些时候我们希望xhr.response返回的就是我们想要的数据类型。比如:响应返回的数据是纯JSON字符串,但我们期望最终通过xhr.response拿到的直接就是一个 js 对象,我们该怎么实现呢?
有2种方法可以实现,一个是level 1就提供的overrideMimeType()方法,另一个是level 2才提供的xhr.responseType属性。

xhr.overrideMimeType()

overrideMimeTypexhr level 1就有的方法,所以浏览器兼容性良好。这个方法的作用就是用来重写responsecontent-type,这样做有什么意义呢?比如:server 端给客户端返回了一份document或者是 xml文档,我们希望最终通过xhr.response拿到的就是一个DOM对象,那么就可以用xhr.overrideMimeType('text/xml; charset = utf-8')来实现。

再举一个使用场景,我们都知道xhr level 1不支持直接传输blob二进制数据,那如果真要传输 blob 该怎么办呢?当时就是利用overrideMimeType方法来解决这个问题的。

下面是一个获取图片文件的代码示例:

var xhr = new XMLHttpRequest();
//向 server 端获取一张图片
xhr.open('GET', '/path/to/image.png', true);

// 这行是关键!
//将响应数据按照纯文本格式来解析,字符集替换为用户自己定义的字符集
xhr.overrideMimeType('text/plain; charset=x-user-defined');

xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
    //通过 responseText 来获取图片文件对应的二进制字符串
    var binStr = this.responseText;
    //然后自己再想方法将逐个字节还原为二进制数据
    for (var i = 0, len = binStr.length; i < len; ++i) {
      var c = binStr.charCodeAt(i);
      //String.fromCharCode(c & 0xff);
      var byte = c & 0xff; 
    }
  }
};

xhr.send();

代码示例中xhr请求的是一张图片,通过将 responsecontent-type 改为'text/plain; charset=x-user-defined',使得 xhr 以纯文本格式来解析接收到的blob 数据,最终用户通过this.responseText拿到的就是图片文件对应的二进制字符串,最后再将其转换为 blob 数据。

xhr.responseType

responseTypexhr level 2新增的属性,用来指定xhr.response的数据类型,目前还存在些兼容性问题,可以参考本文的【XMLHttpRequest的兼容性】这一小节。那么responseType可以设置为哪些格式呢,我简单做了一个表,如下:

xhr.response 数据类型说明
""String字符串默认值(在不设置responseType时)
"text"String字符串
"document"Document对象希望返回 XML 格式数据时使用
"json"javascript 对象存在兼容性问题,IE10/IE11不支持
"blob"Blob对象
"arrayBuffer"ArrayBuffer对象

下面是同样是获取一张图片的代码示例,相比xhr.overrideMimeType,用xhr.response来实现简单得多。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/path/to/image.png', true);
//可以将`xhr.responseType`设置为`"blob"`也可以设置为`" arrayBuffer"`
//xhr.responseType = 'arrayBuffer';
xhr.responseType = 'blob';

xhr.onload = function(e) {
  if (this.status == 200) {
    var blob = this.response;
    ...
  }
};

xhr.send();

小结

虽然在xhr level 2中,2者是共同存在的。但其实不难发现,xhr.responseType就是用来取代xhr.overrideMimeType()的,xhr.responseType功能强大的多,xhr.overrideMimeType()能做到的xhr.responseType都能做到。所以我们现在完全可以摒弃使用xhr.overrideMimeType()了。

如何获取response数据

xhr提供了3个属性来获取请求返回的数据,分别是:xhr.responsexhr.responseTextxhr.responseXML

  • xhr.response

    • 默认值:空字符串""

    • 当请求完成时,此属性才有正确的值

    • 请求未完成时,此属性的值可能是""或者 null,具体与 xhr.responseType有关:当responseType"""text"时,值为""responseType为其他值时,值为 null

  • xhr.responseText

    • 默认值为空字符串""

    • 只有当 responseType"text"""时,xhr对象上才有此属性,此时才能调用xhr.responseText,否则抛错

    • 只有当请求成功时,才能拿到正确值。以下2种情况下值都为空字符串"":请求未完成、请求失败

  • xhr.responseXML

    • 默认值为 null

    • 只有当 responseType"text""""document"时,xhr对象上才有此属性,此时才能调用xhr.responseXML,否则抛错

    • 只有当请求成功且返回数据被正确解析时,才能拿到正确值。以下3种情况下值都为null:请求未完成、请求失败、请求成功但返回数据无法被正确解析时

如何追踪ajax请求的当前状态

在发一个ajax请求后,如果想追踪请求当前处于哪种状态,该怎么做呢?

xhr.readyState这个属性即可追踪到。这个属性是只读属性,总共有5种可能值,分别对应xhr不同的不同阶段。每次xhr.readyState的值发生变化时,都会触发xhr.onreadystatechange事件,我们可以在这个事件中进行相关状态判断。

  xhr.onreadystatechange = function () {
    switch(xhr.readyState){
      case 1://OPENED
        //do something
            break;
      case 2://HEADERS_RECEIVED
        //do something
        break;
      case 3://LOADING
        //do something
        break;
      case 4://DONE
        //do something
        break;
    }
状态描述
0UNSENT (初始状态,未打开)此时xhr对象被成功构造,open()方法还未被调用
1OPENED (已打开,未发送)open()方法已被成功调用,send()方法还未被调用。注意:只有xhr处于OPENED状态,才能调用xhr.setRequestHeader()xhr.send(),否则会报错
2HEADERS_RECEIVED (已获取响应头)send()方法已经被调用, 响应头和响应状态已经返回
3LOADING (正在下载响应体)响应体(response entity body)正在下载中,此状态下通过xhr.response可能已经有了响应数据
4DONE (整个数据传输过程结束)整个数据传输过程结束,不管本次请求是成功还是失败

如何设置请求的超时时间

如果请求过了很久还没有成功,为了不会白白占用的网络资源,我们一般会主动终止请求。XMLHttpRequest提供了timeout属性来允许设置请求的超时时间。

xhr.timeout

单位:milliseconds 毫秒
默认值:0,即不设置超时

很多同学都知道:从请求开始 算起,若超过 timeout 时间请求还没有结束(包括成功/失败),则会触发ontimeout事件,主动结束该请求。

【那么到底什么时候才算是请求开始 ?】
——xhr.onloadstart事件触发的时候,也就是你调用xhr.send()方法的时候。
因为xhr.open()只是创建了一个连接,但并没有真正开始数据的传输,而xhr.send()才是真正开始了数据的传输过程。只有调用了xhr.send(),才会触发xhr.onloadstart

【那么什么时候才算是请求结束 ?】
—— xhr.loadend事件触发的时候。

另外,还有2个需要注意的坑儿:

  1. 可以在 send()之后再设置此xhr.timeout,但计时起始点仍为调用xhr.send()方法的时刻。

  2. xhr为一个sync同步请求时,xhr.timeout必须置为0,否则会抛错。原因可以参考本文的【如何发一个同步请求】一节。

如何发一个同步请求

xhr默认发的是异步请求,但也支持发同步请求(当然实际开发中应该尽量避免使用)。到底是异步还是同步请求,由xhr.open()传入的async参数决定。

open(method, url [, async = true [, username = null [, password = null]]])

  • method: 请求的方式,如GET/POST/HEADER等,这个参数不区分大小写

  • url: 请求的地址,可以是相对地址如example.php,这个相对是相对于当前网页的url路径;也可以是绝对地址如http://www.example.com/example.php

  • async: 默认值为true,即为异步请求,若async=false,则为同步请求

在我认真研读W3C 的 xhr 标准前,我总以为同步请求和异步请求只是阻塞和非阻塞的区别,其他什么事件触发、参数设置应该是一样的,事实证明我错了。

W3C 的 xhr标准中关于open()方法有这样一段说明:

Throws an "InvalidAccessError" exception if async is false, the JavaScript global environment is a document environment, and either the timeout attribute is not zero, the withCredentials attribute is true, or the responseType attribute is not the empty string.

从上面一段说明可以知道,当xhr为同步请求时,有如下限制:

  • xhr.timeout必须为0

  • xhr.withCredentials必须为 false

  • xhr.responseType必须为""(注意置为"text"也不允许)

若上面任何一个限制不满足,都会抛错,而对于异步请求,则没有这些参数设置上的限制。

之前说过页面中应该尽量避免使用sync同步请求,为什么呢?
因为我们无法设置请求超时时间(xhr.timeout0,即不限时)。在不限制超时的情况下,有可能同步请求一直处于pending状态,服务端迟迟不返回响应,这样整个页面就会一直阻塞,无法响应用户的其他交互。

另外,标准中并没有提及同步请求时事件触发的限制,但实际开发中我确实遇到过部分应该触发的事件并没有触发的现象。如在 chrome中,当xhr为同步请求时,在xhr.readyState2变成3时,并不会触发 onreadystatechange事件,xhr.upload.onprogressxhr.onprogress事件也不会触发。

如何获取上传、下载的进度

在上传或者下载比较大的文件时,实时显示当前的上传、下载进度是很普遍的产品需求。
我们可以通过onprogress事件来实时显示进度,默认情况下这个事件每50ms触发一次。需要注意的是,上传过程和下载过程触发的是不同对象的onprogress事件:

  • 上传触发的是xhr.upload对象的 onprogress事件

  • 下载触发的是xhr对象的onprogress事件

xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
    if (event.lengthComputable) {
      var completedPercent = event.loaded / event.total;
    }
 }

可以发送什么类型的数据

void send(data);

xhr.send(data)的参数data可以是以下几种类型:

  • ArrayBuffer

  • Blob

  • Document

  • DOMString

  • FormData

  • null

如果是 GET/HEAD请求,send()方法一般不传参或传 null。不过即使你真传入了参数,参数也最终被忽略,xhr.send(data)中的data会被置为 null.

xhr.send(data)中data参数的数据类型会影响请求头部content-type的默认值:

  • 如果dataDocument 类型,同时也是HTML Document类型,则content-type默认值为text/html;charset=UTF-8;否则为application/xml;charset=UTF-8

  • 如果dataDOMString 类型,content-type默认值为text/plain;charset=UTF-8

  • 如果dataFormData 类型,content-type默认值为multipart/form-data; boundary=[xxx]

  • 如果data是其他类型,则不会设置content-type的默认值

当然这些只是content-type的默认值,但如果用xhr.setRequestHeader()手动设置了中content-type的值,以上默认值就会被覆盖。

另外需要注意的是,若在断网状态下调用xhr.send(data)方法,则会抛错:Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest'。一旦程序抛出错误,如果不 catch 就无法继续执行后面的代码,所以调用 xhr.send(data)方法时,应该用 try-catch捕捉错误。

try{
    xhr.send(data)
  }catch(e) {
    //doSomething...
  };

xhr.withCredentialsCORS 什么关系

我们都知道,在发同域请求时,浏览器会将cookie自动加在request header中。但大家是否遇到过这样的场景:在发送跨域请求时,cookie并没有自动加在request header中。

造成这个问题的原因是:在CORS标准中做了规定,默认情况下,浏览器在发送跨域请求时,不能发送任何认证信息(credentials)如"cookies"和"HTTP authentication schemes"。除非xhr.withCredentialstruexhr对象有一个属性叫withCredentials,默认值为false)。

所以根本原因是cookies也是一种认证信息,在跨域请求中,client端必须手动设置xhr.withCredentials=true,且server端也必须允许request能携带认证信息(即response header中包含Access-Control-Allow-Credentials:true),这样浏览器才会自动将cookie加在request header中。

另外,要特别注意一点,一旦跨域request能够携带认证信息,server端一定不能将Access-Control-Allow-Origin设置为*,而必须设置为请求页面的域名。

xhr相关事件

事件分类

xhr相关事件有很多,有时记起来还挺容易混乱。但当我了解了具体代码实现后,就容易理清楚了。下面是XMLHttpRequest的部分实现代码:

interface XMLHttpRequestEventTarget : EventTarget {
  // event handlers
  attribute EventHandler onloadstart;
  attribute EventHandler onprogress;
  attribute EventHandler onabort;
  attribute EventHandler onerror;
  attribute EventHandler onload;
  attribute EventHandler ontimeout;
  attribute EventHandler onloadend;
};

interface XMLHttpRequestUpload : XMLHttpRequestEventTarget {

};

interface XMLHttpRequest : XMLHttpRequestEventTarget {
  // event handler
  attribute EventHandler onreadystatechange;
  readonly attribute XMLHttpRequestUpload upload;
};

从代码中我们可以看出:

  1. XMLHttpRequestEventTarget接口定义了7个事件:

    • onloadstart

    • onprogress

    • onabort

    • ontimeout

    • onerror

    • onload

    • onloadend

  2. 每一个XMLHttpRequest里面都有一个upload属性,而upload是一个XMLHttpRequestUpload对象

  3. XMLHttpRequestXMLHttpRequestUpload都继承了同一个XMLHttpRequestEventTarget接口,所以xhrxhr.upload都有第一条列举的7个事件

  4. onreadystatechangeXMLHttpRequest独有的事件

所以这么一看就很清晰了:
xhr一共有8个相关事件:7个XMLHttpRequestEventTarget事件+1个独有的onreadystatechange事件;而xhr.upload只有7个XMLHttpRequestEventTarget事件。

事件触发条件

下面是我自己整理的一张xhr相关事件触发条件表,其中最需要注意的是 onerror 事件的触发条件。

事件触发条件
onreadystatechange每当xhr.readyState改变时触发;但xhr.readyState由非0值变为0时不触发。
onloadstart调用xhr.send()方法后立即触发,若xhr.send()未被调用则不会触发此事件。
onprogressxhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次;xhr.onprogress在下载阶段(即xhr.readystate=3时)触发,每50ms触发一次。
onload当请求成功完成时触发,此时xhr.readystate=4
onloadend当请求结束(包括请求成功和请求失败)时触发
onabort当调用xhr.abort()后触发
ontimeoutxhr.timeout不等于0,由请求开始即onloadstart开始算起,当到达xhr.timeout所设置时间请求还未结束即onloadend,则触发此事件。
onerror在请求过程中,若发生Network error则会触发此事件(若发生Network error时,上传还没有结束,则会先触发xhr.upload.onerror,再触发xhr.onerror;若发生Network error时,上传已经结束,则只会触发xhr.onerror)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode4xx时,并不属于Network error,所以不会触发onerror事件,而是会触发onload事件。

事件触发顺序

当请求一切正常时,相关的事件触发顺序如下:

  1. 触发xhr.onreadystatechange(之后每次readyState变化时,都会触发一次)

  2. 触发xhr.onloadstart
    //上传阶段开始:

  3. 触发xhr.upload.onloadstart

  4. 触发xhr.upload.onprogress

  5. 触发xhr.upload.onload

  6. 触发xhr.upload.onloadend
    //上传结束,下载阶段开始:

  7. 触发xhr.onprogress

  8. 触发xhr.onload

  9. 触发xhr.onloadend

发生abort/timeout/error异常的处理

在请求的过程中,有可能发生 abort/timeout/error这3种异常。那么一旦发生这些异常,xhr后续会进行哪些处理呢?后续处理如下:

  1. 一旦发生aborttimeouterror异常,先立即中止当前请求

  2. readystate 置为4,并触发 xhr.onreadystatechange事件

  3. 如果上传阶段还没有结束,则依次触发以下事件:

    • xhr.upload.onprogress

    • xhr.upload.[onabort或ontimeout或onerror]

    • xhr.upload.onloadend

  4. 触发 xhr.onprogress事件

  5. 触发 xhr.[onabort或ontimeout或onerror]事件

  6. 触发xhr.onloadend 事件

在哪个xhr事件中注册成功回调?

从上面介绍的事件中,可以知道若xhr请求成功,就会触发xhr.onreadystatechangexhr.onload两个事件。 那么我们到底要将成功回调注册在哪个事件中呢?我倾向于 xhr.onload事件,因为xhr.onreadystatechange是每次xhr.readyState变化时都会触发,而不是xhr.readyState=4时才触发。

xhr.onload = function () {
    //如果请求成功
    if(xhr.status == 200){
      //do successCallback
    }
  }

上面的示例代码是很常见的写法:先判断http状态码是否是200,如果是,则认为请求是成功的,接着执行成功回调。这样的判断是有坑儿的,比如当返回的http状态码不是200,而是201时,请求虽然也是成功的,但并没有执行成功回调逻辑。所以更靠谱的判断方法应该是:当http状态码为2xx304时才认为成功。

  xhr.onload = function () {
    //如果请求成功
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
      //do successCallback
    }
  }

结语

终于写完了......
看完那一篇长长的W3C的xhr 标准,我眼睛都花了......
希望这篇总结能帮助刚开始接触XMLHttpRequest的你。

最后给点扩展学习资料,如果你:

查看原文

ireeoo 关注了专栏 · 2017-04-27

啃先生

前腾讯前端开发工程师,后来有一年时间经历参与创业,目前在券商。坚持原创,分享前端开发经验,创业故事,互联网行业思考。 @深圳

关注 89

ireeoo 赞了文章 · 2016-11-27

webpack多页应用架构系列(四):老式jQuery插件还不能丢,怎么兼容?

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006887523
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang

前言

目前前端虽处于百花齐放阶段,angular/react/vue竞相角逐,但毕竟尚未完全成熟,有些需求还是得依靠我们的老大哥jQuery的。

我个人对jQuery并不反感,但我对jQuery生态的停滞不前相当无奈,比如说赫赫有名的bootstrap(特指3代),在webpack上打包还得靠个loader的,太跟不上时势了。况且,bootstrap还算好的,有些jquery插件都有一两年没更新了,连NPM都没上架呢,可偏偏就是找不到它们的替代品,项目又急着要上,这可咋办呐?

别急,今天就教你适配兼容老式jQuery插件。

老式jQuery插件为和不能直接用webpack打包?

如果你把jQuery看做是一个普通的js模块来加载(要用到jQuery的模块统统先require后再使用),那么,当你加载老式jQuery插件时,往往会提示找不到jQuery实例(有时候是提示找不到$),这是为啥呢?

要解释这个问题,就必须先稍微解释一下jQuery插件的机制:jQuery插件是通过jQuery提供的jQuery.fn.extend(object)jQuery.extend(object)这俩方法,来把插件本身实现的方法挂载到jQuery(也即$)这个对象上的。传统引用jQuery及其插件的方式是先用<script>加载jQuery本身,然后再用同样的方法来加载其插件;jQuery会把jQuery对象设置为全局变量(当然也包括了$),既然是全局变量,那么插件们很容易就能找到jQuery对象并挂载自身的方法了。

而webpack作为一个遵从模块化原则的构建工具,自然是要把各模块的上下文环境给分隔开以减少相互间的影响;而jQuery也早已适配了AMD/CMD等加载方式,换句话说,我们在require jQuery的时候,实际上并不会把jQuery对象设置为全局变量。说到这里,问题也很明显了,jQuery插件们找不到jQuery对象了,因为在它们各自的上下文环境里,既没有局部变量jQuery(因为没有适配AMD/CMD,所以就没有相应的require语句了),也没有全局变量jQuery

怎么来兼容老式jQuery插件呢?

方法有不少,下面一个一个来看。

ProvidePlugin + expose-loader

首先来介绍我最为推荐的方法:ProvidePlugin + expose-loader,在我公司的项目,以及我个人的脚手架开源项目webpack-seed里使用的都是这一种方法。

ProvidePlugin的配置是这样的:

  var providePlugin = new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    'window.jQuery': 'jquery',
    'window.$': 'jquery',
  });

ProvidePlugin的机制是:当webpack加载到某个js模块里,出现了未定义且名称符合(字符串完全匹配)配置中key的变量时,会自动require配置中value所指定的js模块。

如上述例子,当某个老式插件使用了jQuery.fn.extend(object),那么webpack就会自动引入jquery(此处我是用NPM的版本,我也推荐使用NPM的版本)。

另外,使用ProvidePlugin还有个好处,就是,你自己写的代码里,再!也!不!用!require!jQuery!啦!毕竟少写一句是一句嘛哈哈哈。

接下来介绍expose-loader,这个loader的作用是,将指定js模块export的变量声明为全局变量。下面来看下expose-loader的配置:

/*
    很明显这是一个loader的配置项,篇幅有限也只能截取相关部分了
    看不明白的麻烦去看本系列的另一篇文章《webpack多页应用架构系列(二):webpack配置常用部分有哪些?》:https://segmentfault.com/a/1190000006863968
 */
{
  test: require.resolve('jquery'),  // 此loader配置项的目标是NPM中的jquery
  loader: 'expose?$!expose?jQuery', // 先把jQuery对象声明成为全局变量`jQuery`,再通过管道进一步又声明成为全局变量`$`
},

你或许会问,有了ProvidePlugin为嘛还需要expose-loader?问得好,如果你所有的jQuery插件都是用webpack来加载的话,的确用ProvidePlugin就足够了;但理想是丰满的,现实却是骨感的,总有那么些需求是只能用<script>来加载的。

externals

externals是webpack配置中的一项,用来将某个全局变量“伪装”成某个js模块的exports,如下面这个配置:

    externals: {
      'jquery': 'window.jQuery',
    },

那么,当某个js模块显式地调用var $ = require('jquery')的时候,就会把window,jQuery返回给它。

与上述ProvidePlugin + expose-loader的方案相反,此方案是先用<script>加载的jQuery满足老式jQuery插件的需要,再通过externals将其转换成符合模块化要求的exports。

我个人并不太看好这种做法,毕竟这就意味着jQuery脱离NPM的管理了,不过某些童鞋有其它的考虑,例如为了加快每次打包的时间而把jQuery这些比较大的第三方库给分离出去(直接调用公共CDN的第三方库?),也算是有一定的价值。

imports-loader

这个方案就相当于手动版的ProvidePlugin,以前我用requireJS的时候也是用的类似的手段,所以我一开始从requireJS迁移到webpack的时候用的也是这种方法,后来知道有ProvidePlugin就马上换了哈。

这里就不详细说明了,放个例子大家看看就懂:

// ./webpack.config.js

module.exports = {
    ...
    module: {
        loaders: [
            {
                test: require.resolve("some-module"),
                loader: "imports?$=jquery&jQuery=jquery", // 相当于`var $ = require("jquery");var jQuery = require("jquery");`
            }
        ]
    }
};

总结

以上的方案其实都属于shimming,并不特别针对jQuery,请举一反三使用。另外,上述方案并不仅用于shimming,比如用上ProvidePlugin来写少几个require,自己多多挖掘,很有乐趣的哈~~

补充

误用externals(2016-10-17更新)

有童鞋私信我,说用了我文章的方案依然提示$ is not a function,在我仔细分析后,发现:

  1. 他用的是我推荐的ProvidePlugin + expose-loader方案,也就是说,他已经把jquery打包进来了。
  2. 但是他又不明就里得配了externals:
  externals: {
    jquery: 'window.jQuery',
  },
  1. 然而实际上他并没有直接用<script>来引用jQuery,因此window.jQuery是个null。
  2. 结果,他的jquery插件获得的$就是个null了。

这里面我们可以看出,externals是会覆盖掉ProvidePlugin的。

但这里有个问题,expose-loader的作用就是设置好window.jQuery和window.$,那window.jQuery怎么会是null呢?我的猜想是:externals在expose-loader设置好window.jQuery前就已经取了window.jQuery的值(null)了。

说了这么多,其实关键意思就是,不要手贱不要手贱不要手贱(重要的事情说三遍)!

示例代码

诸位看本系列文章,搭配我在Github上的脚手架项目食用更佳哦(笑):Array-Huang/webpack-seed(https://github.com/Array-Huang/webpack-seed)

附系列文章目录(同步更新)

本文首发于Array_Huang的技术博客——实用至上,非经作者同意,请勿转载。
原文地址:https://segmentfault.com/a/1190000006887523
如果您对本系列文章感兴趣,欢迎关注订阅这里:https://segmentfault.com/blog/array_huang
查看原文

赞 31 收藏 108 评论 59

ireeoo 回答了问题 · 2016-07-16

css3聊天气泡框随内容变化四周不变是怎么样做成的

聊天窗口本身有个position:relative,四个角上采用绝对定位,这样,无论内容这么变化,四个角依旧在那里,位置不变。至于四个角是采用before,after伪类来写,还是采用单独的标签来写,见仁见智

关注 9 回答 7

ireeoo 发布了文章 · 2016-03-05

阿里云部署gitlab填坑记

本文同步自我的博客
地址:http://www.reeoo.me/archives/gitlab.html

我的博客是基于hexo生成的静态博客,每次写完文章,hexo g一下,然后把生成的文件用ftp上传到server,虽然一切正常,但是感觉这种方式很low,想尝试一下自动部署,无奈在服务器上部署git服务器多次,都没能成功,经群里面的大神推荐,于是尝试一把gitlab

像往常一样,不会搞,先在网上找一篇教程,CentOS安装GitLab,按照这篇,搞了一边,各种坑,边填坑边记录吧。

软件环境

先来说说我的软件环境

软件版本
Systemcentos6.7
Python2.6
Ruby2.1.5
Git2.7.0
Redis2.0
GitLab7.8.4
GitLab Shell2.6.0

大概如此。

yum源切换

由于天朝众所周知的原因,为了提高软件安装速度,可以将yum源设置为阿里云开源镜像。

cd /etc/yum.repos.d
wget -O CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-6.repo

但是某些包还是安装的很慢。

必要软件包

yum -y install libicu-devel patch gcc-c++ readline-devel zlib-devel libffi-devel openssl-devel make autoconf automake libtool bison libxml2-devel libxslt-devel libyaml-devel zlib-devel openssl-devel cpio expat-devel gettext-devel curl-devel perl-ExtUtils-CBuilder perl-ExtUtils-MakeMaker

安装Git

//查看当前git版本
git --version
// 如果小于1.7.10则先卸载
yum remove git
// 下载最新的git并安装
wget -O git-src.zip https://github.com/git/git/archive/master.zip
unzip git-src.zip
cd git-src
make prefix=/usr/local all
make prefix=/usr/local install
ln -fs /usr/local/bin/git* /usr/bin/

安装Ruby环境

mkdir /tmp/ruby && cd /tmp/ruby
curl --progress ftp://ftp.ruby-lang.org/pub/ruby/ruby-2.1.5.tar.gz | tar xz
// 这步有点慢,等的我差点睡着了,基本上一秒钟0.1%    
cd ruby-2.1.5
./configure --disable-install-rdoc
make && make install

ln -s /usr/local/bin/bundle /usr/bin/bundle
ln -s /usr/local/bin/ruby /usr/bin/ruby
ln -s /usr/local/bin/gem /usr/bin/gem

// 设置ruby gem源为淘宝源
gem source -r https://rubygems.org/
gem source -a https://ruby.taobao.org/  //这里应该是https,原文是`http`,也是个坑
gem install bundler --no-ri --no-rdoc

安装MySQL及初始化GitLab库

yum install mysql mysql-devel mysql-server -y
/etc/init.d/mysqld start
chkconfig mysqld on

// 登录mysql创建gitab的帐号和数据库
mysql> CREATE USER 'gitlab'@'localhost' IDENTIFIED BY 'gitlab';
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER ON `gitlabhq_production`.* TO 'gitlab'@'localhost';

安装Redis

yum -y install redis
/etc/init.d/redis start
chkconfig redis on

添加git帐号并允许sudo

useradd --comment 'gitlab' git
echo "git ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers

测试是否可以用git帐号登录数据库

sudo -u git -H mysql -u gitlab -p -D gitlabhq_production

安装GitLab

cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-8-stable gitlab
cd /home/git/gitlab
sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml
   
//编辑git路径, gitlab的host:port
vim config/gitlab.yml
// bin_path: /usr/local/bin/git
// host: localhost
// port: 80 
   
// 给文件夹添加相应的权限
chown -R git log/
chown -R git tmp/
chmod -R u+rwX  log/
chmod -R u+rwX  tmp/
   
// 创建必要的文件夹,以及复制配置文件
sudo -u git -H mkdir /home/git/gitlab-satellites
sudo -u git -H mkdir tmp/pids/
sudo -u git -H mkdir tmp/sockets/
sudo chmod -R u+rwX  tmp/pids/
sudo chmod -R u+rwX  tmp/sockets/
sudo -u git -H mkdir public/uploads
sudo chmod -R u+rwX  public/uploads
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
sudo -u git -H cp config/initializers/rack_attack.rb.example
config/initializers/rack_attack.rb
// 可能某些文件夹已经存在了,直接跳过~~~
// 配置数据库连接信息
sudo -u git cp config/database.yml.mysql config/database.yml
sudo -u git -H vim  config/database.yml
vim config/database.yml
// production:
// username: gitlab
// password: "gitlab

安装GitLab-Shell

cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-shell.git -b v2.6.0
cd gitlab-shell/
sudo -u git -H cp config.yml.example config.yml

// 编辑配置文件, 设置gitlab_url, redis-cli, log-level...
vim config.yml
// gitlab_url: "http://localhost/"
// /usr/bin/redis-cli

// 安装git-shell
sudo -u git -H ./bin/install

安装需要ruby的gems

在执行下面的命令之前先要把gem的默认源改为淘宝的,虽然前面改过了,但这里还是默认的没改的,不知道怎么回事,需要vim Gemfile,修改source "https://rubygems.org"https://ruby.taobao.org/,注意是https不是http

cd /home/git/gitlab
sudo -u git -H bundle install --deployment --without development test postgres aw  

这个过程可能会出现下面的错误:

An error occurred while installing rugged (0.21.2), and Bundler cannot continue.
Make sure that `gem install rugged -v '0.21.2'` succeeds before bundling.

需要安装:

sudo yum -y install cmake

然后重新执行就OK了。

初始化数据库(创建GitLab相关表)

sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production

这步完成会出现用户名和密码

Administrator account created:
login.........root
password......5iveL!fe

安装启动文件以及日志切割文件

cp lib/support/init.d/gitlab /etc/init.d/gitlab
cp lib/support/init.d/gitlab.default.example /etc/default/gitlab
cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab

设置git帐号信息

$ sudo -u git -H git config --global user.name "Reeoo Shen"
$ sudo -u git -H git config --global user.email "ireeoo@163.com"
$ sudo -u git -H git config --global core.autocrlf input

这一步虽然设置了这些信息,但是到后面执行检查的时候,还是会说你没有设置。。。

安装nginx

我的服务器本来就装的nginx,这一步我就不写了
更改权限,启动nginx

nginx -t
chown -R git:git /var/lib/nginx/
/etc/init.d/nginx start

检测当前环境

sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production

输出:

[root@iZ234fbksx3Z gitlab]# sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production

System information
System:        CentOS 6.7
Current User:    git
Using RVM:    no
Ruby Version:    2.1.5p273
Gem Version:    2.2.2
Bundler Version:1.11.2
Rake Version:    10.3.2
Sidekiq Version:3.3.0

GitLab information
Version:    7.8.4
Revision:    019ffd9
Directory:    /home/git/gitlab
DB Adapter:    mysql2
URL:        http://testxxx.reeoo.me
HTTP Clone URL:    http://testxxx.reeoo.me/some-project.git
SSH Clone URL:    git@testxxx.reeoo.me:some-project.git
Using LDAP:    no
Using Omniauth:    no

GitLab Shell
Version:    2.6.0
Repositories:    /home/git/repositories/
Hooks:        /home/git/gitlab-shell/hooks/
Git:        /usr/local/bin/git

拉取gitlab静态资源文件

sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production

启动gitlab

/etc/init.d/gitlab start

检测各个组件是否正常工作

sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production

这里就是坑了,各种no。。。。
是不是我看错教程了,我怀疑这个教程本来就是跑不通的,尼玛。。。
经过将近一天的折腾,终于把问题降到了就剩下一个了:

Checking Environment ...

Git configured for git user? ... yes

Checking Environment ... Finished

Checking GitLab Shell ...

GitLab Shell version >= 2.5.4 ? ... OK (2.6.0)
Repo base directory exists? ... yes
Repo base directory is a symlink? ... no
Repo base owned by git:git? ... yes
Repo base access is drwxrws---? ... yes
Satellites access is drwxr-x---? ... yes
hooks directories in repos are links: ... 
reeoo / mytest ... ok
Running /home/git/gitlab-shell/bin/check
Check GitLab API access: OK
Check directories and files: 
    /home/git/repositories: OK
    /home/git/.ssh/authorized_keys: OK
Test redis-cli executable: redis-cli 2.4.10
Send ping to redis server: PONG
gitlab-shell self-check successful

Checking GitLab Shell ... Finished

Checking Sidekiq ...

Running? ... yes
Number of Sidekiq processes ... 1

Checking Sidekiq ... Finished

Checking LDAP ...

LDAP is disabled in config/gitlab.yml

Checking LDAP ... Finished

Checking GitLab ...

Database config exists? ... yes
Database is SQLite ... no
All migrations up? ... yes
Database contains orphaned GroupMembers? ... no
GitLab config exists? ... yes
GitLab config outdated? ... no
Log directory writable? ... yes
Tmp directory writable? ... yes
Init script exists? ... yes
Init script up-to-date? ... no
  Try fixing it:
  Redownload the init script
  For more information see:
  doc/install/installation.md in section "Install Init Script"
  Please fix the error above and rerun the checks.
projects have namespace: ... 
reeoo / mytest ... yes
Projects have satellites? ... 
reeoo / mytest ... yes
Redis version >= 2.0.0? ... yes
Ruby version >= 2.0.0 ? ... yes (2.1.5)
Your git bin path is "/usr/local/bin/git"
Git version >= 1.7.10 ? ... yes (2.7.0)

Checking GitLab ... Finished

就是这个

Init script up-to-date? ... no
  Try fixing it:
  Redownload the init script
  For more information see:
  doc/install/installation.md in section "Install Init Script"
  Please fix the error above and rerun the checks.

有人说可以忽略这个,我也没找到解决方案,只能暂时忽略了,不知道后面的本地clone仓库里面的代码老是要求输入密码,是不是跟这个也有关系。

最后访问testxxx.reeoo.me出现502 Bad GateWay
查询nginx日志,
tail /var/log/nginx/gitlab_error.log
发现是"/home/git/gitlab/public/favicon.ico.html" failed (13: Permission denied)

加上权限,chmod 775 /home/gittestxxx.reeoo.me可以访问了(之前的那个检查里面有好多有问题的时候,貌似testxxx.reeoo.me也是可以正常访问的),新建个项目,发现不能commit
Your changes could not be committed, because the file has been changed(在gitlab服务器上commit
commit就报这个错。。。
发现gitlab-shell/config.yml里面的gitlab_url: "testxxx.reeoo.me:80"是这样的,
改成gitlab_url: "http://testxxx.reeoo.me"重启再试,我靠,好了,Your changes have been successfully committed

问题

  1. 前面说到设置了git的信息,但执行检查的时候,还是会说你没有设置git信息,
    参考Error in Gitlab: git configured for git user … no try fixing it

解决了

  1. 关于redis配置的错误:

        sudo -u git ./check
        Check GitLab API access: OK
        Check directories and files:
                /home/git/repositories: OK
                /home/git/.ssh/authorized_keys: OK
        Test redis-cli executable: redis-cli 2.4.10
        Send ping to redis server: Could not connect to Redis at /var/run/redis/redis.sock: No such file or directory

    到/home/git/gitlab-shell/config.yml里面把socket注掉,重新运行检查就好了:

        redis:
          bin: /usr/bin/redis-cli
          # host: 127.0.0.1
          # port: 6379
          # pass: redispass # Allows you to specify the password for Redis
          database: 0
          #socket: /var/run/redis/redis.sock # Comment out this line if you want to use TCP //把这行注掉
          namespace: resque:gitlab
  2. 期间还遇到一个问题,说可用内存不足的(我的阿里云服务器单核1G内存),网上查了一下,貌似需要做别的处理,挺麻烦,直接又花了400RMB把内存升级到了2G。

  3. 最后一个问题,也是发这篇文章的时候还没解决的:
    把ssh key上传到gitlab之后,clone仓库,老是要求输入密码,但是密码怎么输都不对,不知道怎么回事,百度谷歌了N多资料,也没有解决,目前gitlab算是部署了,但是不能clone,更别提pull,commit,push了,继续折腾中。。。。

等把这个问题解决了,在折腾自动部署的功能。

最后希望遇到过上面第三个问题的网友,说说解决方法,或者探讨一下,谢谢!

查看原文

赞 1 收藏 31 评论 21

认证与成就

  • 获得 20 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-07-01
个人主页被 538 人浏览