IVWEB_jeremygao

IVWEB_jeremygao 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

IVWEB_jeremygao 发布了文章 · 2018-04-08

手把手教会你小程序登录鉴权

导语

为了方便小程序应用使用微信登录态进行授权登录,微信小程序提供了登录授权的开放接口。乍一看文档,感觉文档上讲的非常有道理,但是实现起来又真的是摸不着头脑,不知道如何管理和维护登录态。本文就来手把手的教会大家在业务里如何接入和维护微信登录态

接入流程

这里官方文档上的流程图已经足够清晰,我们直接就该图展开详述和补充。

img

首先大家看到这张图,肯定会注意到小程序进行通信交互的不止是小程序前端和我们自己的服务端,微信第三方服务端也参与其中,那么微信服务端在其中扮演着怎样的角色呢?我们一起来串一遍登录鉴权的流程就明白了。

1. 调用wx.login生成code

wx.login()这个API的作用就是为当前用户生成一个临时的登录凭证,这个临时登录凭证的有效期只有五分钟。我们拿到这个登录凭证后就可以进行下一步操作:获取openidsession_key

wx.login({
    success: function(loginRes) {
        if (loginRes.code) {
            // example: 081LXytJ1Xq1Y40sg3uJ1FWntJ1LXyth
        }
    }
});

2. 获取openid和session_key

我们先来介绍下openid,用过公众号的童鞋应该对这个标识都不陌生了,在公众平台里,用来标识每个用户在订阅号、服务号、小程序这三种不同应用的唯一标识,也就是说每个用户在每个应用的openid都是不一致的,所以在小程序里,我们可以用openid来标识用户的唯一性。

那么session_key是用来干嘛的呢?有了用户标识,我们就需要让该用户进行登录,那么session_key就保证了当前用户进行会话操作的有效性,这个session_key是微信服务端给我们派发的。也就是说,我们可以用这个标识来间接地维护我们小程序用户的登录态,那么这个session_key是怎么拿到的呢?我们需要在自己的服务端请求微信提供的第三方接口https://api.weixin.qq.com/sns/jscode2session,这个接口需要带上四个参数字段:

参数
appid小程序的appid
secret小程序的secret
js_code前面调用wx.login派发的code
grant_type'authorization_code'

从这几个参数,我们可以看出,要请求这个接口必须先调用wx.login()来获取到用户当前会话的code。那么为什么我们要在服务端来请求这个接口呢?其实是出于安全性的考量,如果我们在前端通过request调用此接口,就不可避免的需要将我们小程序的appid和小程序的secret暴露在外部,同时也将微信服务端下发的session_key暴露给“有心之人”,这就给我们的业务安全带来极大的风险。除了需要在服务端进行session_key的获取,我们还需要注意两点:

  • session_key和微信派发的code是一一对应的,同一code只能换取一次session_key。每次调用wx.login(),都会下发一个新的code和对应的session_key,为了保证用户体验和登录态的有效性,开发者需要清楚用户需要重新登录时才去调用wx.login()
  • session_key是有失效性的,即便是不调用wx.login,session_key也会过期,过期时间跟用户使用小程序的频率成正相关,但具体的时间长短开发者和用户都是获取不到的
function getSessionKey (code, appid, appSecret) {
    var opt = {
        method: 'GET',
        url: 'https://api.weixin.qq.com/sns/jscode2session',
        params: {
            appid: appid,
            secret: appSecret,
            js_code: code,
            grant_type: 'authorization_code'
        }
    };
    return http(opt).then(function (response) {
        var data = response.data;
        if (!data.openid || !data.session_key || data.errcode) {
            return {
                result: -2,
                errmsg: data.errmsg || '返回数据字段不完整'
            }
        } else {
            return data
        }
    });
}

3. 生成3rd_session

前面说过通过session_key来“间接”地维护登录态,所谓间接,也就是我们需要自己维护用户的登录态信息,这里也是考虑到安全性因素,如果直接使用微信服务端派发的session_key来作为业务方的登录态使用,会被“有心之人”用来获取用户的敏感信息,比如wx.getUserInfo()这个接口呢,就需要session_key来配合解密微信用户的敏感信息。

那么我们如果生成自己的登录态标识呢,这里可以使用几种常见的不可逆的哈希算法,比如md5、sha1等,将生成后的登录态标识(这里我们统称为'skey')返回给前端,并在前端维护这份登录态标识(一般是存入storage)。而在服务端呢,我们会把生成的skey存在用户对应的数据表中,前端通过传递skey来存取用户的信息。

可以看到这里我们使用了sha1算法来生成了一个skey:

const crypto = require('crypto');

return getSessionKey(code, appid, secret)
    .then(resData => {
        // 选择加密算法生成自己的登录态标识
        const { session_key } = resData;
        const skey = encryptSha1(session_key);
    });
    
function encryptSha1(data) {
    return crypto.createHash('sha1').update(data, 'utf8').digest('hex')
}

4. checkSession

前面我们将skey存入前端的storage里,每次进行用户数据请求时会带上skey,那么如果此时session_key过期呢?所以我们需要调用到wx.checkSession()这个API来校验当前session_key是否已经过期,这个API并不需要传入任何有关session_key的信息参数,而是微信小程序自己去调自己的服务来查询用户最近一次生成的session_key是否过期。如果当前session_key过期,就让用户来重新登录,更新session_key,并将最新的skey存入用户数据表中。

checkSession这个步骤呢,我们一般是放在小程序启动时就校验登录态的逻辑处,这里贴个校验登录态的流程图:

img2

下面代码即校验登录态的简单流程:

let loginFlag = wx.getStorageSync('skey');
if (loginFlag) {
    // 检查 session_key 是否过期
    wx.checkSession({
        // session_key 有效(未过期)
        success: function() {
            // 业务逻辑处理
        },
    
        // session_key 过期
        fail: function() {
            // session_key过期,重新登录
            doLogin();
        }
    });
) else {
    // 无skey,作为首次登录
    doLogin();
}

5. 支持emoji表情存储

如果需要将用户微信名存入数据表中,那么就确认数据表及数据列的编码格式。因为用户微信名可能会包含emoji图标,而常用的UTF8编码只支持1-3个字节,emoji图标刚好是4个字节的编码进行存储。

这里有两种方式(以mysql为例):

1.设置存储字符集

在mysql5.5.3版本后,支持将数据库及数据表和数据列的字符集设置为utf8mb4,因此可在/etc/my.cnf设置默认字符集编码及服务端编码格式

// my.cnf

[client]
default-character-set=utf8mb4
[mysql]
default-character-set=utf8mb4
[mysqld]
character-set-client-handshake = FALSE
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

设置完默认字符集编码及服务端字符集编码,如果是对已经存在的表和字段进行编码转换,需要执行下面几个步骤:

  • 设置数据库字符集为utf8mb4
ALTER DATABASE 数据库名称 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;
  • 设置数据表字符集为utf8mb4
ALTER TABLE 数据表名称 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  • 设置数据列字段字符集为utf8mb4
ALTER TABLE 数据表名称 CHANGE 字段列名称 VARCHAR(n) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

这里的COLLATE指的是排序字符集,也就是用来对存储的字符进行排序和比较的,utf8mb4常用的collation有两种:utf8mb4_unicode_ciutf8mb4_general_ci,一般建议使用utf8mb4_unicode_ci,因为它是基于标准的Unicode Collation Algorithm(UCA)来排序的,可以在各种语言进行精确排序。这两种排序方式的具体区别可以参考:What's the difference between utf8_general_ci and utf8_unicode_ci

2.通过使用sequelize对emoji字符进行编码入库,使用时再进行解码

这里是sequelize的配置,可参考Sequelize文档

{
       dialect: 'mysql',    // 数据库类型
       dialectOptions: {    
         charset: 'utf8mb4',
         collate: "utf8mb4_unicode_ci"
      },
}

最后

前面讲了微信小程序如何接入微信登录态标识的详细流程,那么如何获取小程序中的用户数据以及对用户敏感数据进行解密,并保证用户数据的完整性,我将在下一篇文章给大家做一个详细地介绍。

腾讯IVWEB团队的工程化解决方案feflow已经开源:Github主页:feflow

如果对您的团队或者项目有帮助,请给个Star支持一下哈~

查看原文

赞 77 收藏 93 评论 12

IVWEB_jeremygao 发布了文章 · 2018-03-15

如何写一个通用的README规范

背景

我们平常在进行项目开发时,一般都会把代码上传至代码托管平台上方便管理和维护。目前大家使用的托管平台最多的还是Github,国内外还有一些比较知名的代码托管平台,比如Gitlab、BitBucket,码云和码市等。

但我们在多人合作开发下,经常碰到的最头疼的问题是,其他开发者在交接给我们一个项目时只是对项目目前现有的功能简单的描述了下,我们在后续迭代功能时突然发现连最基本的项目如何运行都没有给我们交代,当时心中一万只那个什么马奔腾而过,只能去查看package.json的scripts,自己意会了。

那么问题来了,我们在交接一个项目时,如何保证项目能快速完整地交付给基友,从此过上无忧无虑的生活呢?答案是我们只需要甩给他一份标准规范的README。

    • -

规范的README需要哪些内容

我们通过一张截图一起来看看一份简单的README规范都有哪些内容:

image1

上面的readme规范模板在我们feflowREADME规范里可以看到
    • -

那么我们一起来探讨下,一份规范完整的README规范都应该有哪些内容呢?

1. 项目描述
2. 如何运行
3. 业务介绍
4. 项目备注

每一项都有哪些作用?

  • 项目描述

    需要说明我们的项目名,项目功能简述,代码仓库地址,以及该项目的第一负责人。谁交接给我们的项目,谁就是该项目的第一负责人。

  • 如何运行

    1. 开发环境配置。一般是我们需要的一些运行环境配置。
    2. 开发&发布 命令。我们怎么通过命令开启本地开发,以及构建发布。
    3. 代理配置。如果我们的项目在本地开发时需要用到一些代理工具,例如fiddler或whistle等,我们需要列出代理的配置项。最好是直接导出一个代理配置的文件,放在项目下
    4. 发布。如果我们有用到一些发布平台,最好贴上项目的发布模块和发布单,减少我们发布的时间成本。
    5. 错误告警及监控。相信我们平常都会对线上的项目部署错误告警和监控日志的服务,方便我们及时排查现网问题,这里我们可以加入项目的一些监控属性ID
    6. 接口API。这里我们需要贴入项目中拉去的后台接口地址以及描述,还有我们的接口负责人,当后台服务异常,可以直接联系到后台同学。
    7. 数据上报。我们在平常项目里都有加入一些数据上报,给产品同学统计业务数据用,这里我们最好阐述下都有哪些数据的上报。如果上报出问题,产品妹子找过来,我们不至于是一脸懵逼。
  • 业务介绍

    对于前端来说,我们一个项目可能不止一个页面,那么我们需要给出以下信息:

    1. 业务入口地址及渠道链接 即我们整个项目的入口页面,或者比较重要的页面地址。一般入口页面,我们可能会在多个渠道进行投放,那么需要列出所有的渠道链接
    2. 各页面及描述 列出我们项目内的所有页面信息,比如下面这样:

      页面目录页面描述页面链接参数描述
      index首页https://now.qq.com
  • 项目备注 项目中需要告诉其他开发者一些关键信息,比如我们页面打包构建,需要注意哪些问题等等,这些信息虽然不是必须的,但是可以帮助其他开发者降低开发的风险成本。

最后

上面是我们一个规范的README所需的一些信息和内容,加粗内容是我认为README里的一些必需信息,大家也可以在此基础上针对自己项目实际的开发场景来扩展一些规范信息。

腾讯IVWEB团队的工程化解决方案feflow已经开源:Github主页:https://github.com/feflow/feflow

如果对您的团队或者项目有帮助,请给个Star支持一下哈~

查看原文

赞 3 收藏 2 评论 0

IVWEB_jeremygao 发布了文章 · 2018-02-09

利用yeoman构建项目generator

本文作者:ivweb qcyhust 原文出处:IVWEB社区未经同意,禁止转载

导语

在一个项目的初始化阶段我们一般会做什么呢?如果有一个可参考的项目,是不是会复制这个项目,然后修改成新项目?如果是要在项目中增加一个新页面或是新组件,在开始的时候是不是会复制粘贴先前已存在的页面、组件代码。这些初始化时复制粘贴的操作意味着我们即将着手的项目有大量的结构代码(比如构建脚本,开发脚手架)是存在共性的,在开发过程中,新建一个页面,新开发一个组件,甚至新写一个路由都可能利用一个相同结构的代码来往里面填写新的内容。那么一个能帮助开发者生成自定制结构文件的小工具就会在这中使用场景下派上用场,它能让开发者的工作焦点回到真正的业务逻辑开发上,同时也能为团队开发体统一份统一的代码规范。

图片描述

简介

yeoman是一个可以帮助开发者快速开启一个新项目的工具集。yoeman提出一个yeoman工作流的概念,通过脚手架工具(yo),构建工具(grunt gulp等)和包管理器(npm bower等)的配合使用让开发者专注于业务的解决上而不是其他小事情。在yeoman的官网中可以搜索到很多用于初始化项目的generator,可以用于快速开启项目。同时yeoman也提供给开发者如何定义自己的generator,所有我们自己开发的generator都作为一个插件可以通过yo工具创建出我们需要的结构。

自己创建的generator可以是很简单的创建几个模板页面,也可以通过和用户交互构建一套量身定制的项目,取决于项目初始化的策略。可以利用yeoman的generator-generator工具来开始构建自己的generator。

从一个简单的例子开始

先从一个简单的模板页面入手,创建简单的generator。假设我们的需要的demo项目目录结构是这样的:

├───index.html
|───styles/
|    └───style.css
├───scripts/
    └───main.js

之前提到,我们的generator是一个插件,所以首先需要创建成一个node module包,在yeoman中这个包的名字应该是generator开头的,那么我们这个generator就叫做generator-demo。每一个包的keyword中必须包含yeoman-generator。files属性要指向项目的模板目录。

第一步是通过npm init或是自己手动创建generator的package.json,项目依赖yeoman-generator。也可以利用generator-generator来初始化。

{
    "name": "generator-demo",
    "version": "0.1.0",
    "description": "",
    "files": [
        "generators"
    ],
    "keywords": [
        "yeoman-generator"
    ],
    "main": "generators/app/index.js",
    "dependencies": {
        "yeoman-generator": "^1.0.0"
    }
}

我们的generator项目目录:

├───package.json
└───generators/
    └───app/
        └───index.js
        └───templates/
            ├───index.html
            ├───styles/
            │   └───style.css
            └───scripts/
                └───main.js    

第二步就是往template中填充内容,也就是demo项目的三个基本文件的内容。这里简单提供一个例子: template/index.html

<!DOCTYPE html>
<html lang="zh_CN">
<head>
    <title>generator-demo</title>
    <link rel="stylesheet" href="styles/style.css">
</head>
<body>
    <h1>helle <%= name %></h1>
    <script data-original="scripts/main.js"></script>
</body>
</html>

yeoman采用ejs模板语法,可以在模板文件中传入参数。

template/styles/style.css

* {
  margin: 0;
  padding: 0;
}

template/sctipts/main.js

'use strict';

window.onload = function() {
    console.log('generator success');
};

到这一步后就是扩展generator。yeoman提供了一个基础的generator,它有自己的生命周期和事件,功能强大。可以通过扩展这个基础generator来实现我们项目的初始化需求。于是第三步就是编辑app/index.js来扩展它:

const Generator = require('yeoman-generator');
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const utils = require('../utils');
const log = utils.log;

module.exports = class extends Generator {
  constructor(args, opts) {
      super(args, opts);

      this.props = {
          projectName: 'demo',
          name: 'world'
      };
  }

  writing() {
      const { projectName, name } = this.props;
      const temps = {
        'index.html': { name: this.props.name }
      };

      fs.readdir(this.sourceRoot(), (err, items) => {
          for(let item of items) {
              if(temps[item]) {
                  this.fs.copyTpl(
                      this.templatePath(item),
                      this.destinationPath(projectName, item),
                      temps[item]
                  );
              } else {
                  this.fs.copy(
                      this.templatePath(item),
                      this.destinationPath(projectName, item)
                  );
              }
          }
      });
  }

  end() {
      log.info('generator success');
  }
};

第四步就是运行generator。yoeaman的henerator是一个全局npm module,我们在本地开发的generator可以通过软连接的方式生成它的全局npm包。在generator-demo的根目录下运行npm link,它会在本地的全局npm目录下安装我们新建的generator。

在确定本地已经安装yo工具(npm install -g yo)后,在你需要初始化demo项目的地方运行yo demo,等命令执行完毕,就可以看到新建的项目了。

图片描述图片描述图片描述

run loop

在扩展基础generator时,我们可以给实例添加自定义的方法,每一个添加进去的方法都会在generator调用的时候被调用,而且通常来讲,这些方法是按照顺序调用的。除非是已下划线_开头的私有方法,或是定义在实例上的方法。

module.exports = class extends Generator {
    constructor(args, opts) {
        super(args, opts);

        this.task = () =>  {
            this.log('instance task');
        }
     }

      method1() {
          this.log('method 1');
      }

      method2() {
          this.log('method 2');
      }

     _task() {
        this.log('private task');
     }
};

// 输出:
// 'method 1'
// 'method 2'

每一个方法在yeoman中都被认为是一个任务,这些任务都会被run loop调用。yeoman的run loop是一个有优先级的队列系统。采用Grouped-queue来维护yeoman的事件队列。除了自定义的方法外,yeoman有很多特殊的事件方法,按照优先级排序:

  1. initializing - 初始化开始
  2. prompting - 调用this.prompt()与用户产生交互
  3. configuring - 创建配置文件(package.json,config.js等)
  4. default - 方法都不匹配这些优先级时,就会是default优先级(自定义方法会被划入default)
  5. writing - 创建项目文件
  6. conflicts - 文件创建中产生冲突的处理
  7. install - 调用(npm, bower)包install
  8. end - 结束项目初始化 其他自定义方法在configuring和writing按顺序优先级调用。 图片描述图片描述

    更复杂的交互

    现在我们来给generator增加用户交互和package.json,让它能构建出一个更复杂的项目。还是修改app/index.js,首先增加prompting:

prompting() {
  return this.prompt([{
      type: 'input',
      name: 'projectName',
      message: '请输入项目名字',
      default: 'default-name'
  }, {
      type: 'confirm',
      name: 'package',
      message: '需要package.json文件',
      default: true
  }, {
      type: 'input',
      name: 'name',
      message: '请输入你的名字',
      default: 'world'
  }]).then((answers) => {
      this.log('create project: ', answers.projectName);
      this.log('by: ', answers.name);
      this.props = answers;
  });
}

增加configuring:

configuring() {
  const { projectName, name } = this.props;
  let packageSettings = {
    name: projectName,
    version: '0.0.1',
    description: 'YOUR DESCRIPTION - Generated by generator-demo',
    main: '',
    scripts: {},
    repository: '',
    keywords: [],
    author: name,
    devDependencies: {},
    dependencies: {}
  };

  this.fs.writeJSON(this.destinationPath(projectName, 'package.json'), packageSettings);
}

package.json可以直接创建也可以利用模板文件创建或是将其中的属性抽象到配置文件中,这样方便修改。

总结

yeoman genenrator的功能远不只本文演示的这些,它还支持异步事件(prompting本身就返回一个promise对象)、install依赖包等等。

一个genenrator也不只是创建一个模板,它同时支持多种模板的需要,比如我们有个复杂的项目,files里面可以添加多个generator,主generator负责初始化项目的时候创建项目的主要文件并安装好各种依赖,在项目的开发中,我们需要增加一个container或是router的话,调用对应的genenrator即可,生成的模板可以将注意力放在内容上,提高开发效率。

腾讯NOW直播前端工程化解决方案feflow正式开源啦~: https://github.com/feflow/feflow 感兴趣的可以star一下哈~
查看原文

赞 1 收藏 1 评论 0

IVWEB_jeremygao 提出了问题 · 2016-08-15

socket.io 安卓端send失败中断程序问题

通过h5使用socket.io向服务器send消息时,用安卓机测试,服务器并没有接收到消息,但是用iphone测试是正常的。另外,安卓机测试时,send后程序直接中断。。。请问该怎么解决?

socketIO.emit('send', msg);

关注 1 回答 0

认证与成就

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

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-08-15
个人主页被 830 人浏览