手摸手带你撸一个网络请求库(rollup,babel)

1,709 阅读7分钟

rollup是一款默认支持ES模块的打包工具,地址:rollup英文官网。这里强烈建议不要看中文的官网,因为不是实时更新,所以很多说法是旧,直接跟着步骤做可能会出错。

babel主要作用是把js中的es201xstage-x的新语法转换成es5,让js代码能够运行在低版本的浏览器上。这次用到的babel版本是7。

接下来开始"手摸手"用rollup+babel+XMLHttpRequest封装一个网络请求的库,这里尽可能详细的步骤和思路列出,方便各位读者的学习之旅。会按照rollup使用、babel配置、封装XMLHttpRequest的顺序来,保证文章内容的连贯性。

为什么是rollup?

首先rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,一般用于第三方库的打包。和webpack一样,支持ES6模块化打包,Tree-shaking等特性。更由于简单的API和使用方式被越来越多的第三方库(Vue、React)作为构建工具所使用。

那为什么不直接用webpack呢?是因为webpack本身开箱即用的特性,为我们生成一些webpack自带的功能代码,例如模块加载。但由于现在开发的是js库,并不是项目,所以这些代码对于我们来说并不合适,反而还会增大打包后文件的大小。因此对于js库的开发,rollup就更为合适。

webpack自带模块加载代码:

webpack.jpg

rollup配置

由于rollup默认就对es模块导入导出的有所支持,所以js库里的模块间的导入导出直接用export、importes方式。rollup的配置和webpack的配置有点相似,需要配置基础的入口,打包输出的文件配置,还有插件配置等等,但是相对于webpack而言就简单多了,只需要简单配置即可。

初始化项目

执行npm init -y初始化项目,生成package.json文件:

package.json

"scripts": {
  "watch": "rollup -c rollup.config.js --watch",
  "build": "rollup -c rollup.config.js"
},

配置两个命令,watch实时监听文件变化用于开发,build用于打包输出最终文件。

配置文件(rollup.config.js)

rollup.config.js

在配置rollup前,先安装rollup插件依赖,这里需要注意rollup插件都是以@rollup/*开头:

npm i rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-babel @babel/core --save-dev

@rollup/plugin-node-resolve:使rollup拥有加载解析node_modules的第三方包的能力;

@rollup/plugin-commonjs:把文件或者模块里面的commonjs模块转化成es模块的方式;之所以要转换是因为rollup支持es模块的导入导出方式;

@rollup/plugin-babelrollup版本的babel,转换箭头函数class等语法;如果要对js新的实例的方法Promise,Set,Map这些进行转换es5,则需要其他的babel插件。

@babel/core: 是babel核心包,使用babel必须要安装这个依赖

package.jpg

项目根目录下创建rollup.config.js:

import commonjs from '@rollup/plugin-commonjs'; // 把commonjs模块转成是es6模块方式
import nodeResolve from '@rollup/plugin-node-resolve'; // rollup能够加载解析node_modules的第三方包
import babel from '@rollup/plugin-babel'; // 转换babel

export default {
    input: 'src/main.js', // 入口文件
    external: [], // 不需要rollup处理第三方包,作为外部依赖,例如:['lodash']
    output: {
        file: 'dist/bundle.js', // 打包输出文件位置
        format: 'umd', // 打包输出文件模块方式(amd | cjs | es | iife | umd | system)
        globals: {}, // 一般配合external一起使用,例如 {lodash: '_'}
        name: 'axiosmini', // 如果format是iife/umd,这个字段是必填的。浏览器条件下,相当于在window上扩展了这个名字作为这个库的全局对象。
        sourcemap: true,  // 生成sourcemap文件
    },

    plugins: [
        nodeResolve(), // 能够加载项目node_modules中的第三方包
        commonjs(), // 把commonjs的模块方式转化成import/export(rollup默认支持es的模块导入导出)
        babel({
            babelHelpers: 'runtime', // 
            exclude: 'node_modules/**' // 排除掉node_modules的第三方库
        })
    ]
}

搭建基础目录结构

新建入口文件:src/main.js

import AxiosMini from './core/AxiosMini';

var axiosmini = new AxiosMini();

export default axiosmini;

新建核心文件:src/core/AxiosMini.js

export default function AxiosMini(config) {
    this.config = config;
}

projectdir.jpg

现在可以通过npm run watch进行项目打包,最终会在dist目录下生成bundle.js文件和bundle.js.map文件

bundle.js

bundle.jpg

通过上面的配置,rollup就能帮助我们把es模块的源码进行依赖解析,最终按照output.format的转换格式打包出最终我们想要的文件格式内容。

babel配置

这里为什么还需要配置babel呢?因为js库的源码中可能会用到js新版本的语法特性,所以为了兼容低版本的浏览器,还是有必要使用babel的转换成es5的。

用到的第三方babel相关包:

@babel/core // babel内核

@babel/plugin-transform-runtime //配合rollup中runtime的调用

@babel/preset-env //babel7 新预制env

babel.config.js

module.exports = {
    "presets": [
        [
            "@babel/preset-env"
        ]
    ],
    "plugins": [
        "@babel/plugin-transform-runtime"
    ]
}

注意:@babel/polyfill 由于其污染性强,引入不灵活等弊端,babel7.4后已经被官方废弃了。 babel-runtime因为不支持原型方法和要结合polyfill使用, 在7.0.0后也移除了其polyfillcore-jsbabel7通过预制的env中的useBuiltIns来配置polyfill

封装网络请求核心代码

这里实现的方式模仿axios库,其中带有拦截器和get、post原型方法的封装。

项目目录

下面的代码中用axiosmini表示当前类库的全局变量名称。

|-- axiosmini
		|-- babel.config.js         // babel配置
    |-- package-lock.json       // 第三方包依赖
    |-- package.json
    |-- rollup.config.js        // rollup配置
    |-- dist                    // rollup打包后的文件
    |   |-- bundle.js
    |   |-- bundle.js.map
    |-- public                  // 测试打包后的代码功能
    |   |-- index.html
    |-- src											// 封装源码目录
        |-- main.js							// 项目入口文件
        |-- core								// 核心代码
        |   |-- AxiosMini.js    // AxiosMini构造函数代码(包含拦截器和get,post原型方法)
        |   |-- Interceptors.js // 拦截器构造函数
        |   |-- xhr.js          // XMLHttpRequest网络请求封装
        |-- utils								// 工具类
            |-- index.js				// mixin,forEach...

这里只贴出部分核心代码,完整的代码地址(代码地址),需要的可以下载。

入口文件

因为要支持axiosmini(config)axiosmini.create(config)两种方式的调用,所以这里的axiosmini本质上是一个函数。create方法和axiosmini直接调用没有区别。

main.js

import AxiosMini from './core/AxiosMini';
import { mixin } from './utils'

function createInstance(config) {
    // 通过构造函数创建出context对象
    var context = new AxiosMini(config);

    // 把原型链上request方法的this指向上一步的context对象
    var instance = AxiosMini.prototype.request.bind(context);

    // 为request方法添加上AxiosMini原型链上的方法(get,post),还有实例化后对象的属性
    mixin(instance, AxiosMini.prototype, context);

    // 返回的本质上经过加工的request方法
    return instance;
}

var axiosmini = createInstance({});

function create(config) {
    return createInstance(config || {});
}

axiosmini.create = create;

export default axiosmini;

核心构造函数

这个文件主要是对AxiosMini的构造函数进行封装。包括核心的request方法,拦截器interceptors,原型方法get,post

这里说一下拦截器的实现思路:首先拦截器本质上就是一个数组,每一次的请求拦截成功和失败的回调函数都作为这个数组的其中一个元素,响应拦截也是一样。interceptors.push({fulfill:function() {}, reject:function(){}});那么这个数组怎么使用呢?通过构造函数AxiosMini上的对请求拦截数组和响应拦截数组分别进行遍历,把它们进行加工到一个chain数组里,主要是把请求拦截数组的每一个元素按顺序放到chain数组的最前面,把响应拦截数组的每一个元素按照顺序放到chain数组的最后位置。chain数组的中间其实就是真正的网络请求。通过循环遍历调用该数组中的每一个元素,就能控制请求拦截---网络请求---响应拦截这样的顺序。具体代码如下

AxiosMini.js

import Interceptors from './Interceptors'
import xhr from './xhr'
import { mixin, forEach } from '../utils'

// 构造函数
export default function AxiosMini(config) {
    this.config = config;
    this.interceptors = {
        request: new Interceptors,
        response: new Interceptors
    }
}

AxiosMini.prototype.request = function request(config) {
    this.config = this.config || {};
    mixin(this.config || {}, {
        method: config.method || 'get',
        url: config.url || '',
        headers: config.headers || {},
        data: config.data || {},
        params: config.params || {},
        timeout: config.timeout
    })
		
    var chain = [xhr, undefined];

    // 获取请求拦截器上的拦截方法数组
    var reqHandler = this.interceptors.request.handler;
    forEach(reqHandler, function (h) {
        chain.unshift(h.fulfilled, h.rejected);
    })
    // 获取响应拦截器上的拦截方法数组
    var resHandler = this.interceptors.response.handler;
    forEach(resHandler, function (h) {
        chain.push(h.fulfilled, h.rejected)
    })
		
    var p = Promise.resolve(this.config);

    while (chain.length) {
        p = p.then(chain.shift(), chain.shift());
    }

    return p;
}

// 扩展原型上的get方法
AxiosMini.prototype.get = function get(url, config) {
    return this.request(mixin(config || {}, { method: 'get', url }));
}

// 扩展原型上的post方法
AxiosMini.prototype.post = function post(url, config) {
    return this.request(mixin(config || {}, { method: 'post', url }));
}

总结

本文通过使用rollupbabel从0到1封装一个第三方库,熟悉常用的rollup配置和babel配置,实现一个拥有拦截器和原型方法get、post的网络请求库。

如果读者发现有不妥或者可以改善的地方,欢迎在评论区指出。如果觉得写得不错或者对你有所帮助,可以点赞、评论、转发分享,谢谢~