wind

wind 查看完整档案

杭州编辑浙江工业大学  |  计算机科学与技术 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

wind 赞了回答 · 2019-06-30

解决redux-thunk有什么用?

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这是redux-thunk所有的源代码,默认情况下redux只能dispatch一个plain object,例如:

dispatch({
    type: 'SOME_ACTION_TYPE',
    data: 'xxxx'
});

使用 redux-thunk 之后,可以dispatch一个函数了,这个函数会接收dispatch, getState作为参数,在这个函数里你就可以干你想干的事情,在任何地方随意dispatch了,例如下面这个ajax请求:

dispatch(function (dispatch) {
    $.get('/api/users', function(users) {
        dispatch({
            type: 'FETCH_USERS_SUCCESS',
            users: users,
        });
    });
});

关注 5 回答 2

wind 赞了文章 · 2019-06-09

利用 JavaScript 数据绑定实现一个简单的 MVVM 库

MVVM 是 Web 前端一种非常流行的开发模式,利用 MVVM 可以使我们的代码更专注于处理业务逻辑而不是去关心 DOM 操作。目前著名的 MVVM 框架有 vue, avalon , angular 等,这些框架各有千秋,但是实现的思想大致上是相同的:数据绑定 + 视图刷新。出于好奇和一颗愿意折腾的心,我自己也沿着这个方向写了一个最简单的 MVVM 库 ( mvvm.js ),总共 2000 多行代码,指令的命名和用法与 vue 相似,在这里分享一下实现的原理以及我的代码组织思路。

思路整理

MVVM 在概念上是真正将视图与数据逻辑分离的模式,ViewModel 是整个模式的重点。要实现 ViewModel 就需要将数据模型(Model)和视图(View)关联起来,整个实现思路可以简单的总结成 5 点:

  1. 实现一个 Compiler 对元素的每个节点进行指令的扫描和提取;

  2. 实现一个 Parser 去解析元素上的指令,能够把指令的意图通过某个刷新函数更新到 dom 上(中间可能需要一个专门负责视图刷新的模块)比如解析节点 <p v-show="isShow"></p> 时先取得 Model 中 isShow 的值,再根据 isShow 更改 node.style.display 从而控制元素的显示和隐藏;

  3. 实现一个 Watcher 能将 Parser 中每条指令的刷新函数和对应 Model 的字段联系起来;

  4. 实现一个 Observer 使得能够对对象的所有字段进行值的变化监测,一旦发生变化时可以拿到最新的值并触发通知回调;

  5. 利用 Observer 在 Watcher 中建立一个对 Model 的监听 ,当 Model 中的一个值发生变化时,监听被触发,Watcher 拿到新值后调用在步骤 2 中关联的那个刷新函数,就可以实现数据变化的同时刷新视图的目的。

效果示例

首先粗看下最终的使用示例,与其他 MVVM 框架的实例化大同小异:

<div id="mobile-list">
    <h1 v-text="title"></h1>
    <ul>
        <li v-for="item in brands">
            <b v-text="item.name"></b>
            <span v-show="showRank">Rank: {{item.rank}}</span>
        </li>
    </ul>
</div>
var element = document.querySelector('#mobile-list');
var vm = new MVVM(element, {
    'title'   : 'Mobile List',
    'showRank': true,
    'brands'  : [
        {'name': 'Apple', 'rank': 1},
        {'name': 'Galaxy', 'rank': 2},
        {'name': 'OPPO', 'rank': 3}
    ]
});

vm.set('title', 'Top 3 Mobile Rank List'); // => <h1>Top 3 Mobile Rank List</h1>

模块划分

我把 MVVM 分成了五个模块去实现: 编译模块 Compiler 、解析模块 Parser 、视图刷新模块 Updater 、数据订阅模块 Watcher 和 数据监听模块 Observer 。流程可以简述为:Compiler 编译好指令后将指令信息交给解析器 Parser 解析,Parser 更新初始值并向 Watcher 订阅数据的变化,Observer 监测到数据的变化然后反馈给 Watcher ,Watcher 再将变化结果通知 Updater 找到对应的刷新函数进行视图的刷新。

上述流程如图所示:

图片描述

下文就介绍下这五个模块实现的基本原理(代码只贴重点部分,完整的实现请到我的 Github 翻阅)

1. 编译模块 Compiler

Compiler 的职责主要是对元素的每个节点进行指令的扫描和提取。因为编译和解析的过程会多次遍历整个节点树,所以为了提高编译效率在 MVVM 构造函数内部先将 element 转成一个文档碎片形式的副本 fragment 编译对象是这个文档碎片而不应该是目标元素,待全部节点编译完成后再将文档碎片添加回到原来的真实节点中。

vm.complieElement 实现了对元素所有节点的扫描和指令提取:

vm.complieElement = function(fragment, root) {
    var node, childNodes = fragment.childNodes;
    // 扫描子节点
    for (var i = 0; i < childNodes.length; i++) {
        node = childNodes[i];
        if (this.hasDirective(node)) {
            this.$unCompileNodes.push(node);
        }
        // 递归扫描子节点的子节点
        if (node.childNodes.length) {
            this.complieElement(node, false);
        }
    }
    // 扫描完成,编译所有含有指令的节点
    if (root) {
        this.compileAllNodes();
    }
}

vm.compileAllNodes 方法将会对 this.$unCompileNodes 中的每个节点进行编译(将指令信息交给 Parser ),编译完一个节点后就从缓存队列中移除它,同时检查 this.$unCompileNodes.length 当 length === 0 时说明全部编译完成,可以将文档碎片追加到真实节点上了。

2. 指令解析模块 Parser

当编译器 Compiler 把每个节点的指令提取出来后就可以给到解析器解析了。每一个指令都有不同的解析方法,所有指令的解析方法只要做好两件事:一是将数据值更新到视图上(初始状态),二是将刷新函数订阅到 Model 的变化监测中。这里以解析 v-text 为例描述一个指令的大致解析方法:

parser.parseVText = function(node, model) {
    // 取得 Model 中定义的初始值 
    var text = this.$model[model];
    // 更新节点的文本
    node.textContent = text;
    // 对应的刷新函数:
    // updater.updateNodeTextContent(node, text);
    
    // 在 watcher 中订阅 model 的变化
    watcher.watch(model, function(last, old) {
        node.textContent = last;
        // updater.updateNodeTextContent(node, text);
    });
}

3. 数据订阅模块 Watcher

上个例子,Watcher 提供了一个 watch 方法来对数据变化进行订阅,一个参数是模型字段 model 另一个是回调函数,回调函数是要通过 Observer 来触发的,参数传入新值 last 和 旧值 old , Watcher 拿到新值后就可以找到 model 对应的回调(刷新函数)进行更新视图了。model 和 刷新函数是一对多的关系,即一个 model 可以有任意多个处理它的回调函数(刷新函数),比如:v-text="title"v-html="title" 两个指令共用一个数据模型字段。

添加数据订阅 watcher.watch 实现方式为:

watcher.watch = function(field, callback, context) {
    var callbacks = this.$watchCallbacks;

    if (!Object.hasOwnProperty.call(this.$model, field)) {
        console.warn('The field: ' + field + ' does not exist in model!');
        return;
    }

    // 建立缓存回调函数的数组
    if (!callbacks[field]) {
        callbacks[field] = [];
    }
    // 缓存回调函数
    callbacks[field].push([callback, context]);
}

当数据模型的 field 字段发生改变时,Watcher 就会触发缓存数组中订阅了 field 的所有回调。

4. 数据监听模块 Observer

Observer 是整个 mvvm 实现的核心基础,看过有一篇文章说 O.o (Object.observe) 将会引爆数据绑定革命,给前端带来巨大影响力,不过很可惜,ES7 草案已经将 O.o 给废弃了!目前也没有浏览器支持!所幸的是还有 Object.defineProperty 通过拦截对象属性的存取描述符(get 和 set) 可以模拟一个简单的 Observer :

// 拦截 object 的 prop 属性的 get 和 set 方法
Object.defineProperty(object, prop, {
    get: function() {
        return this.getValue(object, prop);
    },

    set: function(newValue) {
        var oldValue = this.getValue(object, prop);
        if (newValue !== oldValue) {
            this.setValue(object, newValue, prop);
            // 触发变化回调
            this.triggerChange(prop, newValue, oldValue);
        }
    }
});

然后还有个问题就是数组操作 ( push, shift 等) 该如何监测?所有的 MVVM 框架都是通过重写该数组的原型来实现的:

observer.rewriteArrayMethods = function(array) {
    var self = this;
    var arrayProto = Array.prototype;
    var arrayMethods = Object.create(arrayProto);
    var methods = 'push|pop|shift|unshift|splice|sort|reverse'.split('|');
    
    methods.forEach(function(method) {
        Object.defineProperty(arrayMethods, method, function() {
            var i = arguments.length;
            var original = arrayProto[method];
            
            var args = new Array(i);
            while (i--) {
                args[i] = arguments[i];
            }
            
            var result = original.apply(this, args);

            // 触发回调
            self.triggerChange(this, method);

            return result;
        });
    });
    
    array.__proto__ = arrayMethods;
}

这个实现方式是从 vue 中参考来的,觉得用的很妙,不过数组的 length 属性是不能够被监听到的,所以在 MVVM 中应避免操作 array.length

5. 视图刷新模块 Updater

Updater 在五个模块中是最简单的,只需要负责每个指令对应的刷新函数即可。其他四个模块经过一系列的折腾,把最后的成果交给到 Updater 进行视图或者事件的更新,比如 v-text 的刷新函数为:

updater.updateNodeTextContent = function(node, text) {
    node.textContent = text;
}

v-bind:style 的刷新函数:

updater.updateNodeStyle = function(node, propperty, value) {
    node.style[propperty] = value;
}

双向数据绑定的实现

表单元素的双向数据绑定是 MVVM 的一个最大特点之一:

图片描述

其实这个神奇的功能实现原理也很简单,要做的只有两件事:一是数据变化的时候更新表单值,二是反过来表单值变化的时候更新数据,这样数据的值就和表单的值绑在了一起。

数据变化更新表单值 利用前面说的 Watcher 模块很容易就可以做到:

watcher.watch(model, function(last, old) {
    input.value = last;
});

表单变化更新数据 只需要实时监听表单的值得变化事件并更新数据模型对应字段即可:

var model = this.$model;
input.addEventListenr('change', function() {
    model[field] = this.value;
});

其他表单 radio, checkbox 和 select 都是一样的原理。

以上,整个流程以及每个模块的基本实现思路都讲完了,语言表达能力不太好,如有说的不对写的不好的地方,希望大家能够批评指正!

结语

折腾这个简单的 mvvm.js 是因为原来自己的框架项目中用的是 vue.js 但是只是用到了它的指令系统,一大堆功能只用到四分之一左右,就想着只是实现 data-binding 和 view-refresh 就够了,结果没找这样的 javascript 库,所以我自己就造了这么一个轮子。

虽说功能和稳定性远不如 vue 等流行 MVVM 框架,代码实现可能也比较粗糙,但是通过造这个轮子还是增长了很多知识的 ~ 进步在于折腾嘛!

目前我的 mvvm.js 只是实现了最本的功能,以后我会继续完善、健壮它,如有兴趣欢迎一起探讨和改进~

源代码传送门: https://github.com/tangbc/sugar

查看原文

赞 41 收藏 275 评论 32

wind 赞了文章 · 2019-06-09

面试题:你能写一个Vue的双向数据绑定吗?

在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理。本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧。结合注释,希望能让大家有所收获。

1、原理

Vue的双向数据绑定的原理相信大家也都十分了解了,主要是通过 Object对象的defineProperty属性,重写data的set和get函数来实现的,这里对原理不做过多描述,主要还是来实现一个实例。为了使代码更加的清晰,这里只会实现最基本的内容,主要实现v-model,v-bind 和v-click三个命令,其他命令也可以自行补充。

添加网上的一张图

2、实现

页面结构很简单,如下

<div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">增加</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>

包含:

 1. 一个input,使用v-model指令
 2. 一个button,使用v-click指令
 3. 一个h3,使用v-bind指令。

我们最后会通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释

var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })

首先我们需要定义一个myVue构造函数:

function myVue(options) {
  
}

为了初始化这个构造函数,给它添加一 个_init属性

function myVue(options) {
  this._init(options);
}
myVue.prototype._init = function (options) {
    this.$options = options;  // options 为上面使用时传入的结构体,包括el,data,methods
    this.$el = document.querySelector(options.el); // el是 #app, this.$el是id为app的Element元素
    this.$data = options.data; // this.$data = {number: 0}
    this.$methods = options.methods;  // this.$methods = {increment: function(){}}
  }

接下来实现_obverse函数,对data进行处理,重写data的set和get函数

并改造_init函数

 myVue.prototype._obverse = function (obj) { // obj = {number: 0}
    var value;
    for (key in obj) {  //遍历obj对象
      if (obj.hasOwnProperty(key)) {
        value = obj[key]; 
        if (typeof value === 'object') {  //如果值还是对象,则遍历处理
          this._obverse(value);
        }
        Object.defineProperty(this.$data, key, {  //关键
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`获取${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
            }
          }
        })
      }
    }
  }
 
 myVue.prototype._init = function (options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
   
    this._obverse(this.$data);
  }

接下来我们写一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新

function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //指令名称,例如文本节点,该值设为"text"
    this.el = el;             //指令对应的DOM元素
    this.vm = vm;             //指令所属myVue实例
    this.exp = exp;           //指令对应的值,本例如"number"
    this.attr = attr;         //绑定的属性值,本例为"innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp]; //比如 H3.innerHTML = this.data.number; 当number改变时,会触发这个update函数,保证对应的DOM内容进行了更新。
  }

更新_init函数以及_obverse函数

myVue.prototype._init = function (options) {
    //...
    this._binding = {};   //_binding保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
    //...
  }
 
  myVue.prototype._obverse = function (obj) {
    //...
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {    // 按照前面的数据,_binding = {number: _directives: []}                                                                                                                                                  
          _directives: []
        };
        //...
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          //...
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {  // 当number改变时,触发_binding[number]._directives 中的绑定的Watcher类的更新
                item.update();
              })
            }
          }
        })
      }
    }
  }

那么如何将view与model进行绑定呢?接下来我们定义一个_compile函数,用来解析我们的指令(v-bind,v-model,v-clickde)等,并在这个过程中对view与model进行绑定。

 myVue.prototype._init = function (options) {
   //...
    this._complie(this.$el);
  }
 
myVue.prototype._complie = function (root) { root 为 id为app的Element元素,也就是我们的根元素
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {  // 对所有元素进行遍历,并进行处理
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {  // 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);  //bind是使data的作用域与method函数的作用域保持一致
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // 如果有v-model属性,并且元素是INPUT或者TEXTAREA,我们监听它的input事件
        node.addEventListener('input', (function(key) {  
          var attrVal = node.getAttribute('v-model');
           //_this._binding['number']._directives = [一个Watcher实例]
           // 其中Watcher.prototype.update = function () {
           //    node['vaule'] = _this.$data['number'];  这就将node的值保持与number一致
           // }
          _this._binding[attrVal]._directives.push(new Watcher(  
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value; // 使number 的值与 node的value保持一致,已经实现了双向绑定
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) { // 如果有v-bind属性,我们只要使node的值及时更新为data中number的值即可
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

至此,我们已经实现了一个简单vue的双向绑定功能,包括v-bind, v-model, v-click三个指令。效果如下图

附上全部代码,不到150行

<!DOCTYPE html>
<head>
  <title>myVue</title>
</head>
<style>
  #app {
    text-align: center;
  }
</style>
<body>
  <div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">增加</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>
</body>

<script>
  function myVue(options) {
    this._init(options);
  }

  myVue.prototype._init = function (options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;

    this._binding = {};
    this._obverse(this.$data);
    this._complie(this.$el);
  }
 
  myVue.prototype._obverse = function (obj) {
    var value;
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {                                                                                                                                                          
          _directives: []
        };
        value = obj[key];
        if (typeof value === 'object') {
          this._obverse(value);
        }
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`获取${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`更新${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {
                item.update();
              })
            }
          }
        })
      }
    }
  }

  myVue.prototype._complie = function (root) {
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
        node.addEventListener('input', (function(key) {
          var attrVal = node.getAttribute('v-model');
          _this._binding[attrVal]._directives.push(new Watcher(
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value;
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) {
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

  function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //指令名称,例如文本节点,该值设为"text"
    this.el = el;             //指令对应的DOM元素
    this.vm = vm;             //指令所属myVue实例
    this.exp = exp;           //指令对应的值,本例如"number"
    this.attr = attr;         //绑定的属性值,本例为"innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp];
  }

  window.onload = function() {
    var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })
  }
</script>

如果喜欢请关注我的Github,给个Star吧,我会定期分享一些JS中的知识,^_^

查看原文

赞 130 收藏 176 评论 31

wind 关注了专栏 · 2019-03-28

Go语言

walkerqiao's golang

关注 157

wind 关注了用户 · 2019-03-28

WalkerQiao @walkerqiao

专注前端开发。个人博客地址: http://www.mh611.com/blog http://joescott.coding.me/blog 。 两个博客内容相同,均为个人学习记录, 欢迎有兴趣的一起探讨前端技术。

关注 125

wind 赞了文章 · 2019-03-28

Go微服务 - 第二部分 - 构建第一个Go微服务

第二部分: Go微服务 - 构建我们的第一个服务

第二部分包含:

  • 设置我们的Go工作空间。
  • 构建我们第一个微服务。
  • 通过HTTP使用Gorilla Web Toolkit来提供一些JSON服务。

介绍

虽然通过HTTP提供JSON服务不是内部服务和外部服务的唯一选择,但本文聚焦的是HTTP和JSON. 使用RPC机制和二进制消息格式(例如Protocol Buffer)也用于内部通信或外部通信也是非常有趣的,特别是当外部消费者属于另外一个系统的时候。Go语言有内置的RPC支持,并且gRPC也是完全值得看看的。 然而,我们现在只聚焦基于由http包和Gorilla Web Toolkit提供的HTTP。

另外一个需要考虑的方面是很多有用的框架(安全、跟踪等等), 依赖于HTTP头来传输参与者正在进行的请求状态。我们在本文中将看到的例子是我们如何在头中传递相关ID和OAuth票据。虽然其他协议当然也支持类似的机制, 很多框架都是以HTTP构建的,我更愿意尽可能的保持我们的集成更加直接。

设置Go工作空间

如果你是一个经验丰富的Go开发者,你可以随意跳过本节内容。 以我拙见,Go语言工作空间结构需要一些时间来适应。一般来说我习惯使用项目根作为工作空间的根,Go语言约定了如何恰当的构造工作空间,因此go编译器可以查找源代码和依赖,有点不正统, 将源代码放在子目录下源码控制路径后以src命名的目录中.我强烈推荐读下官方指南和本文,然后再开始。 我希望我就是这样的。

安装SDK

在开始写我们第一行代码之前(或check out完整代码之前), 我们需要安装Go语言SDK。建议按照官方指导来操作,直接操作就足够了。

设置开发环境

在这些博客系列中,我们将使用我们安装的内置的Go SDK工具来构建和运行我们的代码,以及按照惯用方式来设置Go的工作空间。

1. 创建工作空间的根目录

所有命令都基于OS X或Linux开发环境。 如果你运行的是Windows, 请采用必要的指令。

mkdir ~/goworkspace
cd goworkspace
export GOPATH=`pwd`

这里我们创建了一个根目录,然后将GOPATH环境变量赋于那个目录。这就是我们的工作空间的根目录,我们所写的所有Go语言代码和第三方类库都在它下面。我推荐添加这个GOPATH到.bash_profile文件或类似的配置文件中,这样不需要每次都为每个控制台窗口重置它。

2. 为我们第一个项目创建文件夹和文件

鉴于我们已经在工作空间的根目录(例如,和在GOPATH环境变量中指定相同的目录), 执行下面的语句:

mkdir -p src/github.com/callistaenterprise

如果你希望遵循自己的编码,可以执行下面的命令:

cd src/github.com/callistaenterprise
mkdir -p goblog/accountservice
cd goblog/accountservice
touch main.go
mkdir service

或者你可以clone这个git仓库,包含相同代码,并切换到P2分支。 从上面你创建的src/github.com/callistaenterprise/goblog.git目录, 执行下面的命令。

git clone https://github.com/callistaenterprise/goblog.git
cd goblog
git checkout P2

记住: $GOPATH/src/github.com/callistaenterprise/goblog是我们项目的根目录,并且实际是存储在github上面的。

那么我们结构已经足够可以很方便开始了。 用你喜欢的IDE打开main.go文件。

创建服务 - main.go

Go语言中的main函数就是你具体做事的地方 - Go语言应用程序的入口点。 下面我们看看它的具体代码:

package main

import (
    "fmt"
)

var appName = "accountservice"

func main() {
    fmt.Printf("Starting %v\n", appName)
}

然后运行该程序:

> go run path/to/main.go
Starting accountservice

就是这样的,程序只打印了一个字符串,然后就退出了。是时候添加第一个HTTP端点了。

构建HTTP web服务器

注意: 这些HTTP示例的基础是从一个优秀的博客文章派生出来的, 见参考链接。

为了保持代码整洁,我们把所有HTTP服务相关的文件放到service目录下面。

启动HTTP服务器

在service目录中创建webservice.go文件。

package service

import (
    "log"
    "net/http"
)

func StartWebServer(port string) {
    log.Println("Starting HTTP service at " + port)
    err := http.ListenAndServe(":"+port, nil) // Goroutine will block here

    if err != nil {
        log.Println("An error occured starting HTTP listener at port " + port)
        log.Println("Error: " + err.Error())
    }
}

上面我们使用内置net/http包执行ListenAndServe, 在指定的端口号启动一个HTTP服务器。

然后我们更新下main.go代码:

package main

import (
    "fmt"
    "github.com/callistaenterprise/goblog/accountservice/service" // 新增代码
)

var appName = "accountservice"

func main() {
    fmt.Printf("Starting %v\n", appName)
    service.StartWebServer("6767") // 新增代码
}

然后再次运行这个程序,得到下面的输出:

> go run *.go
Starting accountservice
2017/01/30 19:36:00 Starting HTTP service at 6767

那么现在我们就有一个HTTP服务器,它监听localhost的6767端口。然后curl它:

> curl http://localhost:6767
404 page not found

得到404完全是意料之中的,因为我们还没有添加任何路由呢。

Ctrl+C停止这个web服务器。

添加第一个路由

是时候让我们的服务器提供一些真正的服务了。我们首先用Go语言结构声明我们的第一个路由,我们将使用它来填充Gorilla路由器。 在service目录中,创建一个routes.go文件。

package service

import (
    "net/http"
)

// Define a single route, e.g. a human readable name, HTTP method and the pattern the function that will execute when the route is called.

type Route struct {
    Name        string
    Method      string
    Pattern     string
    HandlerFunc http.HandlerFunc
}

// Defines the type Routes which is just an array (slice) of Route structs.
type Routes []Route

var routes = Routes{
    Route{
        "GetAccount", // Name
        "GET",        // HTTP method
        "/accounts/{accountId}", // Route pattern
        func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "application/json; charset=UTF-8")
            w.Write([]byte("{\"result\":\"OK\"}"))
        },
    },
}

上面代码片段,我们声明了一个路径/accounts/{accountId}, 我们后面会用curl来访问它。Gorilla也支持使用正则模式匹配、schemes, methods, queries, headers值等等的复杂路由。因此不限于路径和路径参数。

我们在响应的时候,硬编码了一个小的JSON消息:

{
    "result": "OK"
}

我们还需要一些模式化的代码片段,将我们声明的路由挂钩到实际的Gorilla Router上。 在service目录,我们创建router.go文件:

package service

import (
    "github.com/gorilla/mux"
)

// Function that returns a pointer to a mux.Router we can use as a handler.
func NewRouter() *mux.Router {
    // Create an instance of the Gorilla router
    router := mux.NewRouter().StrictSlash(true)

    // Iterator over the routes we declared in routes.go and attach them to the router instance
    for _, route := range routes {
        // Attach each route, uses a Builder-like pattern to set each route up.
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

导入依赖包

在router.go中的import区域, 我们声明了依赖github.com/gorilla/mux包。 我们可以通过go get来获取依赖包的源代码。

WRAPPING UP

我们可以再回到webserver.go文件,在函数StartWebServer开始位置加入下面两行代码。

func StartWebServer(port string) {
    r := NewRouter()
    http.Handle("/", r)
}

这就将我们刚创建的Router绑定到http.Handle对/路径的处理。然后重新编译并运行修改后的代码:

> go run *.go
Starting accountservice
2017/01/31 15:15:57 Starting HTTP service at 6767

然后另开一个窗口,curl如下:

> curl http://localhost:6767/accounts/10000
{"result":"OK"}

很好,我们现在有了我们第一个HTTP服务。

信息及性能(FOOTPRINT AND PERFORMANCE)

鉴于我们正在探索基于Go的微服务,由于惊人的内存占用和良好的性能,我们最好能快速进行基准测试来看看它们如何执行的。
我已经开发了一个简单的Gatling测试, 可以使用GET请求对/accounts/{accountId}进行捶打。 如果之前你是直接从https://github.com/callistaen...,那么你的源代码中就包含有负载测试代码goblog/loadtest。或者可以直接查看https://github.com/callistaen...

你自己运行一下负载测试

如果你需要自己运行负载测试工具,确保accountservice服务已启动,并且运行在localhost的6767端口上。并且你已经checkout我们的P2分支的代码。你还需要Java的运行环境以及需要安装Apache Maven。

改变目录到goblog/loadtest目录下面,在命令行中执行下面的命令。

mvn gatling:execute -Dusers=1000 -Dduration=30 -DbaseUrl=http://localhost:6767

这样就会启动并运行测试。参数如下:

  • users: 模拟测试的并发用户数.
  • duration: 测试要运行的秒数.
  • baseUrl: 我们要测试的服务的基础路径。当我们把它迁移到Docker Swarm后,baseUrl修改修改为Swarm的公共IP. 在第5部分会介绍。

首次运行,mvn会自动安装一大堆东西。安装完后,测试完成之后,它会将结果写到控制台窗口,同时也会产生一个报告到target/gatling/results中的html中。

结果

注意: 稍后,当我们的服务运行到Docker Swarm模式的Docker容器中时, 我们会在那里做所有基准测试并捕获度量。

在开始负载测试之前,我们的基于Go的accountservice内存消耗可以从macbook的任务管理器中查看到,大概如下:

clipboard.png

1.8MB, 不是特别坏。让我们使用Gatling测试,运行每秒1000个请求。需要记住一点,我们使用了非常幼稚的实现,我们仅仅响应一个硬编码的JSON响应。

clipboard.png

服务每秒1000个请求,占用的内存也只是增加到28MB。 依然是Spring Boot应用程序启动时候使用内存的1/10. 当我们给它添加一些真正的功能时,看这些数字变化会更加有意思。

性能和CPU使用率

clipboard.png

提供每秒1000个请求,每个核大概使用8%。

clipboard.png

注意,Gatling一回合子微秒延迟如何, 但是平均延迟报告值为每个请求0ms, 花费庞大的11毫秒。 在这点上来看,我们的accountservice执行还是表现出色的,在子毫秒范围内大概每秒服务745个请求。

下一章

在下一部分, 我们将真正的让accountservice做一些有意义的事情。 我们会添加一个简单的嵌入数据库到Account对象,然后提供HTTP服务。我们也会看看JSON的序列化,并检查这些增加对于足迹和性能的影响。

参考链接

查看原文

赞 17 收藏 29 评论 0

wind 赞了回答 · 2019-03-17

解决vuerouter怎么点击打开新的页面,就是a标签里的target=“blank”

const {href} = this.$router.resolve({
        name: 'foo',
        query: {
          bar
        }
      })
window.open(href, '_blank')

2.1.0版本后,使用路由对象的resolve方法解析路由,可以得到location、router、href等目标路由的信息。得到href就可以使用window.open开新窗口了。

关注 6 回答 4

wind 关注了用户 · 2019-01-29

Cam @cam

React.js, Redux, Relay, Ruby, Rails advocatoR. 是不是有 R 的都显得很牛逼

关注 222

wind 关注了专栏 · 2019-01-29

会影

关注前端开发

关注 106

wind 赞了回答 · 2019-01-28

使用flex布局,子元素怎么高度自适应?

父元素上增加align-items:flex-start; 交叉轴的对齐方式,默认是stretch

关注 4 回答 4

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

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