背景
介绍了axios的二次封装用于支持常规请求及自定义请求,并对同一时间内的相同请求做拦截处理(如果您没有阅读过这篇文章,建议您花费3分钟大致了解)。恰逢最近准备写一个跨框架组件库(工作量很大,前端三个小伙伴利用空闲时间在卷,待组件库完善后会分享给大家,敬请期待),需要学习发布npm包,昨天就想着利用空闲时间把之前写的去除重复请求的axios封装发布为npm包,便于代码复用,回馈社区的同时也能学以致用。
阅读本文,你将收获:
- 从0开始创建并发布npm的全过程
- 一个持续迭代且简单实用的axios请求去重工具库
工具库准备
创建一个新项目,包含package.json
{
"name": "drrq",
"type": "module",
"version": "1.0.0"
}
复制代码
功能实现 /src/index.js
npm i qs axios
主要思路是用请求的url和参数作为key记录请求队列,当出现重复请求时,打断后面的请求,将前面的请求结果返回时共享给后面的请求。
import qs from "qs";
import axios from "axios";
let pending = []; //用于存储每个ajax请求的取消函数和ajax标识
let task = {}; //用于存储每个ajax请求的处理函数,通过请求结果调用,以ajax标识为key
//请求开始前推入pending
const pushPending = (item) => {
pending.push(item);
};
//请求完成后取消该请求,从列表删除
const removePending = (key) => {
for (let p in pending) {
if (pending[p].key === key) {
//当前请求在列表中存在时
pending[p].cancelToken(); //执行取消操作
pending.splice(p, 1); //把这条记录从列表中移除
}
}
};
//请求前判断是否已存在该请求
const existInPending = (key) => {
return pending.some((e) => e.key === key);
};
// 创建task
const createTask = (key, resolve) => {
let callback = (response) => {
resolve(response.data);
};
if (!task[key]) task[key] = [];
task[key].push(callback);
};
// 处理task
const handleTask = (key, response) => {
for (let i = 0; task[key] && i < task[key].length; i++) {
task[key][i](response);
}
task[key] = undefined;
};
const getHeaders = { 'Content-Type': 'application/json' };
const postHeaders = { 'Content-Type': 'application/x-www-form-urlencoded' };
const fileHeaders = { 'Content-Type': 'multipart/form-data' };
const request = (method, url, params, headers, preventRepeat = true, uploadFile = false) => {
let key = url + '?' + qs.stringify(params);
return new Promise((resolve, reject) => {
const instance = axios.create({
baseURL: url,
headers,
timeout: 30 * 1000,
});
instance.interceptors.request.use(
(config) => {
if (preventRepeat) {
config.cancelToken = new axios.CancelToken((cancelToken) => {
// 判断是否存在请求中的当前请求 如果有取消当前请求
if (existInPending(key)) {
cancelToken();
} else {
pushPending({ key, cancelToken });
}
});
}
return config;
},
(err) => {
return Promise.reject(err);
}
);
instance.interceptors.response.use(
(response) => {
if (preventRepeat) {
removePending(key);
}
return response;
},
(error) => {
return Promise.reject(error);
}
);
// 请求执行前加入task
createTask(key, resolve);
instance(Object.assign({}, { method }, method === 'post' || method === 'put' ? { data: !uploadFile ? qs.stringify(params) : params } : { params }))
.then((response) => {
// 处理task
handleTask(key, response);
})
.catch(() => {});
});
};
export const get = (url, data = {}, preventRepeat = true) => {
return request('get', url, data, getHeaders, preventRepeat, false);
};
export const post = (url, data = {}, preventRepeat = true) => {
return request('post', url, data, postHeaders, preventRepeat, false);
};
export const file = (url, data = {}, preventRepeat = true) => {
return request('post', url, data, fileHeaders, preventRepeat, true);
};
export default { request, get, post, file };
复制代码
新增示例代码文件夹/example
示例入口index.js
import { exampleRequestGet } from './api.js';
const example = async () => {
let res = await exampleRequestGet();
console.log('请求成功 ');
};
example();
复制代码
api列表api.js
import { request } from './request.js';
// 示例请求Get
export const exampleRequestGet = (data) => request('get', '/xxxx', data);
// 示例请求Post
export const exampleRequestPost = (data) => request('post', '/xxxx', data);
// 示例请求Post 不去重
export const exampleRequestPost2 = (data) => request('post', '/xxxx', data, false);
// 示例请求Post 不去重
export const exampleRequestFile = (data) => request('file', '/xxxx', data, false);
复制代码
全局请求封装request.js
import drrq from '../src/index.js';
const baseURL = 'https://xxx';
// 处理请求数据 (拼接url,data添加token等) 请根据实际情况调整
const paramsHandler = (url, data) => {
url = baseURL + url;
data.token = 'xxxx';
return { url, data };
};
// 处理全局接口返回的全局处理相关逻辑 请根据实际情况调整
const resHandler = (res) => {
// TODO 未授权跳转登录,状态码异常报错等
return res;
};
export const request = async (method, _url, _data = {}, preventRepeat = true) => {
let { url, data } = paramsHandler(_url, _data);
let res = null;
if (method == 'get' || method == 'GET' || method == 'Get') {
res = await drrq.get(url, data, preventRepeat);
}
if (method == 'post' || method == 'POST' || method == 'Post') {
res = await drrq.post(url, data, preventRepeat);
}
if (method == 'file' || method == 'FILE' || method == 'file') {
res = await drrq.file(url, data, preventRepeat);
}
return resHandler(res);
};
复制代码
测试功能
代码写完后,我们需要验证功能是否正常,package.json加上
"scripts": {
"test": "node example"
},
复制代码
执行npm run test
功能正常,工具库准备完毕。
(eslint和prettier读者可视情况选用)
打包
一般项目的打包使用webpack,而工具库的打包则使用rollup
安装 Rollup
通过下面的命令安装 Rollup:
npm install --save-dev rollup
复制代码
创建配置文件
在根目录创建一个新文件 rollup.config.js
export default {
input: "src/index.js",
output: {
file: "dist/drrp.js",
format: "esm",
name: 'drrp'
}
};
复制代码
- input —— 要打包的文件
- output.file —— 输出的文件 (如果没有这个参数,则直接输出到控制台)
- output.format —— Rollup 输出的文件类型
安装babel
如果要使用 es6 的语法进行开发,还需要使用 babel 将代码编译成 es5。因为rollup的模块机制是 ES6 Modules,但并不会对 es6 其他的语法进行编译。
安装模块
rollup-plugin-babel 将 rollup 和 babel 进行了完美结合。
npm install --save-dev rollup-plugin-babel@latest
npm install --save-dev @babel/core
npm install --save-dev @babel/preset-env
复制代码
根目录创建 .babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
]
]
}
复制代码
兼容 commonjs
rollup 提供了插件 rollup-plugin-commonjs,以便于在 rollup 中引用 commonjs 规范的包。该插件的作用是将 commonjs 模块转成 es6 模块。
rollup-plugin-commonjs 通常与 rollup-plugin-node-resolve 一同使用,后者用来解析依赖的模块路径。
安装模块
npm install --save-dev rollup-plugin-commonjs rollup-plugin-node-resolve
复制代码
压缩 bundle
添加 UglifyJS 可以通过移除注上释、缩短变量名、重整代码来极大程度的减少 bundle 的体积大小 —— 这样在一定程度降低了代码的可读性,但是在网络通信上变得更有效率。
安装插件
用下面的命令来安装 rollup-plugin-uglify:
npm install --save-dev rollup-plugin-uglify
复制代码
完整配置
rollup.config.js 最终配置如下
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import { uglify } from 'rollup-plugin-uglify';
import json from '@rollup/plugin-json'
const paths = {
input: {
root: 'src/index.js',
},
output: {
root: 'dist/',
},
};
const fileName = `drrq.js`;
export default {
input: `${paths.input.root}`,
output: {
file: `${paths.output.root}${fileName}`,
format: 'esm',
name: 'drrq',
},
plugins: [
json(),
resolve(),
commonjs(),
babel({
exclude: 'node_modules/**',
runtimeHelpers: true,
}),
uglify(),
],
};
复制代码
在package.json中加上
"scripts": {
"build": "rollup -c"
},
复制代码
即可执行npm run build将/src/index.js打包为/dist/drrq.js
发包前的准备
准备npm账号,通过npm login或npm adduser。这里有一个坑,终端内连接不上npm源,需要在上网工具内复制终端代理命令后到终端执行才能正常连接。
准备一个简单清晰的readme.md
修改package.json
完整的package.json如下
{
"name": "drrq",
"private": false,
"version": "1.3.5",
"main": "/dist/drrq.js",
"repository": "https://gitee.com/yuanying-11/drrq.git",
"author": "it_yuanying",
"license": "MIT",
"description": "能自动取消重复请求的axios封装",
"type": "module",
"keywords": [
"取消重复请求",
],
"dependencies": {
"axios": "^1.2.0",
"qs": "^6.11.0"
},
"scripts": {
"test": "node example",
"build": "rollup -c"
},
"devDependencies": {
...
}
}
复制代码
- name 包名称 一定不能与npm已有的包名重复,想一个简单易记的
- private 是否为私有
- version 版本
- main 入口文件位置
- repository git仓库地址
- author 作者
- license 协议
- description 描述
- keywords 关键词,便于检索
每个 npm 包都需要一个版本,以便开发人员在安全地更新包版本的同时不会破坏其余的代码。npm 使用的版本系统被叫做 SemVer,是 Semantic Versioning 的缩写。
不要过分担心理解不了相较复杂的版本名称,下面是他们对基本版本命名的总结: 给定版本号 MAJOR.MINOR.PATCH,增量规则如下:
- MAJOR 版本号的变更说明新版本产生了不兼容低版本的 API 等,
- MINOR 版本号的变更说明你在以向后兼容的方式添加功能,接下来
- PATCH 版本号的变更说明你在新版本中做了向后兼容的 bug 修复。
表示预发布和构建元数据的附加标签可作为 MAJOR.MINOR.PATCH 格式的扩展。
最后,执行npm publish就搞定啦