ACChe

ACChe 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

举头思算法,低头写代码...

个人动态

ACChe 收藏了文章 · 3月30日

JS箭头函数之:为何用?怎么用?何时用?

在现代JS中最让人期待的特性就是关于箭头函数,用=>来标识。箭头函数有两个主要的优点:

  • 更简短的函数;
  • 更直观的作用域和this的绑定(不绑定this)

因为这些优点,箭头函数比起其他形式的函数声明更受欢迎。比如,受欢迎的airbnb eslint configuration库会强制使用JavaScript箭头函数创建匿名函数。
然而,箭头函数有优点,也有一些“缺点”。这就需要在使用的时候做一些权衡。下面就从为何用、怎么用、何时用,这个三部分做一些总结。

为何用?

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this

更简短的函数

var elements = ['h1', 'div', 'span', 'section'];

elements.map(function(el) {
  return el.length; // 返回数组: [2, 3, 4, 7]
});

// 从上面的普通函数可以改写为如下的箭头函数
elements.map((el) => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数只有一个参数时,可以省略参数的圆括号
elements.map(el => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数体只有一个`return`语句时,可以省略`return`关键字和方法体的花括号
elements.map(el => el.length); // [2, 3, 4, 7]

// 在这个例子中,因为我们只需要`length`属性,所以可以使用参数结构
// 需要注意的是字符串`"length"`是我们想要获得的属性名称,而`elLength`则只是个变量名,可以替换成任意合法的变量名
elements.map(({ "length": elLength }) => elLength); // [2, 3, 4, 7]

不绑定this

在箭头函数出现之前,每个新定义的函数都有它自己的this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中则为undefined,如果该函数被作为"对象方法"调用则为基础对象等)。
而箭头函数并没有它自己的执行上下,实际上,这就意味着代码中的thisarguments都是继承它的父函数

const obj = {
  name: 'test object',
  createAnonFunction: function() {
    return function() { 
      console.log(this.name);
      return this;
    }
  },
  createArrowFunction: function() {
    return () => {
      console.log(this.name);
      return this;
    }
  }
}

const anon = obj.createAnonFunction();
anon(); // undefined
anon() === window // true
  
const arrow = obj.createArrowFunction();
arrow(); // 'test object'
arrow() === obj // true

第一个匿名参数有自己的上下文(指向的并非obj对象),当被赋值给anon变量且调用时,this发生了改变,指向了window。另一个,箭头函数与创建它的函数有相同的上下文,故指向obj对象。

通过call或者apply调用

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this),它们的第一个参数会被忽略。

var adder = {
  base: 1,
  add: function(a) {
    var f = v => v + this.base;
    return f(a);
  },
  addByCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base: 2
    };
    return f.call(b, a)
  }
}

adder.add(1); // 2
adder.addByCall(1); // 2

不绑定arguments

箭头函数不绑定Arguments对象。因此,在本示例中,arguments只是引用了封闭作用域内的arguments:

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的arguments对象,arguments[0]是 n
  return f(); 
}

foo(1); // 2

在大多数情况下,使用剩余参数是相对使用arguments对象的更好选择。

function foo(arg) {
  var f = (...agrs) => args[0];
  return f(arg);
}
foo(1); // 1

function foo(arg1, arg2) {
  var f = (...args) => args[1];
  return f(arg1, arg2);
}
foo(1, 2); // 2

怎么用?

优化代码

比如你有一个有值的数组,你想去map遍历每一项,这时箭头函数就非常推荐:

const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());

一个及其常见的例子就是返回一个对象的某个值:

const names = objects.map(object => object.name);

类似的,当用forEach来替换传统for循环的时候,实际上箭头函数会直观的保持this来自于父一级:

this.examples.forEach(example => {
  this.runExample(example);
});

Promise和Promise链

当在编写异步编程时,箭头函数也会让代码更加直观和简洁。
这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});

对象转换

箭头函数的另一个常见而且十分有用的地方就是用于封装的对象转换。
例如在Vue.js中,有一种通用模式,就是使用mapStateVuex存储的各个部分,直接包含到Vue组件中。
这涉及到定义一套mappers,用于从原对象到完整的转换输出,这在组件问题中实十分有必要的。这一系列简单的转换,使用箭头函数是最合适不过的。比如:

export default {
  computed: {
    ...mapState([
      'results',
      'users'
    ])
  }
}

何时用?(不推荐使用场景)

使用new操作符

箭头函数不能用作构造器,和new一起使用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

使用prototype属性

箭头函数没有prototype属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

使用yield关键字

yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作生成器。

深层调用

如果你将函数定义为箭头函数,并且在他们之间来回调用,当你调试bug的时候你将被代码困惑,甚至得到如下的错误信息:

{anonymous}(){anonymous}(){anonymous}(){anonymous}(){anonymous}() //anonymous

常见错误

返回对象字面量

记住用params => { object: literal }这种简单的语法返回对象字面量是行不通的。

var func = () => { foo: 1 };
func(); // undefined

var func = () => { foo: function() {} };   
// SyntaxError: function statement requires a name

这是因为花括号{}里面的代码被解析为一系列语句(即 foo 被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:

var func = () => ({foo: 1});

换行

箭头函数在参数和箭头之间不能换行。

var func = ()
           => 1; 
// SyntaxError: expected expression, got '=>'

解析顺序

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

let callback;

callback = callback || function() {}; // ok

callback = callback || () => {};      
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {});    // ok

更多示例

使用三元运算符

var foo = a => a > 15 ? 15 : a;

foo(10); // 10
foo(16); // 15

闭包

// 标准的闭包函数
function Add() {
  var i = 0;
  return function() {
    return (++i);
  }
}

var add = Add();
add(); // 1
add(); // 2

// 箭头函数体的闭包(i = 0是默认参数)
var Add = (i = 0) => { return (() => (++i)) };
var add = Add();
add(); // 1
add(); // 2

// 因为仅有一个返回,return及括号也可以省略
var Add = (i = 0) => () => (++i);

箭头函数递归

var fact = (x) => ( x == 0 ?  1 : x*fact(x-1) );
fact(5);       // 120

总结

箭头函数是JS语言中十分特别的属性,并且使很多情形中代码更加的变化莫测。尽管如此,就像其他的语言特性,他们有各自的优缺点。因此我们使用它应该仅仅是作为一种工具,而不是无脑的简单的全部替换为箭头函数。

查看原文

ACChe 赞了文章 · 3月30日

JS箭头函数之:为何用?怎么用?何时用?

在现代JS中最让人期待的特性就是关于箭头函数,用=>来标识。箭头函数有两个主要的优点:

  • 更简短的函数;
  • 更直观的作用域和this的绑定(不绑定this)

因为这些优点,箭头函数比起其他形式的函数声明更受欢迎。比如,受欢迎的airbnb eslint configuration库会强制使用JavaScript箭头函数创建匿名函数。
然而,箭头函数有优点,也有一些“缺点”。这就需要在使用的时候做一些权衡。下面就从为何用、怎么用、何时用,这个三部分做一些总结。

为何用?

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this

更简短的函数

var elements = ['h1', 'div', 'span', 'section'];

elements.map(function(el) {
  return el.length; // 返回数组: [2, 3, 4, 7]
});

// 从上面的普通函数可以改写为如下的箭头函数
elements.map((el) => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数只有一个参数时,可以省略参数的圆括号
elements.map(el => {
  return el.length; // [2, 3, 4, 7]
});

// 当箭头函数体只有一个`return`语句时,可以省略`return`关键字和方法体的花括号
elements.map(el => el.length); // [2, 3, 4, 7]

// 在这个例子中,因为我们只需要`length`属性,所以可以使用参数结构
// 需要注意的是字符串`"length"`是我们想要获得的属性名称,而`elLength`则只是个变量名,可以替换成任意合法的变量名
elements.map(({ "length": elLength }) => elLength); // [2, 3, 4, 7]

不绑定this

在箭头函数出现之前,每个新定义的函数都有它自己的this值(在构造函数的情况下是一个新对象,在严格模式的函数调用中则为undefined,如果该函数被作为"对象方法"调用则为基础对象等)。
而箭头函数并没有它自己的执行上下,实际上,这就意味着代码中的thisarguments都是继承它的父函数

const obj = {
  name: 'test object',
  createAnonFunction: function() {
    return function() { 
      console.log(this.name);
      return this;
    }
  },
  createArrowFunction: function() {
    return () => {
      console.log(this.name);
      return this;
    }
  }
}

const anon = obj.createAnonFunction();
anon(); // undefined
anon() === window // true
  
const arrow = obj.createArrowFunction();
arrow(); // 'test object'
arrow() === obj // true

第一个匿名参数有自己的上下文(指向的并非obj对象),当被赋值给anon变量且调用时,this发生了改变,指向了window。另一个,箭头函数与创建它的函数有相同的上下文,故指向obj对象。

通过call或者apply调用

由于箭头函数没有自己的this指针,通过call()或者apply()方法调用一个函数时,只能传递参数(不能绑定this),它们的第一个参数会被忽略。

var adder = {
  base: 1,
  add: function(a) {
    var f = v => v + this.base;
    return f(a);
  },
  addByCall: function(a) {
    var f = v => v + this.base;
    var b = {
      base: 2
    };
    return f.call(b, a)
  }
}

adder.add(1); // 2
adder.addByCall(1); // 2

不绑定arguments

箭头函数不绑定Arguments对象。因此,在本示例中,arguments只是引用了封闭作用域内的arguments:

function foo(n) {
  var f = () => arguments[0] + n; // 隐式绑定 foo 函数的arguments对象,arguments[0]是 n
  return f(); 
}

foo(1); // 2

在大多数情况下,使用剩余参数是相对使用arguments对象的更好选择。

function foo(arg) {
  var f = (...agrs) => args[0];
  return f(arg);
}
foo(1); // 1

function foo(arg1, arg2) {
  var f = (...args) => args[1];
  return f(arg1, arg2);
}
foo(1, 2); // 2

怎么用?

优化代码

比如你有一个有值的数组,你想去map遍历每一项,这时箭头函数就非常推荐:

const words = ['hello', 'WORLD', 'Whatever'];
const downcasedWords = words.map(word => word.toLowerCase());

一个及其常见的例子就是返回一个对象的某个值:

const names = objects.map(object => object.name);

类似的,当用forEach来替换传统for循环的时候,实际上箭头函数会直观的保持this来自于父一级:

this.examples.forEach(example => {
  this.runExample(example);
});

Promise和Promise链

当在编写异步编程时,箭头函数也会让代码更加直观和简洁。
这是箭头函数的理想位置,特别是如果您生成的函数是有状态的,同时想引用对象中的某些内容。

this.doSomethingAsync().then((result) => {
  this.storeResult(result);
});

对象转换

箭头函数的另一个常见而且十分有用的地方就是用于封装的对象转换。
例如在Vue.js中,有一种通用模式,就是使用mapStateVuex存储的各个部分,直接包含到Vue组件中。
这涉及到定义一套mappers,用于从原对象到完整的转换输出,这在组件问题中实十分有必要的。这一系列简单的转换,使用箭头函数是最合适不过的。比如:

export default {
  computed: {
    ...mapState([
      'results',
      'users'
    ])
  }
}

何时用?(不推荐使用场景)

使用new操作符

箭头函数不能用作构造器,和new一起使用会抛出错误。

var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

使用prototype属性

箭头函数没有prototype属性。

var Foo = () => {};
console.log(Foo.prototype); // undefined

使用yield关键字

yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作生成器。

深层调用

如果你将函数定义为箭头函数,并且在他们之间来回调用,当你调试bug的时候你将被代码困惑,甚至得到如下的错误信息:

{anonymous}(){anonymous}(){anonymous}(){anonymous}(){anonymous}() //anonymous

常见错误

返回对象字面量

记住用params => { object: literal }这种简单的语法返回对象字面量是行不通的。

var func = () => { foo: 1 };
func(); // undefined

var func = () => { foo: function() {} };   
// SyntaxError: function statement requires a name

这是因为花括号{}里面的代码被解析为一系列语句(即 foo 被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:

var func = () => ({foo: 1});

换行

箭头函数在参数和箭头之间不能换行。

var func = ()
           => 1; 
// SyntaxError: expected expression, got '=>'

解析顺序

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

let callback;

callback = callback || function() {}; // ok

callback = callback || () => {};      
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {});    // ok

更多示例

使用三元运算符

var foo = a => a > 15 ? 15 : a;

foo(10); // 10
foo(16); // 15

闭包

// 标准的闭包函数
function Add() {
  var i = 0;
  return function() {
    return (++i);
  }
}

var add = Add();
add(); // 1
add(); // 2

// 箭头函数体的闭包(i = 0是默认参数)
var Add = (i = 0) => { return (() => (++i)) };
var add = Add();
add(); // 1
add(); // 2

// 因为仅有一个返回,return及括号也可以省略
var Add = (i = 0) => () => (++i);

箭头函数递归

var fact = (x) => ( x == 0 ?  1 : x*fact(x-1) );
fact(5);       // 120

总结

箭头函数是JS语言中十分特别的属性,并且使很多情形中代码更加的变化莫测。尽管如此,就像其他的语言特性,他们有各自的优缺点。因此我们使用它应该仅仅是作为一种工具,而不是无脑的简单的全部替换为箭头函数。

查看原文

赞 21 收藏 17 评论 0

ACChe 赞了文章 · 3月30日

Go Web编程--给自己写的服务器添加错误和访问日志

错误日志和访问日志是一个服务器必须支持的功能,我们教程里使用的服务器到目前为止还没有这两个功能。正好前两天也写了篇介绍logrus日志库的文章,那么今天的文章里就给我们自己写的服务器加上错误日志和访问日志的功能。在介绍添加访问日志的时候会介绍一种通过编写中间件获取HTTP响应的StausCodeBody的方法。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp11获取本文源代码

初始化日志记录器

我们先来做一下初始化工作,在项目里初始化记录错误日志和访问日志的记录器Logger

// ./utils/vlog
package vlog

import (
    "github.com/sirupsen/logrus"
    "os"
)

var ErrorLog *logrus.Logger
var AccessLog *logrus.Logger
var errorLogFile = "./tmp/log/error.log"
var accessLogFile = "./tmp/log/access.log"
func init () {
    initErrorLog()
    initAccessLog()
}

func initErrorLog() {
    ErrorLog = logrus.New()
    ErrorLog.SetFormatter(&logrus.JSONFormatter{})
    file , err := os.OpenFile(errorLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
    if err != nil {
        panic(err)
    }
    ErrorLog.SetOutput(file)
}

func initAccessLog() {
    AccessLog = logrus.New()
    AccessLog.SetFormatter(&logrus.JSONFormatter{})
    file , err := os.OpenFile(accessLogFile, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0755)
    if err != nil {
        panic(err)
    }
    AccessLog.SetOutput(file)
}
  • 我们新定义一个packageinit函数中来初始化记录器,这样服务器成功启动前就会初始化好记录器。
  • /tmp/log这个目录要提前创建好,执行init函数时会自动创建好access.logerror.log

添加错误日志

我们创建服务器使用的net/http包的Server类型中,有一个ErrorLog字段供开发者设置记录错误日志用的记录器Logger,默认使用的是log包默认的记录器(应该是系统的标准错误):

type Server struct {
   Addr    string  // TCP address to listen on, ":http" if empty
   Handler Handler // handler to invoke, http.DefaultServeMux if nil
   ...
   // ErrorLog specifies an optional logger for errors accepting
   // connections, unexpected behavior from handlers, and
   // underlying FileSystem errors.
   // If nil, logging is done via the log package's standard logger.
   ErrorLog *log.Logger
     ...
}

我们之前在创建服务器的时候自己实现了Server类型的对象,那么现在要做的就是将上面初始化好的错误日志的记录器指定给ServerErrorLog字段。

func main() {
  ...
  // 将`logrus`的Logger转换为io.Writer
    errorWriter := vlog.ErrorLog.Writer()
  // 记得关闭io.Writer
    defer errorWriter.Close()

    server := &http.Server{
        Addr:    ":8080",
        Handler: muxRouter,
    // 用记录器转换成的io.Writer创建log.Logger
        ErrorLog: log.New(vlog.ErrorLog.Writer(), "", 0),
    }
    ...
}

添加好错误日志的记录器后,我们找个路由处理函数,在里面故意制造运行时错误验证一下是否能记录到错误。

func (*HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   ints := []int{0, 1, 2}
   fmt.Fprintf(w, "%v", ints[0:5])
}

在上面处理函数中,通过切片表达式越界故意制造了一个运行时错误,打开error.log后能看到文件里已经记录到这个运行时错误及其Stack trace

添加访问日志

Server对象可以设置错误日志的记录器不一样,访问日志只能是我们通过自己编写中间件的方式来实现了。在记录访问日志的中间件里我们会记录ipmethodpathqueryrequest_bodystatusresponse_body这些个字段的内容。

statusresponse_body两个字段来自请求对应的响应。响应在net/http包里是用http.ResponseWriter接口表示的

type ResponseWriter interface {
    Header() Header

    Write([]byte) (int, error)

    WriteHeader(statusCode int)
}

接口本身以及net/http提供的实现都没有让我们进行读取的方法,所以在编写的用于记录访问日志的中间件里需要对net/http库本身实现的ResponseWriter做一层包装。

利用Go语言结构体类型嵌套匿名类型后,结构体拥有了被嵌套类型的所有导出字段和方法的特性,我们可以很方便地对原来的ResponseWriter做一层包装,然后只重新实现需要更改的方法即可:

type ResponseWithRecorder struct {
   http.ResponseWriter
   statusCode int
   body bytes.Buffer
}

func (rec *ResponseWithRecorder) WriteHeader(statusCode int) {
   rec.ResponseWriter.WriteHeader(statusCode)
   rec.statusCode = statusCode
}

func (rec *ResponseWithRecorder) Write(d []byte) (n int, err error) {
   n, err = rec.ResponseWriter.Write(d)
   if err != nil {
      return
   }
   rec.body.Write(d)

   return
}

定义好新的类型后我们重新实现了WriteHeaderWrite方法,在向原来的ReponseWriter中写入后也会向ResponseWriteRecoder.statusCodeResponseWriteRecoder.body写入对应的数据。这样我们就可以在中间件里通过这两个字段访问响应码和响应数据了。

记录访问日志的中间件定义如下:

func AccessLogging (f http.Handler) http.Handler {

    // 创建一个新的handler包装http.HandlerFunc
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        buf := new(bytes.Buffer)
        buf.ReadFrom(r.Body)
        logEntry := vlog.AccessLog.WithFields(logrus.Fields{
            "ip": r.RemoteAddr,
            "method": r.Method,
            "path": r.RequestURI,
            "query": r.URL.RawQuery,
            "request_body": buf.String(),

        })

        wc := &ResponseWithRecorder{
            ResponseWriter: w,
            statusCode: http.StatusOK,
            body: bytes.Buffer{},
        }

        // 调用下一个中间件或者最终的handler处理程序
        f.ServeHTTP(wc, r)

        defer logEntry.WithFields(logrus.Fields{
            "status": wc.statusCode,
            "response_body": wc.body.String(),
        }).Info()

    })
}

Router上应用创建好的AccessLogging中间件后,就可以正常的记录服务器的访问日志了。

// router/router.go
func RegisterRoutes(r *mux.Router) {
    ...
    // apply Logging middleware
    r.Use(middleware.Logging(), middleware.AccessLogging)
    ...
}

不过有两点需要注意一下

  • 这里为了演示获取响应数据记录了response_body字段,如果是接口响应内容记录下还可以,但是如果是HTML还是不记录的为好。
  • 初始化ResponseWithRecorder时默认设置了statusCode时因为,服务器正确返回响应时不会显式调用WriteHeader方法,只有在返回NOT_FOUND之类的错误的时候才会调用WriteHeader方法,针对这种情况需要在初始化的时候把statusCode的默认值设置为200

现在再访问服务器后打开access.log会看到刚刚的访问日志,就能看到刚刚请求的urlmethod,客户端IP等信息了。

{"ip":"......","level":"info","method":"GET","msg":"","path":"/index/","query":"","request_body":"","response_body":"Hello World1","status":200,"time":"2020-03-26T04:21:46Z"}

注意:文章只为说明演示方便,获取IP的方法无法获取代理后的真实IP,请悉知。

前文回顾

深入学习用Go编写HTTP服务器

Go Web 编程--超详细的模板库应用指南

Go Web编程--使用Go语言创建静态文件服务器

Go Web编程--SecureCookie实现客户端Session管理

关于如何收集,标准化和集中化处理Golang日志的一些建议

查看原文

赞 5 收藏 2 评论 0

ACChe 赞了文章 · 3月17日

Go Web编程--SecureCookie实现客户端Session管理

f1786a17fea647790228ee7a47bc02d649da5fda.jpg
Web应用开发中Session是在用户和服务器之间进行交换的非持久化交互信息。当用户登录时,可以在用户和服务器之间生成Session,然后来回交换数据,并在用户登出时销毁Sessiongorilla/sessions软件包提供了易于使用的Go语言Session实现。该软件包提供了两种不同的实现。第一个是文件系统存储,它将每个会话存储在服务器的文件系统中。另一个是Cookie存储,它使用我们上篇文章讲的SecureCookie在客户端上存储会话。同时还提供了用户自定义Session存储实现的选项,我们可以根据应用的需求自己实现Session存储。因为我们的教程是学会使用为目的就不大费周章的去实现MySQL或者Redis版本的Session存储了,我们直接使用软件包提供的Cookie实现来完成本节的Session相关内容。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp09获取本文源代码

使用Cookie存储用户Session的优缺点

客户端使用Cookie管理用户Session较之在服务器进行用户的Session管理会有一些优势。客户端Session增加了应用程序的可伸缩性,因为所有的会话数据都存储在用户端,因此可以将用户的请求平衡到不同的远端服务器,也不必在服务器端对所有用户的会话进行统一管理,所以使用Cookie存储用户Session会更简单一些。

当然有优势就必定有劣势,客户端Cookie的整体大小是有限制的。目前,Google Chrome浏览器将Cookie限制为4096个字节。

客户端会话还意味着无法终止会话,从而导致注销不完整。如果用户在退出前保存了Cookie中的会话信息,则他们可以使用该会话信息创建一个新的Cookie,然后继续使用该应用程序,为了最大程度地降低安全风险,我们可以将会话Cookie设置为在合理的时间内过期,使用加密后的ScureCookie存储数据,同时还要避免在其中存储敏感信息(即使是服务端管理Session也不应该存储类似密码这种敏感信息)。

总之在考虑使用客户端还是服务端存储用户Session时一定要根据应用的使用场景来选择,这一点很重要。

安装gorilla/sessions

在开始编码前先来安装一下gorilla/sessions软件包,

$ go get github.com/gorilla/sessions

并简单看一下软件包功能特性的介绍

  • 方便地设置签名(也可以选择加密)的Cookie
  • 自带将会话存储在Cookie或服务端文件系统中的SessionStore实现。
  • 支持Flash消息:读取即销毁的会话数据。
  • 支持方便地切换会话数据的持久化方式。
  • 为不同的Session存储提供统一的接口和基础设施。

演示用户Session设计实现

我们今天的示例代码是用gorilla/sessions提供的CookieSessionStore实现一个简单的系统登录功能。

我们会定义如下几个路由:

  • /user/login 用户登录验证,验证成功后在用户Session数据中标记用户是已验证的。
  • /user/logout 用户登出,会在Session中标记用户是未认证的。
  • /user/secret 通过用户Session判断用户是否已认证,未认证返回403 Forbidden错误。

为了达到演示目的的同时减少文章中出现过多代码,我们不会做前端页面,通过命令行cURL直接请求上面几个URL验证我们的系统登录功能。

初始化工作

我们现在项目的handler目录下新建一个user子目录,用于存放使用到用户Session的处理程序

...
handler/
└── user/
    └── init.go
    └── login.go
    └── logout.go
    └── secret.go
...
main.go

其下的四个分别是包的初始化程序init.go以及存放上面说的三个路由处理程序的.go源文件。

初始化Session存储

我们把Session存储的初始化工作放在user包的init函数中,这样首次导入user包时即可完成相关的初始化工作。

package user

import "github.com/gorilla/sessions"

const (
    //64位
    cookieStoreAuthKey = "..."
    //AES encrypt key必须是16或者32位
    cookieStoreEncryptKey = "..."
)

var sessionStore *sessions.CookieStore

func init () {
    sessionStore = sessions.NewCookieStore(
        []byte(cookieStoreAuthKey),
        []byte(cookieStoreEncryptKey),
    )

    sessionStore.Options = &sessions.Options{
        HttpOnly: true,
        MaxAge:   60 * 15,
    }

}

实现登录验证

// login.go
var sessionCookieName = "user-session"
func Login(w http.ResponseWriter, r *http.Request) {
    session, err := sessionStore.Get(r, sessionCookieName)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    // 登录验证
    name := r.FormValue("name")
    pass := r.FormValue("password")
    _, err = logic.AuthenticateUser(name, pass)
    if err != nil {
        http.Error(w, err.Error(), http.StatusUnauthorized)
        return
    }
    // 在session中标记用户已经通过登录验证
    session.Values["authenticated"] = true
    err = session.Save(r, w)

    fmt.Fprintln(w, "登录成功!", err)
}
  • 我们将浏览器Cookie中存储用户SessionCookie-Name设置成了user-session
  • 登录验证就是简单的用户名和密码查找匹配的用户,在之前的文章应用数据库应用 ORM两篇文章中有在MySQL数据库中创建users表,并介绍了怎么使用ORM操作数据库,没有看过的同学可以回看一下。
  • 登录验证成功后在Sessionauthenticated中标记了用户已通过认证。session.Values是类型map[interface{}]interface{}的别名,所以可以往其中存储任意类型的数据。

实现登出

登出我们这里就是简单的将Sessionauthenticated的值设置成了false.

//logout.go
func Logout(w http.ResponseWriter, r *http.Request) {
   session, _ := sessionStore.Get(r, sessionCookieName)
   
   session.Values["authenticated"] = false
   session.Save(r, w)
}

使用Session认证用户

//secret.go
func Secret(w http.ResponseWriter, r *http.Request) {
   session, _ := sessionStore.Get(r, sessionCookieName)

   if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
      http.Error(w, "Forbidden", http.StatusForbidden)
      return
   }

   fmt.Fprintln(w, "这里还是空空如也!")
}
  • 使用Session中存储的数据值都是接口类型的,所以使用时要先对其进行类型断言session.Values["authenticated"].(bool)
  • 如果authenticated的值不为true或者是从Session中获取不到对应的值,这里直接返回HTTP 403 Forbidden错误。

注册路由

// router.go
func RegisterRoutes(r *mux.Router) {
  ...
  userRouter := r.PathPrefix("/user").Subrouter()
  userRouter.HandleFunc("/login", user.Login).Methods("POST")
  userRouter.HandleFunc("/secret", user.Secret)
  userRouter.HandleFunc("/logout", user.Logout)
  ...
}

验证已实现的Session管理功能

编写完上面的Session管理的功能后,重启服务器,然后使用cURL分别请求URL验证一下效果。

curl -XPOST   -d 'name=Klein&password=123' \
     -c - http://localhost:8000/user/login

-c选项表示将Cookie写入到后面的文件中,完整格式是-c -<file_name>,短横线后不带文件名表示把Cookie写入到标准输出中。

我们可以在下图里看到,Cookie中的user-session存储的就是加密后的Session数据了

图片

如果请求中不携带这个Cookie访问/user/secret会直接返回HTTP 403错误

图片

那么接下来在使用cURL请求/user/secret时带上上面返回的Cookie值,看看请求是否能成功

curl --cookie "user-session=MTU4m..." http://localhost:8000/user/secret

图片

Cookie加密后的值太长了,搞得字儿好小,cURL执行的结果显示服务器成功地响应了我们的请求。你们试验的时候换成自己生成的Cookie值请求就可以啦。

你们实践时也可以用PostMan代替cURL试验,不过感觉PostMan的返回不如cURL来的明显。

Go Web 编程系列的每篇文章的源代码都打了对应版本的软件包,供大家参考。公众号中回复gohttp09获取本文源代码

前文回顾

Go Web 编程--如何确保Cookie数据的安全传输

Go Web编程--应用ORM

Go Web编程--应用ORM

五分钟用Docker快速搭建Go开发环境

深入学习用Go编写HTTP服务器

查看原文

赞 1 收藏 0 评论 0

ACChe 关注了专栏 · 2月17日

网管叨bi叨

公众号:网管叨bi叨 | Golang、PHP、Laravel、Docker等学习经验分享

关注 836

ACChe 报名了系列讲座 · 2月5日

【思否编程公开课】网络安全之 Kali 渗透入门实战

网络安全法的颁布后对企业网络安全建设有严格的要求,如果企业出现安全事故,同样需要承担法律责任;致使大量企业迫切需要安全技术人员加入,大家对网络安全的技能需求也不断提高; 不少人在学习网络安全时候觉得门槛高,需要掌握计算机各方面的知识外还需要寻找各种工具,来回折腾之后发现也没有渗透测试成功,致使很多人还没进入门就放弃, 在这节公开课中,我将带领大家学习kali 系统的工具使用,kali一个包含主流安全工具的一个集合系统,在课程当中会以渗透实践的方式来让大家学习渗透测试技术,帮助安全工程师在工作中找出更多网络安全问题 主要内容有: - Kali系统的安装与汉化 - 使用nmap对目标安全扫描 - 使用MSF对Windows渗透 - 使用ettercap中间人欺骗

ACChe 收藏了文章 · 2月4日

golang结构体指针解析

golang指针可以这样理解:本身为一个整型常量,但由于其声明时为指针,因此拥有了特殊的能力,即在其前增加 * ,即可直接访问内存编号为该整型常量的数据。而对于某个定义的常量,在前面加 & ,也可以获得数据的内存编号。
当我们定义结构体指针时,问题出现了:

type name int8
type first struct {
    a int
    b bool
    name
}
func main(){
    var type1 = first{1, false, 2}
    var type_pointer *first = &type1
    fmt.Println(type_pointer)
    }

上面的代码打印的结果为:
&{1 false 2}
由此可推论,结构体本身并没有代表其自身的内存地址,存在内存地址的是结构体内部的数据信息,只得打印出这样的数据。

因此,我们进行如下打印:

fmt.Println(&type_pointer.a, type_pointer.a, &type_pointer, (*type_pointer).a)

输出结果为:
0x110120f8 1 0x11006128 1

可以这样解释:
&type_pointer.a:等同于&(*type_pointer).a
type_pointer.a:等同于(*type_pointer).a
上面这两种可以理解为golang语言的简便写法写法(如果不这么理解没法解释输出),不推荐这样写,
&type_pointer:指针本身的内存地址
(*type_pointer).a:指针指向的结构体,该结构体内部的a数据

此外,类似于*type_pointer.a这种写法是非法的。

查看原文

ACChe 赞了文章 · 2月4日

golang结构体指针解析

golang指针可以这样理解:本身为一个整型常量,但由于其声明时为指针,因此拥有了特殊的能力,即在其前增加 * ,即可直接访问内存编号为该整型常量的数据。而对于某个定义的常量,在前面加 & ,也可以获得数据的内存编号。
当我们定义结构体指针时,问题出现了:

type name int8
type first struct {
    a int
    b bool
    name
}
func main(){
    var type1 = first{1, false, 2}
    var type_pointer *first = &type1
    fmt.Println(type_pointer)
    }

上面的代码打印的结果为:
&{1 false 2}
由此可推论,结构体本身并没有代表其自身的内存地址,存在内存地址的是结构体内部的数据信息,只得打印出这样的数据。

因此,我们进行如下打印:

fmt.Println(&type_pointer.a, type_pointer.a, &type_pointer, (*type_pointer).a)

输出结果为:
0x110120f8 1 0x11006128 1

可以这样解释:
&type_pointer.a:等同于&(*type_pointer).a
type_pointer.a:等同于(*type_pointer).a
上面这两种可以理解为golang语言的简便写法写法(如果不这么理解没法解释输出),不推荐这样写,
&type_pointer:指针本身的内存地址
(*type_pointer).a:指针指向的结构体,该结构体内部的a数据

此外,类似于*type_pointer.a这种写法是非法的。

查看原文

赞 2 收藏 2 评论 0

ACChe 提出了问题 · 1月28日

多个项目应该怎样配置数据库的docker?

一开始写了一个项目,用docker-compose来创建了api的服务、数据库和nginx(分别三个docker容器),现在想要做另外一个项目,应该再新建一个数据库docker容器来配合现在的项目吗?可以还是用第一个项目所建的数据库容器来用吗?

关注 1 回答 0

ACChe 赞了回答 · 1月7日

docker-compose每次up之后,之前在容器中所做的修改保不保留?

你肯定是用了 容器卷 ,删除卷容器就好了

关注 6 回答 4

ACChe 收藏了文章 · 2019-11-12

小程序接入「微信对话开放平台」,创建对话客服机器人

当用户在小程序内发起对话,你是在后台抓紧安排人工回复吗?

由于等待回复时间过长,用户产生不满并弃用了你的小程序。

“秒回”是对话的基本要求,及时回复才能给到用户尊重和被重视感。然而,受人力成本的因素影响,专业客服往往需要1对N,在对话需求激增的情况下,回复并不高效。其实,用户发起对话,需求大概分为两种:服务和咨询。当用户只是要获取固定服务的渠道,可以通过AI机器人解答用户的问题

然而目前的情况是,AI对话能力并不是每个小程序开发团队都能自行开发,所以微信AI团队推出了「微信对话开放平台」,向有对话或客服需求的个人、企业或组织开放微信的对话AI能力,帮助他们在小程序配置对话AI机器人

「微信对话开放平台」能做什么?

「微信对话开放平台」是在微信智言与微信智聆两大技术支持下推出的,能够支持文字甚至语音进行AI对话。它既能帮助没有开发能力的开发者快速搭建客服机器人,又为有开发能力的第三方开发者提供API接口, 快速获取对话服务能力。

这些可接入的对话技能不仅包括预置的基础服务技能,如天气、新闻或百科等,还能自定义配置技能,让开发者能够根据场景定制对话或客服技能。

「微信对话开放平台」可应用的场景?

「微信对话开放平台」可应用于客服场景,通过发送指令以获取特定服务或信息,比如:

获取快递信息:在电商平台上,无需等待真人客服,用户只要在聊天界面上发送“快递”等相关文字,即可得知商品所寄的快递。

「微信对话开放平台」也可应用于非客服场景,承担社交功能,培养用户的情感依赖,比如:

解答今天吃什么:在餐饮平台上,通过机器人回答纠结吃什么的用户关于“吃什么”的问题,可以为他们提供建议,并适当地推荐对应的菜品。

如何使用「微信对话开放平台」?

01 创建机器人

① 登录微信对话开放平台配置机器人

登录微信对话开放平台(https://openai.weixin.qq.com),点击开始使用,创建机器人,按规定填写“机器人名称”,“机器人ID”和“验证码”。
在这里插入图片描述

② 创建机器人技能

可以创建自定义对话技能或者选择系统对话技能,进行调试和发布。
在这里插入图片描述
关于自定义技能,要根据自己的场景需求进行创建和配置,具体操作详见文档:
https://openai.weixin.qq.com/...

02 调用微信对话开放平台小程序插件

微信对话开放平台小程序插件,提供两种调用方式,一种是有UI组件式调用,另一种是无UI功能接口调用

① 添加插件

登录小程序,在“设置-第三方服务-插件管理”处搜索并添加 openaiwidget 插件

② 插件配置

在小程序 app.json 中配置, 小程序插件id是 wx8c631f7e9f2465e1 , 当前稳定版本是1.0.1

{
  "pages": [
    "pages/index/index"
  ],
  "plugins": {
    "chatbot": {
      "version": "1.0.1",
      "provider": "wx8c631f7e9f2465e1"
    }
  },
  "requiredBackgroundModes": [
    "audio"
  ],
  "sitemapLocation": "sitemap.json"
}

当使用有UI版本时,需要额外使用WechatSI组件, 只使用接口不使用UI时,使用上面的配置就可以了

{
  "pages": [
    "pages/index/index",
    "pages/newsPage/newsPage",
    "pages/common/common"
  ],
  "plugins": {
    "myPlugin": {
      "version": "dev",
      "provider": "wx8c631f7e9f2465e1"
    },
    "WechatSI": {
      "version": "0.3.1",
      "provider": "wx069ba97219f66d99"
    }
  },
  "requiredBackgroundModes": ["audio"],
  "sitemapLocation": "sitemap.json"
}

③ 获取获得插件所需appid

在微信对话开放平台上,进入“设置-服务接入”获得对应的小程序ID
在这里插入图片描述

④ 无UI, 直接调用插件接口

• 组件初始化

var plugin = requirePlugin("chatbot");

App({
  onLaunch: function() {
    console.log(plugin, "+++");
    plugin.init({
        appid: "P5Ot9PHJDechCYqDFAW1AiK6OtG3Ja", //小程序示例账户,仅供学习和参考
        success: () => {},
        fail: error => {}
    });
  }
});

• 发送query

plugin.send({
  query: "你好",
  success: res => {
    console.log(res);
  },
  fail: error => {}
});

• 返回结果

{
  "ans_node_id": 6666,
  "ans_node_name": "天气服务",
  "answer": "北京今天小雨,温度18到29度,当前温度27度,空气质量轻度污染,今天有雨,略微偏热,注意衣物变化。",
  "answer_open": 1,
  "answer_type": "text",
  "article": "",
  "bid_stat": {
    "curr_time": "20190826-16:34:56",
    "err_msg": "",
    "latest_time": "20190826-16:34:56",
    "latest_valid": true,
    "up_ret": 0
  },
  "confidence": 1,
  "create_time": "1566810973035",
  "dialog_status": "COMPLETE",
  "from_user_name": "o9U-85tEZToQxIF8ht6o-KkagxO0",
  "intent_confirm_status": "",
  "list_options": false,
  "msg": [
    {
      "ans_node_id": 6666,
      "ans_node_name": "天气服务",
      "article": "",
      "confidence": 1,
      "content": "北京今天小雨,温度18到29度,当前温度27度,空气质量轻度污染,今天有雨,略微偏热,注意衣物变化。",
      "debug_info": "",
      "list_options": false,
      "msg_type": "text",
      "resp_title": "天气服务",
      "status": "CONTEXT_FAQ"
    }
  ],
  "ret": 0,
  "skill_id": "",
  "skill_type": "",
  "slot_info": [
    {
      "date": "{\"type\":\"DT_ORI\",\"date_ori\":\"今天\",\"date\":\"2019-08-26\",\"date_lunar\":\"2019-08-26\",\"week\":\"1\",\"slot_content_type\":\"2\",\"modify_times\":\"0\"}"
    },
    {
      "from_loc": "{\"type\":\"LOC_CHINA_CITY\",\"country\":\"中国\",\"city\":\"北京\",\"city_simple\":\"北京\",\"loc_ori\":\"北京\",\"slot_content_type\":\"2\",\"modify_times\":\"1\"}"
    }
  ],
  "slots_info": [
    {
      "confirm_status": "NONE",
      "end": 0,
      "entity_type": "",
      "norm": "2019-08-26",
      "norm_detail": "",
      "slot_name": "date",
      "slot_value": "{\"type\":\"DT_ORI\",\"date_ori\":\"今天\",\"date\":\"2019-08-26\",\"date_lunar\":\"2019-08-26\",\"week\":\"1\",\"slot_content_type\":\"2\",\"modify_times\":\"0\"}",
      "start": 0
    },
    {
      "confirm_status": "NONE",
      "end": 6,
      "entity_type": "LOC_CHINA_CITY",
      "norm": "{\"type\":\"LOC_CHINA_CITY\",\"country\":\"中国\",\"city\":\"北京\",\"city_simple\":\"北京\",\"loc_ori\":\"北京\"}",
      "norm_detail": "",
      "slot_name": "from_loc",
      "slot_value": "{\"type\":\"LOC_CHINA_CITY\",\"country\":\"中国\",\"city\":\"北京\",\"city_simple\":\"北京\",\"loc_ori\":\"北京\",\"slot_content_type\":\"2\",\"modify_times\":\"1\"}",
      "start": 0
    }
  ],
  "status": "CONTEXT_FAQ",
  "title": "天气服务",
  "to_user_name": "10808"
}

⑤ 有UI,直接使用标记组件,无需接口调用

• 组件初始化

var plugin = requirePlugin("chatbot");

App({
  onLaunch: function() {
    console.log(plugin, "+++");
    plugin.init({
      appid: "P5Ot9PHJDechCYqDFAW1AiK6OtG3Ja", //小程序示例账户,仅供学习和参考
      success: () => {},
      fail: error => {}
    });
  }
});

• 在组件内进行配置
在页面的配置json内进行配置,比如 pages/index/index.json

{
  "usingComponents": {
    "chat": "plugin://chatbot/chat"
  }
}

• 使用组件

<view>
    <chat bind:backHome="goBackHome" />
</view>
 // goBackHome回调 返回上一级页面
  goBackHome: function () {
    wx.navigateBack({
      delta: 1
    })
  }

• 每次返回结构后,触发 queryCallback

<view>
    <chat bind:queryCallback="getQueryCallback" bind:backHome="goBackHome" />
</view>
// getQueryCallback回调 返回query与结果
  getQueryCallback: function (e) {
    var listData = this.data.listData
    listData.push(e.detail)
    if (listData.length === 10) {
      wx.navigateTo({
        url: '../newsPage/newsPage',
      })
    }
  },
  // goBackHome回调 返回上一级页面
  goBackHome: function () {
    wx.navigateBack({
      delta: 1
    })
  }

• 初始化配置项

plugin.init({
  ...options
  appid: "PWj9xdSdGU3PPnqUUrTf7uGgQ9Jvn7",
  success: () => {},
  fail: error => {}
  guideList: ["您好"]
});

options 说明:
在这里插入图片描述
小程序接入具体操作详见文档:
https://mp.weixin.qq.com/wxop...

扫码体验:微信对话开放平台小程序插件 应用示例
在这里插入图片描述
P.S.我们最近新建了个WeGeek技术交流群,欢迎小程序开发同好者进群交流,调戏勾搭群里的云开发大神~扫码添加Wegeek小助手即可获取进群方式。
在这里插入图片描述

查看原文

ACChe 赞了回答 · 2019-11-11

解决https能不能一个主机上配置多个web应用

匹配接口前缀,转发一下

location /blog {

proxy_pass http://0.0.0.0:3000;

}

https://mp.weixin.qq.com/s?__...

关注 6 回答 5

ACChe 关注了问题 · 2019-09-04

解决go 在其他package 下面可以访问 main package 里的全局变量吗?

我main package 当中定义并初始化了一个logger

govar logger stdlog.Logger
func main(){
    ....
    logger = stdlog.GetFromFlags()
    ....
}

然后在其他package下面想用这个logger 全局变量 该怎么用?

关注 5 回答 2

ACChe 赞了问题 · 2019-08-07

解决dockerfile 与 docker-compose的区别是什么?

docker-compose是编排镜像, 那么docker-compose是不是可以做Dockerfile的能做的事?

关注 7 回答 5

ACChe 赞了回答 · 2019-08-07

解决dockerfile 与 docker-compose的区别是什么?

我所理解的docker-compose是编排容器的。例如,你有一个php镜像,一个mysql镜像,一个nginx镜像。如果没有docker-compose,那么每次启动的时候,你需要敲各个容器的启动参数,环境变量,容器命名,指定不同容器的链接参数等等一系列的操作,相当繁琐。而用了docker-composer之后,你就可以把这些命令一次性写在docker-composer.yml文件中,以后每次启动这一整个环境(含3个容器)的时候,你只要敲一个docker-composer up命令就ok了。

而dockerfile的作用是从无到有的构建镜像。

两个完全不是一码事

关注 7 回答 5

ACChe 赞了问题 · 2019-06-01

微信小程序打印数组异常

微信小程序打印数组 出现ObservableArray {$mobx: ObservableArrayAdministration}

打印数组出现下面这个东西,一直到999 都是undefined。迷茫
clipboard.png

关注 1 回答 0

ACChe 赞了问题 · 2019-03-05

解决什么是前端渲染?什么是服务端渲染?两者区别及应用场景?

我现在就只是前端用用框架写一写SPA,然后后台只是处理下数据库相关的数据,前后就restful形式的API传递下数据,至于这些个渲染毫无概念。
所谓的服务端渲染就是指在后台生成html文件传给前端,那对于react和Vue这种引入了虚拟DOM的呢?
前端渲染是指什么,解析html生成DOM树然后绘制页面?
前端渲染和客户端渲染是指同一个意思吧?
前端渲染和服务端渲染应用场景都有哪些?

关注 12 回答 3

ACChe 收藏了问题 · 2019-03-05

什么是前端渲染?什么是服务端渲染?两者区别及应用场景?

我现在就只是前端用用框架写一写SPA,然后后台只是处理下数据库相关的数据,前后就restful形式的API传递下数据,至于这些个渲染毫无概念。
所谓的服务端渲染就是指在后台生成html文件传给前端,那对于react和Vue这种引入了虚拟DOM的呢?
前端渲染是指什么,解析html生成DOM树然后绘制页面?
前端渲染和客户端渲染是指同一个意思吧?
前端渲染和服务端渲染应用场景都有哪些?

ACChe 收藏了文章 · 2019-02-19

Docker 入门(三):持续集成、持续部署

在软件开发过程中,如果我们每一次提交的代码都能够进行一次完整的编译、测试、打包、发布,就能及早发现问题、及早修复,在保证代码质量的同时让产品快速迭代。这就是持续集成(CI)、持续部署(CD)的好处。

目前 CI/CD 的方案有很多,本文将展示一个用 Docker + Jenkins 实现的完整过程。

本文的 CI/CD 流程

开发人员提交代码到自己的分支并 push 到远程仓库 ==> 触发远程仓库(GitHub/GitLab)的 Webhooks ==> Jenkins 接到通知自动执行之前准备好的一个流程(克隆代码,对代码进行编译、测试、打包,没有问题后会执行 docker 命令进行镜像构建)==> 最终发布到测试服务器中。

环境说明

  • 本文选用的测试环境是阿里云的服务器,所以全程也是在服务器上操作的,无需本地安装 docker,当然在本地操作也是可以的。
  • 本文选用的远程代码库是 GitHub 公有仓库,如果是私有仓库或 GitLab,步骤会略有不同。
  • 本文中所用的 Jenkins 也是用的 docker 版,并不是直接安装在宿主机上的。

开始一个 docker 应用

要演示整个过程,就得有一个应用,这里我们用一个 create-react-app 为例,无需 IDE,一个 terminal 即可搞定。

  • 首先创建 react-app 和 Dockerfile
4、5 行是增加了一个设置,是关掉 webpack 的 host 检查,如果不加此项,访问绑定域名的服务器就会被 webpack-dev-server 拦截。
$ npm install -g create-react-app
$ create-react-app my-app
$ cd my-app
$ touch .env
$ echo DANGEROUSLY_DISABLE_HOST_CHECK=true > .env
$ touch Dockerfile
  • 将以下内容写入 Dockerfile:
为了简单,我们这里直接采用 npm start 的方式启动它,就不 build 了,安装 cnpm 是为了提高依赖的下载速度
FROM node:8.11.1-slim

WORKDIR /home/app

COPY . ${WORKDIR}

RUN npm install -g cnpm --registry=https://registry.npm.taobao.org \
    && cnpm install

EXPOSE 3000

ENTRYPOINT [ "npm", "start" ]
  • 到这里就完全好了,单元测试什么的就暂且忽略。

为什么要使用 Jenkins

如果没有 Jenkins,就上面那个例子,我们想要将自己的代码集成并且部署到服务器,可能要经历以下步骤:

  • 1、将代码 push 到仓库
  • 2、ssh 登录服务器,克隆代码到宿主机(宿主机还要安装 git)
  • 3、执行以下命令完成镜像构建和部署
$ cd repository
$ docker build -t test .
$ docker run -d -p 80:3000 --name my-react test

可以看到上面那个过程需要人工操作,非常繁琐,这还没算上对代码进行测试,如果每次提交了代码都要来一个这样的过程,那是真的没法专心搞开发了。

如果用了 Jenkins,上面的整个过程都可以自动化完成。

初始化 Jenkins

Jenkins 的官网是 jenkins.io,它有很多种安装方式,例如下载 war 包到宿主机上,然后用 java -jar jenkins.war 命令启动。但是这种安装方式非常不利于管理和服务器的迁移,完全是在给 docker 托后腿。所以我选择用 docker 版的 jenkins。

使用 docker 版的 jenkins 是需要注意很多细节的

  • 首先我们要重写官方 jenkins 镜像
$ vi Dockerfile
FROM jenkins/jenkins:lts

USER root

RUN echo deb http://mirrors.aliyun.com/debian wheezy main contrib non-free \
    deb-src http://mirrors.aliyun.com/debian wheezy main contrib non-free \
    deb http://mirrors.aliyun.com/debian wheezy-updates main contrib non-free \
    deb-src http://mirrors.aliyun.com/debian wheezy-updates main contrib non-free \
    deb http://mirrors.aliyun.com/debian-security wheezy/updates main contrib non-free \
    deb-src http://mirrors.aliyun.com/debian-security wheezy/updates main contrib non-free \
    > /etc/apt/sources.list \
    && apt-get update \
    && apt-get install -y libltdl-dev
这里用了 jenkins 最新稳定版最为基础镜像,主要干了两件事:一、将账户改为 root,避免后面不必要的权限问题;二、安装 libltdl-dev ,它是为了解决用 jenkins 调用容器外部 docker 命令时发生以下错误的问题。(第 4~10 行是为了换阿里源提高速度)
docker: error while loading shared libraries: libltdl.so.7: cannot open shared object file: No such file or directory
  • 在启动刚刚重写好的 jenkins 镜像的时候还需要挂载三个宿主机的目录到容器内,第一个 jenkins_home 是为了对容器内 jenkins 的所有改动做数据持久化。最后两个目录是为了能让容器内的 jenkins 调用并操作容器外的 docker
$ vi docker-compose.yml
version: "3"
services:
 jenkins:
  build: .
  image: my_jenkins
  ports:
    - "8090:8080"
    - "50000:50000"
  container_name: my_jenkins
  volumes:
    - "/home/jenkins_home:/var/jenkins_home"
    - "/var/run/docker.sock:/var/run/docker.sock"
    - "/usr/bin/docker:/usr/bin/docker"
为了看起来清晰,我写了一个 docker-compose.yml 文件,将这个文件和之前的 Dockerfile 放在同一个目录中,可以用以下命令快速启动 jenkins,启动之后新构建的镜像和容器都名为 my_jenkins。
$ docker-compose up -d
  • 启动 jenkins 后浏览器访问 ip:8090 可看到初始化页面

这里要输入密码,它给出了密码在容器内的位置,我们要将路径改成宿主机上的,然后 cat 一下就能看到密码。
$ cat /home/jenkins_home/secrets/initialAdminPassword 
  • 将密码粘贴进去然后点继续,下一个页面选择插件,点默认推荐的就好了

  • 接着按提示创建一个账户

  • 之后就能够使用 jenkins 了

配置 Webhooks

要让 jenkins 操作本机上的 docker 的前提是它收到了我们 push 代码的通知,而这个通知就是由 GitHub 上的 Webhooks 来完成的,所以要将这两者关联起来。

  • 配置 Webhooks 之前首先要更改一下安全设置
打开全局安全配置,软后进行如下操作,否则 Webhooks 连接不成功,设置好了别忘了点保存。

  • 开始创建任务
点击首页的「开始创建一个新任务」,起个名字,选择流水线。

  • 生成身份令牌
点击「构建触发器」,选择「触发远程构建」,然后随便填写一段字符,然后把 URL 复制下来,记得把「JENKINS_URL」和「TOKEN_NAME」替换为相应的值,例如下图最终得到的 URL 就是 111.11.1.1:8090/job/test/build?token=123456,记下 URL 后点保存。

  • 去 GitHub 创建 Webhooks
打开我们要 push 代码的仓库,点击「Add webhook」

然后将刚才记下的回调 URL 填写到这里即可

此时,可以尝试一下 push 代码到仓库,正常情况下,jenkins 就会自动进行构建,虽然没有配置要构建什么,但是它也会进行这个任务,如果构建历史中自动出现了一个颜色是蓝色的任务则代表整个自动触发的过程是配置成功的。

编写自动任务脚本进行 CI/CD

  • 点击上面那个任务的「配置」,切换到流水线这里。本文不介绍流水线语法,就用 shell 命令来编写整个过程。但我们首先还是要点击「流水线语法」

  • 将示例步骤切换到「sh: Shell Script」,编写好 shell 脚本,熟悉 linux 命令的话,这个过程也应该很容易。写好之后点击「生成流水线脚本」,之后把生成好的流水线脚本复制下来

  • 将生成好的 流水线脚本复制到这里就好了,不过要把它复制到一个 node{} 里面才行。

以下是我写的生成好的流水线脚本,记得把定义的四个变量替换一下。
node {
    sh '''#!/bin/sh

    REPOSITORY_NAME="你的仓库名"
    REPOSITORY_URL="你的仓库地址"
    IMAGE_NAME="给你要构建的镜像起个名字"
    CONTAINER_NAME="给你要构建的容器起个名字"
       
    echo "清除仓库目录"
    rm ${REPOSITORY_NAME} -r
       
    echo "克隆远程仓库"
    git clone ${REPOSITORY_URL}

    echo "删除之前的镜像和容器"
    docker stop ${CONTAINER_NAME}
    docker rm ${CONTAINER_NAME}
    docker rmi ${IMAGE_NAME}
    
    echo "构建镜像"
    cd ${REPOSITORY_NAME}
    docker build -t ${IMAGE_NAME} .
    
    echo "发布应用"
    docker run -d -p 80:3000 --name ${CONTAINER_NAME} ${IMAGE_NAME}'''

}
  • 最后可以提交一次代码或者点击「立即构建」,就会自动完成整个过程,在「控制台输出」那里可以看到构建过程。

此时,浏览器访问 ip 就能看到更新过的应用了,这就是一个 CI/CD 过程,整个过程省略的测试环节,可自行加上。

后记

用一个测试服务器来做 CI/CD,能够更及时的发现问题、解决问题,提高代码质量。

但是本文所展示的过程缺陷也很明显,在更新应用时,是会先停掉容器,再启动新容器的,不能做到无宕机更新。而且整个过程也没有服务监控什么的,不能很好地了解无服务的运行状态。

总之,到目前为止,我们已经能够很好地将 docker 用在日常的开发中了。

点击查看博客原文

查看原文

ACChe 关注了标签 · 2018-12-07

flutter

clipboard.png

Flutter 是 Google 用以帮助开发者在 iOS 和 Android 两个平台开发高质量原生 UI 的移动 SDK。

Flutter is Google’s mobile app SDK for crafting high-quality native interfaces on iOS and Android in record time. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

Flutter 官网:https://flutter.dev/
Flutter 中文资源:https://flutter-io.cn/
Flutter Github:https://github.com/flutter/fl...

关注 947