feng 发布的文章

JSON.parse与JSON.stringify

平时我们用JSON.parse或者JSON.stringify的时候,经常只会传入一个参数,即:要序列化的对象或者反序列化的对象字符串.实际上这两个方法都可以传入第2个function参数去处理对应的value值.

function函数支持两个参数key,value分别对应处理后的key,value,可以通过函数去动态改变对应的value值.

    //JSON.stringify 示范代码.
    let data = {
        test: 'test',
        msg: '成功',
        data: {
            value: 1,
            text: 'hello',
            time: new Date()
        },
        time: new Date(),
        fun: function (a, b) {
            console.log(a + b);
        }
    }

    let jsonStr = JSON.stringify(data, (key, value) => {
        switch (key) {
            case 'test':
                return undefined;   //返回undefined,最终不会序列化
            case 'time':
                return value.replace('T', ' ');
            default:
                if (typeof value === 'function') { 
                    //简单处理函数的序列化方式,实际上真正的处理要复杂的多
                    return value.toString();
                }
                return value;
        }
    }, 4); //stringify还支持第3个参数格式化字符串(4代表4个空格,也可以输入字符串去代替.)

使用JSON.stringify的时候也要注意对象是否有toJSON这个方法.如果存在的话最终序列化会调用toJSON这个方法(一般情况不太可能重写js的序列化方法)

同样的,JSON.parse也支持第2个参数去处理最终的value值.

    let jsonObj = JSON.parse(jsonStr, (key, value) => {
        if (key === 'time') {
            return new Date(value);
        } else if (typeof value !== 'object' && /^function/.test(value)) {
            //简单的对函数进行处理.
            return eval('(' + value + ')');
        }
        return value;
    });

这让我们有些情况下处理序列化及反序列化得到很大的灵活性,不过似乎没有找到修改key的办法(除了改写toJSON),如果有的话请千万告诉我.^_^

初探redux用法

之前前端的技术栈偏向vue,针对整个生态圈环境,认为确实非常有必要看一下react,
确实react的生态圈是vue没法相比的,并且react native也对我非常有意义.

在阅读react技术文档过程中,redux相对理解的时间花费了长了一点.在这里记录一下,加深印象.

Redux提供一个createStore函数来生成一个Store

createStore接受第一个参数为function(我们叫做reducer),为了便于理解,以后再说后面的参数

reducer函数接受两个参数,
第1个是state,即当前store中存储的数据对象,可以有一个默认值.
第2个是action,是一个object对象,必须要有type属性,其他属性随意.

这个function的功能是根据对应type值,做出不同的处理,生成一个新的state被Store存储.

接下来我们在需要做出动作改变数据的地方,使用store.dispath(action)
这里的action就是之前createStore中的reducer的第2个参数.(当然按照规定也要有一个type属性)

当我们调用store.dispath的时候,Redux会自动去调之前我们传入的function,通过不同的type去做不同的处理,最后Store会根据reducer返回的对象生成一个新的store

最后,还有一个最重要的store.subscribe函数,他用来注册需要监听的函数.当store中的state数据发生了变化时,他会自动调用所有注册过的函数,在注册的函数内部可以通过store.getState()获取数据快照,来对组件进行渲染.

当程序庞大到一定程度时,单个reducer函数会变的非常庞大,Redux提供了combineReducers的方法.通过传入一个对象,对象中包裹多个reducer(多个reducer一般是分多个js去编写),来生成一个整体的reducer函数.使得程序维护变的简单.

React-Redux则变的简单很多.
他主要提供2个功能,

  1. Provider 组件
    Provider组件可以把传入的store对象,传给他下面的所有子组件.并且注册组件的render函数.省去我们手动注册的过程(就是说我们可以忘记subscribe函数)
  2. connect方法
    connect是一个高阶组件,他接收2个参数,第一个为映射的state,可以用他来返回你想要的筛选处理过后的state,一个是映射dispatch 用来映射你容器内需要用到的生成action的方法.
    最终他会返回一个函数,通过这个函数调用传入你的原始容器,将会生成一个新的容器,这个容器的props会注入你所有映射的state和映射的dispatch,来方便你使用.

socket.io框架搭建简介

在线游戏,实时通讯都需要用到websocket技术.
其中搭建websocket最方便的就是nodejs,node的高并发,js的单线程,异步处理的优势无限放大.

socket.io是基于nodeJS的websocket的socket开发框架.相对原生websocket的优势在于他的浏览器兼容性以及封装输出统一性.

源码下载:socket.io搭建源码
在这里吐槽一下杭州的电信,github访问速度超慢,动不动直接封死.连我连接的SSR服务器也经常封住...
(无限流量套餐的可以用4G无线连,一般4G都是不封的)

服务器端socket.io搭建

首先下载socket.io, 客户端代码也可以放同一个服务器端口下,可以下载express搭建一个node静态网页服务器

npm init -y
npm install --save socket.io
npm install --save express

然后新建一个index.js,写入内容

const express = require('express');
const path = require('path');
let app = express();

const http = require('http').Server(app);
let io = require('socket.io')(http);

//搭建静态客户端页面.
app.use(express.static(path.join(__dirname, 'statics')));

//=================socket==========================

io.on('connection', socket => {
    console.log('a user connected:' + socket.id);
});


http.listen(3000, () => {
    console.log('服务已启动,http://localhost:3000');
});

然后在控制台中运行node index.js

当看到控制台中跳出"服务已启动,http://localhost:3000"字样后,就说明服务器已经成功启动了.
服务启动

客户端socket.io搭建

首先建一个文件夹,专门存放静态页面(个人习惯叫做statics,如果文件夹不叫这个名字的话注意要修改服务器的对应路径:app.use(express.static(path.join(__dirname, 'statics')));)

新建一个index.html的首页.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>socket.io test</title>
</head>

<body>
  <div id='message'>


  </div>
</body>
<!-- 也可以把js下载下来存放在自己的服务器上 -->
<!-- <script src="https://cdn.bootcss.com/socket.io/2.0.4/socket.io.js"></script> -->
<script src="https://cdn.bootcss.com/socket.io/2.0.4/socket.io.slim.js"></script>
<script src="index.js"></script>
</html>

然后再新建一个index.js

var socket = io();  //只要这一句就可以连接了.

//socket连接成功事件
socket.on('connect',function(){
    appendText("连接成功 " + socket.id);
});

//追加文字到html页面上
function appendText(strText){
    var p = document.createElement("p");
    p.innerText = strText;

    document.getElementById("message").appendChild(p);
}

然后用浏览器访问http://localhost:3000/就可以看到链接成功的输出了.

服务器返回文字

web浏览器显示内容

通过webpack搭建socket.io客户端.

用 webpack搭建socket.io客户端框架的话尽量不要和服务器端的放一起,不方便开发以及调试,当然打完包后还是可以放在服务器端目录下面的

附上我的测试代码的package.json

  ...
  "scripts": {
    "build": "webpack --config ./webpack.config.js --env production",
    "start": "webpack-dev-server --config ./webpack.config.dev.js --env development --open"
  },
  "devDependencies": {
    "clean-webpack-plugin": "^0.1.18",
    "html-webpack-plugin": "^2.30.1",
    "uglifyjs-webpack-plugin": "^1.2.2",
    "webpack": "^3.11.0",
    "webpack-dev-server": "^2.11.1"
  },
  "dependencies": {
    "socket.io-client": "^2.0.4"
  }
  ...

首先在根目录新建入口模板页面 index.impl.html

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>socket.io client</title>
</head>

<body>
    <h1>socket client</h1>
    <div id="message"></div>
</body>
<!-- 通过cdn引用socket.io -->
<script src="https://cdn.bootcss.com/socket.io/2.0.4/socket.io.slim.js"></script>
<script type="text/javascript" src="<%=htmlWebpackPlugin.files.chunks.index.entry%>"></script>

</html>

新建一个lib文件夹,专门用来存放需要打包的文件

./lib/config.js (配置文件)

let config = {
    socketUrl: 'http://localhost:3000' ,    //socket.io 服务器路径. 
};

export { config as default };

./lib/index.js

import config from './config';
//import io from 'socket.io-client';    //也可以把socket.io和自己的代码打包在一起.

const socket = io(config.socketUrl);

socket.on('connect',()=>{
    console.log('connect ' + socket.id);

    appendText("连接成功 " + socket.id);
});

//追加文字到html页面上
function appendText(strText){
    var p = document.createElement("p");
    p.innerText = strText;

    document.getElementById("message").appendChild(p);
}

再新建一个webpack.config.dev.js (开发专用的config)

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
    devtool: 'eval-source-map',
    devServer: {
        host: process.env.HOST,
        port: 8080,
        inline: true,
        //historyApiFallback: true,
        // overlay: {
        //     errors: true,
        //     warnings: true
        // }
        //hotOnly:true
    },
    entry: {
        index: './lib/index.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name]-[hash].js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: 'index.impl.html',
            inject: false
        }),

    ]
};

发布正式webpack.config.js:

const HtmlWebpackPlugin = require('html-webpack-plugin');       //html模板插件
const CleanWebpackPlugin = require("clean-webpack-plugin");     //清除历史打包文件
const Uglify = require('uglifyjs-webpack-plugin');              //压缩js代码

const path = require('path');

module.exports = {
    entry: {
        index: './lib/index.js'
    },
    output: {
        path: path.join(__dirname, './dist'),
        filename: '[name]-[hash].js'
    },
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: 'index.impl.html',
            inject: false
        }),
        new CleanWebpackPlugin('./dist/*.*', {
            root: __dirname,
            verbose: true,
            dry: false
        }),
        new Uglify(),        //压缩js代码
    ]
};
npm install
npm start        //启动调试
npm run build    //打包文件

当浏览器自动打开 http://localhost:8080/ 则说明框架搭建成功
(注意服务端程序不要停止,否则连接不上)

webpack客户端

微信自动发送消息接口开发(三)

上一篇已经能够成功监听微信的消息了,现在只剩最后一个功能发送消息了.
发消息我只做了两个功能:发送文字和发送图片,其他的不太用的到,就没有做.

首先是发送普通文字消息

发送文字

发送文字很简单,如图只有一个接口webwxsendmsg BaseRequest和之前的监听消息是一样的,另外一个Msg Type:1就是文本消息(Type就是监听消息的时候返回的Type类型)ClientMsgId和LocalID在源码中都是当前时间戳+4位随机数字,FromUserName当然是填自己的UserName了(监听里初始化的时候的自己的UserName),ToUserName当然是填发送方了. Content就是具体要发送的内容了.

如果发送中文出来乱码的话注意发送的data编码得是utf-8

当返回的消息BaseResponse.Ret == 0 的时候,就表示你发送成功了.

接下来是发送图片消息

发送图片通过抓包可以发现是两个接口

1.上传文件 webwxuploadmedia?f=json
上传图片

其他的根据抓到的包来填值都没问题
主要有一个参数是uploadmediarequest要观察一下源码来填内容.
request参数

参数比较多不一一说明,其中FileMd5其实没关系,不用管随便传个值都可以,我是对图片名称+图片大小进行md5加密后传送过去的.

上传图片需要用multipart/form-data形式发送,这里附上我自己封装的上传方法供大家参考

    //上传图片消息
    private HttpResult WebWXUpLoadPicture(Contact model, string filePath)
    {
        string boundary = "----WebKitFormBoundary" + DateTime.Now.Ticks.ToString("x");

        var fileInfo = new FileInfo(filePath);

        List<FormItemModel> data = new List<FormItemModel>();
        //id
        data.Add(new FormItemModel("id", "WU_FILE_" + _fileId++));
        //name
        data.Add(new FormItemModel("name", fileInfo.Name));
        //type
        data.Add(new FormItemModel("type", System.Web.MimeMapping.GetMimeMapping(filePath)));
        //lastModifiedDate
        data.Add(new FormItemModel("lastModifiedDate", fileInfo.LastWriteTime.ToString("yyyy-MM-dd tt hh:mm:ss")));
        //size
        data.Add(new FormItemModel("size", fileInfo.Length.ToString()));
        //mediatype
        data.Add(new FormItemModel("mediatype", "pic"));
        //uploadmediarequest
        string md5 = Utils.EncryptToMD5(fileInfo.Name + fileInfo.Length);
        PicModel pic = new PicModel()
        {
            UploadType = 2,
            BaseRequest = new BaseRequest(this._wxModel),
            TotalLen = fileInfo.Length,
            FromUserName = this.User.UserName,
            ToUserName = model.UserName,
            FileMd5 = md5
        };
        data.Add(new FormItemModel("uploadmediarequest", JsonHelper.SerializeObject(pic)));
        //webwx_data_ticket
        string ticket = this.GetCookie("webwx_data_ticket");
        data.Add(new FormItemModel("webwx_data_ticket", ticket));
        //pass_ticket
        data.Add(new FormItemModel("pass_ticket", this._wxModel.pass_ticket));
        //file
        data.Add(new FormItemModel("filename", fileInfo.Name, filePath));

        var bytes = CreateFileByte(data, boundary);

        HttpItem item = new HttpItem()
        {
            URL = "https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxuploadmedia?f=json",
            Method = "POST",
            PostDataType = PostDataType.Byte,
            ContentType = "multipart/form-data; boundary=" + boundary,
            PostEncoding = Encoding.UTF8,
            PostdataByte = bytes
        };
        HttpHelper http = new HttpHelper();

        var rst = http.GetHtml(item);

        return rst;
    }
    //实体类模型
    public class FormItemModel
    {
    public FormItemModel(string key, string value)
    {
        this.Key = key;
        this.Value = value;
    }

    public FormItemModel(string key, string name, string path)
    {
        this.Key = key;
        this.Value = name;
        this.FilePath = path;
    }

    /// <summary>  
    /// 表单键,request["key"]  
    /// </summary>  
    public string Key { set; get; }
    /// <summary>  
    /// 表单值,上传文件时为文件名
    /// </summary>  
    public string Value { set; get; }
    /// <summary>  
    /// 是否是文件  
    /// </summary>  
    public bool IsFile
    {
        get
        {
            if (string.IsNullOrEmpty(FilePath) && FileContent == null) return false;

            return true;
        }
    }

    public byte[] GetFileBytes(Encoding encoding)
    {
        if (encoding == null) encoding = Encoding.UTF8;
        if (IsFile)
        {
            try
            {
                if (!string.IsNullOrEmpty(FilePath))
                {
                    using (FileStream fileStream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        byte[] bytes = new byte[fileStream.Length];
                        fileStream.Read(bytes, 0, bytes.Length);
                        return bytes;
                    }
                }
                else
                {
                    byte[] bytes = new byte[FileContent.Length];
                    FileContent.Read(bytes, 0, bytes.Length);
                    FileContent.Seek(0, SeekOrigin.Begin);
                    return bytes;
                }
            }
            catch (Exception)
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

    //如果是文件,下面两个只要填一个即可.

    /// <summary>  
    /// 上传的文件路径 
    /// </summary>  
    public string FilePath { set; get; }

    /// <summary>
    /// 文件流
    /// </summary>
    public Stream FileContent { get; set; }
}

//核心postdata 字节创建方法
private byte[] CreateFileByte(List<FormItemModel> formItems, string boundary, Encoding encoding = null)
    {
        if (encoding == null) encoding = Encoding.UTF8;
        Stream postStream = new MemoryStream();
        //文件模板
        string fileFormdataTemplate =
        "\r\n--" + boundary +
        "\r\nContent-Disposition: form-data; name=\"{0}\"; filename=\"{1}\"" +
        "\r\nContent-Type: application/octet-stream" +
        "\r\n\r\n";
        //文本数据模板  
        string dataFormdataTemplate =
            "\r\n--" + boundary +
            "\r\nContent-Disposition: form-data; name=\"{0}\"" +
            "\r\n\r\n{1}";

        foreach (var item in formItems)
        {
            string formdata = null;
            if (item.IsFile)
            {   //文件
                formdata = string.Format(
                fileFormdataTemplate,
                item.Key, //表单键  
                item.Value);
            }
            else
            {   //文本
                formdata = string.Format(
                dataFormdataTemplate,
                item.Key,
                item.Value);
            }

            byte[] formdataBytes = null;

            if (postStream.Length == 0)
                formdataBytes = encoding.GetBytes(formdata.Substring(2));
            else
                formdataBytes = encoding.GetBytes(formdata);

            //写入流
            postStream.Write(formdataBytes, 0, formdataBytes.Length);

            if (item.IsFile)
            {
                var bytes = item.GetFileBytes(encoding);
                if (bytes == null)
                {
                    this.PrintErrlog("读取文件错误:" + JsonHelper.SerializeObject(item));
                    throw new Exception("读取文件错误");
                }
                postStream.Write(bytes, 0, bytes.Length);
            }
        }

        var footer = encoding.GetBytes("\r\n--" + boundary + "--\r\n");
        postStream.Write(footer, 0, footer.Length);

        byte[] buffer = StreamToBytes(postStream);

        return buffer;

    }





如果上传图片成功,服务器会返回一个MediaId,相当于图片已经成功上传到微信服务器了,这个就是图片的key

文件上传返回

2.发送图片消息 webwxsendmsgimg

发送图片消息

发送图片接口就比较简单了,和文字消息差不多,要改的就是Msg中的Type换成3,再加上刚才得到的MediaId,其他的都基本差不多,当返回的消息BaseResponse.Ret == 0 的时候,你就成功了

接下来那些发送小视频,声音等等的由于本人实际需求用不到,就没有继续写了.本来还想写个自动抢红包的,发现网页版根本不支持抢红包,就无从写起了...

微信自动发送消息接口开发(二)

上一篇讲到OAuth鉴权成功,接下去就是监听微信消息的接口了.

成功登陆后,通过抓包和源码查看分析,主要有用到的监听新消息的接口有以下几个:

1. 微信登录初始化列表: webwxinit

微信登录后会立即调用的一个接口,用来初始化一些列表数据,比方说常用联系人列表,广告推送等

微信初始化

这个接口是一个POST接口,传参是一个json对象,注意BaseRequest这个对象存的是oauth鉴权返回的一些值,很多地方都会用到,下面是我做的封装,可以参考一下:

public class BaseRequest
{
    WXModel _model;
    public BaseRequest(WXModel model)
    {
        this._model = model;
    }

    public string DeviceID { get { return "e" + DateTime.Now.Ticks; } }

    public string Sid { get { return _model.wxsid; } }

    public string Skey { get { return _model.skey; } }

    public string Uin { get { return _model.wxuin; } }

}

注意这个接口返回的Content-Type不是很标准,是text/plain 并且没有跟charset字符集,微信整套接口用的全部都是UTF-8,和我一样用HttpHelper的人可能要手动设置一下获取编码,否则获取下来的会和谷歌开发环境的数据一样是乱码.

微信乱码

微信几个接口都存在这样的问题,不再一一叙述

2. 微信获取联系人列表详情: webwxgetcontact

webwxgetcontact是一个Get接口,用来获取联系人内容,没有什么好多说的,参数中r取随机值,其他的在之前鉴权内容中都有返回.

微信联系人列表

3. 微信获取群组列表详情: webwxbatchgetcontact

POST请求,除了有BaseRequest外,再给一个List是要请求的UserName,在一开始的webwxinit接口返回的ChatSet中可以得到,源码是对ChatSet过滤了已经存在的常用个联系人,请求剩下的所有群联系人

微信群列表

4. 微信检查是否有新的数据: synccheck

GET请求 里面其他没什么好说的,值得注意的是synckey 这个参数在init的时候会返回外,每次更新数据的时候也会有,要记得每次请求接口对应更新synckey里面的值.
请求当返回的是window.synccheck={retcode:"0",selector:"0"}则表示没有新的数据更新,其他都是要更新新的消息.
下面是我对synckey的封装.

public class KeyModel
{
    public int Key { get; set; }

    public long Val { get; set; }

    public override string ToString()
    {
        return Key + "_" + Val;
    }
}

public class SyncKey
{
    public List<KeyModel> List { get; set; }

    public int Count { get { return List?.Count ?? 0; } }

    public override string ToString()
    {
        if (List == null || List.Count == 0) return "";
        return string.Join("|", List);
    }
}

5. 微信同步新的消息 webwxsync

POST请求 最重要的一个请求,用来同步更新新的消息.
请求参数其他也没什么好说的,主要通过SyncKey来确定这一次需要更新的内容.rr是一个随机数.

同步消息

里面返回的json中 AddMsgList是新的消息列表,剩下的ModContactList,DelContactList,ModChatRoomMemberList我没有仔细研究.应该就是字面的意思:群人员变动,群删除,群基本信息变动等.
注意这里会返回SyncKey ,需要更新最新的SyncKey.

成功监听到微信消息:(接口中还有不少值,我的监听程序并没有去解析,具体可去看对应接口)

微信监听消息

源码中消息返回type展示:


 /*
        MSGTYPE_TEXT: 1,
        MSGTYPE_IMAGE: 3,
        MSGTYPE_VOICE: 34,
        MSGTYPE_VIDEO: 43,
        MSGTYPE_MICROVIDEO: 62,
        MSGTYPE_EMOTICON: 47,
        MSGTYPE_APP: 49,
        MSGTYPE_VOIPMSG: 50,
        MSGTYPE_VOIPNOTIFY: 52,
        MSGTYPE_VOIPINVITE: 53,
        MSGTYPE_LOCATION: 48,
        MSGTYPE_STATUSNOTIFY: 51,
        MSGTYPE_SYSNOTICE: 9999,
        MSGTYPE_POSSIBLEFRIEND_MSG: 40,
        MSGTYPE_VERIFYMSG: 37,
        MSGTYPE_SHARECARD: 42,
        MSGTYPE_SYS: 1e4,
        MSGTYPE_RECALLED: 10002,  // 撤销消息*/