1 准备
1.1 创建一个npm 组织
为了防止重名,所以申请一个组织来作为包名的前缀,这里我取名light-send,你们自由发挥哈ღ( ´・ᴗ・` )
2.建设项目
2.1 初始化项目
2.1.1 使用npm
快速初始化项目
npm ini -y
2.1.2 安装lerna并初始化
- 首选全局安装
lerna
npm i lerna -g
- 然后在项目里面安装
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"
}
- 添加
.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
- 添加
.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的规则自己根据实际情况配置哦!
- 更新package.json添加
eslit
和prettier
相关依赖
{
"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"
}
}
- 添加
.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
- 添加
.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"
}
}
主要新增以下内容:
- 包文件的入口和esmoudule的入口以及压缩文件路径
- 新增
start
、dev
、build
、test
命令 - 新增
publis
、build
的前置钩子
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 设计
- 首先我们有一个抽象类
Send
- 各个不同请求的插件,需要继承Send并且实现抽象类的功能完成核心功能
- 通过不同SendHelper完成对类的增强
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
的程序使用
- ✅新增
intercepto
r用来存储拦截器,以键值对的方式存储,每个拦截器由request
和response
组成,并且可以为空值 - ✅新增
interceptorOrder
来存储拦截器的执行顺序,每次新增拦截器会把key存入到数组中,后面的拦截器执行顺序都按这个数组来执行 - ✅
options
新增restInterceptor
、restInterceptorRequest
、restInterceptorResponse
、interceptorResponse
、interceptorRequest
属性restInterceptor
入参为interceptorOrder
,通过返回字符串数组决定执行哪些拦截器和拦截器的执行顺序,返回空数组则不执行任何拦截器restInterceptorRequest
、restInterceptorRespons
e和restInterceptor
类似,只是重置的那个request
或者response
interceptorResponse
、interceptorRequest
入参为当前对应拦截器的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
- 运行:
npm run create
创建新的包
- 修改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",
}
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
不是全局类型,所以这里我对它进行了改造
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>;
}
}
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
}
- 修改
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
- 运行:
npm run create
创建新的包
- 修改
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",
}
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
}
}
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
}
- 修改
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之前需要把准备工作做好,如果还是发布失败可以:
- 运行lerna publish from-git,会把当前标签中涉及的NPM包再发布一次,PS:不会再更新package.json,只是执行npm publish
- 运行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
- 如果看到
证明npm link
成功
- 如果看到
证明npm run build
成功
- 将看到选择版本号信息
- 选择版本号
则本次发布成功
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进去处理
- 修改
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({});
- 修改
node
服务添加一个模板路由
@Get('getDiscList')
async getDiscList(): Promise<MusicData> {
return this.musicService.getDiscList();
}
@Get('getDiscList/1')
async getDiscList1(): Promise<MusicData> {
return this.musicService.getDiscList();
}
- 修改
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;
}
}
- 运行结果
这里可以看到按照预期拦截器执行global.request->urlTemplateReplace.request->send->global.request->urlTemplateReplace.response->isSuccess->transformData
5.2.5.3 restInterceptor
- 修改执行顺序
修改App.vue
添加restInterceptor
const { list = [] } = await service.getDiscList({
restInterceptor(order) {
return order.reverse();
},
url: "/api/music/{path}/:id",
data: {
path: "getDiscList",
id: 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
- 修改执行顺序
修改App.vue
添加restInterceptorRequest
const { list = [] } = await service.getDiscList({
restInterceptorRequest(order) {
return order.reverse();
},
url: "/api/music/{path}/:id",
data: {
path: "getDiscList",
id: 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
- 修改执行顺序
修改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
的执行顺序被反转了
- 禁止某个拦截器
修改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
没有执行
- 添加一个局部的拦截器
修改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
一样所以就不举例