GithubHelp home page GithubHelp logo

blog's Introduction

Eyas's github stats

### Hello ~ 👋

I'm Eyas.

  • 一个很普通的开发者
  • 以前做 Web 前端开发的,不过现在转方向了,但还会经常关注前端,对前端热爱不减
  • 现在做 Golang 后端开发,有时搞搞 NodeJs,后端的世界很精彩,我很喜欢
  • 闲时还会搞搞运维的东西,只是感兴趣的玩玩 k8s, swarm, docker, shell 等,不是很专业
  • 有时还搞一搞 Rust, Wasm 这种新玩意,但这些东西太高端我用不起,而且也没地方给我用啊

开源贡献

做过一些自己觉得还不错的开源工具

  • npm 仓库,我发布的npm 包都在这里,有些自认为比较好的东西有:
    • vuex-proxy 我把 vuex 给进(mó)化(gǎi)了,终于可以在 vuex 放飞自我了
    • gql-api-loader 嫌 Graphql 查询太难用了,搞了个插件,可以扔掉重重的 apollo 了
  • Go Toolkit Golang 工具集,把通用的工具能封装的都封装了,心血之作

logo

blog's People

Contributors

eyasliu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

docker 最省资源运行WordPress

背景

我想要运行WordPress程序,启动一个展示类的简单网站,但是我不想在我机器上安装php运行环境,也不想装mysql,因为在我看来他们的安装过程都是很繁琐,配置多,升级困难,不好维护。如果用docker 容器则不会有这些问题。

需求分析

那问题来了,既然WordPress依赖php和mysql,那么运行在docker里面和运行在主机不是一样的占用资源吗?对于这个问题,我的回答是,php是必须的,但是mysql并不是必须的,我可以使用sqlite替代mysql。sqlite是一种非常轻量级关系型数据库,他的资源占用非常的低,而且目前Linux所有发行版都默认附带sqlite,所以是不需要安装的。所以结果就是 只需要php,不需要mysql。

镜像选择

既然需要php运行环境,那就需要php镜像。但是在 dockerhub 能搜到 WordPress 镜像和 php 镜像,怎么选呢?

  • php 镜像只提供了最基本的运行环境,和最基本的php扩展
  • WordPress 镜像是基于 php 镜像,启用了WordPress 运行所需的所有扩展

所以很明显,当然是选择 WordPress 镜像。

最简操作

我想到了这个方法,肯定别人也早就想到了,早在几年前就有人做好了,如果觉得下面的方法太麻烦太难懂,想速成,就这样:

docker run -d -p 80:80 dorwardv/wordpress-sqlite-nginx-docker

这样就启动了一个基于 nginx 的 wordpress ,使用的 sqlite 插件。

这个项目的地址: https://github.com/dorwardv/wordpress-sqlite-nginx-docker

如果你要用这个方法,那下文的那么多内容其实不用看了。如果你想学到更多的东西,请继续看下文。

启动 WordPress 容器

默认镜像

在 WordPress 的默认镜像(latest)中,是使用php的默认镜像,php的默认镜像,是使用的debian系统,加上apache,暴露出 80 端口。所以在docker运行WordPress最快的方式就是

docker run -p 80:80 wordpress

命令运行完,就可以使用浏览器打开 http://localhost 应该就能看到WordPress安装界面了。
注:运行前请确定80端口没有被占用,如果被占用,可以使用另外的端口,在-p参数指定即可,如 -p 8080:80

alpine 镜像

上面是可以正常的运行,但是资源占用可能比在主机中运行更耗资源,要使php占用资源尽可能的小,首选alpine镜像。alpine镜像只提供了最小化系统运行所需。WordPress的alpine镜像tag为 4.8.1-fpm-alpine,没错,php的 alpine 镜像只有fpm版本。

php的 alpine 镜像使用

php:alpine镜像只启动了php-fpm,fpm监听9000端口(php服务都是监听9000端口),但是这个端口并不是web端口,而是 unix 端口,具体怎么用,我们要先从php的运行流程说起。

先看一段nginx 的 php 配置

server { 
  // 其他配置
  // ......
  // ......
  location ~ [^/]\.php(/|$) { 
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
      return 404;
    }

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO       $fastcgi_path_info;
    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php; 
  } 
}

当浏览器访问一个url时,访问的是nginx或者apache的web服务器,web服务器根据url匹配到这个请求应该给 fastcgi 处理,这个时候fastcgi 会把匹配到的php文件给 127.0.0.1:9000 处理,这个9000端口就是php启动的cgi服务,php服务接收到fastcgi 的请求后执行php文件,然后把执行的结果返回给fastcgi,fastcgi在给回 web服务器,web服务器再响应给用户,用户就能看到php代码执行的结果了。

写了那么长,其实可以简单地概括成:web服务器接收到请求后,让php服务执行,然后把执行结果返回。

再回到php:alpine镜像,其实这个镜像启动的9000端口就是php服务,WordPress的alpine镜像是基于php:alpine,只是在原本镜像基础上增加了php扩展和把wp源码放在了镜像中。所以说启动WordPress镜像的结果就只是启动了一个php服务罢了,这个php服务本身不接收不处理任何web请求。所以,我们还是需要一个apache或者nginx。

由于nginx比apache更轻量级,所以我选择了nginx,当然,是docker的nginx:alpine镜像。

另一个问题,WordPress源码是在WordPress镜像的/usr/src/wordpress里面,运行的时候会把 /usr/src/wordpress复制到 /var/www/html,然后定义了/var/www/html为挂载目录。这个过程可以在这里看到。这里就需要用到docker的volumes-from参数了,用这个参数指定一个容器启动一个新容器,会让新容器直接使用被指定的容器挂载目录。所以使用这个特性就可以让nginx使用到WordPress镜像的源码了。

先启动wordpress镜像,并暴露出端口

docker run -p 9000:9000 --name wordpress-fpm wordpress:4-fpm-alpine

第二部,新建一个nginx配置文件vhost.conf,内容如下

server { 
  listen 80; 
  server_name localhost; 
  root /var/www/html; 

  index index.php; 

  location / {
    try_files $uri $uri/ /index.php?$args;
  }

  rewrite /wp-admin$ $scheme://$host$uri/ permanent;

  location ~ [^/]\.php(/|$) { 
    fastcgi_split_path_info ^(.+?\.php)(/.*)$;
    if (!-f $document_root$fastcgi_script_name) {
      return 404;
    }

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO       $fastcgi_path_info;
    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php; 
  } 
}

第三部,启动nginx容器

docker run -p 80:80 -v `pwd`/vhost.conf:/etc/nginx/conf.d/default.conf --volumes-from wordpress-fpm nginx:alpine

就这样,访问http://localhost 就能到吗wordpress安装界面了

数据库

wordpress总算是开始安装了,不过说好的不用mysql而转用sqlite呢。

官方镜像中,在docker-entrypoint.sh 有mysql数据库的检查代码,我们要先去掉它,先把文件提取出来,删除 TERM=dumb php -- <<'EOPHP' ........ $mysql->close(); EOPHP 的这大段php代码即可,改好后留着备用,待会定制镜像时会放进新镜像中

wordpress 使用sqlite 需要用到一个插件: SQLite Integration

插件的使用方法是,把插件解压放到plugins目录,然后把该插件目录的 db.php 放到 wp-content 目录下,在执行安装过程,就能发现跳过了输入数据库信息的过程。

定制镜像

把插件也放在镜像里面,每次启动就不用重新复制插件了。这样需要使用 Dockerfile 定制镜像。我们基于wordpress:4-fpm-alpine 镜像去定制

FROM wordpress:4-fpm-alpine
MAINTAINER [email protected]

COPY docker-entrypoint.sh /usr/local/bin/

RUN curl -o sqlite-plugin.zip https://downloads.wordpress.org/plugin/sqlite-integration.1.8.1.zip && \
    unzip sqlite-plugin.zip -d /usr/src/wordpress/wp-content/plugins/ && \
    cp /usr/src/wordpress/wp-content/plugins/sqlite-integration/db.php /usr/src/wordpress/wp-content && \
    chmod +x /usr/local/bin/docker-entrypoint.sh && \
    rm -rf sqlite-plugin.zip

Dockerfile 有了,构建一下新的镜像

docker build -t fpm-sqlite-wp:latest .

构建好后就可以启动了

docker run -d -p 9000:9000 --name fpm-wp fpm-sqlite-wp
docker run -d -p 80:80 --name nginx --volumes-from fpm-wp nginx:alpine

到此为止,镜像定制完成。

如果使用docker-compose,配置可以这样写

version: '2'
services:
  fpm-wp:
    image: fpm-sqlite-wp
    networks: 
      - mywp
    expose:
      - '9000'
  
  nginx:
    image: nginx:1.11.10-alpine
    depends_on:
      - fpm-wp
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    volumes_from:
      - fpm-wp
    networks:
     - mywp
    ports:
      - '80:80'

结语

整个过程是很繁琐,但是这种定制在一个稍微复杂项目中我觉得不算什么,我的项目中nginx是做了很多事情的,所以是必须的,而且结果的确达到了节省资源的目的。

~ 按位非 操作符的妙用

document: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators

~ 操作符,叫做 按位非,它的用途就是将q其操作数当做32位的比特序列进行反转。

有符号 32 位整数

所有的按位操作符都会将操作数转成补码形式的有符号32位整数,就是将不足32位的二进制,用0填充至32位,其中最左位是符号位,决定数字是正数还是负数。而且只会处理整数,如果操作数为浮点数,将会舍弃小数部分。如数字 5 的二进制为

101

转成补码形式的32位整数是

00000000000000000000000000000101

那对其反转比特位(~5)后,就是

11111111111111111111111111111010

负数的二进制就是对其正数的比特位取反,所以结果得到的数字就是-6

~ 按位非

经上述例子可以得到一个公式:

~x = -(x + 1)

并且是取整数,接下来讨论一下该操作付的妙用

配合 indexOf

我们使用indexOf时,如果有匹配项则返回匹配的位置,无匹配项返回 -1,判断一个元素是否存在可这样写

arr.indexOf(5) === -1

将-1代入公式得

~-1 = -(-1 + 1) 
    = 0
// ~-1 = 0

所以可简写为

~arr.indexOf(5)

取整

因为按位操作符只操作整数部分,非整数部分会被舍弃,可用于取整操作

~~2.5333 // 2

用两个按位非操作符,就可以取整了,相当于 parseInt()

注意

由于按位操作符只能操作32位的整数,所以他能操作的最大数为 (2^32 - 1),如果大于这个数字,那么公式 -(x + 1) 不再适用

JavaScript基础知识与面向对象编程

目标:熟练掌握JavaScript基础知识与面向对象概念

大纲

  • 基本概念
    • 语法
    • 基本数据类型
    • 操作符
    • 引用类型
      • Object
      • Array
      • Function
      • 包装类型
  • 执行环境
    • 闭包
    • this
  • 面向对象
    • 创建对象
      • 工厂模式
      • 构造函数
      • 原型模式
    • 继承
      • 原型链继承
      • 借用构造函数
      • 组合式继承

基本概念

语法

JavaScript的语法借鉴C语言语法,与php基本一致

  • 区分大小写
  • 标识符
  • 注释
  • 语句
  • 关键字

基本数据类型

一共5种基本数据类型

  • Undefined
  • Null
  • Boolean
  • Number
  • String

使用 typeof 可以查看数据类型

Undefined && Null

Undefined 与 Null 类型都是只有一个值

  • Undefined -> undefined
  • Null -> null

undefined 表示变量未定义,或者定义了但是未赋值
null 表示空对象指针,不指向任何对象,本质上他是Object类型

Boolean

Boolean 类型共两个值 truefalse,表示真和假

基本类型转换时,会将以下值转换为false

  • 空字符串 ''
  • 数值 0 和 NaN
  • null
  • undefined

其他都会转为 true

Number

  • 整数
  • 浮点数:由于计算会有误差,不要将浮点数用于判断
  • NaN:非数值(Not a Number),用于本来要返回数值的操作数
    • NaN 自己不等于自己: NaN == NaN // false

数值转换

Number() 可以把任何值转换为数值,parseInt()parseFloat()将字符串转换为数值。

转换规则

  • Boolean值, true -> 1 , false -> 0
  • Number值,原样输出
  • null -> 0
  • undefined -> NaN
  • 字符串
    • 只有数字:字符串变数值 '123', '12.3', '-123', '0xf'
    • 空字符串:0
    • 其他:NaN

String

  • 用单引号或者引号,两者无区别
  • 任何数据类型都有 toString() 方法,可转换为字符串

操作符

  • + - * / ++ -- += -= 等操作符与PHP完全一致
  • ! < > <= >= || && 等于PHP完全一致

tips

  • =====:
    • == 会先将左右两边先转换为相同数据类型后,再比较 2 == [2] // true
    • === 原样比较 2 === '2' // false
  • a || b 当a为true时,完全忽略b,可用于设置默认值 var x = x || 100
  • a && b 当a为true时,执行b,当a为false时,忽略b,可用于判断 x && alert('hello')
  • !!a 用于类型转换为Boolean

引用类型

引用类型是一种数据结构,用于将数据和功能组织在一起。

Object 类型

创建一个Object

// 使用 new
var person = new Object();
person.name = 'Eyas'

// 使用字面量
var person = {
  name: 'Eyas'
}

以面向对象概念理解的话,Object类型是所有引用类型的基类,Array,Function,包装类型等都属于Object的子类

Array 类型

创建Array

// 使用new
var persons = new Array

// 使用字面量
var persons = []

栈方法

push : 将元素添加到数组末尾,返回数组长度
pop: 返回最后一项元素并删除

队列方法

shift: 返回第一项并删除
unshift: 将元素添加带数组第一项,返回数组长度

迭代方法

  • every() 对所有元素运行给定函数,如果所有都返回true,则返回true
  • some() 对所有元素运行给定函数,如果有一项返回true,则返回true
  • filter() 对所有元素运行给定函数,返回所有返回true的项组成数组
  • forEach() 对所有元素运行给定函数,不返回
  • map() 对所有元素运行给定函数,每次的返回结果组成的数组

tips

  • 判断是否为数组,使用 Array.isArray()
  • 转换字符串:默认会以逗号分隔元素 '' + [1,2,3]
  • 排序:reverse() 元素顺序反转,sort()对元素调用toString方法然后确定顺序
  • 合并数组concat() [1,2,3].concat([4,5,6])
  • 查找元素,判断是否有该元素 indexOf() [1,2,3].indexOf(4)

Function

定义

// 函数声明
function say(str){}

// 函数表达式
var say = function(str){}

注意事项

  • 不执行return 以后的语句
  • 没有重载
  • arguments 是参数集合,类数组,但没有push,pop等操作
  • 函数名只是一个指向函数对象的变量

包装类型

为了便于操作基本类型数据,每当读取一个基本类型值的时候,后台会创建一个对应的基本包装类型的对象,让我们能够调用一些方法来操作这些数据

  • Boolean
    • toString 返回字符串 'true' 和 'false'
  • Number
    • toString 将数值转换为字符串
    • toFixed 指定小数位数,以字符串返回
  • String
    • length 字符串长度
    • concat 拼接字符串
    • substr, substring, slice 切割字符串
    • trim 去掉前后空格
    • ...
  • Null 和 Undefined 没有任何可操作方法,他们访问属性会报错

执行环境(作用域)

执行环境定义了变量或函数有权访问的其他数据,决定了他们的各自行为。每个执行环境都有一个与之关联的变量对象,环境中的所有变量和函数都保存在这个变量中,但是我们无法访问这个变量,在解析器执行时会在后台使用

作用域链

每个函数都有自己的执行环境,当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问

var Name = 'Tenmic';
var Version = '1.0.0'

function appName(){
  var fullName = function(name, version){
    return name + ' V' + version
  }

  return fullName(Name, Version)
}

appName() // Tenmic V1.0.0

闭包

闭包是指有权访问另一个函数作用域中的变量的函数,创建方式就是在一个函数内部创建另一个函数

var addNum = function(num){
  return function(num2){
    return num + num2
  }
}

var addOne = addNum(1);
var addTen = addNum(10);

addOne(3)  // 4
addTen(15) // 25

this

this对象是在运行时基于函数的作用域绑定的,在全局函数中,this等于window,当函数被某个对象的方法调用时,this等于那个对象。在编写闭包中,this依情况而定

function say(){
  // this === window
}

// jQuery 
$('body').show()  // show 里面的this指向 $('body')

var obj = {
  name: 'Tenmic',
  getNameFun: function(){
    // this == obj
    return this.name
  }
}

绑定this

在运行中,this变量可以显式的绑定

  • bind通常在定义之后绑定this,暂时不执行
  • call 与 apply在绑定this后立刻执行

面向对象编程

创建对象

工厂模式

// 工厂模式
function createPerson(name, age){
  var o = {
    name: name,
    age: age
  }

  return o;
}
var person = createPerson('Eyas', 24)

构造函数模式

// 构造函数
function Person(name, age){
  this.name = name;
  this.age = age;
  this.getInfo = function(){
    return this.name + '-' + this.age
  }
}

var peroson = new Person('Eyas', 24)
  • 构造函数没有显式创建对象
  • 没有return

new 关键字的执行过程

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this 指向新对象)
  3. 执行代码
  4. 返回新对象

构造函数就是一个普通的函数,与一般函数没有什么不同,任何函数都可以用 new 来调用。

缺点
每个函数都要创建一次

原型模式

我们创建的每个函数都有一个prototype,这是一个指向一个对象的指针,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

prototype 是通过调用构造函数而创建的那个对象实例的原型对象

function Person(){}

Person.prototype.name = 'Eyas';
Person.prototype.age = 24;
Person.prototype.getInfo = function(){
  return this.name + '-' + this.age
}

var person = new Person()

person.name // Eyas
person.getInfo() // Eyas-24
理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。所有原型对象自动获得一个constructor属性,constructor指向函数本身

function hello(){}
hello.prototype
hello.prototype.constructor === hello  // true

在查找对象属性时,首先获取对象内部属性,如果获取不到,则沿着原型链逐步往上查找,最终仍未找到则返回undefined

继承

JavaScript是基于原型链的继承。利用原型让一个引用类型继承另一个引用类型的属性和方法。

原型继承

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。如果我们手动的让原型对象等于另一个原型的实例,此时的原型对象将包含另一个原型的实例。如此层层递进,构成实例与原型的链条,实现了继承

function Person(name){
  this.name = name
  this.getName = function(){
    return this.name
  }
}

function Student(){}
Student.prototype = new Peroson()

var student = new Student
student.getName()  // getName 来自于 Person

借用构造函数继承

很明显的,上述原型继承无法控制Person的传入参数,我们使用借用构造函数方式继承

function Person(name, age){
  this.name = name;
  this.age = age;
}
function Student(){
  Person.apply(this, arguments);
}
Student.prototype = new Person

这种方式,虽然解决了参数传递,但是依然解决得不彻底,参数的限制太大了

组合式继承

function Person(name){
  this.name = name;
  this.age = age;
}
function Student(name, age){
  Person.call(this, name);
  this.age = age
}
Student.prototype = new Person

这种继承方式综合了上述两者继承方式,可以近乎完美实现继承

前端博客缓存机制

继上一篇文章 #2 ,使用github api搭建了一个博客,托管于github pages。这么做优点很显著,功能齐全,seo友好,issues页面也好看,github用户互动方便等等。但是,在某些方面收到了阻碍。

遇到的问题

  • 接口偏慢,有部分网络还把github墙了
  • 接口访问次数受限,对于普通的api请求限制为了每个ip每小时最多请求60次

解决方案

给网站加个缓存,把每次请求回来的数据存到浏览器,需要数据的时候从浏览器中拿。由于数据来源于缓存,速度回非常快

流程

当应用需要请求数据时,首先检查一下本地缓存中是否存在数据,如果存在则从缓存中获取,直接返回给应用,如果不存在,就先请求api,请求成功后先将数据存到缓存中,再从缓存中读取数据,返回给应用。这样,应用的数据统一都来自缓存,而api请求成功后,都统一存到缓存,这样统一流程更方便实现与维护。

缓存更新机制

考虑一下几个问题:

  • 这是博客应用,而且github还有一个永远最新的备份。所以应用数据有所延时其实完全没关系
  • 网站流量很小,页面请求不多
  • 博客都是文章,文章都是字,读文章不会经常刷新页面

结论是: 每当进入首页的时候刷新列表,而且还是后台静默更新。进入文章详情后,更新文章的评论数据。 因为评论数据无法一次性全拿回来

流程图

dataflow

实现方案

技术选型:

  • lowdb:快速,简便,以lodash驱动的简易数据库
  • localStorage:lowdb支持
  • underscore-db:提供工具函数方便操作lowdb数据库

code

详见文件 post.js

未来展望

  • localStorage 储存空间有限,如果能换成indexedDB的话就更好了,lowdb作者没有做indexedDB驱动,但是提供了一个开放api,或许可以自己写个驱动
  • 接口请求每小时60次限制可使用github 开发者应用升级为5000次每小时,但是这需要一个后端服务

构建 Golang 应用最小 Docker 镜像

我通常使用docker运行我的 golang 程序,在这里分享一下我构建 docker 镜像的经验。我构建 docker 镜像不仅优化构建后的体积,还要优化构建速度。

示例应用

首先贴出代码例子,我们假设要构建一个 http 服务

package main

import (
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	fmt.Println("Server Ready")
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		c.String(200, "hello world, this time is: "+time.Now().Format(time.RFC1123Z))
	})
	router.GET("/github", func(c *gin.Context) {
		_, err := http.Get("https://api.github.com/")
		if err != nil {
			c.String(500, err.Error())
			return
		}
		c.String(200, "access github api ok")
	})

	if err := router.Run(":9900"); err != nil {
		panic(err)
	}
}

说明:

  • 这里选择 Gin 作为例子,是为了演示我们有第三方包条件下要优化构建速度
  • main函数第一行打印了一行字,为了演示后面启动时遇到的一个坑
  • 跟路由打印了时间,为了演示后面遇到的关于时区的坑
  • 路由 github 尝试访问 https://api.github.com,为了演示后面遇到的证书坑

这里我们可以先试一试构建后包的体积

$ go build -o server
$ ls -alh | grep server
-rwxrwxrwx 1 eyas eyas  14.6M May 29 10:26 server

14.6MB,这是一个http服务的 hello world,当然这是因为使用了 gin ,所以有些大,如果用标准包 net/http 写的 hello world,体积大概是接近 7 MB

Dockerfile 的进化

版本一,初步优化

先看看第一个版本

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

说明:

  • 设置 GOPROXY 是为了提升构建速度
  • 先复制 go.modgo.sum ,然后 go mod download,是为了防止每次构建都会重新下载依赖包,利用docker构建缓存提升构建速度
  • go build 时加上 -ldflags "-s -w" 去除构建包的调试信息,减小go构建后程序体积,大概能减小 1/4
  • 使用了多阶段构建,也就是 FROM XXX as xxx ,在构建程序包的时候,使用带编译环境的镜像去构建,运行的时候其实完全不需要go的编译环境,所以在运行阶段使用docker的空镜像 scratch 去运行。这部是减小镜像体积最有效的方法了。

好了,下面开始构建镜像

$ docker build -t server .
...
Successfully built 8d3b91210721
Successfully tagged server:latest

到了这一步,构建成功,看看镜像大小

$ docker images
server          latest         8d3b91210721      1 minutes ago        11MB

11MB,还行,现在运行一下

$ docker run -p 9900:9900 server
standard_init_linux.go:211: exec user process caused "no such file or directory"

发现启动报错了,而且main函数的第一行打印语句都没有出现,所以整个程序完全没有运行。错误原因是缺少库依赖文件。这其实是构建的 go 程序还依赖底层的 so 库文件,不信可以在物理机编译后看看它的依赖

$ go build -o server
$ ldd server
        linux-vdso.so.1 (0x00007ffcfb775000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9a8dc47000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9a8d856000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9a8de66000)

这是不是跟我们的认知有点出入呢,说好无依赖的呢,结果还是有几个依赖库文件呢,虽然这几个依赖都是最底层的,一般操作系统都会有,可谁叫我们选了 scratch,这个镜像里面除了linux内核以外真的什么都没了。

这是因为go build 是默认启用 CGO 的,不信你可以试试这个命令 go env CGO_ENABLED,在 CGO 开启情况下,无论代码有没有用CGO,都会有库依赖文件,解决方法也很简单,手动指定关闭CGO就行,而且包体积并不会增加哦,还会减少呢

$ CGO_ENABLED=0 go build -o server
$ ldd server
        not a dynamic executable

版本二,解决运行时报错

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

改动点: go build 前加了 CGO_ENABLED=0

$ docker build -t server .
...
Successfully built a81385160e25
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

正常启动了,我们访问一下试试,访问之前看看当前时间

$ date
Fri May 29 13:11:28 CST 2020

$ curl http://localhost:9900       
hello world, this time is: Fri, 29 May 2020 05:18:28 +0000

$ curl http://localhost:9900/github
Get "https://api.github.com/": x509: certificate signed by unknown authority

发现有问题

  • 当前系统时间是 13:11:28 ,但是根据由显示的时间是 05:11:53,其实是docker 容器内的时区不对,默认是 0 时区,可是我们国家是 东8区
  • 尝试访问 https://api.github.com/ 这是 https 站点,报证书错误

解决问题

  • 在容器放置根证书
  • 设置容器时区

版本三,解决运行环境时区与证书问题

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
  apk add --no-cache ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 阶段,安装了 ca-certificates tzdata 两个库,在runner阶段,将时区配置和根证书复制了一份

$ docker build -t server .
...
Successfully built e0825838043d
Successfully tagged server:latest
$ docker run -p 9900:9900 server
[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] GET    /github                   --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :9900

访问一下试试

$ date
Fri May 29 13:27:16 CST 2020

$ curl http://localhost:9900       
hello world, this time is: Fri, 29 May 2020 13:27:16 +0800

$ curl http://localhost:9900/github
access github api ok

一切正常了,看看当前镜像大小

$ docker images
server          latest         e0825838043d      9 minutes ago        11.3MB

才 11.3MB,已经很小了,但是,还可以更小,就是把构建后的包再压缩一次

版本四,进一步减小体积

FROM golang:1.14-alpine as builder
WORKDIR /usr/src/app
ENV GOPROXY=https://goproxy.cn
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
  apk add --no-cache upx ca-certificates tzdata
COPY ./go.mod ./
COPY ./go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\
 upx --best server -o _upx_server && \
 mv -f _upx_server server

FROM scratch as runner
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/src/app/server /opt/app/
CMD ["/opt/app/server"]

在 builder 阶段,安装了 upx ,并且go build 完成后,使用 upx 压缩了一下,执行一下构建,你会发现这个构建时间变长了,这是因为我给 upx 设置的参数是 --best ,也就是最大压缩级别,这样压缩出来的后会尽可能的小,如果嫌慢,可以降低压缩级别从 -1-9 ,数字越大压缩级别越高,也越慢。我使用 --best 构建完成后看看镜像体积。

$ docker build -t server .
...
Successfully built 80c3f3cde1f7
Successfully tagged server:latest
$ docker images
server          latest         80c3f3cde1f7      1 minutes ago        4.26MB

这下子可小了,才 4.26MB,再去试试那两个接口,一切正常。优化到此结束。

总结

要减小镜像体积,首先多阶段构建这很重要,这样就可以把编译环境和运行环境分开。

另外,选择 scratch 这个镜像其实很不明智,它虽然很小,但是它太原始了,里面什么工具都没有,程序启动后,连容器都进不去,就算进去了什么都做不了。所以就算一昧的追求尽可能小的镜像体积,也不建议选择 scratch 作为运行环境,我暂时只踩到小部分的坑,后面还有更多坑没踩,我也没有兴趣继续踩 scratch 的坑。

建议选择 alpine ,alpine 的镜像大小是 5.61MB 这个大小其实还是镜像解压后的大小,实际上下载镜像的时候,只需要下载 2.68 MB 。还有,上文所有我说的镜像体积,全都是指解压后的镜像体积,和实际上传下载时的体积是不一样的,docker自己会压缩一次再传输镜像

还有个很小的镜像是 busybox,它的体积是 1.22MB,下载 705.6 KB ,有大部分的linux命令可用,但是运行环境还是很原始,有兴趣可以去尝试

无论是 alpine 还是 busybox ,他们都会上述时区和证书问题,同样按照上面方法就能解决

了解一下字节码

字节码编程

字节码,在编程中无处不在,但是在业务层中也许您也用不着,但是了解一下还是有好处的。开发稍微底层一些的逻辑基本都会碰到

常识与基本概念

我们都听过一些概念:

  • int 类型占用32位
  • 一个 ascii 码字符占一个字节
  • 一个 utf8 汉字字符占用3个或4个字节(utf8编码字节长度是可变的,占用1-6个字节不等)
  • 0B00010100 这是一个二进制数字(2^4 + 2^2),024 这是一个8进制的数字(8^1 * 2 + 8^0 * 4), 0X14 这是一个16进制数字(16^1 * 1 + 16^0 * 4),他们都是表示十进制的 20
  • 操作系统有 32 位和 64 位

上面这些例子中,有些单位是字节(byte),有些是 (bit),这个 指的是比特位,也就是二进制位, 8 个比特位等于 1 个字节 8bit = 1byte,为什么大多数情况要把比特换算成字节来表示呢?因为计算机中的内存条的最小单元就是字节。可以理解成内存条是有一个个格子组成的,每个格子存储一个字节,也就是 8 个比特位。每个格子都有它的唯一编号,称为内存地址。

当操作系统是32位时,最多能标记 2^32 个格子,也就是 4GB,当操作系统是64位时,最多能标记 2^64个格子,也就是 17179869184GB。所以说32位的系统的内存最多只能有4GB,推荐现在的所有操作系统都升级到 64 位的。

另外在某些编程语言中,如Go, C, int 类型的长度是和操作系统有关的,在32位系统上int是32位,在64位系统上int是64位的,当然有些编程语言是固定长度的,如java的int永远都是32位,下文为了统一口径,将 int 类型都视为 32 位

深入

编程语言的类型

var num int = 0X14

比如C语言的 int 类型,它是 32 位的,也就是 4 个字节,一个int类型数字需要占用 4 个内存格子,那么放在内存应该是这样存储的(字节之间没有空格,这里只是为了好看才加的空格)

0X00 00 00 14

转成字节(8进制)

0B0000 0000 0000 0024

所以还是转成16进制好看些,我们看到的内存地址也通常是16进制的。

大序端与小序端

将高位数的地址放在前面还是后面,这成了个问题,可能以前做计算机的大佬们没商量好,就导致有了大序端和小序端。有点类似于文字的方向 ltr 和 rtl,也就是文字是从左往右念,还是从右往左念,大部分语言是从左往右的了,比如中文和英文。

  • 大序端,把高位放在前面,比如数字 21,读作: 二十一, 2 是高位,在前面,1是低位放在后面,对应内存的存储也是,高位的地址放在前面,低位放在后面
  • 小序端,和大序端相反,把高位放在后面,自行脑补,反正用得少,也不用去理解小序端

另外也叫大端法、小端法,或者大端对齐、小端对齐,其实说的都是一个概念。在网络传输中有规定,必须要使用大序端进行传输,于是在网络这么发达的今天,大序端是主流。小序端比较少遇到,遇到的时候麻烦您再转一次吧

后文的所有示例,都基于大序端

操作技巧

类型转换

大部分编程语言都会有 byte 或者 uint8 这个类型,它的一个值就是一个字节,它是跟内存中的字节对等的,又因为所有的运行中的值都是存在内存中的,所以字节数组可以用来表示所有数据,那是不是字节数组可以和所有的数据类型进行转换呢,可以,但是这前提是理解编程语言内部的序列和反序列化才能这么干,实际上即使知道理解了也不会那么干。

但是对于常用的数据类型和字节数组的转换我们还是得需要知道如何转换的。

数字转字节数组 []byte

数字的转换其实上文作为例子已经展示过了,总结一下流程

  1. 确定该数字类型的长度,每 8 位一个字节,如 int32 的字节数组长度是 4 ,int64 是的字节数组长度是 8
  2. 把数字换算成16进制,回填到低位字节位,其余字节位用0补齐数组长度

如把 int32 类型的 20 数字转成字节数组

20 = 0X00000014
bytes = [4]byte{0X00,0X00,0X00,0X14}

事实上,这种计算方法是很 low 的,更好的方法应该是使用位移才对。

字符串转字节数组 []byte

字符串是有编码的,常用的编码有 ascii, utf8, gbk 等等,通常推荐只用 utf8, 在学习字符串转字节数组前,我们先来了解下什么是字符串编码吧。

字符编码

简单的解释就是一个映射表(码表),一个id(也称:码点、码位)对应了一个字符,不同编码的不同点在于他们的映射表有多大,占用多少空间,仅此而已。

  • 如 ascii 编码,先看看它的映射表,一个数字对应一个英文字符,码表总共只有 127 个记录,一个字符占用的空间永远是 1 个字节
  • 如 utf8 编码,也看看它的映射表,很大吧,根本看不完,所以utf8 可以表示非常非常多的字符,但是utf8的编码是兼容 ascii 的,也就是ascii编码可以直接转成utf8不会有任何异常,utf8 的一个字符占用空间长度是可变的,1 ~ 6 字节之间,它能表示 2^(6 * 8) = 2^48 个字符,足够容纳进去所有语言的文字了吧,其中utf8的编码有些规范,自行搜索去了解吧
  • 其他如 gbk,gb2312的原理和utf8一样,只是这是专为中文而优化的字符编码,每个字符占用的空间没有utf8那么多,也就是内存占用会少一些,但对于当今来说这点内存占用真的不算什么吧。

提到了 utf8,那就不得不说一下 Unicode 了。

  • Unicode 它是一个字符集,它为每一个字符分配一个码点,它是一个标准,一个规范,定义了一系列的规则。
  • utf8, utf16, utf32 是字符编码,他们实现了编码和解码的过程。他们的码点是跟 Unicode 不一样的,但是都会有一套固定的转换规则,所以根据 Unicode 码点和他们的码点是可以互转的

他们的关系有点像是接口和实现类,但又不是那么回事。


再继续看看字符转字节数组的操作,这个其实没什么好说的,按照码表对照来转就是了,比如 utf8 编码的 根据码表查到就是 0Xe6b7a6,转成字节数组 []byte{e6, b7, a6},它的 Unicode 码点是 \u6DE6

字符串转字节数组同理,就是多个字符串的转换结果拼接起来。通常不需要我们自己去计算,按理说标准库应该都直接有集成,还是直接用标准库吧。

// Golang 例子
str := "你说得对"
bytes := []byte(str)

JavaScript 中的字节码

JS 有没有 byte 类型?没有。但它有 ArrayBuffer,但是 ArrayBuffer 它是用来存储数据的,它的值无法修改,无法给它赋值,需要转成 TypedArray 或者 DataView 才能修改。TypedArray 和 DataView 的用途有些不一样

  • TypedArray 就是数组,就是用来操作数组一样去操作二进制数据,比如遍历(map,forEach,every,entries,reduce等)、过滤(filter)、查找(find,indexOf,includes等)、合并、切片(slice,splice)、填充(fill),等等,自由度很高,任意操作
  • DataView 是为了将数值转换成 ArrayBuffer,注意是只处理数值

TypedArrayDataView 实例有什么方法我就不多说,直接看文档来得实在,这里提供几个例子

数值类型转ArrayBuffer

JavaScript 的类型中,它只有 Number 这个数据类型,它的类型长度其实我不知道,但是我知道它最大能安全处理的数字是 2^53 - 1,好像长度是 53,超过这数字处理不了,但是也还能存储大致的值不会报错,所以,千万别用 64 位去存储js的数字,那是不可靠的,我建议把js的number转成4个字节(即32位)。

function numToBuffer(num) {
    const buffer = new ArrayBuffer(4) // 4个字节长度,即 32 位
    const view = new DataView(buffer) // ArrayBuffer 不能直接编辑,只能转换成 DataView 或者 TypedArray 才能编辑,这里是处理数值,所以用 DataView
    view.setInt32(0, num) // 从第0个字节开始填充 num 以 int32 类型转成的字节
    return view.buffer // 再把 DataView 转成 ArrayBuffer
}

字符串转 ArrayBuffer

还记得当时字符串转字节数组说要根据字符编码吗,js的字符串默认是 Unicode 编码,得需要转成 utf8 编码。怎么转呢?还是用开源库吧,这个: encode-utf8 ,它还有个对应的utf8解码库: decode-utf8

Golang 中的字节码

Golang 的基础类型 byteuint8 他们是等价的

type byte = uint8

所以字节数组就是 []byte

数值类型 和 []byte 互转

使用标准库做类型转换

import (
    "bytes"
    "encoding/binary"
)

func IntToByteArray(num int64) []byte {
    buf := new(bytes.Buffer)
	binary.Write(buf, binary.BigEndian, num)
	return buf.Bytes()
}

func ByteArrayToInt(arr []byte) int64 {
    data := binary.BigEndian.Uint64(arr)
	return int64(data)
}

字符串 与 []byte 互转

这个写过go的应该没有人不知道了,不啰嗦了

总结

字节码操作在编程过程中是非常重要的技能点,用不用得着另外说,学会了至少能装逼,会让人不明觉厉。

react 快速入门

React是Facebook开源的一个用于构建用户界面的Javascript库,专注于MVC架构中的V,即视图。

组件

我们可以创建一个有特殊功能的组件,在需要的地方可以反复的用。在编写应用的时候,我们是编写一个又一个的组件,然后组合成一个完整应用。

定义组件

继承 React 的 Component 类可以创建一个组件

class Hello extends React.Component{
  constructor(props, context){
    super()
    this.state = {} // 定义组件状态
  }

  render(){ // render 必须,渲染组件的html结构
    return <div></div>
  }
}

如果一个组件不需要使用状态,可以以函数的方式编写组件

const Hello = (props, context) => {
  return <div></div>
}

const Hello2 = props => <div></div>

JSX

JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。React 可以用来做简单的 JSX 句法转换。目前babel可以转换 JSX 语法为 JavaScript

JSX 是一个可选工具,目的是为了方便的书写HTMl与布局

基本语法

  • 组件名的首字母必须大写
  • 如果是渲染组件,组件名的变量名必须在作用域范围内
  • 根节点必须只有一个
  • 标签必须有闭合
  • 属性表达式:把表达式用大括号 {} 包起来,不要用引号 ""
  • 子节点:XML格式包围子节点
  • 注释:作为子节点,用 {} 包围注释部分
  • 判断:只能使用三元运算符做判断
  • 循环:不支持循环,只能使用遍历
var Nav = props => <div class="navbar"></div> // 组件名首字母大写
var myNavbar = <Nav color="blue" /> // Nav 变量必须在当前作用域
var myElement = <h1 className={"hello" + "demo"}>Hello {name}</h1> // 属性表达式
var myHello = <h1>hello {name ? 'world' : name}</h1> // 判断

var Comment => props => (
  <div>
    {// 这是单行注释}
    {/* 这是多行注释 */}
    hello
  </div>
)

var Each = <div>{[1,2,3].map(item => <span>{item}</span>)}</div> // 遍历

自定义属性

JSX 默认会转译所有字符串,防止各种 XSS 攻击

<div>{'First &middot; Second'}</div>

万不得已,可以直接使用原始 HTML。

<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />

如果往原生 HTML 元素里传入 HTML 规范里不存在的属性,React 不会显示它们。如果需要使用自定义属性,要加 data- 前缀

<div data-custom-attribute="foo" />

css 类名的 class ,和 label 的 for 属性,都是js的关键字,不能用于属性名,所以使用 classNamehtmlFor 代替

<div className="myclass"></div>
<label htmlFor="demo"></label>

属性 props

组件对外公开一个简单的属性(Props)来实现功能,但内部细节可能有非常复杂的实现。组件要将数据传递给子组件,使用组件属性的形式传递下去,在子组件通过 this.props 访问传递的属性。绝对不能手动更改 props值

class Hello extends React.Component{
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

<Hello name="Tenmic"></Hello>

props 验证

参考文档 http://reactjs.cn/react/docs/reusable-components.html

React.PropTypes 提供很多验证器来验证传入数据的有效性。当向 props 传入无效数据时,JavaScript 控制台会抛出警告

在组件内部定义静态变量 propTypes 可设置

class Hello extends React.Component{
  static propTypes = {
    name: React.PropTypes.string // name 必须为字符串
  }
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

默认props

设置静态变量 defaultProps 设置默认的 props

class Hello extends React.Component{
  static defaultProps = {
    name: 'Tenmic'
  }
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

状态 state

大部分组件的工作应该是从 props 里取数据并渲染出来。但是,有时需要对用户输入、服务器请求或者时间变化等作出响应,需要使用 state ,表示组件的不同状态。

State 应该包括那些可能被组件的事件处理器改变并触发用户界面更新的数据

  • 定义状态在组件的构造方法中定义state变量
  • 修改状态,只能通过 setState 方法
  • 只要修改状态,就会触发组件的重渲染
class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(){
    this.setState({
      open: !this.state.open
    })
  }
  render(){
    return <div 
      className={this.state.open ? 'active' : ''
      onClick={this.toggle}
      }></div>
  }
}

生命周期

在组件的调用到销毁的过程中,会触发不同阶段的声明周期,

componentWillMount

整个周期只调用一次,在初始化渲染执行之前立刻调用

componentDidMount

在初始化渲染执行之后立刻调用一次,在生命周期中的这个时间点,组件拥有一个 DOM 展现

componentWillReceiveProps

在组件接收到新的 props 的时候调用。在初始化渲染的时候,该方法不会调用,只会在发生重渲染时调用参数 nextProps 获取即将传递过来的 props

shouldComponentUpdate

在接收到新的 props 或者 state,将要渲染之前调用。该方法在初始化渲染的时候不会调用,根据返回值决定组件是否需要更新

componentWillUpdate

在接收到新的 props 或者 state 之前立刻调用,在初始化渲染的时候该方法不会被调用。

使用该方法做一些更新之前的准备工作。

componentDidUpdate

在组件的更新已经同步到 DOM 中之后立刻被调用。该方法不会在初始化渲染的时候调用。

使用该方法可以在组件更新之后操作 DOM 元素

componentWillUnmount

在组件从 DOM 中移除的时候立刻被调用。

在该方法中执行任何必要的清理,比如无效的定时器,或者清除在 componentDidMount 中创建的 DOM 元素。

refs

为了方便的获取组件渲染后的DOM节点,可使用 ref,this.refs 存储了组件内所有的ref

class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(){
    console.log(this.refs.sidebar)
  }
  render(){
    return <div ref="sidebar" onClick={this.toggle}></div>
  }
}

事件系统

事件处理器将会传入虚拟事件对象的实例,一个对浏览器本地事件的跨浏览器封装。它有和浏览器本地事件相同的属性和方法,包括 stopPropagation() 和 preventDefault(),但是没有浏览器兼容问题。

支持的事件: http://reactjs.cn/react/docs/events.html

事件绑定

听过在标签中设置 on + 事件名 可设置事件处理函数

class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(event){
    console.log(event)
  }
  render(){
    return <div onClick={this.toggle}></div>
  }
}

context

为了方便的将数据传递给子组件,使用context将数据传递给所有的后代组件

定义传递的数据

在父组件,内部定义 childContextTypes,定义传递那些变量以及什么数据类型

class App extends React.Component{
  static childContextTypes = {
    name: React.PropTypes.string
  }
  // other code
}

getChildContext 函数返回需要传递的具体数据

class App extends React.Component{
  static childContextTypes = {
    name: React.PropTypes.string
  }
  getChildContext(){
    return {
      name: 'Tenmic'
    }
  }

  // other code

}

子组件获取数据

在子组件,通过定义 contextTypes 定义需要哪些值,然后通过 this.context 访问

class ChildComp extends React.Component{
  static contextTypes = {
    name: React.PropTypes.string
  }

  render(){
    this.context.name // 'Tenmic' ,从父组件获取,
  }
}

样式

在 React 中,行内样式并不是以字符串的形式出现,而是通过一个特定的样式对象来指定。在这个对象中,key 值是用驼峰形式表示的样式名,而其对应的值则是样式值,通常来说这个值是个字符串

var divStyle = {
  color: 'white',
  backgroundImage: 'url(' + imgUrl + ')',
  WebkitTransition: 'all', // 注意这里的首字母'W'是大写
  msTransition: 'all' // 'ms'是唯一一个首字母需要小写的浏览器前缀
};

React.render(<div style={divStyle}>Hello World!</div>, mountNode);

样式的 key 用驼峰形式表示,是为了方便与JS中通过DOM节点获取样式属性的方式保持一致

electron 构建打包总结

最近启动了一个新项目 eyasliu/electron-startkit,在打包构建上有一些研究,主要是针对于 windows 程序的打包,其他平台的构建方式没有深入

试过了两个打包工具,

  • electron-packager
  • electron-builder

另外对优化打包后的体积优化有一些研究

electron-packer

这个工具做的事情很单一:构建好之后把程序放在一个文件夹下,文件夹的 [应用名].exe 就是应用启动入口,我们写的代码都被放到了 resource/app[.asar] 里面。这就是这个工具做的全部事情。

尽管做的事情单一,但是其提供了很多可选项

正是因为它的功能比较单一,所以用法也很简单,而且构建好的包很通用,可以配合其他工具一起使用,比如 electron-winstaller 或者 electron-wix-msi 进一步的打包成为 msi 安装包, 单个 exe 执行文件等等

使用方法:

# 命令行形式: 
# electron-packager <sourcedir> <appname> --platform=<platform> --arch=<arch> [optional flags...]
electron-packager . DesktopApp --platform=win32 --arch=x64 --out=build --icon=icons/icon --asar
// 使用 js api
const packager = require('electron-packager')
packager({
  arch: process.env.ARCH_TARGET || 'x64',
  asar: true,
  dir: path.join(__dirname, './'),
  icon: path.join(__dirname, './icons/icon'),
  out: path.join(__dirname, './build'),
  overwrite: true,
  platform: process.env.BUILD_TARGET || 'all'packager({
}).then(() => {
  console.log('build done!')
}).catch(err => {
  console.error('build error!!!')
})

这里给出一点配置的建议:

  • asar: true 设置为 true 可以使自己写的代码经过加密并打包到一个app.asar 文件中
  • prune: true 设置为 true,只会将 package.json 的 dependencies 依赖包复制到 resources/app, 否则会将devDependencies 的依赖包也放进 resources/app 下面。当然,在最新版它默认就是 true[/斜眼笑]

配合其他工具二次打包

配合 electron-winstaller 打包成 msi

更多更详细配置见:https://github.com/electron/windows-installer#usage

const packager = require('electron-packager')
const electronInstaller = require('electron-winstaller');

packager({ /*上一步的配置*/}).then(() => {
  appDirectory: './build/[your app name]-[platform]-[arch]', // 上一步使用 packager 打包好的目录
  outputDirectory: './build/msi-installer',
  authors: 'My App Inc.',
  msi: true // 指定要 msi
}).then(() => {
  console.log('build msi installer done')
})

这样打包好之后 setup.exe 就是单个 exe 执行文件,setup.msi 就是windows 安装文件。

配合 electron-wix-msi 几乎和 electron-winstaller 一模一样,只是配置项有些不太一样。

electron-builder

官方网站与官方文档:https://www.electron.build/

electron-builder 这个工具就很强大了,几乎涵盖了构建所需要的大部分功能。官方的描述

A complete solution to package and build a ready for distribution Electron, Proton Native or Muon app for macOS, Windows and Linux with “auto update” support out of the box.
一个完整的解决方案,可以为macOS,Windows和Linux打包并构建一个可供分发的 Electron,Proton Native 或Muon应用程序,并提供开箱即用的“自动更新”支持。

所以它不但提供了构建功能,还提供了自动更新的功能。下面先看看它的构建功能

构建包

相关链接:

  1. https://www.electron.build/configuration/configuration
  2. https://www.electron.build/configuration/win
  3. https://www.electron.build/configuration/nsis

使用步骤:

  1. 编写配置文件:
  • package.json 中加一个 build 字段,里面就是放它的配置项
  • 或者通过指定 --config <path/to/yml-or-json5-or-toml> 使用对应的配置文件,如果不指定,默认是 electron-builder.json
  1. 编写配置,请点击上方的相关链接查看,贴出一份我的配置
{
  "productName": "Desktop APP",
  "appId": "Personal.DesktopApp.Startkit.1.0.0",
  "copyright": "Copyright © 2018 ${author}",
  "directories": {
    "output": "build"
  },
  "asar": true,
  "artifactName": "${productName}-${version}.${ext}",
  "compression": "maximum",
  "files": [
    "dist/electron/**/*"
  ],
  "dmg": {
    "contents": [
      {
        "x": 410,
        "y": 150,
        "type": "link",
        "path": "/Applications"
      },
      {
        "x": 130,
        "y": 150,
        "type": "file"
      }
    ]
  },
  "mac": {
    "icon": "build/icons/icon.icns"
  },
  "win": {
    "icon": "build/icons/icon.ico",
    "target": "nsis",
    "legalTrademarks": "Eyas Personal"
  },
  "nsis": {
    "allowToChangeInstallationDirectory": true,
    "oneClick": false,
    "menuCategory": true,
    "allowElevation": false
  },
  "linux": {
    "icon": "build/icons"
  },
  "electronDownload": {
    "mirror": "http://npm.taobao.org/mirrors/electron/"
  }
}

部分配置项解读

配置项分为 公共部分特定平台部分,公共部分主要是设置程序通用的选项

公共部分
  • appId 必须要设置,是一个程序的唯一标识符,还与后面的程序自动更新有关
  • asar 设置为 true 可以把自己的代码合并并加密
  • productName 指定一下程序名称,这个对于后面创建桌面快捷方式和开始菜单都有关系
  • compression 压缩级别,如果要打包成安装包的话建议设为 maximum 可以使安装包体积更小,当然打包时间会长一点点
win 特定部分

这里只讲 windows 相关的构建,特地说下windows的配置

  • target 打包的类型,决定了打包后的文件类型
    • nsis 打包成一个独立的 exe 安装程序,下方会详细说nsis配置
    • nsis-web web安装程序(其实我还有些懵这是什么),总之打包之后生成一个 exe 文件和一个 7z 压缩包,双击 exe 会直接启动应用
    • portable 打包成单个 exe 独立执行程序,双击该 exe 直接启用应用,如果要用这个,请将 compression 设置为 store,不然你的应该会启动的非常慢
    • appx 打包成 Windows Store(windows 应用商店)的程序,只有 windows 10 才可用
    • msi 打包成 msi 安装程序
    • squirrel 使用 squirrel 打包,该工具已不再维护,但是还可用,建议使用nsis替代
    • 7z 压缩成 7z 文件,解压后可双击打开
    • zip 压缩成 zip 文件,解压后可双击打开
    • tar.xz 压缩成 tar.xz 文件,解压后可双击打开
  • legalTrademarks 公司名称

所以如果想要程序可以安装,请选择 msi 或者 nsis,如果想要作为独立执行程序,使用选择 portable

下面说下nsis的配置

nsis

对于打包 windows 程序来说,nsis这个配置是最主要的,nsis 本身是一个 Windows 系统下安装程序制作程序 http://nsis.sourceforge.net

这里对于它的一些配置特地说明

  • oneClick 建议为 false,可以让用户点击下一步、下一步、下一步的形式安装程序,如果为true,当用户双击构建好的程序,自动安装程序并打开,即:一键安装(one-click installer)
  • perMachine 安装的时候是否为所有用户安装
  • allowToChangeInstallationDirectory 建议为 true,是否允许用户改变安装目录,默认是不允许
  • installerIcon uninstallerIcon installerHeader installerHeaderIcon installerSidebar uninstallerSidebaruninstallDisplayName 定制安装与卸载的图标和界面
  • displayLanguageSelector 安装的时候是否让用户选择语言
  • createDesktopShortcut 是否创建桌面图标
  • createStartMenuShortcut 是否在开始菜单创建入口
  • menuCategory 是否在开始菜单创建一个分类入口
  • include 指定要包含 nsis 的脚本,基于内置的nsis脚本进一步扩展
  • script 指定自定义使用 nsis 的脚本,完全自己控制nsis 的打包
include nsis

相关文档:

  1. nsis 官网 http://nsis.sourceforge.net/Main_Page
  2. electron-builder 的内置nsis 脚本 https://github.com/electron-userland/electron-builder/tree/master/packages/app-builder-lib/templates/nsis
  3. electron-builder 自定义 nsis 说明: https://www.electron.build/configuration/nsis#custom-nsis-script

如果你要自己完全自定义 nsis 脚本,那么你应该并不需要 electron-builder,所以这里只接受自定义部分 nsis 脚本,首先将 nsis 配置的 include 指定脚本

{
  "nsis": {
    "oneClick": false,
    "include": "./builder.nsi",
  }
}

以下是对官方文档简单表达

  1. electron-builder 暴露 customHeader, preInit, customInit, customUnInit, customInstall, customUnInstall, customRemoveFiles,这几个区块
  • customHeader 自定义安装、卸载界面的头部
  • preInit 在安装前会执行脚本
  • customInit 安装初始化
  • customUnInit 卸载初始化
  • customInstall 安装脚本
  • customUnInstall 卸载脚本
  • customRemoveFiles 重写卸载的时候要移除哪些文件
  1. 定义了 BUILD_RESOURCES_DIR 和 PROJECT_DIR 这两个变量

MUI2.nsh Modern UI 2.0

中文文档:http://www.ccav1.com/mui2/

其实electron-builder 内部引入了 mui2 作为定制安装界面,如果我们要定制安装界面,自然离不开它,比如要加个欢迎步骤和许可协议不走,只需要声明一下

!include "MUI2.nsh"

; 欢迎页面
!insertmacro MUI_PAGE_WELCOME
; 许可协议页面
!insertmacro MUI_PAGE_LICENSE "${BUILD_RESOURCES_DIR}\resources\LICENCE.txt"

然后就有欢迎界面也协议页面了

示例

贴出一份我的nsi脚本作为例子

; 引入 Modern UI 2.0
!include "MUI2.nsh"

; ; MUI Settings
!define MUI_ABORTWARNING

; 欢迎页面
!insertmacro MUI_PAGE_WELCOME
; 许可协议页面
!insertmacro MUI_PAGE_LICENSE "${BUILD_RESOURCES_DIR}\resources\LICENCE.txt"

; 初始化开始菜单
!define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir"
!define MUI_STARTMENUPAGE_NODISABLE
Var StartMenuFolder
!insertmacro MUI_PAGE_STARTMENU Application $StartMenuFolder

ShowInstDetails show
ShowUnInstDetails show
SpaceTexts show

; 安装脚本
!macro customInstall

  ; 写入开始菜单
  CreateDirectory "$SMPROGRAMS\$StartMenuFolder"
  !insertmacro MUI_STARTMENU_WRITE_BEGIN Application
  CreateShortCut "$SMPROGRAMS\$StartMenuFolder\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe"
  CreateShortCut "$SMPROGRAMS\$StartMenuFolder\卸载 ${PRODUCT_NAME}.lnk" "$INSTDIR\Uninstall ${PRODUCT_NAME}.exe"
  !insertmacro MUI_STARTMENU_WRITE_END
!macroend

; 卸载脚本
!macro customUnInstall

  ; 删除开始菜单
  !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuFolder
  Delete "$SMPROGRAMS\$StartMenuFolder\${PRODUCT_NAME}.lnk"
  Delete "$SMPROGRAMS\$StartMenuFolder\卸载 ${PRODUCT_NAME}.lnk"
  RMDir "$SMPROGRAMS\$StartMenuFolder"
!macroend


命令行的使用

先去这里看看命令行的所有选项 https://www.electron.build/cli

在使用命令行工具的时候必须要指定 平台(platform) 和 架构(arch),它还提供了快捷选项,比如 --linux --win --x64 --ia32 等等,这里我建议再指定一下 --config 使用对应的构建配置文件

# 默认就是 build 命令
electron-builder --config scripts/builder.json --win --64
#等价于
electron-builder build --config scripts/builder.json --win --64

js API

尚不明确,无文档

构建包体积优化

包体积的优化有以下几点:

package.json

在打包的过程中,electron 会读取package.json 的 dependencies 依赖列表,并使用该列表重新解析依赖,将解析到的依赖复制到 resources/app 中,也就是我们的代码资源包,所以优化体积最主要就是减小 node_modules 的体积,那么 dependencies 的依赖尽量少,那么 node_modules 体积就会小,所以尽可能的把不必要的依赖包不要放到 dependencies,而是放到 devDependencies. 尤其安装的时候注意

npm install -D xxx
npm install --save-dev xxx

yarn add --dev xxx

webpack

使用 webpack 合并压缩代码,可以混淆代码并减小代码体积,在webpack的 externals 配置设置为 package.json 的 dependencies。在以后的编码中如果一个包你能确定它只有 js 文件用上了,就把它放到 devDependencies ,如果出了js 文件外还有其他二进制文件,就放到 dependencies。

比如 lodash 放到 devDependencies, sqlite3 因为包含有二进制文件就放到 dependencies。

// weback.config.js
const npmPackage = require('./package.json')
module.exports = {
  // ...
  externals: [ ...Object.keys(npmPackage.dependencies) ],
  // ...
}

至此,node_modules 的体积应该是大幅度减少了。

yarn autoclean

因为在构建过程中,解析好依赖后是直接复制当前目录的 node_modules 下面的包,那么用 yarn autoclean 命令自动清理掉 node_modules 下面没用的文件,比如 .md, .txt, doc/ 等等文件全清理掉,进一步优化 node_modules 体积

yarn autoclean --init
yarn autoclean

自动更新

官方文档: https://www.electron.build/auto-update

要使用自定更新功能,那么首先在配置文件中要配置一下 publish,你需要准备好一个静态服务器,你要有权限往静态服务器的目录上传文件,就这样

{
  "publish": [
    {
      "provider": "generic",
      "url": "http://localhost:8888/download/",
      "channel": "latest"
    }
  ],
}
  • provider 使用哪种策略作为更新服务,这里指定用 generic,因为这是最简单最通用的
  • url 静态服务器的url
  • channel 频道,会决定检查更新的时候请求哪个文件,如果是 latest,就会请求 latest.yml

然后在main进程的入口文件加上代码

require("electron-updater").autoUpdater.checkForUpdatesAndNotify()

如果按照上面的配置,那么更新流程是:

  1. 请求 ${url}/${channel}.yml 文件,对比本地的客户端版本,如果没有新版本,什么都不做,否则下一步
  2. 请求 ${url}/${latest.yml中定义的文件名} 下载好安装包
  3. 提醒用户新安装包下载好了,下次重启更新,或者立即更新
  4. 自动安装更新

每当构建好一个版本,把构建完后生成的 latest.yml 和安装包上传到那个静态服务器的目录,就可以实现版本更新了,如果你有兴趣,完全可以自己动手写个后端服务基于这个流程解锁更多姿势

其中上面的更新流程,你应该留意到了一切都是自动完成,如果想要自定义这些规则,可以这样

  1. 监听autoUpdater的事件,一有更新状态变化就会发事件
  • error 检查更新错误
  • checking-for-update 正在检查更新
  • update-available 有新的可用更新
  • update-not-available 没有可用的更新,也就是当前是最新版本
  • download-progress 正在下载更新版,会有更新进度对象
  • update-downloaded 新安装包下载完成
  1. 还有一些 api 可以控制流程
  • .checkForUpdates() 执行一次检查更新
  • .checkForUpdatesAndNotify() 执行一次检查更新,如果有新的可用更新,还会自定弹出一个自带的通知提示告诉用户有新的更新
  • .downloadUpdate(cancellationToken) 执行下载安装包
  • .quitAndInstall(isSilent, isForceRunAfter) 退出应用并安装更新, isSilent 是否静默更新,isForceRunAfter更新完后是否立即运行
  1. 还有一些配置项
  • autoDownload = true 有可用更新时是否自动下载
  • autoInstallOnAppQuit = true 如果安装包下载好了,那么当应用退出后是否自动安装更新
  • allowPrerelease = false 是否接受开发版,测试版之类的版本号
  • allowDowngrade = false 是否可以回退版本,比如从开发版降到旧的稳定版

更优雅的更新

其实有个更好的更新方案,每次安装更新的时候都是全量下载安装包更新,但是electron的应用呢,通常只会更新 resources/app.asar 这个文件,但是这个文件是非常小的,在更新的时候可以只更新这一个文件,这样更轻量,更可控,这就需要自己动手写个后端服务配合了,当然这里只是给出一个方案并没有demo。

展示

以下是最终配置后的安装过程截图

项目地址 https://github.com/eyasliu/electron-startkit

qq 20180913165941
qq 20180913165951
qq 20180913165958
qq 20180913170005
qq 20180913170018
qq 20180913170022

一个异想天开的网页视频播放器

本文全程无干货,不建议阅读

开头

首先,我们写 web 前端的时候,如果要播放一个视频该怎么做呢?想都不用想,肯定是 html 的 video 标签啊。多简单,给video.src 附一个值,它就可以自动播放了。

video 标签的局限性

video 标签用起来当然方便,但是呢,他也有很大的局限性。

首先我们想要给视频加滤镜怎么办?我要播放其他格式视频怎么办?那就没办法了,目前的video 标签还没那么多功能。

如果要给视频加滤镜,现在的video标签是没有办法做到的,它没有暴露任何相关滤镜处理视频的API。如果我想要在网页播放 hls, flv, mkv, rmvb. avi 这种格式的视频怎么办,光是靠 video 标签也是没办法做到的,它只支持很有限的那几种格式罢了。

当 Video 遇上 MSE

所谓 MSE (Media Source Extensions API) 就是给 video 加上了流媒体功能,传入视频流给 video 播放。这就扩展 video 的 API,能实现一些以前做不了的事。首先是流媒体功能,可以播放直播流了,其次是可以给视频做重编码后使用video标签播放。

xqq 在哔哩哔哩的时候实现了一个 flv.js 播放器,能播放flv视频。它的原理,就是通过获取 flv 的数据流,实时转换成 mp4 的数据流,然后通过 MSE 喂给 video 标签,video 标签发现这是 mp4 格式的视频流,然后就给它播放。这里有两个关键点,重编码视频数据流,和 MSE。

类似的项目还有 hls.js

但是,MSE的虽然扩展了video标签功能,但是对视频流的局限更大了,不仅需要mp4格式,还需要是 fragmented mp4 才能使用。但是我想要的是不仅于此

一些不切实际的想法

有 flv.js 这么一个例子,我就突发奇想,是不是可以给其他的视频流也给转成 mp4 的视频流,然后同理可得,实现网页播放器可播放多种数据格式的视频,比如 mkv, hls, avi, rmvb, rm 等等。一番恶补音视频知识后,发现有些阻碍,flv 和 mp4 这是两种视频容器,他们的视频编码可能是相同的,都是 H264,flv 转 mp4 其实只是转换了容器,视频编码其实不需要转的。其他的视频容器格式可不一定是 H264,而且就算是H264,那格式格式转换那还是一道跨不过去的坎。那么如果我想做一个能像本地播放器那样能播放大部分主流格式的视频,我该怎么做呢?

其实我这个想法有点天马行空了,按照我平常学到的知识来说,当然可以否决这种需求。web播放器怎么可能做到像本地播放器那么强大呢?

希望的火苗

再恶补一些知识后,其实我发现这其实是有希望能做到的,所谓的不可能做到。其实是知识面限制了我的想象力。不过这涉及到了一些比较新,也比较难的知识面了。

wasm 可以使得在浏览器高效的执行其他语言的逻辑,由于是新特性,兼容性不太好,但是也不差,各个平台的新版主流浏览器都已支持。有个项目叫 ffmpeg.js,直接把 ffmpeg 编译成 wasm 了,而且他还提供了自定义编译脚本,使得 ffmpeg.js 的最终构建包尽可能的小。这就给视频做软解带来了希望。

WebGL 是用于构建图形应用的,这个其实是挺长时间的标准API了,兼容性不错。可以用于渲染视频图像。

然后是声音,使用 AudioContext

WebWork,这也是必须的,视频软解耗费的CPU过高,如果没有 Work ,页面将会严重的卡 CPU

这下子,理论上的各种必须的条件都满足了,就看接下来的施工了

可预见的缺陷

  • 首先是兼容性,上面的特性都比较新,在老旧浏览器肯定是用不起来的。
  • 有些视频是无法流媒体播放的,比如 AVI
  • CPU 占用会很高,毕竟软解嘛,软解就是用 CPU 做视频解码
  • WebGL渲染视频,这还得看硬件是否支持,如果不支持也没辙
  • rmvb 等 Real 系视频的编解码是不公开的,不确定能否支持

大言不惭的目标

实现一个可扩展、可嵌入、兼容 html video 标准,可播放任意视频格式的 Web 视频播放器。

目前进展

正在建文件夹

RequireJs 基础知识

官方网站:http://requirejs.org/

用途:

  • 实现js文件的异步加载
  • 管理模块之间的依赖性,便于代码的编写和维护

入口文件

在引入requirejs文件的script标签中使用data-main定义入口文件,入口文件得js后缀可省略

<script src="js/require.js" data-main="js/main"></script>

requirejs 配置

在入口文件的头部,或者在引入requirejs前,定义requirejs的配置

require.config({

  baseUrl: '/',   // 基础路径
  path:{    // path 定义文件路径,相当于给路径取个别名
    backbone: 'js/lib/backbone.js',
    underscore: 'js/lib/underscore.js',
    jquery: 'js/lib/jquery.js'
  },
  shim:{  // 对于非AMD规范的库,声明依赖,暴露的变量名
    backbone: {
      deps: ['jquery', 'underscore'],
      export: 'Backbone'
    }
  }
})

导入模块

在入口文件中导入依赖包

require(['underscore', 'jquery'], function(_, $){
  // init project
})

AMD模块的规范

require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。

定义模块

使用define函数定义一个模块,模块的主体部分是一个匿名函数,函数的返回值是模块暴露的方法与变量

无依赖

具体来说,就是模块必须采用特定的define()函数来定义。 如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

define(function(){
  return {}
})

有依赖

如果模块有依赖,第一个参数为依赖数组,第二参数模块主体

define(['underscore', 'jquery'], function(_, $){
  return {}
})

加载非规范模块

在配置文件的shim中,定义非AMD规范的库,引入的时候就可以直接按照规范去引入非规范的模块

插件

插件列表:https://github.com/requirejs/requirejs/wiki/Plugins

插件的使用见插件文档,通常来说是使用感叹号分隔 !

define(['babel!es6-module'], function(module){
  // code
})

redux 快速入门

应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。惟一改变 state 的办法是触发 action,一个描述发生什么的对象。为了描述 action 如何改变 state 树,你需要编写 reducers。

三大原则

  • 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  • State 是只读的: 惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  • 使用纯函数来执行修改: 为了描述 action 如何改变 state tree ,你需要编写 reducers

基础

action

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般使用 store.dispatch() 将action 传到store

const action = {
  type: 'ADD_TODO',
  text: 'Build my first Redux app'
}

store.dispatch(action)

一个action必须有type字段,描述action做的事情,其他字段都为可选项,是action携带的数据,只作为传递数据用途。

action 创建函数

Action 创建函数 就是生成 action 的方法。可以简单的理解为调用这个函数就会创建action并且自动调用store.dispatch()。当我们使用react-redux工具时,可以自动绑定dispatch,所以函数可以简化为这样:

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

// 在组件中调用,自动dispatch
this.props.addTodo('test task')

redux会dispatch函数的返回值

reducer

action 只是描述了有事情发生了这一事实,reducer根据action的描述怎么去更新状态。reducer就是一个纯函数,接收旧的state和action,返回新的state。使用函数默认值设置初始状态。

function todo(state = {todos: []}, action){
  switch(action.type){
    case 'ADD_TODO':
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: state.todos.length + 1,
            text: action.text
          }
        ]
      }
    default:
      return state;
  }
}

注意:永远不要在reducer做这些事

  • 修改传入参数

  • 执行有副作用操作,如api请求和路由跳转

  • 调用非纯函数

  • 不修改state,直接返回一个新对象state

    谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算

拆分 reducer

应用一旦复杂了,reduer会非常长,为了更好的模块化管理,拆分成多个小reducer,然后合并。Redux 提供了 combineReducers() 工具类合并reducer。但只能有一个根reducer,相当于react组件只有一个根标签。

import { combineReducers } from 'redux';

const todoCrud = (state, action) => {}
const todoVisable = (state, action) => {}

const todo = combineReducers({
  crud: todoCrud,
  visable: todoVisable
})

export default todo;

store

action 来描述“发生了什么”,reducers 来根据 action 更新 state 。Store就是把他们联系到一起的对象。Redux 应用只有一个单一的 store

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

创建store

redux 提供 createStore() 工具创建store,接收根reducer作为参数

import {createStore} from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer)

搭配react

Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。他只是一个状态管理工具而已。

redux与react搭配使用react-redux工具。

Provider

<Provider /> 包围需要使用redux状态的组件。如根组件。provider 需要传递 store 进去。

import { render } from 'react-dom';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import App from './containers/App';
import todoApp from './reducers';

let store = createStore(todoApp);

let rootElement = document.getElementById('root')
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

connect && bindActionCreators

在Provider包围的范围内,通过 react-redux 提供的 connect() 方法将包装好的组件连接到Redux。传入组件需要的状态与action 生成函数。

bindActionCreators 可以将普通的函数自动绑定变为 action生成函数

import {connect, bindActionCreators} from 'redux';
import {addTodo} from './actions';

class App extends React.Component{
  handlerClick(e){
    this.props.todos // 来自于redux的 state.todos
    this.props.addTodo('this is my task') // 调用 action 生成函数
  }
  render(){
    return <div onClick={this.handlerClick}></div>
  }
}

// 将 reudx 的状态传递给组件,可在组件的props获取
function mapStateToProps(state){
  return {
    todos: state.todos
  }
}
// 将普通函数绑定转化为action生成器
function mapDispatchToProps(dispatch){
  return bindActionCreators({addTodo}, dispatch);
}

// 连接
export default connect(mapStateToProps, mapDispatchToProps)(App)

我们可以使用 es7 的 decorator 简化代码书写。

@connect(
  state => ({todos: state.todos})
  disatch => bindActionCreators({todos, dispatch})
)
export default class App extends React.Component{}

中间件

在 action 被发起之后,到达 reducer 之前可以使用中间件处理action。

异步action

redux-thunk 是处理异步action的redux中间件,它的所有代码如下

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

    return next(action);
  };
}

如果action是函数,那就需要显示调用 dispatch 才能真正触发action,使用方法如下

const getTodo(){
  return dispatch => {
    request.get(url).end((err, res) => {
      dispatch({
        type: 'GET_TODO',
        data: res.body
      })
    })
  }
}

GraphQL Go 笔记

github.com/graph-gophers/graphql-go 遇到一些坑

框架选型

别人已经对比过了,很详细 https://medium.com/open-graphql/choosing-a-graphql-server-library-in-go-8836f893881b

简单的概括就是:

  • github.com/samsarahq/thunder 用struct 的 tag 定义 schema,回调形式定义resolver
  • github.com/graphql-go/graphql 用配置形式定义 schema,resolve 是其中一个配置项
  • github.com/99designs/gqlgen 单独文件定义schema,并自动生成 resolver,可惜不支持 go module
  • github.com/graph-gophers/graphql-go 单独文件定义 schema,以接口形式实现schema的字段

我要在 go module中单独文件定义schema,只有 gophers 符合,自从gqlgen支持了 go module 后,这才是我最想要的,原因是

  1. gophers 动态读取schema文件,当然可以把schema文件编成go文件。最麻烦的在于这是在运行时检查类型的,而且resolver要精确到最底层
  2. gqlgen 使用配置文件定义schema文件位置、编译输出的路径之类,当然schema支持多文件,而且是写好schema后编译为go文件,还会根据schema的type和input自动生成 model,resolve只需要精确到model就可以

安装gqlgen的时候,建议先禁用go module

GO111MODULE=off go get -u github.com/99designs/gqlgen

返回类型是否指针

  • 如果字段类型是必填,返回的类型不能为指针,如: String! 需要返回 string 类型
  • 如果字段类型是选填,返回的类型一定为指针,如: String 需要返回 *string 类型

这应该是 string 类型永远都会有值,默认值是 "",但是指针类型可以为 nil,可判断为空

Mutation 和 Query 不能分组

graphql-go 无关,是graphql 的 schema 本身就不支持 将 mutation 和 query 分成多个地方,如果 mutation 比较多的话,估计会比较难以管理,query 的根查询倒是问题不大,目前还没有发现什么好方案

lowdb 前后端轻量级数据库

what is lowdb

项目地址:https://github.com/typicode/lowdb

当我第一次接触到 lowdb 的时候,被它的方便小巧,使用简单的吸引住了。lowdb 是一个轻量级的数据库。可用于服务端与前端。在服务端,以一个json文件形式永久存储数据。在前端,可使用 localStorage 永久存储。另外,无论前后端,都可以使用内存作为存储。

why lowdb

其实喜欢这个工具的原因,是有以下几点:

  • 操作api 是完全基于 lodash 的,lodash API 非常强大与方便,相当于有了一个强大且熟悉的 ORM
  • 数据库轻巧,基本不怎么消耗内存空间
  • 体积小巧,lodash是我一直使用的工具,添加了lowdb后打包文件只增加了 2.x KB 的体积

usage

server 端

server 使用一个 json 文件作为数据库

import low from 'lowdb';
import storage from 'lowdb/file-sync';
// 初始化
const db = low('db.json', {storage});
// 数据库增删改查
db('posts').push({title: 'lowdb is awesome'})

前端

在前端使用localStorage 的一个键值对作为存储数据库

// ...
import storage from 'lowdb/broser';
// ...

内存

lowdb 还可以运行在内存中,前后端都可以用

import low from 'lowdb';
const db = low();
// ...

缺点

尽管 lowdb 很酷炫,但是它依然也有他的不足

  • 速度慢,它毕竟只是一个文件,当然比不过一个数据库系统的速度
  • 不支持索引与事务等等数据库功能,原因同上
  • 效率低下,每次的写入和读取,其实是对整个数据库库进行操作。比如修改一个地方,它是先把整个数据库数据序列化,在重新对插入字符串到库中。
  • 因为 lodash api 原因吧,它好像没有批量操作的接口

尝试 lowdb 与 indexedDB 的结合

当我第一次体验了lowdb的便利性,我就想把前端的 localStorage 驱动换成 indexedDB,因为indexedDB 是类似于 NoSql 的数据库,拥有事务、索引,存储空间非常大,存储类型众多,api 纯异步等原因。当我在网上搜索的时候,发现根本没有相关的库,我当时就想自己编写一个,但是最后还是失败了。网上现在没有这种库是有道理的,因为这跟lowdb的工作方式有关,它只能操作字符串当做数据库存储,所以 indexeddb 的驱动做出来,效率、速度等方面将会比 localStorage 更糟糕。

redux 的action creator 解决冲突

action creator

在使用redux的过程中,经常会使用一种函数叫action creator,他们专门为触发action而生,应用逻辑基本都是在这里执行,一个redux应用充斥着大批大批的action creator,所以action creator的模块化非常重要,否则项目进行到一定程度时将变得非常难以维护

模块化

通常我们将action的操作类型进行分组,比如一个post.js里面专门操作文章,photo.js里面专门操作相册。

组件绑定 action creator

在 react 应用中,绑定 action creator 使用 bindActionCreators 函数,参数传入一个都是纯函数的对象,将会把里面的函数全部放到props中。

如果对象中的元素不是函数,将会被 完全忽略

那么问题来了,如果我传入两个相同名字的函数的时候,肯定有一个会被覆盖。

// post.js 
export function getList(){}

// photo.js
export function getList(){}

// bind action creator
function mapDispatchProps(dispatch){
  return bindActionCreators({
    ...postAction,
    ...photoAction,
    fooObj: {} // 将会被忽略
  }, dispatch)
}

// component
this.props.getList()  // 哪个getList会被调用呢

但是那两个都是必须的,这个时候除了修改函数名外好像没有其它办法了。

redux-bind-action-groups

这时候有个插件出生了: redux-bind-action-groups , 他在 bindActionCreators 的基础上添加了对象的支持

分组

这时候,我觉得能把ac进行分组很有必要,在绑定 action creator 的时候,对象里面的元素依然是对象,在往里一层才是函数。而且为了兼容性,还可以传入函数,实现原来的操作,这样在迁移的时候可以不改动一行代码

function mapDispatchProps(dispatch){
  return bindActionCreators({
    postAction,
    photoAction,
    fooObj: () => {}
  }, dispatch)
}

this.props.postAction.getList();
this.props.photoAction.getList();
this.props.fooObj();

这时候,在将 bindActionCreators 替换成 redux-bind-action-groups 的时候,完全不用修改代码即可添加新功能

// old code
import {bindActionCreators} from 'redux';
function mapDispatchProps(dispatch){
  return bindActionCreators({
    ...
  }, dispatch)
}

// new code
import bindActionCreators from 'redux-bind-action-groups';
/* 其他不用变 */

jQuery 基础知识

jQuery && $

在jquery中, $ 变量是 jQuery 变量的简写

jQuery === $  // true

$ 方法

  • $ 方法接收一个包含 CSS 选择器的字符串,然后用这个字符串去匹配一组元素
  • $ 方法接收原始 HTML 标记字符串,动态创建由 jQuery 对象包装的 DOM 元素。同时设置一系列的属性、事件等
  • $ 方法接收一个函数作为参数,绑定一个在DOM文档载入完成后执行的函数。$(document).ready()的简写

事件

事件的触发会有三个阶段,捕获,处于目标,冒泡

  • 捕获:事件由最上层元素接收,然后逐步向更具体的元素传播
  • 处于目标:在最具体的元素中触发
  • 冒泡:事件开始时,由最具体的元素接收,然后逐步向上传播

捕获阶段不会触发事件,事件会在处于目标阶段开始,逐渐冒泡触发

事件绑定

// 使用通用事件名作为函数
$('button').click(function(event){
  // event 为事件对象
})

// 使用 bind 或者on
$('button').bind('click', function(event){})
$('button').on('click', function(event){})

// 使用 on 事件委托
$('body').on('click', 'button', function(event){})

阻止冒泡

事件处理函数有一参数 event,在该函数中调用event.stopPropagation() 可阻止事件冒泡

$('button').click(function(event){
  event.stopPropagation()
})

扩展

$.fn是指jquery的命名空间,加上fn上的方法及属性,会对jquery实例每一个有效。

扩展为了不污染全局变量,应使用立即执行函数包裹

(function($, document, window){
  $.fn.myExtention = function(){
    // extendsion code
    // ...
  }
})(jQuery, document, window)

更优雅的编写 React 组件 —— stateless functions

纯函数

这篇文章 简单的介绍了一下纯函数

现在再来简单的概括一下:

纯函数就是相同的输入,永远都会有相同的输出,没有任何可观察的副作用

一个函数,如果与外界完全没有任何联系,那么内部的逻辑完全不受外界影响,所以永远会有相同的输出,如:

let addOne = x => x+1

他的输出永远都是对传入的值 +1 , 如果跟外界有联系,如

let shouldAdd = false;
let addOne = x => shouldAdd ? x+1 : x;

这样函数的输出,将会取决于外界环境,相同的输入,并不一定会有相同的输出,这样子函数就不纯了。

react state

组件的状态,是决定组件在不同时刻拥有的不同表现方式

有状态组件

编写组件的时候,组件可以有状态(state),一个组件在不同状态可以有不同输出。如果把一个组件看成是一个函数的话,那么以相同的输入(props),将会根据内部状态(state)的不同会得到不同的输出。那么这就不是纯函数了

// ...other code

render(){
    return this.state.open ? <div>open</div> : <span>close</span>
}

如上例子,state是组件内部决定的,外界无法控制,所以会根据state产生不同的输出

无状态组件

如果一个组件没有状态(state),那么组件的输出方式,将完全取决于两个参数:propscontext,只要有相同的 props 和 context ,那么他们的输出绝对是相同的。将组件比喻成函数的话,相同的输入(props 和 context) 永远都会有相同的输出

// ...other code

render(){
    return this.props.open ? <div>open</div> : <span>close</span>
}

如上例子,props是我们的输入,只要输入相同,那么他的输出也一定相同。

函数式无状态组件

我们编写组件的时候,并不是所有的组件都是需要状态的。由于react是以数据驱动,数据决定了react组件的输出结果,所以为了让我们组件更好控制,应该尽量的少使用状态。

编写函数式无状态组件

在 react 0.14 版本之后,提供了一种新的组件编写方式,就是 stateless functions,使用纯函数创建组件。

function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}
ReactDOM.render(<HelloMessage name="Sebastian" />, mountNode);

对比

我们用传统的方式和函数方式去创建组件,对比下语法

// 传统语法 —— es5
var Hello = React.createClass({
  getDefaultProps: function(){
    return {
      name: 'world'
    }
  },
  render: function(){
    return (
      <div>Hello {name}</div>
    )
  }
})

// 传统语法 —— es6
class Hello extends React.Component{
  constructor(props){
    super()
  }

  static defaultProps = {
    name: 'world'
  }

  render(){
    return (
      <div>Hello {name}</div>
    )
  }
}

// stateless functions
let Hello = ({name}) => <div>Hello {name}</div>
Hello.defaultProps = {
  name: 'world'
}

对比下有没有发现语法变得非常简单了呢

说明

一个无状态函数组件的形式:

let Hello = (props, context) => {
  return <div>Hello {props.name}</div>
}

Hello.defaultProps = {}
Hello.contextTypes = {}

// 使用时,就是使用平常组件时候去使用
<Hello name="Eyas" />

我们来精简这个函数,平时我们可以不用context,甚至连defaultProps都不需要:

let Hello = props => <div>Hello {props.name}</div>

这已经够精简了,我们还可以利用es6的解构赋值再来精简,适用于props的数量少的时候

let Hello = ({name}) => <div>Hello {name}</div>

很清爽的写法,这就完成了一个 react 组件的编写

优点

相比于 class 创建组件

  • 语法更简洁
  • 占内存更小(class 有 props context _context 等诸多属性),首次 render 的性能更好
  • 可以写成无副作用的纯函数
  • 可拓展性更强(函数的 compose,currying 等组合方式,比 class 的 extend/inherit 更灵活)

缺点

无生命周期函数

一个组件就是一个函数,函数应该是谈不上生命周期的,但是组件却是有生命周期,stateless functions 没有生命周期。当然了,我们其实可以使用 高阶组件 去实现生命周期

没有 this

在 stateless functions 中,this 是 undefined,所以是不能使用 this 变量。不过换个角度思考,this 是在运行时随时可以被修改或重新赋值,跟外界环境有着密切的联系,正是不使用this才会让组件变得更纯。

CGO 交叉静态编译

在新的项目中,由于必须要使用sqlite数据库,所以引入了 go-sqlite3 库,而且目标还是安卓开发板,板子是 aarch64 架构,也就是说,目的是要把启用了CGO的项目交叉编译到 aarch64 安卓系统。

本篇文章不局限于 aarch64 架构,其他架构同理,只是到时候下载的工具包不太一样

本文章使用的操作系统为 ubuntu

名词解释:

  • 交叉编译:是在一个平台上生成另一个平台上的可执行文件
  • 静态编译:在编译可执行文件的时候,将可执行文件需要调用的对应库都集成到可执行文件内部,使得可执行文件不需要其他任何依赖就能运行

无CGO项目的交叉静态编译

在不启用CGO的情况下,交叉编译是非常简单的,因为 golang 本身的交叉编译做的非常好

CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags '-s -w --extldflags "-static -fpic"' main.go
  • CGO_ENABLED=0 这个值默认是1,也就是开启的,需要手动指定为关闭,因为CGO是不支持交叉编译的,使用 go env CGO_ENABLED 查看默认值
  • GOOS, GOARCH 构建的平台,GOOS=linux是因为安卓底层就是linux,aarch64 架构直接使用 arm64,如果GOARCH=arm,则要使用 GOARM=7,指定arm版本可选5,6,7,在这里看完整支持列表
  • -ldflags 编译选项,-s -w 去掉调试信息,可以减小构建后文件体积,--extldflags "-static -fpic" 完全静态编译,要交叉编译放到其他系统和架构中运行,静态编译是最好的,不然启动的时候会提示找不到依赖的so文件

这样编译生成的文件就可以任意放到指定平台下运行不会有问题。如果没有CGO,一切都很完美。

CGO 项目的交叉静态编译

记录一下在 CGO 交叉编译过程

交叉编译

刚开始,静态编译是不敢想的了,也只有交叉编译可以试一试,安装两个包,如果是其他架构和系统,安装其他对应的包就行了,本文章都以 aarch64 的 linux 为例子

sudo apt install -y cpp-aarch64-linux-gnu g++-aarch64-linux-gnu

安装好以后,指定gcc和g++编译器为上面两个

CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w' main.go

命令解释

  • CGO_ENABLED=1 开启CGO,因为项目用到了C语言的代码
  • CC=aarch64-linux-gnu-gcc 指定gcc的编译器为 aarch64-linux-gnu-gcc,这个默认值是gcc,也就是当前操作系统和架构使用的gcc,使用命令 $(go env CC) --target-help 可以看看默认gcc支持什么平台
  • CXX=aarch64-linux-gnu-g++ 指定g++的编译器为 aarch64-linux-gnu-g++,规则和 CC 一样,只是用来编C++代码的,如果还用到了C++代码,必须指定该项
  • -ldflags '-s -w' go编译选项,-s -w 去掉调试信息,可以减小构建后文件体积

这样编出来是成功的,整个过程应该会很顺畅,但是把文件放到对应的平台运行,如果运气好,那肯定是运行起来了。如果运气一般会发现提示

./server: No such file or directory

这时候可以用 ldd 看一下

$ ldd server
libdl.so.2
libpthread.so.0
libc.so.6
ld-linux-aarch64.so.1

会发现依赖的so文件好像都有,可以去依赖目录/system/lib(这是安卓的)看看 ,但是运行的时候就是不成功,这里原因我不是太确定。我猜测是这样的,编译器依赖的这些 so 其实是编译器带的那些so,并不是系统的那些,所以在执行的时候这些依赖虽然有,但是并不是执行文件需要的。

这时候,把编译器下面的这几个依赖复制出来,然后执行放到和执行文件同一个目录,这样去执行,应该是可以成功执行的

cd /usr/aarch64-linux-gnu/lib/
cp libdl.so.2 libpthread.so.0 libc.so.6 ld-linux-aarch64.so.1 ~ # 去编译器目录把这几个文件复制出来
chmod +x libdl.so.2 libpthread.so.0 libc.so.6 ld-linux-aarch64.so.1 # 给这几个文件加上可执行权限
scp ...... # 想办法把上面这几个文件复制到对应平台和执行文件一个目录

# 对应平台执行
./libc.so.6 ./libdl.so.2 ./libpthread.so.0 ld-linux-aarch64.so.1 ./server 
# 也可以指定依赖目录
LD_LIBRARY_PATH=. ./server

这样子手动的执行依赖库,就能成功的启动起来了,如果还不行的话,继续往下看吧,下面有把所有so静态编译进去的方法。

静态编译

回看刚刚交叉编译时候的命令,我们是不是在 -ldflags 那里漏掉了刚开始的那个 --extldflags "-static -fpic",现在把它加上去

$ CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w --extldflags "-static -fpic"' main.go
# command-line-arguments
/tmp/go-link-972598095/000015.o: In function `unixDlOpen':
/home/eyas/.GOPATH/pkg/mod/github.com/mattn/[email protected]/sqlite3-binding.c:38461: warning: Using 'dlopen' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000031.o: In function `mygetgrouplist':
/opt/golang/src/os/user/getgrouplist_unix.go:16: warning: Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetgrgid_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:38: warning: Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetgrnam_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:43: warning: Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetpwnam_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:33: warning: Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000030.o: In function `mygetpwuid_r':
/opt/golang/src/os/user/cgo_lookup_unix.go:28: warning: Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-972598095/000004.o: In function `_cgo_26061493d47f_C2func_getaddrinfo':
/tmp/go-build/cgo-gcc-prolog:58: warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking

好吧,报错了。

我不太懂C语言,但是根据错误信息能得知,有些库文件是不能被静态链接的,gcc 编译器不支持。也就是说 cpp-aarch64-linux-gnu 这个编译器不支持静态编译。

MUSL GCC

事实上这卡住了我很长一段时间,直到后面在玩 RUST 的时候发现了个好东西,就是支持gcc静态编译的 musl gcc,才终于解决 CGO 静态编译

先下载 musl 对应的平台的编译器,进入 这里 能发现 musl 其实只能在 x86_64 架构下的 linux 系统才能运行,也就是说只能在 linux 系统下去交叉编译其他平台的执行文件(也不知道我说的对不对)

下载 aarch64 linux 的编译器: https://musl.cc/aarch64-linux-musl-cross.tgz

解压,然后把解压好的目录下 bin 文件路径放到 PATH 环境变量中,再把编译器换成这个

$ CGO_ENABLED=1 CC=aarch64-linux-musl-gcc CXX=aarch64-linux-musl-g++ GOOS=linux GOARCH=arm64 go build -o server -ldflags '-s -w --extldflags "-static -fpic"' main.go

没有报错,成功编译了,不用做什么其他操作,把可执行文件放到对应平台上运行,正常。

完成了,折腾到此结束

函数式编程 基本概念

一等函数

“一等公民”函数可以去任何可以去的地方,很少有限制

  • 函数可以存储为变量
  • 可以作为数组的一个元素
  • 作为对象的成员变量
  • 可在使用时创建
  • 可以被传递给另一个函数(作为参数)
  • 可以被另一个函数返回(作为返回值)

纯函数

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用

  • 函数可以自给自足

  • 无副作用

    • 副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互
    • 只要是跟函数外部环境发生的交互就都是副作用

    纯函数的好处

    • 可缓存
    • 可移植性/自文档化(自给自足)
    • 可测试
    • 合理性
    • 并行

高阶函数

  • 以函数作为参数
  • 返回值是函数
_.each([], item => {}) // lodash each example

函数柯里化 (curry)

curry 的概念很简单: 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

策略性地把要操作的数据放到最后一个参数里

传给函数一些参数,就能得到一个新函数

// curry 前
let sum = add(1, 2) // sum = 3

// curry 后
let addOne = add(1);
let sum = addOne(2); // sum = 3

// curry后简写
let sum = add(1)(2)

代码组合(compose)

组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数.

让代码从右向左运行,而不是由内而外运行,我觉得可以称之为“左倾”

// 两个函数
let addTen = x => x + 10;
let addTwo = x => x + 2;

// 组合前
let tmp = addTen(5)  // tmp = 15
let result = addTwo(tmp); // result = 17

// 组合后
let compose = (f,g) => f(g);
let addTwelve = compose(addTen, addTwo);
let result = addTwelve(5) // result = 17

放飞自我的 Vuex

改变 Vuex 的原因

在开始之前,我表明一下自己的观点:vuex 很优秀,我很认同 vuex 的设计理念,我只是想实现一个用起来更简单的 vuex

只要用过Vue的前端,基本没有人没接触过 Vuex,vuex的 api 简洁明了,功能简单但强大,上手也是很快的。

vuex 的数据流大致是这样的

state -> vue component -> action -> mutation -> state
               |                      ^
               |_________or___________|

这个数据流很清晰,而且职责分的很明确,如果完全按照这个官方标准去编码,项目维护性可变得很高。

mutation

但是,不知道有没有开发者感觉到,编写 action 和 mutation 有些繁琐,因为本来可以一步到位的工作,就因为mutation的限制,不合适做异步、调用action等等原因,不得不分成两步去实现。

mobx 示例

import { observable, computed, action } from 'mobx';
class Counter {
  @observable num = 0
  @computed get numPlus() {
    return this.num + 1
  }
  @action plus() {
    this.num++
  }
  @action reset() {
    this.num = 0
  }
  @action async delayPlus() {
    return new Promise(resolve => {
      setTimeout(() => {
        runInAction(() => {
          this.num++
        })
        resolve()
      }, 500)
    })
  }
}

在使用 mobx 的时候,我被它那种简洁写法吸引了,没有mutation,只是在需要更改状态的函数用 mobx 的工具函数包装一下,很好的控制了状态变更的范围。在代码简洁方面,比 vuex 做的更好。

习惯了mobx这种写法后,在我看来, vuex 的 mutation 根本就是多余的

在 vuex 的文档中,提到了严格模式, 如果状态变更不是有mutation函数引起的,将会抛出错误。

在 action 中其实也是可以直接修改状态的,并且修改后依然是响应式的,对应视图依然会更新,只是 vuex 强烈不建议这么做,不然也不会有严格模式出现。

dispatch & commit

mutation 和 action 的调用,需要使用 dispatch 调用action, 使用commit 调用 mutation,而且参数只能有一个,这个其实能理解,vuex 推崇提交一个更新的 payload,而不是多个payload。这个理念是不错的,但是用起来有时候还是不方便。

watch

在 vue 组件中有 watch,能监听到状态变化后自动触发,这个在 vue 组件这么普遍的功能,在 vuex 居然没有,所以要监听 vuex 状态变化,只能通过在组件作为载体去做 watch 功能。

放飞自我

上面写了使用 vuex 的一些不太舒服的方面点。我该着重解决他们了

改造前思考

vuex 已经被大众所接受,如果我的更改会导致原本 vuex api 不兼容,那很难在原本项目中迁移过去,所以vuex原本的 api 不能动,而是要在原本 api 上扩展。

所以我的做法就是,原本注入到 Vue 组件中的 $store 变量不动,没有对它有任何的侵入或者 hack,而是利用 $store 现有的api,重新构建一个类似 $store 的变量,我取名为 $s。而这次改造我大量使用 Object.defineProperties 去代理原本的 state、action、mutation 等等, 所以我给这个项目命名为 vuex-proxy

改造结果

vuex-proxy 就是我基于以上的所述的优化,我直接引用了原本的readme过来

Vuex Proxy

that mean Vuex Proxy

vue 的增强组件,基于 vuex,让 vuex 更简单

使用方法

首先,vuex 那整套完全兼容,所以可以从 vuex 无缝迁移到 vuex-proxy,但是反之不行,因为 vuex-proxy 在 vuex 的api上有扩展

API

在 vue 组件实例中,增加了一个 $s 属性,这是 vuex-proxy store,事实上这是一个 vuex store 的代理,目的就是为了简化 vuex 使用,当然,原本 vuex 注入的 $store 依然有效

注意在定义 store 的 state,actions,getters,mutation 时,不要和这些 api 名字重复了

vuex-proxy store 格式

在组件内使用 this.$s 访问

store 定义

store 的定义和vuex完全兼容,

{
  // 完全兼容 vuex 的 store 定义
  namespaced: true,
  state: {
    list: [],
    total: 0,
  },
  modules: {},
  // 但是 actions 和 mutations 的作用变得平等,没有区别,并且this指向当前 vuex proxy store,详见下文
  actions: {}, 
  mutations: {},
  
  // 新增 api,在state发生变化的时候,触发函数 
  watch: {
    list(newValue, oldValue) {
      console.log('list change:', oldValue, ' => ', newValue)
    }
  },
}

this.$s.$store

原始的 vuex store,没有任何侵入和 hack

this.$s.$rootVM

挂载 store 最顶层的组件实例

this.$s.$root

最顶层 vuex-proxy store 对象

this.$s.$registerModule

动态注册新模块,参数和功能与 vuex 的 registerModule 基本一致

this.$s.$unregisterModule

动态删除模块,参数和功能与 vuex 的 unregisterModule 基本一致

this.$s.$state

该模块级别的 vuex store 状态数据

this.$s[moduleName]

模块级别的 vuex-proxy store,api 和根 vuex-proxy store 无区别,只是状态数据不一样

this.$s[fieldName]

fieldName 是指 state,getters,actions,mutation 里面的所有字段名,vuex-p 把所有的状态、计算属性、方法都放到了同一层级里面,当你访问 vuex-proxy 的数据时,内部是知道你访问的是 state,还是getters,,还是 actions ,是一个 module,所以这也要求 state,getters,actions,mutation 里面的字段不能有重复,如果有重复则在初始化的时候会报错误

示例

import Vue from 'vue'
import vuexProxy from 'vuex-proxy'

// 使用插件
Vue.use(vuexProxy)

new Vue({
  // 在根组件使用 store 属性定义 vuex-proxy store,vuexp store 的 api 和 vuex store 完全兼容,说明请看下文
  store: {
    // store state 状态数据,和 vuex state 完全一致,无任何变化
    state: {
      num: 0,
    },
    // store getters 计算属性,和 vuex state 完全一致,无任何变化
    getters: {
      numPlus: state => state + 1
    },
    // watch 与 vue 的 watch 相似,当 state 变化后触发,支持 state 和 getters 的监听

    watch: {
      num: 'consoleNum', // 值可以是字符串,表示 action 或 mutation 的函数名
      numPlus(newV, oldV) { // 值可以是函数
        console.log('num change:', oldV, ' => ', newV)
      },
    },
    actions: {
      // 第一种 action 写法,和 vuex state 完全一致,无任何变化,在组件调用的时候,也没有区别,使用 this.$store.dispatch('reset')
      // 注意:该写法
      // this 指向 vuex store
      // 第一个参数是 vuex 的固定格式 { dispatch, commit, getters, state, rootGetters, rootState }
      // 第二个参数是 action 参数
      // 只有两个参数,不支持更多参数
      reset({commit}) {
        commit('RESET_NUM')
      }
      // 第二种 action 写法,增强版本,在组件调用的时候,使用 this.$s.plus()
      // this 指向 vuex-proxy store
      // 参数无限个数,可在里面直接更改 state,把它当做 vuex mutation 来用,支持异步,注意异步函数里的 this 是指向的 vuex-proxy store就没问题了
      plus() {
        return ++this.num
      },
      setNum(n) {
        // 在action 函数内部,可以访问 state
        console.log(this.num)
        // 也可以访问 getters 计算属性
        console.log(this.numPlus)
        // 也可以调用其他 action 和 mutation
        this.plus()
        // 也可以修改 state
        this.num = n
      },
      consoleNum() {
        console.log(this.num)
      }
    },
    mutations: {
      // 第一种 mutations 写法,和 vuex state 完全一致
      RESET_NUM(state) {
        state.num = 0
      }
      // 第二种 mutations 写法,和第二种 action 写法没有区别,用法也没有区别
      resetNum() {
        this.num = 0
      }
    },
    // 嵌套模块,支持无限嵌套
    modules: {
      testMod: {
        state: {
          test: 100
        },
        getters: {},
        actions: {},
      }
    }
  },
  data() { return { name: 'my name is vue plus' } }

  // 映射到计算属性中,用 $computed,完全兼容原本 vue 组件的 computed 功能
  // 使用字符串数组形式,直接写key,多层级直接使用 . 或者 / 分隔,最终映射的key名字是最后一层的key,并且自动绑定了 get 和 set,也就是可以直接给绑定的对象赋值
  $computed: ['num', 'numPlus', 'testMod.test'],
  mounted() {
    this.num = 2 // 相当于 this.$s.num = 2
    this.test = 20 // 相当于 this.$s.testMod.test = 20
  },

  // 使用对象形式
  // this 指向组件实例
  $computed: {
    num: 'num', // 会自动绑定 get 和 set
    xnum: {
      get($s) { return $s.num } // get 函数只有一个参数,该参数为 vuex-proxy store 实例,也就是 this.$s
      set(n, $s) { return $s.num = n } // set 函数有两个参数,第一个是修改后的值,第二个是 this.$s
    },
    numPlus() { // 这算是 get 函数
      return this.$s.num
    },
    myname() {
      // 还可以访问组件内部 data
      return this.name
    },
  },

  // 绑定 actions 和 mutations 到组件实例中
  // 字符串数组形式,根据key名字自动映射,映射后函数的this指向为函数所在的层级的 vuex-proxy store 实例
  $methods: ['plus', 'setNum'],
  $methods: {
    plus: 'plus',
    setNum(n) {
      return this.$s.setNum(n)
    },
    sayMyName() {
      console.log(this.myname)
    }
  },
  watch: {
    // 这样监听值改变,api 无变化
    '$s.num': function(oldv, newv) {
      console.log(this.newv)
    }
  }
})

对比

与 Vuex 对比,api 变化

目的:不破坏 vuex 前提下,让 vuex 变得更简单,更强大

兼容性

Vuex 的原有功能一切正常,可以无缝的将 vuex 迁移到 vuex-proxy

为什么要改变 vuex

vuex 是 vue 官方指定并维护的状态管理插件,和 vue 的结合无疑非常好的,但是在我看来在使用vuex的时候,有一些让我不舒服的地方

  1. actions 和 mutations 的参数,只能有一个,我理解初衷其实是为了只有一个 payload,更好记录,调试,跟踪变更等等,但是却不好用
  2. mutations 的存在,我觉得就是多余的,明明可以直接改状态,为什么还要多包装一层呢。我觉得有几个原因:
    2.1 方便调试工具的 Time Revel,redo,undo,变化跟踪等等。但是相信我,这些功能你基本不会用得上的,调试工具最大的作用就是用来看当前状态数据。
    2.2 隔离 actions 的副作用,让状态变更更好跟踪和调试。但是实际上用的时候,我基本上不会去调试 mutation 函数
  3. 在组件调用的时候,必须要用 dispatch 或 commit 去调用,为什么呢,直接调用不是更好吗

变化点

  1. 初始化时,new vues.Store 是可选的,可以 new vuex.Store 再传入,也可以直接传入,内部自动识别
  2. actions 和 mutations 兼容原有的,并且支持不同写法
  3. state,getters,modules 没有变更
  4. vue开发工具依然可用,不过每次更改状态都会有一个名为 VUEXP_CHANGE_STATE 的 type
  5. vuex 生态的插件都可以继续使用
  6. 新增 watch api,监听 state 和 getters 变化,和 vue 组件的 watch 功能类似

简单地说,就是原有的 vuex 的功能都没有阉割,没有改变,只是增加了其他用法,使其变得使用更简单

react 快速入门

官网: https://facebook.github.io/react/
中文文档: http://reactjs.cn/react/docs/getting-started.html

React是Facebook开源的一个用于构建用户界面的Javascript库,专注于MVC架构中的V,即视图。

组件

我们可以创建一个有特殊功能的组件,在需要的地方可以反复的用。在编写应用的时候,我们是编写一个又一个的组件,然后组合成一个完整应用。

定义组件

继承 React 的 Component 类可以创建一个组件

class Hello extends React.Component{
  constructor(props, context){
    super()
    this.state = {} // 定义组件状态
  }

  render(){ // render 必须,渲染组件的html结构
    return <div></div>
  }
}

如果一个组件不需要使用状态,可以以函数的方式编写组件

const Hello = (props, context) => {
  return <div></div>
}

const Hello2 = props => <div></div>

JSX

JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。React 可以用来做简单的 JSX 句法转换。目前babel可以转换 JSX 语法为 JavaScript

JSX 是一个可选工具,目的是为了方便的书写HTMl与布局

基本语法

  • 组件名的首字母必须大写
  • 如果是渲染组件,组件名的变量名必须在作用域范围内
  • 根节点必须只有一个
  • 标签必须有闭合
  • 属性表达式:把表达式用大括号 {} 包起来,不要用引号 ""
  • 子节点:XML格式包围子节点
  • 注释:作为子节点,用 {} 包围注释部分
  • 判断:只能使用三元运算符做判断
  • 循环:不支持循环,只能使用遍历
var Nav = props => <div class="navbar"></div> // 组件名首字母大写
var myNavbar = <Nav color="blue" /> // Nav 变量必须在当前作用域
var myElement = <h1 className={"hello" + "demo"}>Hello {name}</h1> // 属性表达式
var myHello = <h1>hello {name ? 'world' : name}</h1> // 判断

var Comment => props => (
  <div>
    {// 这是单行注释}
    {/* 这是多行注释 */}
    hello
  </div>
)

var Each = <div>{[1,2,3].map(item => <span>{item}</span>)}</div> // 遍历

自定义属性

JSX 默认会转译所有字符串,防止各种 XSS 攻击

<div>{'First &middot; Second'}</div>

万不得已,可以直接使用原始 HTML。

<div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />

如果往原生 HTML 元素里传入 HTML 规范里不存在的属性,React 不会显示它们。如果需要使用自定义属性,要加 data- 前缀

<div data-custom-attribute="foo" />

css 类名的 class ,和 label 的 for 属性,都是js的关键字,不能用于属性名,所以使用 classNamehtmlFor 代替

<div className="myclass"></div>
<label htmlFor="demo"></label>

属性 props

组件对外公开一个简单的属性(Props)来实现功能,但内部细节可能有非常复杂的实现。组件要将数据传递给子组件,使用组件属性的形式传递下去,在子组件通过 this.props 访问传递的属性。绝对不能手动更改 props值

class Hello extends React.Component{
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

<Hello name="Tenmic"></Hello>

props 验证

参考文档 http://reactjs.cn/react/docs/reusable-components.html

React.PropTypes 提供很多验证器来验证传入数据的有效性。当向 props 传入无效数据时,JavaScript 控制台会抛出警告

在组件内部定义静态变量 propTypes 可设置

class Hello extends React.Component{
  static propTypes = {
    name: React.PropTypes.string // name 必须为字符串
  }
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

默认props

设置静态变量 defaultProps 设置默认的 props

class Hello extends React.Component{
  static defaultProps = {
    name: 'Tenmic'
  }
  render(){
    return <div>Hello {this.props.name}</div>
  }
}

状态 state

大部分组件的工作应该是从 props 里取数据并渲染出来。但是,有时需要对用户输入、服务器请求或者时间变化等作出响应,需要使用 state ,表示组件的不同状态。

State 应该包括那些可能被组件的事件处理器改变并触发用户界面更新的数据

  • 定义状态在组件的构造方法中定义state变量
  • 修改状态,只能通过 setState 方法
  • 只要修改状态,就会触发组件的重渲染
class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(){
    this.setState({
      open: !this.state.open
    })
  }
  render(){
    return <div 
      className={this.state.open ? 'active' : ''
      onClick={this.toggle}
      }></div>
  }
}

生命周期

在组件的调用到销毁的过程中,会触发不同阶段的声明周期,

componentWillMount

整个周期只调用一次,在初始化渲染执行之前立刻调用

componentDidMount

在初始化渲染执行之后立刻调用一次,在生命周期中的这个时间点,组件拥有一个 DOM 展现

componentWillReceiveProps

在组件接收到新的 props 的时候调用。在初始化渲染的时候,该方法不会调用,只会在发生重渲染时调用参数 nextProps 获取即将传递过来的 props

shouldComponentUpdate

在接收到新的 props 或者 state,将要渲染之前调用。该方法在初始化渲染的时候不会调用,根据返回值决定组件是否需要更新

componentWillUpdate

在接收到新的 props 或者 state 之前立刻调用,在初始化渲染的时候该方法不会被调用。

使用该方法做一些更新之前的准备工作。

componentDidUpdate

在组件的更新已经同步到 DOM 中之后立刻被调用。该方法不会在初始化渲染的时候调用。

使用该方法可以在组件更新之后操作 DOM 元素

componentWillUnmount

在组件从 DOM 中移除的时候立刻被调用。

在该方法中执行任何必要的清理,比如无效的定时器,或者清除在 componentDidMount 中创建的 DOM 元素。

refs

为了方便的获取组件渲染后的DOM节点,可使用 ref,this.refs 存储了组件内所有的ref

class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(){
    console.log(this.refs.sidebar)
  }
  render(){
    return <div ref="sidebar" onClick={this.toggle}></div>
  }
}

事件系统

事件处理器将会传入虚拟事件对象的实例,一个对浏览器本地事件的跨浏览器封装。它有和浏览器本地事件相同的属性和方法,包括 stopPropagation() 和 preventDefault(),但是没有浏览器兼容问题。

支持的事件: http://reactjs.cn/react/docs/events.html

事件绑定

听过在标签中设置 on + 事件名 可设置事件处理函数

class Sidebar extends React.Component{
  constructor(){
    super();
    this.state = {open: false}
  }
  toggle(event){
    console.log(event)
  }
  render(){
    return <div onClick={this.toggle}></div>
  }
}

样式

在 React 中,行内样式并不是以字符串的形式出现,而是通过一个特定的样式对象来指定。在这个对象中,key 值是用驼峰形式表示的样式名,而其对应的值则是样式值,通常来说这个值是个字符串

var divStyle = {
  color: 'white',
  backgroundImage: 'url(' + imgUrl + ')',
  WebkitTransition: 'all', // 注意这里的首字母'W'是大写
  msTransition: 'all' // 'ms'是唯一一个首字母需要小写的浏览器前缀
};

React.render(<div style={divStyle}>Hello World!</div>, mountNode);

样式的 key 用驼峰形式表示,是为了方便与JS中通过DOM节点获取样式属性的方式保持一致

拥抱 JSX,它是一个伟大的尝试

react 带来了新的语法,JSX。是一个看起来像XML的JavaScript语法扩展。

有些同学因为不喜欢或不习惯JSX语法,而拒绝学习React。有人觉得JSX看起来太怪异了,但是我觉得JSX是一个伟大的尝试,是科学进步的表现,我们不应该对他有任何偏见。

我们从渲染的历史角度解释一下JSX的前瞻性

渲染的历史

html 与脚本混合

在asp年代和php早期,人们的代码都是html和脚本混合的就像这样子

<?php $name = "world"; ?>
<h1>Hello <?php echo $name; ?></h1>

这种代码的优点是简单。但是缺点是非常难以维护,项目一旦稍微复杂一点,维护它将是一个噩梦,这也决定了这种方式是写不出复杂项目的。所以后来诞生了 MVC 模式的开发方式

MVC 模式

MVC 模式将 view 与逻辑分离了,view 只关心怎么输出变量。这种分离方式使得项目维护性和易用性大大的增强了,并且使得项目更加的规范化。

模板语言

MVC 使 view 与逻辑分离了,但是输出变量还是不方便,所以各种各样的模板语言诞生了,比如什么 Smarty、Twig、Haml、Liquid、Mustache等等,都是为了更好的去渲染模板。这个时候利用模板引擎可以在一定程度上实现组件化了。不过这种组件化只是字符串拼接级别的组件化而已。

前端渲染

随着前端开发的高速发展,前端渲染慢慢登上历史舞台。MVC 模式中的 view 也慢慢的退化,而后端慢慢的演变成了api服务。

前端渲染直接就出现了各种的前端模板引擎,如underscore、Mustache、artTemplate等基于字符串的模板。另外 angular、vue等框架也创造了基于DOM的模板引擎。目前相信很多前端开发的人都已经习惯了这种模板开发方式。

JSX

那么,渲染的历史先进行到这里,我们回过头来看看JSX。我们看看JSX的语法,乍一看,它好像回到了解放前的那种 html和脚本混合 的模式。

const Hello = props => {
  const name = 'world';
  return <h1>Hello {{name}}</h1>
}

但是事实上真的是倒退的发展吗?如果真的是倒退的发展,为什么 React 这个框架在最终不但没有死掉,而且还火起来了呢?这里一定是有原因的。

核心变化

我们纵观渲染的历史发展,他们都有一个共同的特点,都是以 html 为中心,在 html 输出变量,在 html 中嵌入条件判断与循环。无论是脚本混合,模板语言,DOM模板,他们都是围绕着 html而进行的。

而 JSX 是以 js 为中心,在 js 中嵌入 html,是对js的扩展。js是一门脚本语言,本身就是为处理逻辑而生的,在js中嵌入一部分html才是更合理的做法。

以js为中心,最明显的好处就是,可以更加精确和更加方便的控制输出,并且 JSX 相当于是基于DOM的一种模板引擎,所以输出的html更加的符合规范。

JSX的转换

JSX 的最终是会转化为 js,试过将html模板编译为js模板的人就会知道,js模板是远远的比html效率高。首先是少了html模板的网络请求,其次是在执行的时候少了编译的过程,因为在生成js文件的时候就已经被编译好了,不会再客户端浪费资源去编译。

虚拟DOM

JSX 的最大的好处在于,对虚拟DOM的集成。在渲染的时候,在逻辑中就已经明确的整个应用的结构,这时在内存中存储一个DOM结构,在下次渲染的时候对比原本DOM,只渲染发生了变化的一部分。有人说因为虚拟DOM 大大的提升了 React 性能。其实不然,我觉得虚拟DOM的渲染方式,跟传统DOM操作也许会好一点,但是好的并不会非常明显,因为对比DOM节点也是需要计算资源的。

虚拟DOM最大好处在于方便的跟其他平台的集成,比如 react-native 就是基于虚拟DOM,然后渲染出了原生控件,因为react组件可以映射为对应的原生控件。在输出的时候,是输出html DOM,还是安卓控件,还是IOS控件,这是由平台决定了。

所以 React 有一个口号,就是

Learn Once, Write Anywhere

所以,react 的 JSX 是一个伟大的尝试,我们应该拥抱 JSX。

ES6 基础语法

参考文献: http://es6.ruanyifeng.com/

babel

ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。目前浏览器已实现大部分的ES6特性,但是为了兼容性我们需要使用工具babel编译为es5代码。才能让应用正常运行

在浏览器可以使用 chrome 插件 Scratch Js 尝试编写es6,并查看编译后的代码

或者到 babel 在线编译尝试 https://babeljs.io/repl/

let && const

var 声明的变量在整个当前作用域有效,let 和 const声明的变量在代码块有效(块级作用域),cont 声明的变量为常量,不能重新赋值

for(var i = 0; i < 10; i++){}
console.log(i) // 10

for(let i = 0; i < 10; i++){}
console.log(i) // error: i is not defined

解构赋值

按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构

let [a, b, c] = [1, 2, 3]; // a,b,c

let { bar, foo } = { foo: "aaa", bar: "bbb" }; // foo, bar
let { bar: x, foo: y } = { foo: "aaa", bar: "bbb" }; // x, y

对象扩展

定义对象

// 直接写入变量和函数,作为对象的属性和方法
var foo = 'bar';
var baz = {foo};
baz // {foo: "bar"}

// 对象中的函数
var obj = {
  foo(){
    reutrn 'hello'
  }
}

属性名表达式

var key = 'foo';
var obj = {
  [key]: true,
  ['b' + 'ar'] 123
}
obj // {foo: true, bar: 123}

扩展运算符

ES7有一个提案,将Rest解构赋值/扩展运算符(...)引入对象。Babel转码器已经支持这项功能。可以理解为属性展开

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

let a = {...z} // 克隆对象
a // { a: 3, b: 4 }

let arr = [1,2,3,4,5]
let arr2 = [6,7,8]
let oarr1 = [...arr] // 数组克隆
let oarr2 = [...arr, ...arr2] // 合并数组
oarr1 // [1,2,3,4,5]
oarr2 // [1,2,3,4,5]

[...'hello'] // ['h', 'e', 'l', 'l', 'o']

// function
function foo(...args){
  args // [...arguments]
}
foo(1,2,3) // args = [1,2,3]

函数扩展

函数参数默认值

// 基本用法
function foo(x = 10, y = true, z = {bar: 'str'}){
  return [x, y, z];
}
foo() // [10, true, {bar: 'str'}]

// 结合解构赋值
function foo({x = 10} = {}, y){
  return [x, y];
}
foo() // [10, undefined]
foo({x: 100}, false) // [100, false]

函数length属性

返回没有设置默认值的参数个数

(function (a, b, c = 5) {}).length // 2

箭头函数

ES6允许使用“箭头”(=>)定义函数。

var f = x => x; // 相当于 var f = function(x){return x;}
var b = (x, y) => ({x, y})

注意事项

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象,并且this不可改变
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在
  • 不可以使用yield命令,因此箭头函数不能用作Generator函数

函数绑定

函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

function bar(){
  //
}
var foo = {}
foo::bar // 等同于 bar.bind(foo)

Class

ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

//定义类
class Point {
  constructor() {}
  toString(){}
  render(){}
}
// 等价于
function Point(){}
Point.prototype.toSring = function(){}
Point.prototype.render = function(){}

// 类表达式
const Point = class Temp{} // 类的名字为 Point,Temp只会在类的内部使用,Temp可省略
  • constructor 是类的默认方法,用 new 调用时执行的方法
  • 只能使用 new 实例化类
  • 有name属性, Point.name -> 'Point'
  • 不存在变量提升

Class 继承

继承使用 extends 关键字

class ColorPoint extends Point{
  constructor(){
    super() // 调用父类的构造函数,如果有constructor,必须调用,如果没有constructor,后台自动调用
  }
}
  • 只要是有prototype属性的函数,就能被继承
  • 如果继承 null,相当于继承 Function
  • super 代表父类构造函数,也可作为对象调用,可访问父类实例的方法和属性

取值与存值

class Point {
  constructor(){}
  get prop(){}
  set prop(){}
}
let p = new Point
p.prop = 123 // 执行 set prop
p.prop // 执行 get prop

静态属性和方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”

class Foo {
  static classMethod() {
    return 'hello';
  }
  static defaultProps = {}
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: undefined is not a function

Module

javascript 模块体系,模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能

export 导出

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
export function getFullName(){}
export default {firstName, lastName, year} // 默认模块
export * from 'lodash'; // 模块继承,导出lodash模块的所有方法

import 导入

使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。

// main.js

import {firstName, lastName} from './profile';
import {firstName as fn, lastName as ln} from './profile'; // 重命名
import Profile from './profile';
import Profile as p from './profile';
  • import 有变量提升

K8S 的轻量版 K3S 安装教程

K3S 官网 :https://k3s.io/

K8S 和 K3S 是什么关系呢,官网原话

k3s 旨在成为完全兼容的 Kubernetes 发行版,相比 k8s 主要更改如下:

  1. 旧的、Alpha 版本的、非默认功能都已经删除。
  2. 删除了大多数内部云提供商和存储插件,可以用插件替换。
  3. 新增 SQLite3 作为默认存储机制,etcd3 仍然有效,但是不再是默认项。
  4. 封装在简单的启动器中,可以处理大量 LTS 复杂性和选项。
  5. 最小化到没有操作系统依赖,只需要一个内核和 cgroup 挂载。

所以完全可以把 K3S 代替K8S,而且占用的资源还会更小,功能也不会有缺失,还更简单。
K3S 是 K8S 的轻量级版,但并不是阉割版,K8S的功能它都有,也兼容K8S的各种插件和应用。

进到官网,可以看到有条命令可以一键安装

curl -sfL https://get.k3s.io | sh -

如果服务器是在国外,应该是不存在网络问题,用这一条命令就能装成功的。如果服务器在**大陆,会遇到一系列网络问题,在此记录国内服务器安装 K3S 的流程

准备工作

安装指定版本的 docker

这里要注意下,k8S对docker版本是有要求的,不能太高,写本篇文章的时候,K8S的最新版是 v1.14.6,K3S的最新版是 v0.8.1,此时K3S的最新版也是兼容的最新版K8S,这时候的K8S的docker版本最高支持到 18.06,当前docker最新版是 19.03,所以,安装docker的时候要小心版本号。

以 ubuntu 为例

# step 1: 安装GPG证书
$ curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -

# Step 2: 写入软件源信息
$ sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"

# Step 3: 更新源
$ sudo apt-get -y update

# Step 4: 查看当前可安装的docker版本
# centos 使用 yum list docker-ce --showduplicates
$ apt-cache policy docker-ce
docker-ce:
  Installed: 18.06.3~ce~3-0~ubuntu
  Candidate: 5:19.03.1~3-0~ubuntu-bionic
  Version table:
     5:19.03.1~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:19.03.0~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.8~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.7~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.6~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.5~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.4~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.3~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.2~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.1~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     5:18.09.0~3-0~ubuntu-bionic 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
 *** 18.06.3~ce~3-0~ubuntu 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
        100 /var/lib/dpkg/status
     18.06.2~ce~3-0~ubuntu 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     18.06.1~ce~3-0~ubuntu 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     18.06.0~ce~3-0~ubuntu 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages
     18.03.1~ce~3-0~ubuntu 500
        500 https://mirrors.aliyun.com/docker-ce/linux/ubuntu bionic/stable amd64 Packages

# Step 5: 安装指定版本
# centos 使用 sudo yum install docker-ce-18.06.3.ce-3.el7
$ sudo apt install -y docker-ce=18.06.3~ce~3-0~ubuntu

# Step 6: 验证安装
$ sudo docker version

# Step 7: 把当前用户加到docker用户组,以后该用户执行dokcer命令可以不再需要sudo,需要重新登录才会生效
$ sudo usermod -aG docker $USER

下载可执行文件

先本地下载 k3s 的二进制文件,注意url的版本号

https://github.com/rancher/k3s/releases/download/v0.8.1/k3s

下载完成后想办法上传到服务器,这里提供scp方式示例

scp -P 23 ./k3s [email protected]:/home/user/

然后连接上服务器,将k3s文件放到 /usr/local/bin,并加上可执行权限

sudo mv ./k3s /usr/local/bin/
sudo chmod +x /usr/local/bin/k3s

这样在后面安装k3s的时候就不会再去下载k3s文件

下载基础镜像

安装的时候会下载一些在k8s.gcr.io的镜像,被墙了,事先装好,注意镜像版本号是不是当前时间的最新版

docker pull mirrorgooglecontainers/pause:3.1
docker tag mirrorgooglecontainers/pause:3.1 k8s.gcr.io/pause:3.1

开始安装 K3S

安装

curl -sfL https://get.k3s.io | sh -

因为在准备工作的时候下载好了大部分该下载的,所以这个过程应该很快,装完后验证一下

$ sudo kubectl get nodes
NAME             STATUS   ROLES    AGE     VERSION
vm-0-16-ubuntu   Ready    master   3h26m   v1.14.6-k3s.1

修改配置

做如下修改

  • 把默认容器引擎从 Containerd 切换到 Docker,
  • 修改配置文件权限,可以使得执行 kubectl 命令不需要root权限
sudo vim /etc/systemd/system/multi-user.target.wants/k3s.service

需要修改ExecStart的值,将其修改为:

/usr/local/bin/k3s server --docker --no-deploy traefik --write-kubeconfig-mode 664

重启服务

sudo systemctl daemon-reload
sudo systemctl restart k3s 

# 然后查看节点是否启动正常
sudo k3s kubectl get node

到此完成 K3S 的部署

K8S 集群

既然 K3S 是 K8S 的轻量级版,那集群也是有的。

注意:很多云商用的是弹性网卡,比如执行 ip addr 或者 ifconfig 根本找不到ip是外网的那张网卡,尤其注意下面关于ip的配置。

所有机器安装 K3S

按照本文章上面的流程,在所有机器都安装一遍K3S,暂时把所有机器都当做master节点对待,全都独自安装一遍,确保安装好之后使用下面命令能找到自身节点

sudo k3s kubectl get node

配置 master 节点

现在所有机器都准备好了,任意选择一台作为master节点。如果你的所有机器都在同一个网段,比如都是在同一个局域网,则master节点不需要做任何修改就可以直接用,跳过本步骤。如果各个机器是跨网段,跨云商(比如一台机器是阿里云,一台机器是腾讯云,还有一台是亚马逊云),但是需要确保所有机器都有公网IP,则在master节点修改 K3S 配置,把 K3S 的地址绑定为公网IP。

sudo vim /etc/systemd/system/multi-user.target.wants/k3s.service

修改 ExecStart 的字段值为 ,注意其中的ip 12.34.56.78 修改为你的 master 节点的公网ip

/usr/local/bin/k3s server --docker --no-deploy traefik --write-kubeconfig-mode 664 --tls-san 12.34.56.78 --kube-apiserver-arg="advertise-address=12.34.56.78" --kube-apiserver-arg="external-hostname=12.34.56.78"
  • --tls-san 参数是为了增加 tls 证书的ip
  • --docker 使用docker而不是 containerd,看个人喜好了
  • --no-deploy traefik 不部署 traefik,看个人喜好

然后重启服务

sudo systemctl daemon-reload
sudo systemctl restart k3s 

配置 slave 节点

目前master节点配置好了,现在配置 slave 节点。在 K3S 官网最下面其实有讲怎么加入,流程如下

在 maser 节点执行如下命令,注意把ip换成你自己的

$ echo /usr/local/bin/k3s agent --server https://12.34.56.78:6443 --token `sudo cat /var/lib/rancher/k3s/server/node-token`
sudo k3s agent --server https://12.34.56.78:6443 --node-external-ip 12.34.56.78 --token K1022c3b20bd00e2b705ca34551b5e346dcce703dsr132dddfc3f8323b0adb1fb9::node:adb40646cf6fdc1239e3d769b9dbbb9a

--node-external-ip 增加暴露外网的ip

复制好上面命令输出的内容,到所有的 slave 节点编辑配置

sudo vim /etc/systemd/system/multi-user.target.wants/k3s.service

把配置文件中 ExecStart 值修改为上面输出的内容,修改保存后重启 K3S

sudo systemctl daemon-reload
sudo systemctl restart k3s 

验证一下,到maser节点,执行命令查看当前的可用节点,就能看到其他节点已加入进来

$ kubectl get node
NAME                      STATUS   ROLES    AGE   VERSION
iz2ze4grlnalbfkizskkadf   Ready    master   25h   v1.14.6-k3s.1
vm-0-16-ubuntu            Ready    worker   18h   v1.14.6-k3s.1

基于flex的order实现 carousel 轮播图

原因

项目里需要使用轮播图,electron + vue 技术栈,项目应用一旦启动会持续运行24小时,并且机器性能较差,所以很关注两个点

  1. 内存泄漏
  2. 性能

目前社区的轮播组件,大多只是适用于常规 web 应用,经过内部测试后,并不能满足内存和性能方面的要求,所以需要自己实现轮播组件

思路

最开始找到了这篇文章,里面讲解了传统的轮播图实现思路和作者原创的轮播思路,并在文末给出了性能较高的原创方案。

作者的原创方案性能是很高了,但是我注意到每次执行轮播都需要移动一个 DOM 节点,这会触发浏览器重排重绘,性能依旧不够高,还可以继续优化。

首先想到了 flex 布局的 order 属性:https://developer.mozilla.org/zh-CN/docs/Web/CSS/order

兼容性

可以看到只有现代浏览器才支持,如果要兼容老久浏览器就不用考虑本方案了,我的环境是electron 2.0,集成的chrome 61,可放心使用。

实现方案

本文章只记录实现方案与伪代码,不会给出 demo。

基本功能实现

html结构

<div class="carousel">
  <div class="carousel-container" style="transition-duration: 0ms; transform: translate3d(0px, 0px, 0px);">
    <!-- 轮播列表元素 -->
    <div class="carousel-item" style="order: 0;"></div>
    <div class="carousel-item" style="order: 1;"></div>
    <div class="carousel-item" style="order: 2;"></div>
    <div class="carousel-item" style="order: 3;"></div>
    <div class="carousel-item" style="order: 4;"></div>
  </div>
</div>
<style>
  .carousel {
    width: 100%;
  }
  .carousel-container {
    width: 100%;
    display: flex;
    transition-property: transform;
  }
  .carousel-item {
    width: 100%;
  }
</style>

从里面元素开始解释

  1. 父级设置 display: flex ,子级可以通过 order 属性实现排序,这种排序虽然依然会引发重排和重绘,但是开销更小
  2. 外围一层元素,使用 transition 实现 动画,使用 transform 的 translate3d 实现硬件加速与显示范围。在非动画状态,X轴的位置永远都是 0,在动画状态,才给 X轴 赋值,所以整个组件其实就是在做两件事: 顺序X轴位置(也就是动画)
  3. 顺序:非动画状态需要 X 轴一直为0,那么就要保证当前要显示的轮播元素的 order 值最小,我暂时约定最小为 0,因为动画涉及到下一张,所以当前轮播的order 为 0,下一张为 1,其他的只要大于1 即可。
  4. 动画,如果需求是切换的时候不需要动画,那么保证顺序就已经完成了轮播切换了,但需求通常需要动画。动画的实现由三部分,起始状态结束状态重置状态
    1. 起始状态:动画一开始,就是要在当前轮播元素开始,对应的X轴是0,起就是静止状态,所以起止状态不需要设置,默认就是了,所以通常其实状态无需处理
    2. 结束状态:结束的状态是下一张轮播元素完全显示,也就是X增加一个 轮播元素的宽度。动画时间 transition-duration 赋值 500ms,就能实现动画。
    3. 重置状态:动画完成后,重新计算各个元素的 order 值,把 X 轴重设为0,动画时间重设为0

到此就完成了轮播组件的基本功能

功能扩展

自动轮播

先实现一个函数 next() 方法,定时调用

拖动滚动

  1. 记录开始拖动时鼠标位置的 X轴
  2. 移动过程中获取鼠标位置X轴,减去开始拖动时的X轴位置,得到X轴移动的距离,再把这个距离数字赋值给 translate3d 的X轴

反向动画与拖动

通常的轮播都是 从右往左 滚动的,但是有时需要兼容 从左往右,实现方案:

非动画状态无需调整,主要关注动画状态。

  1. 排序:要反过来排序,当前显示的元素 order 为0,下一张为 -1,其他的小于 -1即可
  2. 动画的不同状态都需要调整
  3. 起始状态:X轴位置: -1 * (轮播条数 - 1) * 轮播宽度
  4. 结束状态:X轴位置: -1 * (轮播条数 - 与上条间隔数量) * 轮播宽度
  5. 重置状态:X轴位置:0,排序重置为正向

反向拖动,如果拖动的时候拖动的距离是个正数,则马上更新顺序为反向,如果为负数,马上更新顺序为正向

总结

该方案的性能很高,但是兼容性不太好。而且实现过程中,对元素的排序计算如果涉及到反向动画的话会比较复杂

让异常代码参与业务逻辑

场景

在编写后端服务时,需要处理很多运行时异常,比如用户登录接口/api/v1/auth/login,controller逻辑可能是这样子的

  1. 当前用户是否已登录
  2. 判断验证码是否正确
  3. 判断该用户是否存在
  4. 判断该用户状态是否已禁用
  5. 判断该用户的密码是否正确

以上的逻辑是非常普遍的,每一个步骤的条件不成立都需要返回错误,可能错误信息都不尽相同,写成代码可能是这样子的,以nodejs为例

function AuthLogin(req, res) {
    // 是否已登录
    if (req.isLogin()) {
        res.status(400)
        res.json({msg: "you are already login"})
        return
    }
    // 判断验证码
    if (!req.verifyCapture()) {
        res.status(401)
        res.json({msg: "your capture was wrong"})
        return
    }

    const user = db.User.Where("username = ? OR email = ?", req.body.username, req.body.username).frist()
    // 判断用户是否存在
    if (!user || !user.id) {
        res.status(401)
        res.json({msg: "user is not exit."})
        return
    }
    // 判断用户是否被禁用
    if (user.isForbidden()) {
        res.status(400)
        res.json({msg: "user is forbidden."})
        return
    }
    // 验证密码
    if (!user.verifyPassword(req.body.password)) {
        res.status(401)
        res.json({msg: "your password was wrong"})
        return
    }

    // 成功登陆
    res.login()
    res.json({msg: "ok"})
}

上面代码很啰嗦(当然有些步骤都是用中间件处理的,这里只是为了举例判断条件很多的情况),事实上用一个错误中间件就可以改善。

改进

每当出现判断条件不成立需要立即返回时,统一用 throw 抛出错误,让上层错误中间件统一返回,这个中间件大概是这样子的

function errorMiddleware(req, res, next) {
    try {
        next()
    } catch(e) {
        if (typeof e !== 'object') {
            e = {msg: e}
        }
        res.status(e.status || 400)
        res.json(e)
    }
}

在controller 可以改成

function AuthLogin(req, res) {
    if (req.isLogin()) throw 'you are already login'

    req.verifyCapture() // 在 verifyCapture 函数抛出异常

    const user = db.User.Where("username = ? OR email = ?", req.body.username, req.body.username).frist()

    if (!user || !user.id) throw {status: 401, msg: 'user is not exit'}
    if (user.isForbidden()) throw "user is forbidden."
    if (!user.verifyPassword(req.body.password)) throw {status: 401, msg: 'user is not exit'}

    res.login()
    res.json({msg: "ok"})
}

改成这样,无论是coding感受,逻辑清晰度,思路都更好,当然你也发现了,throw 不只是接受 Error 类型的值,它接受任何类型的值

当然了,如果你用过 koajs 框架,应该早就熟悉了这种模式了,如果是 koa的中间件,应该是这样的

async function errorMiddleware(ctx) {
    try {
        await ctx.next()
    } catch(e) {
        // ...
    }
}

golang 例子

说了那么多,其实我是想总结一下这种方式,顺便记录一下在使用 gin 框架的时候中间件的写法,在go的推荐写法中,使用建议立即去处理各种异常,所以大多项目中都是满屏的 if err != nil {},身为静态语还是有点区别的

func ErrorMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				switch err.(type) {
				case gin.H: // 只认定 gin.H 类型
					e := err.(gin.H)
					code := e["code"]
					if code == nil {
						code = types.CodeUnknowError
					}
					msg := e["msg"]
					if msg == nil {
						msg = "unknow error"
					}
					statusV := e["status"]
					statusCode := http.StatusBadRequest
					if statusV != nil {
						statusCode = statusV.(int)
					}
					ctx.Abort() // 需要显式终止中间件,不然依然会往里层调用
					ctx.JSON(statusCode, gin.H{
						"code": code,
						"msg":  msg,
						"data": &gin.H{},
					})
				default:
					panic(err)
				}
			}
		}()
		ctx.Next()
	}
}

在需要抛异常的的地方需要使用 gin.H 类型

panic(gin.H{
    "status": http.StatusUnauthorized, // 这行可不写,默认 400
    "code": types.CodePermissionDefined,
    "msg": "some message"
})

以上中间件参考 gin.Recovery(),但是,go官方是不推荐这么做的,但是这样确实能精简不少代码

利用 github pages 与 github api 搭建博客

使用github pages

自己维护博客服务器,麻烦,索性直接找个现成的稳定的博客平台,github pages成为首选,这里有几个思路

  • 使用github pages,编写静态网页
  • 使用github pages 与 静态网页生成工具,需要本地电脑编写文章
  • 使用github pages 与 自建后端服务器,需要另外维护一个后端服务器
  • 使用issues 写文章,issues页面就是一个博客网站
  • 使用github pages 与 github api,使用issues写文章

使用github pages,编写静态网页

有很多文档网站是使用这种方式搭建,一般是作为demo页面或者单页文档页面,否则页面稍微一复杂,就变得非常难以维护

使用github pages 与 静态网页生成工具

有很多博客网站与文档网站都是使用这种方式,这是一种最简单最方便的方式,现在流行的工具有 hexo 和 Jekyll 等,这种方式还要在本地维护一个仓库,生成静态页面后再上传,操作过于繁琐,而且每个页面之间切换都需要重新载入所有资源,网页数据传输量较大

工具

使用github pages 与 自建后端服务器

在github pages编写一个单页面应用,数据通过跨域请求来自于自建的服务器,这种方式有三大难点:前端、后端、服务器

前端

需要编写一个单页面应用,这对技术需要一定的水平,基本很少有开源工具

后端

需要后端服务,这可以使用现成工具提供api,比如wordpress、drupal、ghost等

使用issues 写文章

这种方式简单粗暴,直接在issues写文章,评论、标签、提醒神马的都有了,现在其实很流行这种方式,看看这几个博客,都几千个star了

要说它的缺点嘛,就是人人都可以往你博客提交文章,界面千篇一律,而且也不怎么好看

服务器

这个问题很严重,问题来了,你都有自建的服务器了,还要用github pages干嘛呢,哪天自建服务器挂了,博客照样挂。

这种方式可以使用域名来提升逼格。

使用github pages 与 github api

这种方案与上一种对比起来其实没多大区别,唯一的区别就是自建服务换成了github的另一个服务,就是说,github帮我们建好了。
github api:https://developer.github.com/v3/

github pages

github Pages可以被认为是用户编写的、托管在github上的静态网页,如下方式可开启:

  • 当仓库名称为{username}.github.io时自动生成github page首页,页面地址为 http://{username}.github.io
  • 当仓库中有 gh-pages分支时会自动生成github pages,访问地址为:http://{username}.github.io/{reponame}

github api

github 提供了一系列api可让用户操作数据,详细内容可到api官网查看

github pages + github api搭建博客

现在讲解实现github pages + github api的思路,首先我们需要一个单页面应用,应用托管于{username}.github.io仓库。然后我们需要知道如何通过api 获取 issues 内容

单页面

这个单页面很简单,大概只需要两个页面:列表与详情,应用必须有路由系统,而且应当只使用 hash 路由。

issues api

官方文档: https://developer.github.com/v3/issues/

列出了操作issues接口,我们暂时只用到 查看 功能。

列出issues
GET https://api.github.com/repos/eyasliu/blog/issues

每条issues都有详细信息,包括标题、内容、标签、用户,时间等等信息。

可以使用查询过滤或排序issues,比如以最近评论时间排序

GET https://api.github.com/repos/eyasliu/blog/issues?filter=updated
获取单条issues
GET https://api.github.com/repos/eyasliu/blog/issues/1

注意:这里的1是指的是issues对象中的number而不是id

获取评论

GET https://api.github.com/repos/eyasliu/blog/issues/1/comments
获取labels

labels可用作与 分类 或 标签 功能

GET https://api.github.com/repos/eyasliu/blog/labels

域名解析

生成github pages 后有一个二级域名:username.github.io,我们也可以使用自己的域名,方法:

  1. 在仓库根目录新建文件,命名为 CNAME
  2. 文件内容第一行写上域名(不要包含 http:// 和 https:// ),保存并上传至仓库
  3. 在域名解析后台添加cname记录,值为 username.github.io

过两分钟可生效。

经过测试,能绑定多个域名,在CNAME文件中,一行一个域名

electron 性能优化总结

用过electron的人都知道,electron 有两种类型进程,主进程(main) 和 渲染进程(renderer),主进程是 nodejs 运行环境,renderer 是浏览器运行环境,其中 renderer 进程也可以开启 node API ,并且是默认开启。但是是否有人发现其实还有一个 GPU 进程(gpu process),对于renderer进程的性能优化来说,这才是关键。

项目背景

基于 vuejs 的项目,应用启动后需要连续运行24小时不刷新。应用运行在定制的一体机上,操作系统win7,机器性能很差(CPU Intel 赛扬 2.9G 双核,内存 3G),并且还有其他更耗资源的工具服务在运行。

目标:

  • 光是 electron 应用占用的峰值不允许超过 30%
  • 不允许存在内存泄漏

优化过程

  1. 通过 chrome devtool 的 performance 工具优化代码
  2. 调整 浏览器 配置项

undefined, NaN 的真实面目

前几天发现一件有趣的事情

(function test(){
  var undefined = "this is my undefined"
  return undefined;
})() // 返回 "this is my undefined"

undefined的值居然能被重写!

接下来再试了一下 null NaN,发现NaN也能被重写

原因

  • undefined 属于Undefined类型唯一的值,是js基础数据类型之一
  • null 属于 Object 类型,代表空对象

其实它们本质上,就是一个变量名罢了,很普通的一个变量名。既然是变量名,那么给他重新赋值试试看

undefined = 'Hello!';
window.undefined = 'Hi!';
alert(undefined) // 弹出 undefined

重新赋值的时候,不会报错,但是结果是改不了的,而且这种方式的赋值,是属于全局变量的赋值。这个看到这个行为,我立马想起了 Object.defineProperty,于是,用它去修改undefined变量的写入属性试试

Object.defineProperty(window, 'undefined', {
    writable: true
})
// throw error: Uncaught TypeError: Cannot redefine property: undefined(…)

不给重新定义 undefined 的属性。好吧,到了这一步,我已经明白了 undefined 的猫腻了。

总结

undefined 的确就是一个全局变量,值就是 undefined,只不过这个全局变量在js内部规定了不能更改它的属性。但是在局部作用域中原本是没有undefined这个变量,可以使用var关键字去声明这个变量,新声明的undefined跟全局哪个undefined没有任何关系,当然就可以正常赋值了。

究其原因,还是因为 undefined 不是js的关键字导致的

解决方案

undefined 和 NaN 变量能够被更改,那就会导致不安全的可能性,那么如何保证使用的undefined一定是真正的哪个undefined呢,而不是被重写之后的undefined。这里有几种方案

  • 永远不要声明 undefined 这个变量名,因为变量在使用的时候,如果不声明,都会去使用全局变量,而全局变量那个undefined一定是正确的
  • 使用局部作用域时,多加一个参数
(function(p1, p2){
    // 传参时因为undefined变量没有传进来,所以自动设置为undefined
    alert(undefined);
})(val1, val2, undefined)
  • 使用替代表达式
// undefined 可以使用 void(0) 代替
undefined = void(0)
// NaN 可以使用 0/0 代替
NaN = 0/0

CDP (Chrome Devtools Protocol) 踩坑之旅

什么是 CDP

Chrome Devtools Protocol, 就是 Chrome 浏览器用于开发调试的协议,Chrome 开发者工具底层就是调用的该协议,所以 chrome 开发者工具能干的事都是基于 CDP 的接口,就能想到 CDP 能干的事有多少了。事实上 CDP 有些功能在 Chrome 开发者工具并没有体现出来,也就是说 CDP 还更强大。 简单来说,CDP 就是用来控制 Chrome 的方方面面。

事实上 CDP 能控制的并不局限于 Chrome 浏览器,任何实现了该协议的工具都能被控制。比如 Node.js, Firefox(没错,Firefox也实现了 CDP,但只能在 nightly 才能开启),还有所有基于 Chromium 内核的浏览器(360极速浏览器,Oper,edge,搜狗浏览器 等等)。

而任何实现了 CDP 的工具,都能使用 Chrome 开发者工具去调试与分析。所以 Chrome 开发者工具能调试 Node.js, Chromium 内核浏览器,甚至是 Firefox。

CDP 协议版本

CDP 有好几个版本,分别对应了不同的Chrome 版本或者不同端

  • latest 最新版,也叫 tip-of-tree (tot) ,这是个不稳定版本,API 随时可能会发生变化,而且还不保证向后兼容,但是它囊括了所有功能
  • v8-inspector ,这是给基于V8内核,比如 Node.js 6.3+ 就是基于该版本,用于调试和分析 nodejs 程序
  • stable 1.3 ,稳定版1.3,兼容 Chrome 64+ ,是 tot 版本的子集
  • stable 1.2 ,稳定版1.2,兼容 Chrome 54+ ,是 tot 版本的子集

体验 CDP

其实打开 Chrome 开发工具就是最直接的体验了。不过这是用来调试页面的,CDP不仅于此。

启动 CDP 服务器

首先先启动 CDP 服务,这在实现了 CDP 的工具都会有说明,举例如下:

$ google-chrome --remote-debugging-port=9222 # pc chrome
$ adb forward tcp:9222 localabstract:chrome_devtools_remote # 安卓 chrome
$ opera --remote-debugging-port=9222 # Opera 浏览器
$ node --inspect=9222 script.js # Nodejs
$ msedge --remote-debugging-port=9222 # 基于 Chromium 的新版 Edge
$ MicrosoftEdge.exe --devtools-server-port 9222 about:blank # 旧版本 edge
$ firefox --remote-debugging-port 9222 # firefox nightly 浏览器

CDP 服务的默认端口就是 9222, 所以上面即使不指定端口号也是默认启动在 9222 端口上,这是个 http 服务。启动后可以浏览器直接访问 http://localhost:9222/json/version 一下试试

$ curl http://localhost:9222/json/version
{
   "Browser": "Chrome/85.0.4183.83",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
   "V8-Version": "8.5.210.20",
   "WebKit-Version": "537.36 (@94abc2237ae0c9a4cb5f035431c8adfb94324633)",
   "webSocketDebuggerUrl": "ws://localhost:53686/devtools/browser/8ee011a6-03f5-4ee4-bc1e-bf35a3789af0"
}

这是最简单的一种连接方式,即 http

CDP 客户端

服务启动好了,接下来是客户端工具。

官方推荐使用 chrome-remote-interface ,这是个nodejs包,这样安装

$ npm i -g chrome-remote-interface

可以看看 chrome-remote-interface 的说明文档, chrome-remote-interface 既是一个命令行工具,也支持作为工具库编程使用。

$ chrome-remote-interface -h

  Usage: client [options] [command]


  Options:

    -v, --v              Show this module version
    -t, --host <host>    HTTP frontend host
    -p, --port <port>    HTTP frontend port
    -s, --secure         HTTPS/WSS frontend
    -n, --use-host-name  Do not perform a DNS lookup of the host
    -h, --help           output usage information


  Commands:

    inspect [options] [<target>]  inspect a target (defaults to the first available target)
    list                          list all the available targets/tabs
    new [<url>]                   create a new target/tab
    activate <id>                 activate a target/tab by id
    close <id>                    close a target/tab by id
    version                       show the browser version
    protocol [options]            show the currently available protocol descriptor

还可以交互模式使用

$ chrome-remote-interface -p 9222 inspect
>>> Target.getTargetInfo()
{
  targetInfo: {
    targetId: 'D4E5B0E2EAC5E5E2CABEF59C42CB23A1',
    type: 'page',
    title: 'GitHub: Where the world builds software · GitHub',
    url: 'https://github.com/',
    attached: true,
    browserContextId: 'B38349EC1C55BD049E626719D6C919C3'
  }
}
>>> Browser.getVersion()
{
  protocolVersion: '1.3',
  product: 'Chrome/85.0.4183.83',
  revision: '@94abc2237ae0c9a4cb5f035431c8adfb94324633',
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
  jsVersion: '8.5.210.20'
}
>>>

好了, CDP 的 hello world 之旅结束

CDP 开发

CDP 通常用作爬虫、自动化测试场景,所以你能在网上找到的资料,大部分都是讲 CDP 怎么开发爬虫和自动化测试。如果你要做爬虫或者自动化测试,可以先到 Awesome Chrome DevTools 先看看,看看有没有你使用的编程语言的实现。

Puppeteer

这是一个 nodejs 的包,封装了方方面面的高级 API 供使用,是做自动化测试和爬虫的首选工具。它还有其他编程语言的实现

怎么使用 puppeteer 可以出门随便转找教程,满大街都是,而我仅对他的实现感兴趣,这是一个非常好的学习 CDP 的项目,API 优雅,调试也简单,深入研究其原理后还可以自己实现一个。现在我重头开始踩坑

CDP 连接

使用 http 与 websocket 通讯

在上文启动 CDP 服务器那里介绍了CDP的默认端口号是 9222,也只是个默认值,可以随意更改的。但是如果 --remote-debugging-port=0,就有点特殊了,它会随机使用一个没被使用的端口号,而这么做也更安全可靠,因为天知道你指定的端口号会不会被占用。在启动了服务后,它会在进程的 stderr 将调试地址打印出来,这里面就有端口号了。

$ chrome --remote-debugging-port=0 2>&1 | tee

DevTools listening on ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0

一个进程在启动后,默认会分配三个通道(pipe) 0,1,2 。 0 是 stdin,1 是 stdout,2 是 stderr,上述命令含义为将 stderr 重定向到 stdout,然后 tee 读取 stdout 的数据

我们拿到了一个 websocket 地址 ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0,可以看到其实 CDP 是使用 websocket 做通信的。我们用 chrome-remote-interface 连接上试试看

$ chrome-remote-interface inspect -w ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0
>>> Browser.getVersion()
{
  protocolVersion: '1.3',
  product: 'Chrome/85.0.4183.83',
  revision: '@94abc2237ae0c9a4cb5f035431c8adfb94324633',
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
  jsVersion: '8.5.210.20'
}
>>> Target.getTargetInfo()
{
  targetInfo: {
    targetId: '849dd5c4-1938-4e42-98e3-321650fc374a',
    type: 'browser',
    title: '',
    url: '',
    attached: true
  }
}

挺好,能用。不过有坑,可以试试 Page.enable(),后面说。

通过新建管道通讯

通过 websocket 传输没有毛病,而且都是本地服务,速度上应该也不会多大延迟,这是通过网络传输的,还有种方式是使用 IPC(InterProcess Communication 进程间通信),相比于网络传输, IPC 的效率更高,而且完全不依赖网络,更轻量级。

$ chrome --remote-debuggin-pipe

启动后,使用 3 号通道发送数据,使用 4 号通道读取数据。这通常只会在编程时才用到。

怎么用? TODO, 参考 Puppeteer 代码

Session

上文说到启动好CDP服务后,会在 stderr 输出CDP的 websocket 地址,而且用 chrome-remote-interface 确实连接上去了,但是我尝试调用一下 Page.enable() 却报错了,这个命令是启动页面事件通知,在最初的协议版本就有,可是它报错了。

$ chrome-remote-interface inspect -w ws://localhost:56707/devtools/browser/509ddc0f-e333-42bd-b85f-cffbc90143c0
>>> Page.enable()
Uncaught ProtocolError: 'Page.enable' wasn't found
    at C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:93:35
    at Chrome._handleMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:256:17)
    at WebSocket.<anonymous> (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\lib\chrome.js:234:22)
    at WebSocket.emit (events.js:314:20)
    at WebSocket.EventEmitter.emit (domain.js:486:12)
    at Receiver.receiverOnMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\node_modules\ws\lib\websocket.js:825:20)
    at Receiver.emit (events.js:314:20)
    at Receiver.EventEmitter.emit (domain.js:486:12)
    at Receiver.dataMessage (C:\Users\Administrator\AppData\Roaming\npm\node_modules\chrome-remote-interface\node_modules\ws\lib\receiver.js:437:14) {
  request: { method: 'Page.enable', params: undefined },
  response: { code: -32601, message: "'Page.enable' wasn't found" }
}

这样连上以后,默认的会话就是浏览器本身,还不是页面,我要调用页面的协议,就报错了。正确方式是,要么直接用http连接,要么连接指定页面的 websocket。

webpack 按需打包加载

为什么需要按需加载

在一个前端应用中,将所有的代码都打包进一个或几个文件中,加载的时候,把所有文件都加载进来,然后执行我们的前端代码。只要我们的应用稍微的复杂一点点,包括依赖后,打包后的文件都是挺大的。而我们加载的时候,不管那些代码有没有执行到,都会下载下来。如果说,我们 只下载我们需要执行的代码的 话,那么可以节省相当大的流量。也就是我们所说的 按需加载

使用 webpack 的按需加载

webpack 官方文档 其实是有介绍的,不过我还是啰嗦的在总结一下

首先我们要看一看一个加载函数

require.ensure(dependencies, callback, chunkName)

这个方法可以实现js的按需加载,分开打包,webpack 管包叫 chunk,为了打包能正常输出,我们先给webpack配置文件配置一下chunk文件输出路径

// webpack.config.js
module.exports = {
  ...
  output: {
    ...
    chunkFilename: '[name].[chunkhash:5].chunk.js',
    publicPath: '/dist/'
  }
  ...
}

这里顺带一提,打包后的js文件基础路径跟普通的资源(图片或字体文件之类)是一样的,就是publicPath, publicPath可以在运行时再去赋值,方法就是在应用入口文件对变量 __webpack_public_path__ 进行赋值就行,文档在这

每个chunk 都会有一个ID,会在webpack内部生成,当然我们也可以给chunk指定一个名字,就是 require.ensure 的第三个参数

配置文件中

  • [name] 默认是 ID,如果指定了chunkName则为指定的名字。
  • [chunkhash] 是对当前chunk 经过hash后得到的值,可以保证在chunk没有变化的时候hash不变,文件不需要更新,chunk变了后,可保证hash唯一,由于hash太长,这里我截取了hash的5个字符足矣

最简单的例子

// a.js
console.log('a');

// b.js
console.log('b');

// c.js
console.log('c');

// entry.js
require.ensure([], () => {
  require('./a');
  require('./b');
}, 'chunk1');
if(false){
  require.ensure([], () => {
    require('./c');
  }, 'chunk2');
}

将会打包出 3 个文件,基础包、chunk1 和 chunk2,但是chunk2在if判断中,而且永远为false,所以 chunk2 虽然打包了但永远不会被加载

结合 react-router 按需加载

如果需要做按需加载,那么这个 应该怎样定义呢?我们可以按照前端路由来定义这个 ,在react 应用中,react-router 是一个路由解决方案的第一选择,它本身就有一套动态加载的方案

  • getChildRoutes
  • getIndexRoute
  • getComponents

看他们的方法名字就知道他们是干什么的,我也不废话。他们的作用呢,就是在访问到了对应的路由的时候,才会去执行这个函数,如果没有访问到,那么就不会执行。那么我们把加载的函数放在里面就正好合适了,等到访问了该路由的时候,再去执行函数去加载脚本。

根路由

跟路由有点特殊,它一定要先加载一个组件才能渲染,也就是说,在跟路由不能使用按需加载方式,不过这个没关系,根路由用于基础路径,在所有模块都必须加载,所以他的 "需" 其实作用不大。

示例代码

官方有个很简易明了的示例应用, react-router 默认是推荐使用对象去定义路由而不是 jsx,所以这个例子演示了怎么使用 对象的形式定义按需加载模块。

jsx 定义按需加载路由

虽然官方推荐使用对象去定义,但是jsx语法看上去更清晰点,所以还是使用jsx演示,方法很简单,就是把 组件的 props.component 换成 props.getComponent ,函数还是上述例子的函数(记得根路由不要使用getComponent)。

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={(location, callback) => {
      require.ensure([], require => {
        callback(null, require('modules/home'))
      }, 'home')  
    }}></Route>
    <Route path="blog" getComponent={(location, callback) => {
      require.ensure([], require => {
        callback(null, require('modules/blog'))
      }, 'blog')  
    }}></Route>
  </Route>
</Router>

看上去很乱有木有,在jsx中写那么多 js 感觉真难看,把 js 独立出来就是:

const home = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('modules/home'))
  }, 'home')  
}

const blog = (location, callback) => {
  require.ensure([], require => {
    callback(null, require('modules/blog'))
  }, 'blog')  
}

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={home}></Route>
    <Route path="blog" getComponent={blog}></Route>
  </Route>
</Router>

这样整理一下,就好看多了


注意: 或许有人会想,上面重复代码超级多,能不能用一个函数生成器去生成这些重复的函数呢?代码更进一步优化,比如:

const ensureModule = (name, entry) => (location, callback) => {
  require.ensure([], require => {
    callback(null, require(entry))
  }, name)
}

<Router history={history}>
  <Route path="/" component={App}>
    <Route path="home" getComponent={ensureModule('home', 'modules/home')}></Route>
    <Route path="blog" getComponent={ensureModule('blog', 'modules/blog')}></Route>
  </Route>
</Router>

答案是:不能。这样看起来代码没有任何问题,好像更优雅的样子,但是经过亲自实践后,不行!!因为 require函数太特别了,他是webpack底层用于加载模块,所以必须明确的声明模块名,require函数在这里只能接受字符串,不能接受变量 。所以还是忍忍算了

简化 WebSocket, TCP 等长连接的开发

简化 WebSocket, TCP 等长连接的开发

简介

根据之前的项目经验梳理了一下对于服务器端长连接(websocket, tcp)的开发模式,并且总结出了一套解决方案

HTTP 开发模式

先看看HTTP的开发模式有哪些爽点

  • 协议稳定,无论是http 还是 https,他们的协议都是固定不变的,自己无需处理数据包问题
  • 每次请求响应都是一个独立连接(即使因 keep-alive 复用同一个连接,在开发时也是无感知的 )
  • 请求的连接状态,虽然http连接本身是无状态的,但是浏览器会自动处理 Cookie,或者往Header 带 Token,就相当于给这个请求赋予了状态
  • 根据请求路径做路由映射,指定路由处理函数
  • 框架丰富,大部分框架封装好了上下文对象,解析参数,验证参数,响应数据等等操作都特别方便
  • 易调试,因 HTTP 协议本身特别简单,纯粹,它的调试也简单,工具很多

还有其他爽点,可是这些在 WebSocket, TCP 就不适用了。

长连接的痛点

在长连接的开发中,相比于http的开发,会遇到这些问题:

  • 协议不固定,websocket还好,在协议本身已经定义了数据边界,但是数据包的内容依然需要自己解析。TCP 就更不固定了,还需要自己定义数据编解码协议,处理粘包半包问题,才能解析到数据包,解析到了数据包还需要解析数据内容
  • 请求响应这种模式其实在长连接也很常见,只不过都是发生在同一个连接中,响应需要自己手动往连接发数据
  • 会涉及到和其他连接的交互
  • 主动往连接推送数据,或者获取其他连接状态并给其他连接推送数据,或者广播数据
  • 长连接的状态维护,长连接本身是不带任何状态,都要开发者维护,通过业务协议为连接赋予状态

解决方案

Command Service

协议

参考 HTTP 的开发模式。首先要定义一个请求响应的协议规范

// Request 请求协议
type Request struct {
	Cmd     string // 消息命令,用于路由映射
	Seqno   string // 消息编号,在短时间内不能重复
	RawData []byte // 消息原始数据
}

// Response 响应协议
type Response struct {
	Cmd      string      // 消息命令
	Seqno    string      // 消息编号,请求的 Seqno 原样赋值
	Code     int         // 响应状态码
	Msg      string      // 响应消息
	Data     interface{} // 响应的数据
}

Request 是请求数据,Response 是响应数据、服务器推送数据,整个框架将会围绕该协议做进一步的封装。

路由映射,处理上下文

一旦有固定的协议了,那么就可以很方便的做很多事了,比如 HTTP 那么爽,围绕 HTTP 框架的功能,让我们把 HTTP 框架有的东西也给弄过来。

模拟HTTP的开发模式,绑定路由,路由分组,中间件请求处理,参数解析,验证,格式化响应等等。

此外,还有长连接特有的功能,主动推送,获取其他连接状态

srv := cs.New()
srv.Use(cs.Recover()) // 使用中间件
srv.Handle("register", func(c *cs.Context) { // 绑定路由
	var body struct{
		UID int64 `json:"" v:"required#uid必填"`
		Name string `json:"" v:"required#name必填"`
	}
	if err := c.Parse(&body); err != nil {
		panic(err) // panic 给 recover 中间件处理
	}
	c.Set("uid", body.UID) // 给当前连接设置状态
	c.OK(map[string]int64{ // 响应数据
		"timestamp": time.Now().Unix(),
	})
	c.Push(&cs.Response{ // 主动给当前连接推送消息
		Cmd: "welcome",
		Data: "welcome to my server"
	})
	c.Broadcast(&cs.Response{ // 广播,给所有连接推消息
		Cmd: "user_online",
		Data: body,
	})
	for _, sid := range c.GetAllSID() {
		if c.GetState(sid, "uid") != nil { // 获取其他连接的状态,然后给其他连接推消息
			c.PushSID(sid, &cs.Response{
				Cmd: "friend_online",
				Data: map[string]interface{}{
					"name": body.Name,
				}
			})
		}
	}
})

瞧,只要把基础协议一固定,什么都好做了。而且功能比 HTTP 更强,更方便了。

适配器

如果把协议定的很死,那么就少了很多的灵活性,局限就很大了。所以上面的协议,可以理解为业务协议,实际上数据包的协议完全由适配器自己去决定,然后把数据包转成上方的协议就可以完美使用刚刚封装的框架。

比如 tcp 的一个数据包协议为 [数据长度 4byte] + [命令2byte] + [版本号1byte] + [数据不固定长度], 就可以根据该协议提取出 命令,、数据,转化为上方的协议,&cs.Request{Cmd: parsedCmd, Data: parseDataBytesArray}

多适配器共享

该框架做到后面,我发现要将多个适配器共享一套代码变得特别容易,比如 TCP 和 Websocket 共用同一套业务逻辑,只需要实现好适配器就可以了,真是意外惊喜

wsAdapter := xwebsocket.New()
tcpAdapter := xtxp.New("127.0.0.1:5566")
srv := cs.New(httpAdapter, tcpAdapter)
srv.Handle(cs.CmdConnected, func (c *cs.Context) {})

HTTP 主动推送

再到后面,发现要实现 HTTP 主动推送也是特别简单的,基于 SSE 的主动推送,加上 HTTP 的请求响应模式,在加上 cookie 对于连接的状态维护

centos 7 minimal 安装

安装centos

设置网络

nmtui

编辑,将自动启动选上后保存,重启网络

sudo service network restart

检查网络

ip addr

如果有IP地址就正常了

更新源

sudo yum update -y

安装epel,epel有很多软件,有必要装

sudo yum install epel-release

在次更新源

sudo yum update

安装编译工具

sudo yum make gcc gcc-c++

安装桌面 Xfce

桌面有很多,个人喜欢 Xfce

sudo yum groupinstall "X Window System" "Xfce"
sudo yum install xfce4-terminal //xfce 下的命令行工具,不安装的话无法再xfce进入命令行

安装好后就可以以图形方式登录,设置默认图形界面登录

ln -sf /lib/systemd/system/graphical.target /etc/systemd/system/default.target

重启系统,即可进入进入桌面

也可以这样立即进入桌面

startxfce4

安装VMware Tools (如果是虚拟机安装的话)

安装 vmtool 需要编译工具,上面已经安装,此外还要装 内核

sudo yum install -y kernel-devel kernel-headers

安装 perl,vmtool脚本是使用perl语言写的

sudo yum install -y perl

装完内核后要重启 reboot, 挂载光驱

sudo mkdir -p /mnt/cdrom
sudo mount -t auto /dev/cdrom /mnt/cdrom 

复制安装包到用户目录,因为在光驱目录是无法操作的

cp VMwareTools-9.9.0-2304977.tar.gz ~ //复制
cd ~ 
tar -zxvf VMwareTools-9.9.0-2304977.tar.gz // 解压
cd vmware-tools-distrib/
sudo ./vmware-install.pl // 执行,开始安装

然后一路回车即可,期间可能会提示无法找到ifconfig命令,这时输入ip,重启,就可以共享粘贴板,共享文件夹,拖动文件等等vmtool操作。

编辑器

sublime text

一般装linux还装了桌面的人,不用说肯定是搞开发的,一个好的编辑器可是很重要,sublime text 可是很好的选择

wget http://c758482.r82.cf2.rackcdn.com/sublime_text_3_build_3083_x64.tar.bz2 // 下载,写该文章时最新版本为 3083
tar -jxvf sublime_text_3_build_3059_x64.tar.bz2 //解压

Atom 编辑器

wget https://github.com/atom/atom/releases/download/v1.0.15/atom.x86_64.rpm
rpm -ivh atom.x86_64.rpm

初始化 Ubuntu 开发环境

启动一台新的虚拟机时,需要做一些配置

安装必须软件

基础包

sudo apt install -yq wget curl zsh git git-gui make gcc g++

Oh My Zsh

sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

nvm and node

执行前先设置下方的环境变量

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | zsh
nvm i 7
nvm alias default 7

sublime text

pkg=sublime.deb && wget -O $pkg https://download.sublimetext.com/sublime-text_build-3126_amd64.deb && sudo dpkg -i $pkg && rm -f $pkg

docker

curl -sSL http://acs-public-mirror.oss-cn-hangzhou.aliyuncs.com/docker-engine/internet | sh -
# docker 源换成aliyun, xxxxxxx 请换成自己的加速器
sudo echo "{\"registry-mirrors\": [\"https://xxxxxxxx.mirror.aliyuncs.com\"]" | sudo tee -a /etc/default/docker && sudo service docker restart

一些CDN

设置源

npm and yarn

npm config set registry "https://registry.npm.taobao.org" # 给npm设置淘宝源
yarn config set registry "https://registry.npm.taobao.org" # 给 yarn 设置淘宝源

ubuntu, 其实 cn.archive.ubuntu.com 也是 阿里云的镜像,所以如果本身就是 cn 源就不需要修改

  deb http://mirrors.aliyun.com/ubuntu/ natty main restricted universe multiverse
  deb http://mirrors.aliyun.com/ubuntu/ natty-security main restricted universe multiverse
  deb http://mirrors.aliyun.com/ubuntu/ natty-updates main restricted universe multiverse
  deb http://mirrors.aliyun.com/ubuntu/ natty-proposed main restricted universe multiverse
  deb http://mirrors.aliyun.com/ubuntu/ natty-backports main restricted universe multiverse
  deb-src http://mirrors.aliyun.com/ubuntu/ natty main restricted universe multiverse
  deb-src http://mirrors.aliyun.com/ubuntu/ natty-security main restricted universe multiverse
  deb-src http://mirrors.aliyun.com/ubuntu/ natty-updates main restricted universe multiverse
  deb-src http://mirrors.aliyun.com/ubuntu/ natty-proposed main restricted universe multiverse
  deb-src http://mirrors.aliyun.com/ubuntu/ natty-backports main restricted universe multiverse

环境变量

# nvm
export NVM_NODEJS_ORG_MIRROR=http://npm.taobao.org/mirrors/node
export NVM_IOJS_ORG_MIRROR=http://npm.taobao.org/mirrors/iojs

#phantomjs
export PHANTOMJS_CDNURL=https://npm.taobao.org/dist/phantomjs

# electron
export ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/

# node-sass
export SASS_BINARY_SITE=http://npm.taobao.org/mirrors/node-sass

# sqlite3
export SQLITE3_BINARY_SITE=http://npm.taobao.org/mirrors/sqlite3

# python
export PYTHON_MIRROR=http://npm.taobao.org/mirrors/python

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.