使用可插拔插件式封装方法

826 阅读4分钟

本文主要是在自己看过 IMWeb前端社区 分享的 腾讯在线教育小程序开发实践之路 文章之后,对其中提到的 采用可插拔插件式封装方法,来做各种插件的扩展 的理解。作为前端小白,水平有限,以下只是分享下自己的见解。

Koa 中采用中间件模式使用插件,我们可以将其借鉴到平时的开发中,主要使用其中的 compose 函数

function compose(middleware) {
  if (!Array.isArray(middleware)) {
    throw new TypeError('Middleware stack must be an array!');
  }
  for (const fn of middleware) {
    if (typeof fn !== 'function') {
      throw new TypeError('Middleware must be composed of functions!');
    }
  }

  return function(context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index) {
        return Promise.reject(new Error('next() called multiple times'));
      }
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

关于对 compose 函数的理解,不在此赘述,简单说下使用方式

function fetch() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Math.random() < 0.5 ? resolve('Fetch is success') : reject('Fetch is error');
    }, 3000);
  });
}

async function a(ctx, next) {
  console.log('a start');
  let res;
  try {
    res = await next();
  } catch (err) {
    res = Promise.reject(err);
  }
  console.log('a end');
  return res;
}

async function b(ctx, next) {
  console.log('b start');
  await next();
  console.log('b end');
  return fetch();
}

const ctx = { a: 1, b: true };
const p = compose([a, b])(ctx, ctx => {
  console.log('c start');
  console.log('ctx', ctx);
  console.log('c end');
});
p.then(res => {
  console.log('res', res);
}).catch(err => {
  console.log('err', err);
});

执行结果如下

// => a start
// => b start
// => c start
// => ctx { a: 1, b: true }
// => c end
// => b end
// 等待 3 秒
// => a end
// => res Fetch is success 随机成功或失败

下面将以在小程序开发中的需求为例,介绍中间件模式在开发中的使用

使用普通封装方法

简单的对 wx.request 方法进行封装

function fetch(option) {
  return new Promise((resolve, reject) => {
    wx.request({
      ...option,
      success: res => {
        res.statusCode === 200 ? resolve(res) : reject(res);
      },
      fail: reject,
    });
  });
}

fetch({ url: 'https://...' })
  .then(res => {})
  .catch(err => {});

通常情况下,有些请求需要在 header 中携带 token 数据进行身份验证,而有些请求不需要,所以修改 fetch 函数

function fetch(option, config = {}) {
  const { takeToken = true } = config;

  if (takeToken) {
    const { header = {} } = option;
    const token = wx.getStorageSync('token');
    option.header = { ...header, token };
  }

  return new Promise((resolve, reject) => {
    wx.request({
      ...option,
      success: res => {
        res.statusCode === 200 ? resolve(res) : reject(res);
      },
      fail: reject,
    });
  });
}

fetch({ url: 'https://...' }, { takeToken: true })
  .then(res => {})
  .catch(err => {});

目前看起来还好,该功能只是在函数顶部增加了一些逻辑判断处理

此时我们需要有些请求能够在发起请求的时候自动显示 loading,请求完成后自动隐藏 loading,那么继续修改 fetch 函数

function fetch(option, config = {}) {
  const { takeToken = true, wrapLoading = false } = config;

  if (takeToken) {
    const { header = {} } = option;
    const token = wx.getStorageSync('token');
    option.header = { ...header, token };
  }

  if (wrapLoading) wx.showLoading({ title: '加载中' });
  const hide = () => {
    if (wrapLoading) wx.hideLoading();
  };

  return new Promise((resolve, reject) => {
    wx.request({
      ...option,
      success: res => {
        res.statusCode === 200 ? resolve(res) : reject(res);
      },
      fail: reject,
      complete: () => {
        hide();
      },
    });
  });
}

fetch({ url: 'https://...' }, { wrapLoading: false })
  .then(res => {})
  .catch(err => {});

现在再看 fetch 函数,自动显示和隐藏 loading 的功能处理代码不仅仅只出现在一处,请求函数 wx.request 的参数的 complete 方法中也存在

随着功能的逐步增加,fetch 函数会越来越大,功能逻辑代码也越来越多,逻辑代码很容易混杂在一起。如果哪天需要修改或者删除某项功能,那么需要查看整个 fetch 函数的所有部分,确保修改或删除后的功能完善无遗漏。如果函数出现了问题,也要从所有代码中进行排查

使用可插拔插件式封装方法

同样的添加使用 token 功能

function fetch(option) {
  return new Promise((resolve, reject) => {
    wx.request({
      ...option,
      success: res => {
        res.statusCode === 200 ? resolve(res) : reject(res);
      },
      fail: reject,
    });
  });
}

function useToken(ctx, next) {
  const { takeToken = true } = ctx.config;
  if (takeToken) {
    const { header = {} } = ctx.option;
    const token = wx.getStorageSync('token');
    ctx.option.header = { ...header, token };
  }
  return next();
}

function request(option, config = {}) {
  const ctx = { option, config };
  return compose([useToken])(ctx, ctx => fetch(ctx.option));
}

request({ url: 'https://...' }, { takeToken: true })
  .then(res => {})
  .catch(err => {});

保持 fetch 函数代码不动,增加 request 函数作为请求函数,增加 useToken 中间件,并在 request 函数中使用该中间件。可以发现使用 token 的功能逻辑是独立的

继续添加 loading 功能

function fetch(option) {
  return new Promise((resolve, reject) => {
    wx.request({
      ...option,
      success: res => {
        res.statusCode === 200 ? resolve(res) : reject(res);
      },
      fail: reject,
    });
  });
}

function useToken(ctx, next) {
  const { takeToken = true } = ctx.config;
  if (takeToken) {
    const { header = {} } = ctx.option;
    const token = wx.getStorageSync('token');
    ctx.option.header = { ...header, token };
  }
  return next();
}

async function useLoading(ctx, next) {
  const { wrapLoading = false } = ctx.config;
  if (wrapLoading) wx.showLoading({ title: '加载中' });
  let res;
  try {
    res = await next();
  } catch (err) {
    res = Promise.reject(err);
  }
  if (wrapLoading) wx.hideLoading();
  return res;
}

function request(option, config = {}) {
  const ctx = { option, config };
  return compose([useToken, useLoading])(ctx, ctx => fetch(ctx.option));
}

request({ url: 'https://...' }, { wrapLoading: true })
  .then(res => {})
  .catch(err => {});

可以发现,之前的代码并没有做修改,只是增加了 useLoading 中间件,并在 request 函数中使用该中间件

如果之后再有什么新的功能或者功能调整,可以继续以中间件的形式进行增加或对相应中间件进行修改或删除

对比上面两种封装方式,不难发现,以中间件方式的封装,可以将各个功能逻辑相互独立起来,方便维护和理解,其他的优势可以自己对比去发现,这只是个人的见解。