阅读 149

前端进阶 - node基本操作:使用http手撸wx.request

http请求的简单流程可归纳为:

  1. 创建request请求。
  2. 请求发出后,通过回调获取response响应。
  3. 读取response响应流内容。

环境搭建

请根据上一篇《前端进阶 - node基本操作 - Koa快速搭建本地服务》文中代码,启动本地HTTP服务。

快速上手

创建文件request.js

// 创建请求
const req = require('http').request('http://127.0.0.1:3001/hello', {
    method: 'GET',
}, res => {
    // 获取响应状态码和响应头
    const { statusCode, headers } = res
    // 创建一个收集内容的缓存池
    const chunks = []
    // 当有数据返回时,将内容放入缓存
    res.on('data', chunk => {
        chunks.push(chunk)
    })
    // 当响应结束时,汇总请求数据。
    res.on('end', () => {
        // 拼接所有缓存
        const buff = Buffer.concat(chunks)
        // 缓存转码
        const data = buff.toString('utf-8')
        // 输出最终内容
        console.log({ statusCode, headers, data })
    })
})
// 发起请求
req.end()
复制代码

日志打印:

{
  statusCode: 200,
  headers: {
    'content-type': 'text/plain; charset=utf-8',
    'content-length': '6',
    date: 'Mon, 28 Jun 2021 09:24:22 GMT',
    connection: 'close'
  },
  data: '/hello'
}
复制代码

流程拆解

基于上面的代码,我们将其做一下封装,模拟wx.request方法的输入输出格式,实现一版node版本的request方法。封装过程中,有下列问题需要处理:

适配http连接类型

url参数的自适应,我们先只处理 http https 两种协议。

const http = require('http')
const https = require('https')

/**
 * 判断使用 http 或者 https 连接
 * @param {string} url 
 * @returns 
 */
const getHttp = (url) => {
    return /^https:/.test(url) ? http : https
}
复制代码

data参数

上面代码中,urlqueryParams部分是写在url中的。按照wx.request的标准,此部分应支持写入data参数中,使用时应是类似下面的代码:

request({
    method: 'GET',
    url: 'https://www.baidu.com/s',
    data: { wd: 'nodejs' }
})

// GET https://www.baidu.com/s?wd=nodejs
复制代码

我们将这个过程拆分为两部分:

1. data参数解析

const obj2url = (data, enc) =>
    data
        ? typeof data === 'string'
            ? enc ? encodeURIComponent(data) : data
            : Object.keys(data).map(k => `${k}=${encodeURIComponent(data[k])}`).join('&')
        : ''
复制代码

2. data参数与url合并

const combinData = (url, data, enc) => {
    const query = obj2url(data, enc)
    if(query) {
        const flag = url.indexOf('?') > -1 ? '&' : '?'
        return `${url}${flag}${query}`
    }
    return url
}
复制代码

自定义配置header

比如要配置请求的cookie

request({
    method: 'GET',
    url: 'https://www.baidu.com/s',
    data: { wd: 'nodejs' },
    header: { Cookie: 'a=1;b=2' }
})
复制代码

实现setHeaders方法

const setHeaders = (req, header) => {
    // 写入自定义header
    if(header) {
        Object.keys(header).forEach(key => {
            req.setHeader(key.toUpperCase(), header[key])
        })
    }
    // 高优先级header 覆盖
    req.setHeader('USER-AGENT', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36')

}
复制代码

这里需要注意的问题是:

  • 强制写入USER-AGENT,目的是模拟浏览器访问,防止中标人机检测。
  • headerkey全部转大写。理论上,headerkey不区分大小写,但不同的库实现情况不同,确实遇到过发送了大小写不同的两个header的情况。

封装 request

上述问题一一解决后,我们来进行封装:

整合代码

const http = require('http')
const https = require('https')

/**
 * 判断使用 http 或者 https 连接
 * @param {string} url 
 * @returns 
 */
const getHttp = (url) => {
    return /^https:/.test(url) ? https : http
}

const obj2url = (data) =>
    data
        ? Object.keys(data).map(k => `${k}=${encodeURIComponent(data[k])}`).join('&')
        : ''

const combinData = (url, data) => {
    const query = obj2url(data)
    if(query) {
        const flag = url.indexOf('?') > -1 ? '&' : '?'
        return `${url}${flag}${query}`
    }
    return url
}

const setHeaders = (req, header) => {
    // 写入自定义header
    if(header) {
        Object.keys(header).forEach(key => {
            req.setHeader(key.toUpperCase(), header[key])
        })
    }
    // 高优先级header 覆盖
    req.setHeader('USER-AGENT', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36')

}

const request = (options) => {
    const { method = 'GET', url, data, header, success, fail, complete } = options
    const combinUrl = combinData(url, data)
    const H = getHttp(combinUrl)
    // 创建请求
    const req = H.request(combinUrl, {
        method
    }, res => {
        // 获取响应状态码和响应头
        const { statusCode, headers } = res
        // 创建一个收集内容的缓存池
        const chunks = []
        // 当有数据返回时,将内容放入缓存
        res.on('data', chunk => {
            chunks.push(chunk)
        })
        // 当响应结束时,汇总请求数据。
        res.on('end', () => {
            // 拼接所有缓存
            const buff = Buffer.concat(chunks)
            // 缓存转码
            const data = buff.toString('utf-8')
            // 输出最终内容
            const result = { statusCode, headers, data, error: null, message: 'request:ok' }
            typeof success === 'function' && success(result)
            typeof complete === 'function' && complete(result)
        })
        res.on('error', (err) => {
            const error = { error: err, message: 'response:fail' }
            typeof fail === 'function' && fail(error)
            typeof complete === 'function' && complete(error)
        })
    })

    setHeaders(req, header)

    req.on('error', (err) => {
        const error = { error: err, message: 'request:fail' }
        typeof fail === 'function' && fail(error)
        typeof complete === 'function' && complete(error)
    })

    // 发起请求
    req.end()
}

module.exports = request

request({
    method: 'GET',
    url: 'http://127.0.0.1:3001/',
    data: 'a=1',
    success(res) {
        console.log(res)
    },
    fail(err) {
        console.log(2, err)
    }
})
复制代码

输出

{
  statusCode: 200,
  headers: {
    'content-type': 'text/plain; charset=utf-8',
    'content-length': '5',
    date: 'Mon, 28 Jun 2021 09:33:42 GMT',
    connection: 'close'
  },
  data: '/?a=1',
  error: null,
  message: 'request:ok'
}
复制代码

符合预期。

处理post请求

使用POST,与GET本质差异在于:POST的数据会写入请求的body中,也就是说,是带输入发起请求。

我们创建一个postData的二进制变量:

const postData = Buffer.from(M === 'POST' ? obj2url(data) : '', 'utf8')
复制代码

然后在发送请求时,附加该数据:

// 发起请求
req.end(postData)
复制代码

验证一下:

request({
    method: 'POST',
    url: 'http://127.0.0.1:3001/',
    data: { a: 1 },
    success(res) {
        console.log(res)
    },
    fail(err) {
        console.log(2, err)
    }
})
复制代码

输出

% node request.js
{
  statusCode: 200,
  headers: {
    'content-type': 'text/plain; charset=utf-8',
    'content-length': '3',
    date: 'Mon, 28 Jun 2021 09:36:50 GMT',
    connection: 'close'
  },
  data: 'a=1',
  error: null,
  message: 'request:ok'
}
复制代码

返回的data是按照formData格式存贮,符合预期。

完整代码

request.js

const http = require('http')
const https = require('https')

/**
 * 判断使用 http 或者 https 连接
 * @param {string} url 
 * @returns 
 */
const getHttp = (url) => {
    return /^https:/.test(url) ? https : http
}

/**
 * 对象转URL的key:value参数模式
 * @param {*} data 
 * @param {*} enc 
 * @returns 
 */
const obj2url = (data, enc) =>
    data
        ? typeof data === 'string'
            ? enc ? encodeURIComponent(data) : data
            : Object.keys(data).map(k => `${k}=${encodeURIComponent(data[k])}`).join('&')
        : ''

/**
 * 合并现有url和params
 * @param {*} url 
 * @param {*} data 
 * @param {*} enc 
 * @returns 
 */
const combinData = (url, data, enc) => {
    const query = obj2url(data, enc)
    if(query) {
        const flag = url.indexOf('?') > -1 ? '&' : '?'
        return `${url}${flag}${query}`
    }
    return url
}

/**
 * request请求头配置
 * @param {*} req 
 * @param {*} header 
 */
const setHeaders = (req, header) => {
    // 写入自定义header
    if(header) {
        Object.keys(header).forEach(key => {
            req.setHeader(key.toUpperCase(), header[key])
        })
    }
    // 高优先级header 覆盖
    req.setHeader('USER-AGENT', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.106 Safari/537.36')

}

const request = (options) => {
    const { method = 'GET', url, data, header, success, fail, complete } = options
    const M = ((method || 'GET') + '').toUpperCase()
    const combinUrl = M === 'POST' ? url : combinData(url, data, M !== 'POST')
    const postData = Buffer.from(M === 'POST' ? obj2url(data) : '', 'utf8')
    const H = getHttp(combinUrl)
    // 创建请求
    const req = H.request(combinUrl, {
        method
    }, res => {
        // 获取响应状态码和响应头
        const { statusCode, headers } = res
        // 创建一个收集内容的缓存池
        const chunks = []
        // 当有数据返回时,将内容放入缓存
        res.on('data', chunk => {
            chunks.push(chunk)
        })
        // 当响应结束时,汇总请求数据。
        res.on('end', () => {
            // 拼接所有缓存
            const buff = Buffer.concat(chunks)
            // 缓存转码
            const data = decodeURIComponent(buff.toString('utf-8'))

            // 输出最终内容
            const result = { statusCode, headers, data, error: null, message: 'request:ok' }
            typeof success === 'function' && success(result)
            typeof complete === 'function' && complete(result)
        })
        res.on('error', (err) => {
            const error = { error: err, message: 'response:fail' }
            typeof fail === 'function' && fail(error)
            typeof complete === 'function' && complete(error)
        })
    })

    setHeaders(req, header)

    req.on('error', (err) => {
        const error = { error: err, message: 'request:fail' }
        typeof fail === 'function' && fail(error)
        typeof complete === 'function' && complete(error)
    })

    // 发起请求
    req.end(postData)
}

module.exports = request
复制代码

单元测试

request.test.js

const request = require('./request')

describe('Request get', () => {
    it(`get /123`, done => {
        request({
            url: `http://127.0.0.1:3001/123`,
            success({ data }) {
                expect(data).toEqual('/123')
                done()
            }
        })
    })

    it(`get /中文`, done => {
        request({
            url: `http://127.0.0.1:3001/中文`,
            success({ data }) {
                expect(data).toEqual('/中文')
                done()
            }
        })
    })

    it(`get /a=1`, done => {
        request({
            url: `http://127.0.0.1:3001/?a=1`,
            success({ data }) {
                expect(data).toEqual('/?a=1')
                done()
            }
        })
    })

    it(`get /?a=中文`, done => {
        request({
            url: `http://127.0.0.1:3001/?a=中文`,
            success({ data }) {
                expect(data).toEqual('/?a=中文')
                done()
            }
        })
    })
})


describe('Request post', () => {
    it(`post 123`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: '123',
            success({ data }) {
                expect(data).toEqual('123')
                done()
            }
        })
    })

    it(`post 中文`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: '中文',
            success({ data }) {
                expect(data).toEqual('中文')
                done()
            }
        })
    })

    it(`post { a: 1 }`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: { a: 1 },
            success({ data }) {
                expect(data).toEqual('a=1')
                done()
            }
        })
    })

    it(`post '{ a: 1 }'`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: '{ a: 1 }',
            success({ data }) {
                expect(data).toEqual('{ a: 1 }')
                done()
            }
        })
    })

    it(`post { a: "中文" }`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: { a: "中文" },
            success({ data }) {
                expect(data).toEqual(`a=中文`)
                done()
            }
        })
    })

    it(`post '{ a: "中文" }'`, done => {
        request({
            method: 'post',
            url: `http://127.0.0.1:3001`,
            data: '{ a: "中文" }',
            success({ data }) {
                expect(data).toEqual('{ a: "中文" }')
                done()
            }
        })
    })
})
复制代码

测试

包含上一篇的koa.test.js

% npm test -- --verbose

> testnpm@1.0.0 test
> jest "--verbose"

 PASS  ./request.test.js
  Request get
    ✓ get /123 (16 ms)
    ✓ get /中文 (2 ms)
    ✓ get /a=1 (1 ms)
    ✓ get /?a=中文 (2 ms)
  Request post
    ✓ post 123 (1 ms)
    ✓ post 中文 (1 ms)
    ✓ post { a: 1 } (2 ms)
    ✓ post '{ a: 1 }' (1 ms)
    ✓ post { a: "中文" } (2 ms)
    ✓ post '{ a: "中文" }' (1 ms)

 PASS  ./koa.test.js
  Koa service get
    ✓ get /123 (11 ms)
    ✓ get /?123 (2 ms)
    ✓ get /中文 (2 ms)
    ✓ get /?中文 (1 ms)
  Koa service post
    ✓ post '中文' (1 ms)
    ✓ post 'AbC' (2 ms)

Test Suites: 2 passed, 2 total
Tests:       16 passed, 16 total
Snapshots:   0 total
Time:        0.462 s, estimated 1 s
Ran all test suites.
复制代码

测试通过。以上。

文章分类
前端
文章标签