ts加rollup开发请求模块(1)

321 阅读22分钟

1 准备

1.1 创建一个npm 组织

为了防止重名,所以申请一个组织来作为包名的前缀,这里我取名light-send,你们自由发挥哈ღ( ´・ᴗ・` )

2.建设项目

2.1 初始化项目

2.1.1 使用npm快速初始化项目

npm ini -y

2.1.2 安装lerna并初始化

  1. 首选全局安装lerna
npm i lerna -g
  1. 然后在项目里面安装lerna
npm i lerna --save-dev

yarn add lerna -S

然后使用

lerna init

进行lerna的项目初始化

个人比较喜欢yarn所以会在lerna.json中添加npmClient

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "yarn"
}
  1. 添加.editorconfig文件
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
tab_width = 2

[*.md]
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
  1. 添加.gitignore文件
.DS_Store
dist/**/*
build/*
!build/icons
node_modules/
npm-debug.log
npm-debug.log.*
thumbs.db
!.gitkeep
.idea
.vscode
yarn-error.log
/**/node_modules/
/**/dist/
/**/*/coverage/
yarn.lock
/**/*/yarn.lock
package-lock.json
/**/*/package-lock.json

2.2 统一的eslint+ prettier

这里相关的eslint规则和prettier的规则自己根据实际情况配置哦!

  1. 更新package.json添加eslitprettier相关依赖
{
  "name": "light-send",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "lerna": "^4.0.0",
    "@typescript-eslint/eslint-plugin": "^4.26.1",
    "@typescript-eslint/parser": "^4.24.0",
    "eslint": "^7.25.0",
    "eslint-config-prettier": "^7.2.0",
    "eslint-plugin-prettier": "^3.4.0",
    "prettier": "^2.2.1",
    "prettier-eslint": "^12.0.0"
  }
}
  1. 添加.eslintrc.js.eslintignore文件
const DOMGlobals = ["window"];
const NodeGlobals = ["module", "require"];

module.exports = {
  env: {
    browser: true,
    es6: true
  },
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "prettier"
  ],
  plugins: ["prettier"],
  rules: {
    "prettier/prettier": "error",
    "no-unused-vars": "off",
    "no-restricted-globals": ["error", ...DOMGlobals, ...NodeGlobals],
    "no-console": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/no-explicit-any": "off"
  }
};
node_modules/
**/dist/
**/src/type.ts
**/typings/
**/__test__/
**/coverage/
**/__test__/**/*.ts
  1. 添加.prettierrc文件
{
  "printWidth": 120,
  "tabWidth": 2,
  "useTabs": false,
  "singleQuote": false,
  "semi": true,
  "trailingComma": "none",
  "bracketSpacing": true
}

2.3 配置rollup打包ts项目

2.3.1 创建包

运行命令

lerna create @light-send/send packages/send

2.3.2 配置rollup

2.3.2.1 修改package.json文件如下

{
  "name": "@light-send/send",
  "version": "0.0.0",
  "description": "发送接口封装",
  "author": "",
  "homepage": "",
  "license": "ISC",
  // 使用模块的时候的入口文件 这里我用的umd的格式
  "main": "dist/index.js",
  // 使用esmodule会使用的额文件
  "module": "dist/index.esm.js",
  // cjs格式的文件
  "cjsModule": "dist/index.cjs.js",
  // 压缩后的umd文件
  "minMain": "dist/index.min.js",
  // 压缩后的esmodule文件
  "minModule": "dist/index.esm.min.js",
  // 类型定义文件
  "types": "typings/index.d.ts",
  "keywords": [
    "send",
    "axios",
    "fetch",
    "ajax"
  ],
  "directories": {
    "lib": "lib",
    "test": "__tests__"
  },
  "files": [
    "dist",
    "typings"
  ],
  "publishConfig": {
    "access": "public"
  },
  "scripts": {
    // 开发时候的命令,使用nodemon监听rollup.config.js有变化自动重启
    "start": "cross-env NODE_ENV=developemnt nodemon",
    // 执行rollup-w 加载配置文件
    "dev": "node rollup.config.js",
    "build": "cross-env NODE_ENV=production node rollup.config.js",
    // 发布前执行build命令
    "prepublishOnly": "npm run build",
    // build前删除dist文件夹
    "prebuild": "rimraf dist",
    "test": "jest --coverage"
  },
  // 因为用了runtime所以这里配置@babel/runtime-corejs3
  "peerDependencies": {
    "@babel/runtime-corejs3": "^5.3.0",
    "core-js": "^3.20.2"
  },
  "devDependencies": {
    "@babel/core": "^7.14.3",
    "core-js": "^3.20.0",
    "@babel/plugin-transform-runtime": "^7.14.3",
    "@babel/preset-env": "^7.14.2",
    "@babel/preset-typescript": "^7.13.0",
    "@babel/runtime-corejs3": "^7.14.0",
    "@rollup/plugin-babel": "^5.3.0",
    "@rollup/plugin-commonjs": "^21.0.1",
    "@rollup/plugin-node-resolve": "^13.1.1",
    "@types/jest": "^26.0.23",
    "@types/node": "^10.11.0",
    "cross-env": "^7.0.3",
    "jest": "^23.6.0",
    "jest-config": "^23.6.0",
    "nodemon": "^2.0.15",
    "rimraf": "^3.0.2",
    "rollup": "~2.45.2",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-commonjs": "^10.1.0",
    "rollup-plugin-eslint": "^7.0.0",
    "rollup-plugin-json": "^4.0.0",
    "rollup-plugin-node-polyfills": "^0.2.1",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-typescript2": "^0.30.0",
    "ts-jest": "^23.10.2",
    "ts-node": "^7.0.1",
    "tslib": "^2.2.0",
    "typescript": "^4.2.4"
  }
}

主要新增以下内容:

  1. 包文件的入口和esmoudule的入口以及压缩文件路径
  2. 新增startdevbuildtest命令
  3. 新增publisbuild的前置钩子

2.3.2.2 新增rollup配置文件

const path = require("path");
const { nodeResolve  } = require('@rollup/plugin-node-resolve')
const commonjs = require("rollup-plugin-commonjs"); // commonjs模块转换插件
const ts = require("rollup-plugin-typescript2");
const { getBabelOutputPlugin } = require('@rollup/plugin-babel')
const rollup = require('rollup')
const getPath = (_path) => path.resolve(__dirname, _path);
const packageJSON = require("./package.json");
const babel = require("rollup-plugin-babel");
const extensions = [".js", ".ts", ".tsx"];
const babelConfig = require('./babel.config')
const analyze = require('rollup-plugin-analyzer')
const isDev = process.env.NODE_ENV === 'developemnt'
const umdPlugin = babel({
  exclude: "node_modules/**",
  babelrc:false,
  extensions,
  runtimeHelpers: true,
});
const  globals = {
  '@light-send/utils':'lightUtils'
}
const babelPlugin = getBabelOutputPlugin({
  ...babelConfig
});
// ts
const tsPlugin = ts({
  tsconfig: getPath("./tsconfig.json"), // 导入本地ts配置
  extensions
});
const commPlugin = [
  nodeResolve({
    extensions
  }),
  commonjs(),
  tsPlugin,
]
// 基础配置
const commonConf = {
  input: getPath("./src/index.ts"),
  external: ['@babel/runtime-corejs3','@light-send/utils'],
  plugins:[
    ...commPlugin
  ],
};

const umdOutput = [
  {
    file: getPath(packageJSON.umd), // 通用模块
    format: "umd",
    name: "lintSend",
    globals
  },

]

const outputMap = [
  {
    file: getPath(packageJSON.module), // 通用模块
    format: "esm",
    globals
  },
  {
    file: getPath(packageJSON.cjsModule), // 通用模块
    format: "cjs",
    globals
  }
]
if(isDev) {
  const watcher = rollup.watch({
    ...commonConf,
    plugins: commonConf.plugins.concat(umdPlugin),
    output:umdOutput
  })
  watcher.on('event', async (event) => {
    if(event && event.code === 'END') {
      modularityBundle()
    }
  });
} else {

  /**
   * 编译umd文件,这里因为同一个babel配置的话umd模块
   * 会出现require等关键词这浏览器是无法识别的
   * 所以umd模块和其他模块分开打包
   */
 rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat(umdPlugin),
  }).then(async (bundle)=>{
   await bundle.write(umdOutput[0])
   bundle.close
 })
  modularityBundle();
}

/**
 * 编译模块化产物
 */
function modularityBundle() {
  rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat([babelPlugin]),
  }).then(async (bundle)=>{
    for await (const output of outputMap) {
      await bundle.write(output)
    }
    await bundle.close()
  })
}
babel.config.js
module.exports = {
  "presets": [
    [
      "@babel/preset-env"
    ]

  ],
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "helpers": true,
        "regenerator": true,
        "corejs": 3
      }
    ]
  ]
}
nodemon.js
{
  // 观察配置文件变化重新启动exec的命令,这里代码文件的变化dev模式采用了rollup.watch进行观测
  "watch": ["rollup.config.js"],
  "verbose": true,
  "ignore": [],
  "exec": "npm run dev"
}

2.3.2.3 新增ts配置文件

{
  "compilerOptions": {
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "baseUrl": ".",
    "outDir": "./dist",
    "rootDir": "./src",
    // 输出目录
    "sourceMap": true,
    // 是否生成sourceMap
    "target": "ES6",
    // 编译目标
    "module": "esnext",
    "skipLibCheck": true,
    "allowJs": true,
    // 是否编辑js文件
    "strict": true,
    // 严格模式
    "noUnusedLocals": true,
    // 未使用变量报错
    "experimentalDecorators": true,
    // 启动装饰器
    "resolveJsonModule": true,
    // 加载json
    "esModuleInterop": true,
    "removeComments": false,
    // 删除注释
    "declaration": true,
    // 生成定义文件
    "declarationMap": false,
    // 生成定义sourceMap
    "declarationDir": "./dist/types",
    // 定义文件输出目录


    "lib": [
      "esnext",
      "dom",
      "ES2015.Promise"
    ],
    // 导入库类型定义
    "typeRoots": [
      "node_modules/@types",
      "typings",
      "node_modules/@light-send/**/*",
    ]
    // 导入指定类型包
  },
  "include": [
    "src/*",
    "typings/*",
    // 导入目录
  ]
}

2.3.3 测试一下项目配置

2.3.3.1 测试eslint

出现这里的提示的话证明eslint配置成功

2.3.3.2 测试rollup打包是否成功

index.ts中加一点代码:

export enum TEST {
  A
}
export const a = new Promise(() => {});
export const b = [1, 2, 3, 4, 5, 6].includes(4);

然后运行npm start

这里已经成功使用nodemon启动了dev模式

查看dist目录,会生成3个文件

  • umd

  • esmodule

  • cjs

修改rollup.config.js

自动重启了命令。

到这里一个项目就配置完成了,大家可以根据自己的需求自行修改。

2.4 创建项目模板

我们已经完成了项目的配置,但是如果每个项目我们都配置一遍或者粘贴复制,都不是很好的选择,所以我们来完成一个创建项目的脚本

2.4.1 安装依赖

 npm i ejs path-exists kebab-case inquirer fs-extra glob npmlog --save-dev

2.4.2 创建模板

把刚刚的send项目的的配置复制到scipts/template目录下

2.4.2.1 修改模板文件

rollup.config.js

const path = require("path");
const { nodeResolve  } = require('@rollup/plugin-node-resolve')
const commonjs = require("rollup-plugin-commonjs"); // commonjs模块转换插件
const ts = require("rollup-plugin-typescript2");
const { getBabelOutputPlugin } = require('@rollup/plugin-babel')
const rollup = require('rollup')
const getPath = (_path) => path.resolve(__dirname, _path);
const packageJSON = require("./package.json");
const babel = require("rollup-plugin-babel");
const extensions = [".js", ".ts", ".tsx"];
const babelConfig = require('./babel.config')
const globals = {
}
const umdPlugin = babel({
  exclude: "node_modules/**",
  babelrc:false,
  extensions,
  runtimeHelpers: true,
});
const babelPlugin = getBabelOutputPlugin({
  ...babelConfig
});
// ts
const tsPlugin = ts({
  tsconfig: getPath("./tsconfig.json"), // 导入本地ts配置
  extensions
});
const commPlugin = [
  nodeResolve({
    extensions
  }),
  commonjs(),
  tsPlugin,
]
// 基础配置
const commonConf = {
  input: getPath("./src/index.ts"),
  external: [],
  plugins:[
    ...commPlugin
  ],
};

const umdOutput = [
  {
    file: getPath(packageJSON.umdModule), // 通用模块
    format: "umd",
    name: "<%= umdName %>",
    globals
  }
]
const outputMap = [
  {
    file: getPath(packageJSON.main), // 通用模块
    format: "esm",
    globals
  },
  {
    file: getPath(packageJSON.cjsModule), // 通用模块
    format: "cjs",
    globals
  }
]

if(process.env.NODE_ENV === 'developemnt') {
  const watcher = rollup.watch({
    ...commonConf,
    plugins: commonConf.plugins.concat(umdPlugin),
    output:umdOutput
  })
  watcher.on('event', async (event) => {
    if(event && event.code === 'END') {
      modularityBundle()
    }
  });
} else {
  /**
   * 编译umd文件,这里因为同一个babel配置的话umd模块
   * 会出现require等关键词这浏览器是无法识别的
   * 所以umd模块和其他模块分开打包
   */
 rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat(umdPlugin),
  }).then(async (bundle)=>{
   await bundle.write(umdOutput[0])
   bundle.close
 })
  modularityBundle();
}

/**
 * 编译模块化产物
 */
function modularityBundle() {
  rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat(babelPlugin),
  }).then(async (bundle)=>{
    for await (const output of outputMap) {
      await bundle.write(output)
    }
    await bundle.close()
  })
}

package.json中name修改为ejs的变量

  "name": "<%= projectName %>"

redme.md修改为

# `<%= projectName %>`

> TODO: description

## Usage

```
const send = require('<%= projectName %>');

// TODO: DEMONSTRATE API
```

typinggs/index.d.ts

declare module "<%= projectName %>" {
}

declare module "<%= projectName %>/index.umd.js" {
  global {
    interface Window {
    }
  }
}
/**
 * cjs模块类型
 */
declare module "<%= projectName %>/index.cjs.js" {
}

2.4.3 脚本

scripts下面分别新建交互命令command.js、文件生成generate.js和入口文件

command.js

const path = require("path");
const inquirer = require("inquirer");
const keb = require("kebab-case");
const pathExists = require("path-exists").sync;
const groupName = "@light-send";

/**
 * 验证输入的项目名称
 * @param v
 * @returns {boolean}
 */
function isValidateName(v) {
  return /^[a-zA-Z]+([-][a-zA-Z][a-zA-Z0-9]*|[_][a-zA-Z][a-zA-Z0-9]*|[a-zA-Z0-9])*$/.test(v);
}

async function command() {
  const result = {};
  /**
   * 询问用户项目名称
   */
  const { name } = await inquirer.prompt([
    {
      type: "input",
      name: "name",
      message: `请输入项目名称`,
      validate(v) {
        /**
         * 项目名称的规则
         * 1.首字符必须为英文字符
         * 2.尾字符必须为英文或数字,不能为字符
         * 3.字符仅允许"-_"
         */
        const done = this.async();
        setTimeout(() => {
          if (!isValidateName(v)) {
            done(`请输入正确的项目名称:首字符必须为英文字符、尾字符必须为英文或数字,不能为字符、字符仅允许"-_"`);
            return;
          } else {
            done(null, true);
          }
        }, 0);

      }
    }
  ]);
  /**
   * 项目存放的路径,默认为packages下面
   */
  const { local } = await inquirer.prompt([
    {
      type: "input",
      name: "local",
      message: `请输入存放的路径`,
      /**
       * process.cwd() 项目路径
       * package目录
       * 项目名
       */
      default: path.join(process.cwd(), "packages", name)
    }
  ]);
  /**
   * 判断当前路径是否存在
   * 如果路径存在则询问是否覆盖
   * 如果不存在直接返回用户输入的信息方便ejs渲染
   */
  if (pathExists(local)) {
    const { ifContinue } = await inquirer.prompt({
      type: "confirm",
      name: "ifContinue",
      default: false,
      message: "当前文件夹不为空,是否继续创建项目"
    });
    if (!ifContinue) {
      throw new Error("已取消创建");
    }
  }
  result.inputName = name;
  result.name = keb(name).replace(/^-/, "");
  result.local = local;
  result.projectName = `${groupName}/${keb(name)}`;
  result.umdName = keb.reverse(`lint-send-${result.inputName}`);
  return result;
}

module.exports = command;

generate.js

const path = require("path");
const glob = require("glob");
const ejs = require("ejs");
const fsE = require("fs-extra");

/**
 *
 * @param option 拿到command的返回
 * @returns {Promise<unknown>}
 */
function ejsRender(option) {
  /**
   * 拿到用户存放路径
   */
  const { local } = option;
  /**
   * 获得模本的路径
   * @type {string}
   */
  const dir = path.join(process.cwd(), "scripts", "template");
  return new Promise((resolve, reject) => {
    /**
     * 拿到模板里面所有的文件
     */
    glob("**", {
      cwd: dir,
      nodir: true,
      ...option
    }, (error, files) => {
      if (error) {
        reject(error);
      }
      Promise.all(files.map((file) => {
        // 拿到每个文件存放到目标目录
        const filePath = path.join(local, file);
        return new Promise((resolve1, reject1) => {
          // 使用ejs对模板的每个文件进行渲染
          ejs.renderFile(path.join(dir, file), { ...option }, {}, (error, result) => {
            if (error) {
              reject1(error);
            } else {
              try {
                // 拿到结果后先在目标目录创建文件
                fsE.ensureFileSync(filePath);
                // 然后写入文件
                fsE.writeFileSync(filePath, result);
              } catch (e) {
                console.log(e);
              }
              resolve1(result);
            }
          });
        });
      })).then(() => {
        resolve(true);
      }).catch((err) => {
        reject(err);
      });
    });
  });
}

module.exports = ejsRender;

index.js

const command = require('./command')
const ejsRender = require('./generate')
const npmlog = require('npmlog')
/**
 * 打印日志用的插件
 * @type {string}
 */
npmlog.heading = 'light-send'
npmlog.headingStyle = { fg: 'green', bg: 'black' }
async function createProject() {
  try {
    const options = await command()
    await ejsRender(options)
    npmlog.notice('项目已生成完毕')
  }catch (e) {
    npmlog.error(e.message)
  }
  process.exit()
}
createProject()

2.4.4 测试

在项目主目录package.json中新建create命令

 "create": "node ./scripts/index.js"

并运行它

那么到这里我们关于项目建设的部分就完成了。

3.send

3.1 设计

  1. 首先我们有一个抽象类Send
  2. 各个不同请求的插件,需要继承Send并且实现抽象类的功能完成核心功能
  3. 通过不同SendHelper完成对类的增强 image.png

3.2 utils

3.2.1 创建

运行 npm run create新建utils

3.2.2改造

新增typings/Util.d.ts

declare namespace LightUtils {
  type isFunction = (target: any)=> target is Function;
  type getType = (target: any)=> Typings;
  type Utils = {
    getType: getType,
    isFunction:isFunction
  }
  /**
   * 判断是不是函数
   * @param target
   * @return boolean
   */

  type Typings =
    | 'string'
    | 'number'
    | 'null'
    | 'object'
    | 'array'
    | 'promise'
    | 'set'
    | 'date'
    | 'symbol'
    | 'map'
    | 'weakmap'
    | 'regexp'
    | 'weakset'
    | 'undefined'
    | 'boolean'
    | 'function';
}

新增typings/index.d.ts

/// <reference path="./Util.d.ts" />
declare module "@light-send/utils" {
  const utils: LightUtils.Utils
  export = utils
}
declare module "@light-send/utils/dist/index.cjs.js" {
  const utils: LightUtils.Utils
  export = utils
}
declare module "@light-send/utils/dist/index.umd.js" {
  const utils: LightUtils.Utils
  global {
    interface Window { LightSendUtils: LightUtils.Utils; }
  }
  export = utils
}

3.2.3 主要实现

新增src/Judge.ts

import Typings = LightUtils.Typings;
const _prototype = Object.prototype;

/**
 * 获取目标类型
 * @param target
 * @return 'string' | 'number' | 'null' | 'object' | 'array' | 'promise'| 'set' | 'date' | 'symbol' |'map'
 * | 'weakmap' | 'regexp' | 'weakset' | 'undefined' | 'boolean' | 'function
 */

export function getType(target: any): Typings {
  return _prototype.toString.call(target).slice(8, -1).toLowerCase() as Typings;
}
/**
 * 判断是不是函数
 * @param target
 * @return boolean
 */
// eslint-disable-next-line @typescript-eslint/ban-types
export function isFunction(target: any): target is Function {
  return getType(target) === "function";
}

新增src/index.ts

export * from "./Judge";

3.3 Send

3.3.1 类型定义

新增typings/Send.d.ts

declare namespace LightSend {
  interface IOptions<T = any>{
    url: string
    data: T
  }
  abstract class Send<Request extends IOptions = IOptions, Response = any>  {
    protected abstract isSuccess(response: Response): boolean;
    protected abstract transformData<T>(response: Response): T;
    protected abstract exec(options: Request): Promise<Response>;
    public send<T extends Response = Response>(options: Request): Promise<T>
  }
}

新增typings/index.d.ts

/// <reference path="./Send.d.ts" />
/// <reference types="@light-send/utils" />
declare module "@light-send/send" {
  export = LightSend.Send
}
declare module "@light-send/send/index.cjs.js" {
  export = LightSend.Send
}

declare module "@light-send/send/index.umd.js" {
  global {
    interface Window {
      lightSend:LightSend.Send
    }
  }
  export = LightSend.Send
}

3.3.2 基本框架

import IOptions = LightSend.IOptions;

abstract class Send<Request extends IOptions = IOptions, Response = any> {
  /**
   * 判断接口是否成功,这里主要是逻辑上的异常
   * 不同平台不同服务端的返回都是不一样的所以这里需要自己去实现
   * @param response
   * @protected
   */
  protected abstract isSuccess(response: Response): boolean;

  /**
   * 对返回的数据进行转换拿到自己想要的
   * @param response
   * @protected
   */
  protected abstract transformData<T>(response: Response): T;

  /**
   * 取消请求请求的方法
   * @protected
   */
  protected abstract handleCancel(): any;

  /**
   * 核心方法,不同的插件需要实现这个方法,告诉LightSend该如何发送请求
   * @param options
   * @protected
   */
  protected abstract exec(options: Request): Promise<Response>;

  /**
   * 发送请求的方法
   * @param options
   */
  public async send<T = any>(options: Request): Promise<T> {
    try {
      const response = await this.exec(options);
      if (!this.isSuccess(response)) {
        throw response;
      }
      return this.transformData<T>(response);
    } catch (e) {
      console.error("发送失败:", e);
      throw e;
    }
  }
}

export default Send;

3.3.3 添加拦截器

考虑到不是每个插件都像axios那样自带拦截器,或者会使用我们开发的helper,所以这里会添加拦截器提供给没有自带拦截器或者不适用我们helper的程序使用

  • ✅新增interceptor用来存储拦截器,以键值对的方式存储,每个拦截器由requestresponse组成,并且可以为空值
  • ✅新增interceptorOrder来存储拦截器的执行顺序,每次新增拦截器会把key存入到数组中,后面的拦截器执行顺序都按这个数组来执行
  • options新增restInterceptorrestInterceptorRequestrestInterceptorResponseinterceptorResponseinterceptorRequest属性
    • restInterceptor入参为interceptorOrder,通过返回字符串数组决定执行哪些拦截器和拦截器的执行顺序,返回空数组则不执行任何拦截器
    • restInterceptorRequestrestInterceptorResponse和restInterceptor类似,只是重置的那个request或者response
    • interceptorResponseinterceptorRequest入参为当前对应拦截器的order和拦截器数组,通过返回一个拦截器数组来决定执行哪些拦截器,如果返回空数组则不执行任何拦截器

3.3.3.1 修改Send.d.ts添加拦截器的类型

declare namespace LightSend {
  interface IOptions<T = any>{
    url?: string
    data?: T,
    /**
     * 重置request的拦截器执行顺序
     * @param requestOrder 执行器排序列表
     */
    restInterceptorRequest?: (requestOrder: string[]) => string[];
    /**
     * 重置request和response的执行顺序
     * @param interceptorOrder 执行器排序列表
     */
    restInterceptor?: (interceptorOrder: string[]) => string[];
    /**
     * 重置response执行器的执行顺序
     * @param responseOrder 执行器排序列表
     */
    restInterceptorResponse?: (responseOrder: string[]) => string[];
    /**
     * 把response的执行顺序和执行器列表给用户自定义如何自行,
     * @param responseOrder 执行器排序列表
     * @param interceptorResponse
     */
    interceptorResponse?: <Response>(responseOrder: string[], interceptorResponse:Responder<Response>[]) => Responder<Response>[];
    /**
     * 把response的执行顺序和执行器列表给用户自定义如何自行,
     * @param requestOrder 执行器排序列表
     * @param interceptorRequest
     */
    interceptorRequest?: <Request>(requestOrder: string[], interceptorRequest: Requester<Request>[]) => Requester<Request>[];
  }
  abstract class Send<Request extends IOptions = IOptions, Response = any>  {
    private interceptor: Record<string, Partial<IInterceptor<Request, Response>>>
    private interceptorOrder: string[] = [];
    private init(): void;
    public interceptorPush(name: string, interceptor: Partial<IInterceptor<Request, Response>>):void;
    protected abstract interceptorRequest(request: Request): Request;
    protected abstract interceptorResponse(response: Response): Response;
    protected abstract catchError(e: Error | any): Promise<boolean | undefined>;
    private getInterceptorRequests(interceptorOrder: string[]): Array<IRequester<Request>>;
    private getInterceptorResponses(interceptorOrder: string[]): Array<IResponder<Response>>;
    private getInterceptor(interceptorOrder: string[] = []): IInterceptors<Request, Response>;
    private computeGlobalInterceptor(options: Request): IInterceptors<Request, Response>;
    private handleRequestInterceptor(options: Request, requestInterceptor: IRequester<Request>[]): Request;
    private handleResponseInterceptor(
      response: Response,
      responseInterceptor: IResponder<Response>[],
      computeOptions: Request
    ): Response;
    protected abstract isSuccess(response: Response): boolean;
    protected abstract transformData<T>(response: Response): T;
    protected abstract exec(options: Request): Promise<Response>;
    public send<T extends Response = Response>(options: Request): Promise<T> | never
  }
  /**
   * request拦截器执行器
   */
  type Requester<Request> = (options: Request) => Request;
  /**
   * response拦截器执行器
   */
  type Responder<Response, Request> = (response: Response, options: Request) => Response;
  /**
   * 存入全局的拦截器,可以是执行器也可以是空
   */
  type IRequester<Request = any> = Requester<Request> | undefined;
  /**
   * 存入全局的拦截器,可以是执行器也可以是空
   */
  type IResponder<Response = any, Request = any> = Responder<Response, Request> | undefined;
  type TRestInterceptor = (globalInterceptorOrder: string[]) => string[];
  type TComputeInterceptor<T> = (order: string[], interceptor: T) => T;

  /**
   * 单个拦截器
   */
  interface IInterceptor<Request = any, Response = any> {
    request: IRequester<Request>;
    response: IResponder<Response, Request>;
  }

  /**
   * 拦截器队列
   */
  interface IInterceptors<Request = any, Response = any> {
    request: IRequester<Request>[];
    response: IResponder<Response, Request>[];
  }

}

3.3.3.2 修改index.ts文件

import { isFunction } from "@light-send/utils";
import IOptions = LightSend.IOptions;
export default abstract class Send<Request extends IOptions = IOptions, Response = any> {
  protected constructor() {
    this.init();
  }

  /**
   * 默认添加名为global的拦截器
   * @private
   */
  private init(): void {
    this.interceptorPush("global", {
      response: this.interceptorResponse,
      request: this.interceptorRequest
    });
  }

  /**
   * 对外暴露的push方法,需要添加多个的话可以使用这个方法
   * @param name
   * @param interceptor
   */
  public interceptorPush(name: string, interceptor: Partial<LightSend.IInterceptor<Request, Response>>) {
    this.interceptor[name] = interceptor;
    if (!this.interceptorOrder.includes(name)) {
      this.interceptorOrder.push(name);
    }
  }
  /**
   * 拦截器对象,里面以键值对方式保存了拦截器
   * 每个个拦截器分request和response两层,不过他们不是必须的可以为空
   * @private
   */
  private interceptor: Record<string, Partial<LightSend.IInterceptor<Request, Response>>> = {};
  /**
   * 每次往interceptor里面添加拦截器对象,这会往这里push key,以
   * 存储拦截器的执行顺序
   * @private
   */
  private interceptorOrder: string[] = [];

  protected abstract interceptorRequest(request: Request): Request;

  protected abstract interceptorResponse(response: Response): Response;

  /**
   * 判断接口是否成功,这里主要是逻辑上的异常
   * 不同平台不同服务端的返回都是不一样的所以这里需要自己去实现
   * @param response
   * @protected
   */
  protected abstract isSuccess(response: Response): boolean;

  /**
   * 对返回的数据进行转换拿到自己想要的
   * @param response
   * @protected
   */
  protected abstract transformData<T>(response: Response): T;

  protected abstract catchError(e: Error | any): Promise<boolean | void>;

  /**
   * 核心方法,不同的插件需要实现这个方法,告诉LightSend该如何发送请求
   * @param options
   * @protected
   */
  protected abstract exec(options: Request): Promise<Response>;

  /**
   * 获得所有的request拦截器
   * @param interceptorOrder
   * @private
   */
  private getInterceptorRequests(interceptorOrder: string[]): Array<LightSend.IRequester<Request>> {
    return interceptorOrder.map((name) => {
      const interceptor = this.interceptor[name];
      return interceptor.request;
    });
  }
  /**
   * 获得所有的response拦截器
   * @param interceptorOrder
   * @private
   */
  private getInterceptorResponses(interceptorOrder: string[]): Array<LightSend.IResponder<Response>> {
    return interceptorOrder.map((name) => {
      const interceptor = this.interceptor[name];

      return interceptor.response;
    });
  }
  /**
   * 获得所有的拦截器列表
   * @param interceptorOrder
   * @private
   */
  private getInterceptor(interceptorOrder: string[] = []): LightSend.IInterceptors<Request, Response> {
    return interceptorOrder.reduce(
      (result, name) => {
        const interceptor = this.interceptor[name];
        if (!interceptor) {
          result.request.push(undefined);
          result.response.push(undefined);
        } else {
          if (!interceptor.request) {
            result.request.push(undefined);
          } else {
            result.request.push(interceptor.request);
          }
          if (!interceptor.response) {
            result.response.push(undefined);
          } else {
            result.response.push(interceptor.response);
          }
        }

        return result;
      },
      {
        request: [],
        response: []
      } as LightSend.IInterceptors<Request, Response>
    );
  }
  /**
   * 计算需要执行的一些拦截器
   * @param options
   * @private
   */
  private computeGlobalInterceptor(options: Request): LightSend.IInterceptors<Request, Response> {
    const interceptorOrder = [...this.interceptorOrder];
    const {
      interceptorRequest,
      restInterceptor,
      restInterceptorRequest,
      interceptorResponse,
      restInterceptorResponse
    } = options || {};
    const currentOrder: string[] = isFunction(restInterceptor) ? restInterceptor(interceptorOrder) : interceptorOrder;
    let requestOrder = [...currentOrder];
    let responseOrder = [...currentOrder];
    /**
     * 通过排序列表找到得到现在request和response的拦截器数组
     */
    const { request: $globalRequest, response: $globalResponse }: LightSend.IInterceptors<Request, Response> =
      this.getInterceptor(currentOrder);
    let globalRequest: LightSend.IRequester<Request>[] = [...$globalRequest];
    let globalResponse: LightSend.IResponder<Response>[] = [...$globalResponse];
    /**
     * 如果有重置request的方法则执行该方法获得当前的request列表
     * 用户可以通过返回push时设置的拦截器的key选择是否执行某个拦截器
     */
    if (isFunction(restInterceptorRequest)) {
      requestOrder = (restInterceptorRequest as LightSend.TRestInterceptor)(requestOrder);
      globalRequest = this.getInterceptorRequests(requestOrder);
    }
    /**
     * 如果有重置response的方法则执行该方法获得当前的response列表
     * 用户可以通过返回push时设置的拦截器的key选择是否执行某个拦截器
     */
    if (isFunction(restInterceptorResponse)) {
      responseOrder = (restInterceptorResponse as LightSend.TRestInterceptor)(responseOrder);
      globalResponse = this.getInterceptorResponses(responseOrder);
    }
    if (isFunction(interceptorRequest)) {
      globalRequest = (interceptorRequest as LightSend.TComputeInterceptor<LightSend.IRequester<Request>[]>)(
        requestOrder,
        globalRequest
      );
    }
    if (isFunction(interceptorResponse)) {
      globalResponse = (interceptorResponse as LightSend.TComputeInterceptor<LightSend.IResponder<Response>[]>)(
        responseOrder,
        globalResponse
      );
    }
    return {
      request: globalRequest,
      response: globalResponse
    };
  }

  /**
   * 依次执行request拦截器
   * @param options
   * @param requestInterceptor
   * @private
   */
  private handleRequestInterceptor(options: Request, requestInterceptor: LightSend.IRequester<Request>[]): Request {
    return requestInterceptor.reduce((params: Request, item) => {
      if (isFunction(item)) {
        options = item(options);
      }
      return options;
    }, options);
  }

  /**
   * 依次执行response拦截器
   * @param response
   * @param responseInterceptor
   * @param computeOptions
   * @private
   */
  private handleResponseInterceptor(
    response: Response,
    responseInterceptor: LightSend.IResponder<Response>[],
    computeOptions: Request
  ): Response {
    return responseInterceptor.reduce((params: Response, item) => {
      if (isFunction(item)) {
        response = item(response, computeOptions);
      }
      return response;
    }, response);
  }
  /**
   * 发送请求的方法
   * @param options
   */
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  // @ts-ignore
  public async send<T = any>(options: Request): Promise<T> | never {
    /**
     * 通过options计算出拦截器列表
     */
    const { request: globalRequest, response: globalResponse } = this.computeGlobalInterceptor(options);
    /**
     * 执行request拦截器列表,并得到最新的options
     */
    const computeOptions: Request = this.handleRequestInterceptor(options, globalRequest);
    try {
      /**
       * 发送请求拿到结果
       */
      let response = await this.exec(computeOptions);

      /**
       * 执行response拦截器拿到最终结果
       */
      response = await this.handleResponseInterceptor(response, globalResponse, computeOptions);
      if (!this.isSuccess(response)) {
        throw response;
      }
      return this.transformData<T>(response);
    } catch (e) {
      console.error("发送失败:", e);
      /**
       * 如果catchError返回true则不继续往上抛出异常
       */
      if (!(await this.catchError(e))) {
        throw e;
      }
    }
  }
}

3.3.3.3 修改package.json

"peerDependencies": {
    "@babel/runtime-corejs3": "latest",
    "@light-send/utils": "latest",
    "core-js": "^3.20.2"
 },
"devDependencies": { 
  ...
   "@light-send/utils": "file:../utils",
  ...
}

到这里send基本上就完成了

3.4 send-axios

  1. 运行:
npm run create

创建新的包

  1. 修改package.json, 新增依赖
  "peerDependencies": {
    "@babel/runtime-corejs3": "latest",
    "@light-send/send": "latest",
    "@light-send/utils": "latest",
    "axios": "^0.24.0",
    "core-js": "^3.20.2"
  },
  "devDependencies": {
    "axios": "^0.24.0",
    "@light-send/send": "file:../send",
  }
  1. typing目录下新建axios.d.ts文件
declare type AxiosRequestHeaders = Record<string, string>;

declare type AxiosResponseHeaders = Record<string, string> & {
  "set-cookie"?: string[]
};

declare interface AxiosRequestTransformer {
  (data: any, headers?: AxiosRequestHeaders): any;
}

declare interface AxiosResponseTransformer {
  (data: any, headers?: AxiosResponseHeaders): any;
}

declare interface AxiosAdapter {
  (config: AxiosRequestConfig): AxiosPromise;
}

declare interface AxiosBasicCredentials {
  username: string;
  password: string;
}

declare interface AxiosProxyConfig {
  host: string;
  port: number;
  auth?: {
    username: string;
    password: string;
  };
  protocol?: string;
}

declare type Method =
  | 'get' | 'GET'
  | 'delete' | 'DELETE'
  | 'head' | 'HEAD'
  | 'options' | 'OPTIONS'
  | 'post' | 'POST'
  | 'put' | 'PUT'
  | 'patch' | 'PATCH'
  | 'purge' | 'PURGE'
  | 'link' | 'LINK'
  | 'unlink' | 'UNLINK';

declare type ResponseTypes =
  | 'arraybuffer'
  | 'blob'
  | 'document'
  | 'json'
  | 'text'
  | 'stream';

declare interface TransitionalOptions {
  silentJSONParsing?: boolean;
  forcedJSONParsing?: boolean;
  clarifyTimeoutError?: boolean;
}

declare interface AxiosRequestConfig<D = any> {
  url?: string;
  method?: Method;
  baseURL?: string;
  transformRequest?: AxiosRequestTransformer | AxiosRequestTransformer[];
  transformResponse?: AxiosResponseTransformer | AxiosResponseTransformer[];
  headers?: AxiosRequestHeaders;
  params?: any;
  paramsSerializer?: (params: any) => string;
  data?: D;
  timeout?: number;
  timeoutErrorMessage?: string;
  withCredentials?: boolean;
  adapter?: AxiosAdapter;
  auth?: AxiosBasicCredentials;
  responseType?: ResponseTypes;
  xsrfCookieName?: string;
  xsrfHeaderName?: string;
  onUploadProgress?: (progressEvent: any) => void;
  onDownloadProgress?: (progressEvent: any) => void;
  maxContentLength?: number;
  validateStatus?: ((status: number) => boolean) | null;
  maxBodyLength?: number;
  maxRedirects?: number;
  socketPath?: string | null;
  httpAgent?: any;
  httpsAgent?: any;
  proxy?: AxiosProxyConfig | false;
  cancelToken?: CancelToken;
  decompress?: boolean;
  transitional?: TransitionalOptions;
  signal?: AbortSignal;
  insecureHTTPParser?: boolean;
}

declare interface HeadersDefaults {
  common: AxiosRequestHeaders;
  delete: AxiosRequestHeaders;
  get: AxiosRequestHeaders;
  head: AxiosRequestHeaders;
  post: AxiosRequestHeaders;
  put: AxiosRequestHeaders;
  patch: AxiosRequestHeaders;
  options?: AxiosRequestHeaders;
  purge?: AxiosRequestHeaders;
  link?: AxiosRequestHeaders;
  unlink?: AxiosRequestHeaders;
}

declare interface AxiosDefaults<D = any> extends Omit<AxiosRequestConfig<D>, 'headers'> {
  headers: HeadersDefaults;
}

declare interface AxiosResponse<T = any, D = any>  {
  data: T;
  status: number;
  statusText: string;
  headers: AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}

declare interface AxiosError<T = any, D = any> extends Error {
  config: AxiosRequestConfig<D>;
  code?: string;
  request?: any;
  response?: AxiosResponse<T, D>;
  isAxiosError: boolean;
  toJSON: () => object;
}

declare interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {
}

declare interface CancelStatic {
  new (message?: string): Cancel;
}

declare interface Cancel {
  message: string;
}

declare interface Canceler {
  (message?: string): void;
}

declare interface CancelTokenStatic {
  new (executor: (cancel: Canceler) => void): CancelToken;
  source(): CancelTokenSource;
}

declare interface CancelToken {
  promise: Promise<Cancel>;
  reason?: Cancel;
  throwIfRequested(): void;
}

declare interface CancelTokenSource {
  token: CancelToken;
  cancel: Canceler;
}

declare interface AxiosInterceptorManager<V> {
  use<T = V>(onFulfilled?: (value: V) => T | Promise<T>, onRejected?: (error: any) => any): number;
  eject(id: number): void;
}

declare class Axios {
  constructor(config?: AxiosRequestConfig);
  defaults: AxiosDefaults;
  interceptors: {
    request: AxiosInterceptorManager<AxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse>;
  };
  getUri(config?: AxiosRequestConfig): string;
  request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig<D>): Promise<R>;
  get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  delete<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  head<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  options<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<R>;
  post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
  patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<R>;
}

declare interface AxiosInstance extends Axios {
  (config: AxiosRequestConfig): AxiosPromise;
  (url: string, config?: AxiosRequestConfig): AxiosPromise;
}

declare interface AxiosStatic extends AxiosInstance {
  create(config?: AxiosRequestConfig): AxiosInstance;
  Cancel: CancelStatic;
  CancelToken: CancelTokenStatic;
  Axios: typeof Axios;
  readonly VERSION: string;
  isCancel(value: any): boolean;
  all<T>(values: Array<T | Promise<T>>): Promise<T[]>;
  spread<T, R>(callback: (...args: T[]) => R): (array: T[]) => R;
  isAxiosError(payload: any): payload is AxiosError;
}

这里因为axios不是全局类型,所以这里我对它进行了改造

  1. typing目录下新建AxiosSend.d.ts文件
/// <reference types="@light-send/send" />
/// <reference path="./axios.d.ts" />
declare namespace LightAxiosSend{
  interface IOptions<T = any> extends LightSend.IOptions<T>, AxiosRequestConfig<T>{
    cancel?: (source: CancelTokenSource)=> any
  }
  abstract class AxiosSend<Request = LightAxiosSend.IOptions, Response =  AxiosResponse> extends LightSend.Send<Request, Response> {
    private readonly axiosInstance:AxiosInstance;
    public constructor(config: AxiosRequestConfig);
    protected exec(options: Request): Promise<Response>;
  }
}
  1. typing目录下新建index.d.ts文件
/// <reference path="./axios.d.ts" />
/// <reference path="./AxiosSend.d.ts" />
/**
 * esm模块类型
 */
declare module "@light-send/send-axios" {
  const AxiosSend: LightAxiosSend.AxiosSend
  export = AxiosSend
}
/**
 * umd模块类型
 */
declare module "@light-send/send-axios/index.umd.js" {
  const AxiosSend: LightAxiosSend.AxiosSend
  global {
    interface Window {
      lightAxiosSend:LightAxiosSend.AxiosSend
    }
  }
  export = AxiosSend
}
/**
 * cjs模块类型
 */
declare module "@light-send/send-axios/index.cjs.js" {
  const AxiosSend: LightAxiosSend.AxiosSend
  export = AxiosSend
}
/**
 * axios的全局类型定义
 */
declare module "axios" {
  const axios: AxiosStatic;
  export = axios
}
  1. 修改rollup.config.js
const globals = {
  'axios':'axios',
  '@light-send/send':'lightSend'
}

const commonConf = {
  input: getPath("./src/index.ts"),
  // 这里忽略不打包
  external: ['axios','@light-send/send'],
  plugins:[
    ...commPlugin
  ],
};

const umdOutput = [
  {
    file: getPath(packageJSON.umdModule), // 通用模块
    format: "umd",
    name: "lintSendSendAxios",
    // 加入globals
    globals
  }
]
const outputMap = [
  {
    file: getPath(packageJSON.main), // 通用模块
    format: "esm",
    // 加入globals
    globals
  },
  {
    file: getPath(packageJSON.cjsModule), // 通用模块
    format: "cjs",
    // 加入globals
    globals
  }
]

主要实现

import Send from "@light-send/send";
import axios from "axios";
const CancelToken = axios.CancelToken;
/**
 * 这里依然是一个抽象类,只实现具体的exec方法其他的方法还要具体的业务类来实现
 */
export default abstract class AxiosSend extends Send<LightAxiosSend.IOptions, AxiosResponse> {
  private readonly axiosInstance: AxiosInstance;
  protected constructor(config: AxiosRequestConfig) {
    super();
    /**
     * 初始化axios实例
     */
    this.axiosInstance = axios.create(config || {});
  }

  /**
   * 实现exec方法
   * @param options
   * @protected
   */
  protected exec(options: LightAxiosSend.IOptions): Promise<AxiosResponse> {
    if (options.cancel) {
      const source = CancelToken.source();
      options.cancelToken = source.token;
      /**
       * 如果有cancel函数则把取消对象传入
       */
      options.cancel(source);
    }
    return this.axiosInstance(options);
  }
}

3.5 send-ajax

  1. 运行:
npm run create

创建新的包

  1. 修改package.json, 新增依赖
  "peerDependencies": {
    "@babel/runtime-corejs3": "latest",
    "@light-send/send": "latest",
    "@light-send/utils": "latest",
    "core-js": "^3.20.2",
    "jquery": "^3.6.0"
  },
  "devDependencies": {
    "jquery": "^3.6.0",
     "@types/jquery": "^3.5.11",
    "@light-send/send": "file:../send",
  }
  1. typing目录下新建AjaxSend.d.ts文件
/// <reference types="@light-send/send" />
/// <reference types="jquery" />
declare namespace LightAjaxSend {
  import SuccessTextStatus = JQuery.Ajax.SuccessTextStatus;

  interface IOptions<T = any> extends LightSend.IOptions<T> ,Omit<JQuery.AjaxSettings, 'data'> {

  }
  interface IResponse<T = any> extends JQuery.jqXHR{
    data?: T,
    textStatus: SuccessTextStatus
  }
}
  1. typing目录下新建index.d.ts文件

/// <reference types="jquery" />
/// <reference path="./AjaxSend.d.ts" />
/**
 * esm模块类型
 */
declare module "@light-send/send-ajax" {
  const AjaxSend: LightAjaxSend.AjaxSend
  export = AjaxSend
}
/**
 * umd模块类型
 */
declare module "@light-send/send-ajax/index.umd.js" {
  const AjaxSend:LightAjaxSend.AjaxSend
  global {
    interface Window {
      lightAjaxSend:LightAjaxSend.AjaxSend
    }
  }
  export = AjaxSend
}
/**
 * cjs模块类型
 */
declare module "@light-send/send-ajaxs/index.cjs.js" {
  const AjaxSend: LightAjaxSend.AjaxSend
  export = AjaxSend
}
  1. 修改rollup.config.js
const path = require("path");
const { nodeResolve  } = require('@rollup/plugin-node-resolve')
const commonjs = require("rollup-plugin-commonjs"); // commonjs模块转换插件
const ts = require("rollup-plugin-typescript2");
const { getBabelOutputPlugin } = require('@rollup/plugin-babel')
const rollup = require('rollup')
const getPath = (_path) => path.resolve(__dirname, _path);
const packageJSON = require("./package.json");
const babel = require("rollup-plugin-babel");
const extensions = [".js", ".ts", ".tsx"];
const babelConfig = require('./babel.config')
const globals = {
  'jquery':'$',
  '@light-send/send':'lightSend'
}
const umdPlugin = babel({
  exclude: "node_modules/**",
  babelrc:false,
  extensions,
  runtimeHelpers: true,
});
const babelPlugin = getBabelOutputPlugin({
  ...babelConfig
});
// ts
const tsPlugin = ts({
  tsconfig: getPath("./tsconfig.json"), // 导入本地ts配置
  extensions
});
const commPlugin = [
  nodeResolve({
    extensions
  }),
  commonjs(),
  tsPlugin,
]
// 基础配置
const commonConf = {
  input: getPath("./src/index.ts"),
  external: ['jquery', '@light-send/send'],
  plugins:[
    ...commPlugin
  ],
};

const umdOutput = [
  {
    file: getPath(packageJSON.umdModule), // 通用模块
    format: "umd",
    name: "lintSendSendAjax",
    globals
  }
]
const outputMap = [
  {
    file: getPath(packageJSON.main), // 通用模块
    format: "esm",
    globals
  },
  {
    file: getPath(packageJSON.cjsModule), // 通用模块
    format: "cjs",
    globals
  }
]

if(process.env.NODE_ENV === 'developemnt') {
  const watcher = rollup.watch({
    ...commonConf,
    plugins: commonConf.plugins.concat(umdPlugin),
    output:umdOutput
  })
  watcher.on('event', async (event) => {
    if(event && event.code === 'END') {
      modularityBundle()
    }
  });
} else {
  /**
   * 编译umd文件,这里因为同一个babel配置的话umd模块
   * 会出现require等关键词这浏览器是无法识别的
   * 所以umd模块和其他模块分开打包
   */
 rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat(umdPlugin),
  }).then(async (bundle)=>{
   await bundle.write(umdOutput[0])
   bundle.close
 })
  modularityBundle();
}

/**
 * 编译模块化产物
 */
function modularityBundle() {
  rollup.rollup({
    ...commonConf,
    plugins: commPlugin.concat(babelPlugin),
  }).then(async (bundle)=>{
    for await (const output of outputMap) {
      await bundle.write(output)
    }
    await bundle.close()
  })
}

主要实现

import $ from "jquery";
import Send from "@light-send/send";
export default abstract class AjaxSend extends Send<LightAjaxSend.IOptions, LightAjaxSend.IResponse> {
  protected constructor() {
    super();
  }

  /**
   * 实现exec方法
   * @param options
   * @protected
   */
  protected exec(options: LightAjaxSend.IOptions): Promise<LightAjaxSend.IResponse> {
    return new Promise((resolve, reject) => {
      $.ajax({
        ...options,
        success(data, textStatus, jqXHR) {
          resolve({
            data,
            textStatus,
            ...jqXHR
          });
        },
        error(jqXHR, textStatus, errorThrown) {
          reject({
            ...jqXHR,
            textStatus,
            errorThrown
          });
        }
      });
    });
  }
}

4.发布

运行lerna publish先打tag,如果中途有包发布失败,再运行lerna publish的时候,因为Tag已经打上去了,所以不会再重新发布包到NPM,所以在publis之前需要把准备工作做好,如果还是发布失败可以:

  1. 运行lerna publish from-git,会把当前标签中涉及的NPM包再发布一次,PS:不会再更新package.json,只是执行npm publish
  2. 运行lerna publish from-package,会把当前所有本地包中的package.json和远端NPM比对,如果是NPM上不存在的包版本,都执行一次npm publish

4.1 准备工作

4.1.1 更新项目package.json

"scripts": {
    "create": "node ./scripts/index.js",
  	// 执行link 并且重新安装依赖
    "link": "lerna exec npm link && lerna exec npm run bootstrap",
  	// 执行所有模块的build命令构建产物
    "build": "npm run link && lerna exec npm run build",
    // 先构建产物如果成功了之后再执行各个模块的发布,避免因构建问题致使lerna publis失败
    "publish": "npm run build && lerna publish"
  },

4.1.2 更新模块的package.json

 "scripts": {
    "start": "cross-env NODE_ENV=developemnt nodemon",
    "dev": "node rollup.config.js",
    "build": "cross-env NODE_ENV=production node rollup.config.js",
    // build前把package-lock.json和dist删除,因为package-lock.json已经加入忽略
    // 不删除的话lerna publis会提示你需要把package-lock.json提交
    "clearn": "rimraf package-lock.json && rimraf dist",
    // build前执行clearn
    "prebuild": "npm run clearn",
   	// 删除这个发布前执行的命令因为根目录已经有这个钩子了
   
    //"prepublishOnly": "npm run build",
   "bootstrap": "npm i --legacy-peer-deps",
    "test": "jest --coverage"
  },

4.2 发布

发布前把所有文件提交 并登入npm

运行

npm run publish
  1. 如果看到

证明npm link成功

  1. 如果看到

证明npm run build成功

  1. 将看到选择版本号信息

  1. 选择版本号

则本次发布成功

5.简单测试一下

5.1 新建一个nestjs服务

import { Controller, Get, Param, Query } from '@nestjs/common';
import { MusicService } from './MusicService';

import { MusicData, SliderListData } from './type';

@Controller('music')
export class MusicController {
  constructor(private readonly musicService: MusicService) {}

  @Get('getDiscList')
  async getDiscList(): Promise<MusicData> {
    return this.musicService.getDiscList();
  }
  @Get('slider')
  async getSliderList(): Promise<SliderListData> {
    return this.musicService.getSliderList();
  }
  @Get('getDisc')
  async getDisc(@Query('id') id): Promise<any> {
    return this.musicService.getDisc(id);
  }
}

验证一下:

\

5.2 vue项目测试

5.2.1 创建项目

运行vue create命令,并且选择自定义选项,新建一个vue2加typescript的项目,这里就不多介绍了

5.2.2 安装依赖

npm i @light-send/send-axios lodash --save

如果你看到

则之前预装的peerDependencies有了效果,这里需要预装这些依赖

npm i @light-send/send @light-send/utils --save
npm i @babel/runtime-corejs3 core-js --save-dev

分别安装

5.2.3 配置代理

新增vue.config.js

module.exports = {
  devServer: {
    proxy: {
      "/api": {
        target: "http://localhost:3000",
        secure: false,
        changeOrigin: true,
        pathRewrite: {
          "^/api": "/",
        },
      },
    },
  },
};

5.2.4 service模块

新增typing/index.d.ts

declare interface IResult<D = any> {
  code: number;
  subCode: number;
  message: string;
  default: string;
  data: D;
}
declare interface IDisc {
  dissid: string;
  createtime: string;
  commit_time: string;
  dissname: string;
  imgurl: string;
  introduction?: string;
  listennum: number;
}
declare interface IDiscList {
  uin?: string;
  sortId?: string;
  sum?: string;
  sin?: string;
  ein?: string;
  list: IDisc[];
}

新增src/service/index.ts

import AxiosSend from "@light-send/send-axios";
import get from "lodash/get";
export class Service extends AxiosSend {
  protected interceptorRequest(
    request: LightAxiosSend.IOptions
  ): LightAxiosSend.IOptions {
    console.log("interceptorRequest", request);
    return request;
  }

  protected interceptorResponse(response: AxiosResponse): AxiosResponse {
    console.log("interceptorResponse", response);
    return response;
  }

  protected isSuccess(response: AxiosResponse<IResult>): boolean {
    const code = get(response, "data.code");
    console.log("isSuccess", code);
    return code === 0;
  }

  protected transformData<T>(response: AxiosResponse<IResult>): T {
    console.log("transformData", get(response, "data.data"));
    return get(response, "data.data") as T;
  }

  protected async catchError(
    e: Error | AxiosError
  ): Promise<boolean | undefined> {
    console.error("catchError", e);
    return;
  }

  /**
   * 歌曲列表
   * @param options
   */
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    try {
      return this.send<IDiscList>(options);
    } catch (e) {
      return {};
    }
  }
}

export default new Service({});

5.2.5 在App.vue中使用

<template>
  <div id="app">
    <el-table :data="list" style="width: 100%">
      <el-table-column prop="dissname" label="名称" />
      <el-table-column prop="imgurl" label="封面">
        <template v-slot="scope">
          <img :src="scope.row.imgurl" width="180" />
        </template>
      </el-table-column>
      <el-table-column prop="commit_time" label="时间"> </el-table-column>
    </el-table>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import HelloWorld from "./components/HelloWorld.vue";
import service from "@/service";
console.log("service", service);
Component.registerHooks(["created"]);
@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {
  public list: IDisc[] = [];
  async created() {
    /**
     * 发送请求,这里不用担心会有异常,因为service已经全部捕获过了
     */
    const { list = [] } = await service.getDiscList({
      url: "/api/music/getDiscList",
    });
    this.list = list;
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

element组件使用这里就不多介绍了哦

5.2.5.1 结果

这里也看出来typescript的修饰符编译成js后其实没什么效果,按照预期全局拦截器里面只有global,并且执行顺序是request->send->response->isSuccess->transformData

5.2.5.2 测试拦截器效果

我们经常url里面会有一些:id这种带有模板的url,这里添加一个拦截器对这种url进去处理

  1. 修改service
import AxiosSend from "@light-send/send-axios";
import get from "lodash/get";
import { compile } from "path-to-regexp";

/**
 * 把url上带有{}里面的key找出来,替换成对象里面的值
 * @param url
 * @param reg
 * @param data
 */
function replaceTemplate(
  url: string,
  reg: RegExp,
  data: Record<string, string>
) {
  let mathResult;
  if ((mathResult = url.match(reg))) {
    const key = mathResult[1];
    if (key && data[key]) {
      url = url.replaceAll(new RegExp(reg, "g"), data[key]);
    }
  }
  return url;
}
export class Service extends AxiosSend {
  constructor(config: AxiosRequestConfig) {
    super(config);
    this.initUrTemplateReplace();
  }

  /**
   * 添加一组全局拦截器
   * @protected
   */
  protected initUrTemplateReplace() {
    this.interceptorPush("urlTemplateReplace", {
      request(options) {
        // eslint-disable-next-line prefer-const
        let { url, data, params } = options;
        const value = {
          ...(data ? data : {}),
          ...(params ? params : {}),
        };
        if (url) {
          url = replaceTemplate(url, /{(.*?)}/, value);
          url = compile(url)(value);
        }
        options.url = url;
        console.log("initUrTemplateReplace.request", options);
        return options;
      },
      response(res) {
        console.log("initUrTemplateReplace.response", res);
        return res;
      },
    });
  }
  protected interceptorRequest(
    request: LightAxiosSend.IOptions
  ): LightAxiosSend.IOptions {
    console.log("global.request", request);
    return request;
  }

  protected interceptorResponse(response: AxiosResponse): AxiosResponse {
    console.log("global.response", response);
    return response;
  }

  protected isSuccess(response: AxiosResponse<IResult>): boolean {
    const code = get(response, "data.code");
    console.log("isSuccess", code);
    return code === 0;
  }

  protected transformData<T>(response: AxiosResponse<IResult>): T {
    console.log("transformData", get(response, "data.data"));
    return get(response, "data.data") as T;
  }

  protected async catchError(
    e: Error | AxiosError
  ): Promise<boolean | undefined> {
    console.error("catchError", e);
    return;
  }

  /**
   * 歌曲列表
   * @param options
   */
  public async getDiscList(
    options: LightAxiosSend.IOptions
  ): Promise<Partial<IDiscList>> {
    try {
      return this.send<IDiscList>(options);
    } catch (e) {
      return {};
    }
  }
}

export default new Service({});
  1. 修改node服务添加一个模板路由
  @Get('getDiscList')
  async getDiscList(): Promise<MusicData> {
    return this.musicService.getDiscList();
  }
  @Get('getDiscList/1')
  async getDiscList1(): Promise<MusicData> {
    return this.musicService.getDiscList();
  }
  1. 修改App.vue
export default class App extends Vue {
  public list: IDisc[] = [];
  async created() {
    /**
     * 发送请求,这里不用担心会有异常,因为service已经全部捕获过了
     */
    const { list = [] } = await service.getDiscList({
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });
    this.list = list;
  }
}
  1. 运行结果

这里可以看到按照预期拦截器执行global.request->urlTemplateReplace.request->send->global.request->urlTemplateReplace.response->isSuccess->transformData

5.2.5.3 restInterceptor

  1. 修改执行顺序

修改App.vue添加restInterceptor

const { list = [] } = await service.getDiscList({
      restInterceptor(order) {
        return order.reverse();
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });

这里也达到了预期,执行顺序按照数组反转后的顺序执行

  1. 禁止某个拦截器

修改App.vue添加restInterceptor

const { list = [] } = await service.getDiscList({
      restInterceptor(order) {
        return [order[1]];
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });

5.2.5.4 restInterceptorRequest

  1. 修改执行顺序

修改App.vue添加restInterceptorRequest

    const { list = [] } = await service.getDiscList({
      restInterceptorRequest(order) {
        return order.reverse();
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });

这里也达到了预期,执行顺序按照数组反转后的顺序执行

  1. 禁止某个拦截器

修改App.vue添加restInterceptorRequest

   const { list = [] } = await service.getDiscList({
      restInterceptorRequest(order) {
        return [order[1]];
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });

这里global的request并没有被执行,这里response的重置逻辑是一样的就不一一列举了

5.2.5.5 interceptorRequest

  1. 修改执行顺序

修改App.vue添加interceptorRequest

const { list = [] } = await service.getDiscList({
      interceptorRequest(order, request) {
        return request.reverse();
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });
    this.list = list;
  }
}

这里request的执行顺序被反转了

  1. 禁止某个拦截器

修改App.vue添加interceptorRequest

const { list = [] } = await service.getDiscList({
      interceptorRequest(order, request) {
        return [request[0]];
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });
    this.list = list;
  }

这里urlTemplateReplace.request没有执行

  1. 添加一个局部的拦截器

修改App.vue添加interceptorRequest

const { list = [] } = await service.getDiscList({
      interceptorRequest(order, request) {
        request.push((options) => {
          console.log("我是局部的");
          return options;
        });
        return request;
      },
      url: "/api/music/{path}/:id",
      data: {
        path: "getDiscList",
        id: 1,
      },
    });

局部的request拦截器也被执行了,response也和request一样所以就不举例

未完待续