偶遇抽象屎山,拼尽全力重构,勉强战胜

前言

偶遇抽象屎山,强如薪王,觉醒魂5记忆,拼尽全力,勉强战胜。

业务组件优化

这里举两个列子

下拉框优化V1

在后台项目中,一个页面由 搜索框 + table 组成,而搜索框里面存在Select,而下拉框会有 远程搜索 + 下拉滚动 的需求。比如下图的 广告渠道主播

先看老代码:

老代码的逻辑中

  • onDropdownVisibleChange打开时,请求数据,默认一页20条,关闭时,清空
  • onSearch时,做了判断,没有输入值的时候 getChannelData 发请求拿数据(第一页20条)。输入值的时候远程搜索。然后加了防抖,用的是定时器模拟

上面的代码其实问题挺多的

  • 防抖为什么要自己用定时器写,lodash.debounce 不就ok了吗
  • 搜索时,不管有没有输入值值都要发请求,加大了网络开销
  • 搜索框每一次展开时,都会重新调接口
  • 就业务来说,一个下拉搜索框,如果数据多,为什么不多展示几十条,而且都能远程搜索了,下拉滚动就没必要了。并且就本项目来说,搜索后的重名数据不会超过10条(与后端、产品确认)

所以就下面的方向优化

  • 进入页面时,调相关接口初始化一次(第一页,100条),然后记录初始化的下拉框数据 originData
  • 搜索时,输入了值,调接口,如果没有输入值,则直接重置为 originData(这样就少调一次接口)
  • 后续下拉框关闭,再展开时,直接使用 originData(又减少了一次调接口)
  • 当表单点击重置时,直接使用 originData(又减少了一次调接口)
  • 因为交互逻辑是一样的,只是用该组件的地方接口比较多,所以封一个 hook 实现这部分相同的逻辑,再透传 props 给组件内部。这样组件内部不用做额外的处理,使用的时候也简洁些。

具体实现

  • hook 里面处理逻辑,然后暴露相关属性直接传给 Select
ts 复制代码
import { Service } from "@/utils/Axios";
import { IOption, TRecord } from "@/utils/interface";
import { deleteEmptyKey, to } from "@/utils/tools";
import { debounce } from "lodash";
import { useEffect, useRef, useState } from "react";

export interface IUseMountFetchDataProps {
 /**@param 接口Api */
 fetchDataApi: string;
 /**@param 初始化时接口额外参数 */
 request?: TRecord;
 /**@param 搜索时的key */
 searchParamKey: string;
 /**@function 转换数据源为options */
 transformOptions: (orginOption: TRecord[]) => IOption[];
}

export interface IUseMountFetchDataResult {
 /** @param 下拉框选项 */
 options: IOption[];
 /** @function 下拉框搜索 */
 onSearch: (value: string) => void;
}

/**
* 初始化请求下拉框接口
* @param props
* @returns
*/
export const useSearchSelectFetch = (
 props: IUseMountFetchDataProps
): IUseMountFetchDataResult => {
 const { fetchDataApi, request, searchParamKey, transformOptions } = props;

 const [data, setData] = useState<IOption[]>([]);
 const isMount = useRef<boolean>(false);
 const originData = useRef<IOption[]>([]);

 const fetchData = async (otherRequest?: TRecord) => {
   const [err, res] = await to(
     (() => {
       return Service.get(fetchDataApi, {
         params: deleteEmptyKey({
           page: 1,
           size: 100,
           ...request,
           ...otherRequest,
         }),
       });
     })()
   );
   if (!(err || !res)) {
     const data = transformOptions(res?.data?.records ?? []);
     setData(data);
     if (!isMount.current) {
       originData.current = data;
       isMount.current = true;
     }
   }
 };

 const onSearch = debounce((value: string) => {
   if (value.trim()) {
     fetchData({
       [searchParamKey]: value,
     });
   } else {
     setData(originData.current);
   }
 }, 500);

 useEffect(() => {
   fetchData();
 }, []);

 return {
   options: data,
   onSearch,
 };
};

hook 里面的具体做法是

  • 初始化调接口 fetchData,拿到数据后,通过 transformOptions 转换为 Select 的 option
  • 通过 isMount.current 判断是否是第一次,如果是第一次,记录到 originData.current,缓存第一次调接口转换后的数据
  • onSearch搜索时,输入了值就调接口,没有就直接使用 originData.current
  • 最后暴露相关属性

hook搞完了,然后就是封装一下 Select

tsx 复制代码
import { Select } from "antd";
import { FC } from "react";

export interface ITxSearchSelectProps extends SelectProps {}

const TxSearchSelect: FC<ITxSearchSelectProps> = (props) => {
  return (
    <Select
      defaultActiveFirstOption={false}
      filterOption={false}
      showSearch
      allowClear
      {...props}
    />
  );
};

export default TxSearchSelect;

其实就是把常用的属性showSearchallowClear等直接写上,免得每次都写一遍,方便使用

最后就是使用

tsx 复制代码
import { useSearchSelectFetch } from "@/components/TxSearchSelect/hook";
import TxSearchSelect from "@/components/TxSearchSelect";

const streamerProps = useSearchSelectFetch({
    fetchDataApi: "/api/base/v1/live-streamer/get-page",
    searchParamKey: "streamerName",
    transformOptions: (data) =>
        data.map((item) => ({ label: item.streamerName, value: item.id })),
});


<Form.Item name="liveStreamerId" label="主播">
  <TxSearchSelect {...streamerProps} placeholder="请选择主播" />
</Form.Item>

这样就只会在 Form 初始化时,调一次接口,也方便维护了很多

下拉框优化V2

V1版本只适用于页面的搜索表单中,然而,在弹窗里面,其实也希望能使用 TxSearchSelect

在弹窗里面使用,相较于在页面搜索表单中使用,区别在于

  • 希望在弹窗打开时,再初始化
  • 希望在弹窗打开时,进行某种操作后,再进行初始化

那其实弹窗里面的情景,与页面搜索表单的情景,可以理解为初始化的时机不同,基于此对 useSearchSelectFetch 进行优化

ts 复制代码
export interface IUseMountFetchDataProps {
  // ...之前的 props
  
  /**@param 满足某种条件时加载数据,使用时请将 initFetch 设置为 false  */
  refreshFetch?: boolean;
  /**@param 是否挂载时默认加载数据 */
  initFetch?: boolean;
}

export const useSearchSelectFetch = (
  props: IUseMountFetchDataProps
): IUseMountFetchDataResult => {
  const {
    // ... 之前的 props
    refreshFetch,
    initFetch = true,
  } = props;

  // ... 之前的逻辑
  
  useEffect(() => {
    if (refreshFetch) {
      fetchData();
    }
  }, [refreshFetch]);

  useMount(() => {
    if (initFetch) {
      fetchData();
    }
  });

  //...之前的逻辑

};

V2版本默认 initFetch = true,也就是组件挂载就调接口。

如果在弹窗里面使用,设置refreshFetch = 某个条件并且initFetch = false,即挂载时不调接口,满足某种条件后再调接口

在页面中

在弹窗中

到目前为止,是满足了业务需求的。当然后续看情况要不要再优化优化。

搜索表单 Form 优化

搜索表单支持 展开收起重置搜索,老的 Form 有个问题

通过 expand 来渲染展开的项,这有一个很大的问题是:

  • expand 通过动态增删DOM,性能会较差
  • expand === false 时,下面的 Form.Item 是不存在于 DOM树上
  • 如果先展开表单,选择了展开的某一个筛选项,再收起,此时 form.getFieldsValue 是拿不到这个筛选项数据的
  • 展开时,如果 Form.Item 太多,会导致 table 高度变矮, table 也触发了一次重拍重绘

上图所示,先展开选择搜索条件,再折叠,调接口丢失了 分配时间 这个筛选项

针对以上问题,我们封装一个 TXSearchForm

思路

  • 首先,form 内容的结构分成两个 div,用Form包裹住
  • 右侧固定宽度,左侧就自适应剩下的,然后左边内部布局 flex
  • 收起时,overflow: hidden,展开时,overflow: initial
  • 子绝父相,展开时不影响 table 的高度,而是像盖在 table 上面
  • 展开收起时,我们通过 form 的高度变化去设置。高度变化可以通过 ResizeObserver 去监听。

实现:

组件部分

tsx 复制代码
// 项目用的是 mobx 
import { observer, useSyncProps } from "@quarkunlimit/qu-mobx";

const TXSearchForm = observer(function TXSearchForm_(
  props: ITXSearchFormProps
) {
  const root = useStore();
  useSyncProps(root, Object.keys(props), props);
  const { refs, logic } = root;
  const { onReset, className, onSearch, loading, ...reset } = props;

  useEffect(() => {
    if (!refs.contentRef.current) return;
    
    const observer = new ResizeObserver((entries) => {
      for (const entry of entries) {
        const newHeight = entry.contentRect.height;
        logic.changeHeight(newHeight);
      }
    });

    observer.observe(refs.contentRef.current);

    return () => {
      observer.disconnect();
    };
  }, []);

  return (
    <Form
      {...reset}
      className={`tx-search-form ${logic.expand ? "expand" : ""} ${className || ""}`}
      form={props.form || refs.form}
    >
      <div className="tx-search-form-content-wrapper">
        <div
          className="tx-search-form-content border-b-[1px]"
          ref={refs.contentRef}
        >
          {props.children}
        </div>
      </div>
      <div className="tx-search-form-bar">
        <Button onClick={logic.onReset} loading={props.loading}>
          <ReloadOutlined />
          重置
        </Button>
        <Button type="primary" loading={props.loading} onClick={logic.onSearch}>
          <SearchOutlined />
          搜索
        </Button>
        <ExpandBtn />
      </div>
    </Form>
  );
});

export default observer(function TXSearchFormPage(props: ITXSearchFormProps) {
  return (
    <Provider>
      <TXSearchForm {...props} />
    </Provider>
  );
});

然后就是逻辑函数部分

tsx 复制代码
import { makeAutoObservable } from "@quarkunlimit/qu-mobx";
import { ILogic, TLoadingStore } from "./interface";
import { RootStore } from "./";

export class Logic implements ILogic {
  loadingStore: TLoadingStore;
  rootStore: RootStore;
  expand = false;
  renderExpand = false;
  constructor(rootStore: RootStore) {
    this.rootStore = rootStore;
    this.loadingStore = rootStore.loadingStore;
    makeAutoObservable(this, {}, { autoBind: true });
  }

  changeHeight(num: number) {
    this.renderExpand = num > 48;
    if (!this.renderExpand) {
      this.expand = false;
    }
  }

  changeExpand() {
    this.expand = !this.expand;
  }

  onReset() {
    const { refs, propsStore } = this.rootStore;
    const form = propsStore.props.form || refs.form;
    form?.resetFields?.();
    propsStore.props.onReset?.();
    this.expand = false;
  }

  onSearch() {
    const { refs, propsStore } = this.rootStore;
    const form = propsStore.props.form || refs.form;
    const values = form?.getFieldsValue?.();
    propsStore.props.onSearch?.(values);
    this.expand = false;
  }
}

这部分逻辑也简单,主要就是 changeHeight 判断 form 高度如果大于 48px,就认为是展开了,设置一下 expand 即可

最后就是 css 部分

css 复制代码
.tx-search-form {
  display: flex;
  justify-content: space-between;
  padding-bottom: 16px;
  position: relative;
  height: 48px;
  overflow: hidden;
}

.tx-search-form.expand {
  overflow: initial;
}

.tx-search-form .expand-btn {
  padding: 0;
}

.tx-search-form-content-wrapper {
  flex: 1;
  height: 32px;
}

.tx-search-form-content {
  position: absolute;
  width: 100%;
  left: 0;
  top: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  z-index: 30;
  background-color: #fff;
  padding: 0 16px 16px;
  padding-right: 230px;
}

.tx-search-form-bar {
  display: flex;
  gap: 16px;
  position: relative;
  z-index: 40;
}

.tx-search-form .ant-form-item {
  margin-bottom: 0;
}

.tx-search-form .ant-tree-select {
  width: 240px;
}

.tx-search-form .ant-select {
  width: 240px;
}

最终的效果如下

展开的时候,是覆盖在 table 上面的,不会影响 table 的高度,所以 table 就不会触发重拍重绘

然后当我展开选择筛选条件,再收起请求,也能带上参数

然后收起展开也只是改变 css,性能上肯定比动态增删dom好

项目中其他的业务组件也都差不多路子搞的。业务组件的封装仁者见仁吧。至少本篇我认为优化后要好多了。

代码优化

状态全写在一个 store 文件里

复杂的模块,不管是页面、弹窗、抽屉,他们所有的 state、逻辑都写在一个 store 里面,就会导致 store 里面的内容会会很多。

虽然 1200 行我觉得还好,但是太乱了。应该每个模块自己管自己的 store 和逻辑。

像这样拆目录,每个组件都做自己的事情,如果需要数据传递,直接通过 props,而且在 mobx(重构后的状态管理库)里也很方便拿 props 中的内容

同样的接口地址,有多个调用方法

一个接口,定义了四个接口方法

分别用在 首次进入页面表单重置表单搜索表格分页改变

上面的逻辑,其实就调接口时,传递的参数不同。没必要写四个接口方法去调同一个接口

重构后的代码:

只有参数是不同的,而调接口及后续逻辑都一样的,简洁明了

意义不明的 localStorage 缓存

这个是我最不能理解的地方

比如一个表格,点击按钮,弹一个弹窗,此时需要点开弹窗的数据项的id。老代码里面喜欢用 localStorage 来存

就是不管之后的弹窗是简单的还是复杂的,都这样搞。

why,baby tell me why,looking my eyes!都用状态管理器了,又是一个页面里的,为啥非要 localStorage 呢

而且就业务来说,只需要点开弹窗,把数据传进去就行了呀

anyScript

这个我就不多说了,懂得都懂。

时间紧事情多的话,我也会这样。如果只有我一个人写,我也会偷懒。

但多人开发既然选了 ts 就写好,都 any 可能当时是没问题的。但是如果以后我接手了,说不准就 undefined 报错或者类型不一样方法报错之类的,那我又吃屎了不是

JSON TO TypeScript 复制粘贴写个类型,先用着。至于注释可以有空再补。

重复定义

比如一个接口,很多地方都定义了。

其实这个就我个人而言,还能接受。只是在迭代的时候,有些时候想通过搜 api 快速定位代码位置,结果搜出来一大堆,就有点不方便。所以还是给它优化了

重构后,在根目录下建了个 service 文件,专门用来写定义接口、参数类型、响应类型

比如:/api/base/v1/organization/get-page 这个接口,就创建对应的目录

然后在文件里面就写对应的接口

这样搞其实就相当于一个文件就是一个接口,类型也就直接写一个文件里面。方便找,也方便改。

局部方法全局通用

比如上图的 renderEnable 方法,这个方法是 pages/basic/employe/columns 模块页面里定义的。但是全局都在用。那为什么不定义在全局呢。

utils文件夹下面,commonRender 定义通用的一些渲染逻辑

模块过度拆分

重构前所有的 columns 都喜欢单独写一个文件

然后这个 column 通过 hook 的方式定义,然后 column 里面的操作,比如点开弹窗,这个回调函数又写在 store 里面,所以它把 store 整个传了进来

然后再 store 里面拿 columns,再通过 store 暴露出去

最后使用的时候,再通过 store 导入

说真的我第一次看这里的时候,我真搞不懂为什么要这样写。而且 table 不是自带分页吗,所有 table 都不用自己的,然后去使用 Pagination 单独写。我不理解,这跟脱裤子放屁有什么区别。

重构之后:

columns 里面的操作直接从 mobx 里面引入,分页也直接用 table 自带的。简洁明了

useEffect监听依赖触发Form

这里引入一个需求,form表单提交后,调接口,根据接口的返回值,在表单里面提示重复值

最开始的代码是通过 useEffect 去监听 mobx 里面的值,值改变了触发 form.validateFields

这样写存在问题,比如渲染次数增多,导致性能损耗;依赖和form的状态更新冲突等等。

所以我们可以自定义组件去做

定义 RepeatFormItem 组件

tsx 复制代码
import { phoneValidator } from "@/utils/validator";
import { observer } from "@quarkunlimit/qu-mobx";
import { Form, FormListFieldData } from "antd";
import RepeatInfoHoc from "./RepeatInfoHOC";

export interface IRepeatInfoProps extends FormListFieldData {
  index: number;
  type: "phone" | "wechat";
  remove: (index: number | number[]) => void;
}

const RepeatFormItem = observer(function RepeatInfo_({
  index,
  type,
  remove,
  ...restField
}: IRepeatInfoProps) {
  return (
    <Form.Item
      {...restField}
      rules={
        type === "phone"
          ? [
              {
                validator: phoneValidator,
              },
            ]
          : []
      }
      noStyle
    >
      <RepeatInfoHoc type={type} remove={remove} index={index} />
    </Form.Item>
  );
});

export default RepeatFormItem;

对于 Form.Item 内容存在嵌套时,需要通过 HOC 去做

定义 RepeatInfoHoc 组件

tsx 复制代码
import { cn } from "@/utils/tools";
import { observer } from "@quarkunlimit/qu-mobx";
import { Form, Input } from "antd";
import { ChangeEvent } from "react";
import { useStore } from "../store/RootStore";
import RemovePhone from "./RemovePhone";
import { IRepeatInfoProps } from "./RepeatInfo";

export interface IRepeatInfoHocProps
  extends Pick<IRepeatInfoProps, "type" | "index" | "remove"> {
  value?: string;
  onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}

const RepeatInfoHoc = observer(function RepeatInfoHoc_(
  props: IRepeatInfoHocProps
) {
  const { index, remove, type, ...rest } = props;
  const root = useStore();
  const { logic, refs } = root;
  const formValue = Form.useWatch((values) => values, refs.editForm);

  const repeatName = type === "phone" ? "电话" : "微信";

  const handleJudgeRepeat = () => {
    const repeatData =
      type === "phone" ? logic.repeatPhoneList : logic.repeatWechatList;
    const formData =
      type === "phone"
        ? (formValue?.phoneNumber ?? [])
        : (formValue?.wechatNumber ?? []);

    const repeatList = repeatData.reduce(
      (prev: { index: number; ownerName: string }[], item, idx) => {
        if (item.value.includes(formData[index])) {
          return [
            ...prev,
            {
              ownerName: item.ownerName,
              index: idx,
            },
          ];
        }
        return [...prev];
      },
      []
    );

    return repeatList;
  };

  const repeatList = handleJudgeRepeat();

  return (
    <div>
      <Input
        {...rest}
        placeholder="请输入客户电话"
        suffix={<RemovePhone index={index} remove={remove} />}
      />
      {
        <span
          className={cn(
            "hidden",
            Boolean(repeatList.length) ? "text-red-500 inline" : ""
          )}
        >
          该{repeatName}重复,客户当前归属于{repeatList[0]?.ownerName}
        </span>
      }
    </div>
  );
});
export default RepeatInfoHoc;

提示信息通过条件渲染就行了,没必要非得通过 form 去触发校验

其他优化

代码格式化

因为每个人的代码习惯不一样,格式化的配置也不一样,然后有时候代码冲突其实就只有格式化方面的冲突。然后有时候解决冲突的时候还容易冲掉。所以就统一格式化风格

js 复制代码
npm i prettier

然后 package.json 配命令

json 复制代码
"scripts": {
   "fmt": "prettier --write ."
},

然后根目录新建 .prettierrc

js 复制代码
    {
      "semi": true,
      "singleQuote": false,
      "tabWidth": 2,
      "trailingComma": "es5",
      "printWidth": 80
    }

然后根目录新建 ..prettierignore,忽略下 node_modules、dist 下的文件

js 复制代码
    node_modules/
    dist/

最后运行命令

js 复制代码
    yarn fmt

因为是在根目录下创建的配置文件,所以会把整个项目都格式化一遍

单例模式减少请求次数

很多地方都会调同样的接口(这已经是优化了很多地方),并且这些接口的值基本不会变动(和后端确认后)

比如上面这个接口是请求中国的城市地区数据,然后根据需要,通过转换函数转换成需要的数据结构,页面上的体现是这样的:

所以完全可以当用户登录后,调一次接口,将数据存储到单例对象中。需要用的时候,通过单例对象拿即可

  • 新建一个创建单例模式的类 SingletonConstructor

    ts 复制代码
    export interface SingletonConstructor<T> {
      instance: T;
      getInstance(): T;
      new (): Singleton;
    }
    
    export interface Singleton {}
    
    export function createSingleton<T>(): SingletonConstructor<T> {
      return class Singleton {
        static instance: T;
        static getInstance() {
          if (!Singleton.instance) {
            Singleton.instance = new this() as T;
          }
          return Singleton.instance as unknown as T;
        }
      };
    }
  • 创建 AddressHelper 类,以后访问城市数据就通过这个类访问

    ts 复制代码
    /**@function 转换城市树 */
    export const transformCityTree = (
      data: IResBaseV1RegionTree[]
    ): TreeDataNode[] => {
      //...
    };
    
    /**@function 过滤指定 level 的树 */
    export const filterTreeByLevel = (
      data: IResBaseV1RegionTree[],
      targetLevel: number
    ): IResBaseV1RegionTree[] => {
      //...
    };
    
    export class AddressHelper extends createSingleton<IAddressHelper>() {
      // 城市树
      treeData: IResBaseV1RegionTree[] = [];
      // 接口 loading
      loading = false;
    
      private constructor() {
        super();
      }
    
      async getRegionTree() {
        if (this.loading) {
          return;
        }
        this.loading = true;
        const [err, res] = await to(get_region_tree({ maxLevel: 3 }));
        this.loading = false;
        if (err || !res?.data) {
          showErrorInfo({
            err,
            res,
            msg: "获取地区数据失败",
          });
          return;
        }
        this.treeData = res?.data || [];
      }
    
      async getTreeDataWithLevel(isTransform: boolean, level?: number): 
        Promise<IResBaseV1RegionTree[] | TreeDataNode[]> {
            if (level !== 1 && level !== 2) return this.treeData;
            const tree = filterTreeByLevel(this.treeData, level);
            return isTransform ? transformCityTree(tree) : tree;
        }
    }

    由于默认请求的是第三级的数据,而使用的时候不一定是第三级,且可能需要对源数据的结构做转换,所以提供 getTreeDataWithLevel 方法,根据实际情况获取城市数据。

  • 使用 AddressHelper 类 在用户登录后,获取一次城市树

    tsx 复制代码
    async login() {
      //...登录逻辑
      
      //...登录成功后
      AddressHelper.getInstance().getRegionTree();
    }

    然后在需要的地方使用(需要注意的是:getTreeDataWithLevel 本身返回的是个 promise,所以需要我们手动 await 拿数据)

    tsx 复制代码
    async initCityTree() {
        const res = await AddressHelper.getInstance().getTreeDataWithLevel(true, 2);
        this.cityTree = res as TreeDataNode[];
    }
    
    
    <TreeSelect
      //...
      treeData={logic.cityTree}
    />

比如点开一个弹窗,直接用就行了

个人认为单例模式就很适合去存储一些不变的数据。

如果后续这个接口的返回值改了,又或者有其他的逻辑,都只需要在这个单例里面维护即可。

脚手架

sir this way # 搭建公司前端脚手架

因为是第一次搞,所以记录一下。

API环境映射谷歌插件

有些时候测试环境造不出一些历史老数据,而这些历史老数据只在正式环境接口返回。所以为了方便测试,搞了API映射插件

首先是在 web 项目里面创建一个 localHelper ,这个类就是去读取 localStorage

ts 复制代码
export interface ILocalDiy {
  tx_api: string;
}

class LocalHelper {
  static instance: LocalHelper | null = null;
  /** @param diy设置 */
  diy: ILocalDiy | null = null;

  static getInstance() {
    if (!LocalHelper.instance) {
      LocalHelper.instance = new LocalHelper();
    }
    return LocalHelper.instance;
  }

  init() {
    try {
      const local = localStorage.getItem("tx_diy");
      if (!local) {
        return;
      }
      const localObj = JSON.parse(local);
      this.diy = localObj || null;
      console.log(`后端接口已映射至${this.diy?.tx_api}`);
    } catch (e) {
      console.error("后端接口映射失败: ", e);
    }
  }
}

export default LocalHelper;

然后在 axios 里面配一下

然后在测试环境,f12 输入下面的代码,再刷新页面

js 复制代码
localStorage.setItem(
    "tx_diy",
    JSON.stringify({
      tx_api: "https://api.taoxiplan.com",
    })
);

就可以在测试环境映射API到正式环境了。然后测试同事的电脑屏幕比较小,所以顺便就搞个 chrome 插件方便操作

创建插件文件夹

manifest.json配置文件

json 复制代码
{
    "manifest_version": 3,
    "name": "txApi映射",
    "version": "1.0",
    "description": "api映射",
    "icons": {
        "16": "icons/icon.png",
        "48": "icons/icon.png",
        "128": "icons/icon.png"
    },
    "action": {
      "default_popup": "popup.html",
      "default_icon": "icons/icon.png"
    },
    "permissions": ["activeTab", "scripting"]
  }

插件内容 popup.htmlpopup.js

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body style="width: 300px; padding: 15px;">
    <div style="margin-bottom: 10px">
      <input
        type="text"
        id="tx_api_value"
        placeholder="api完整域名,比如:https://baidu.com"
        style="width: 100%"
      />
    </div>
    <button id="create_tx_api">新增/修改</button>
    <button id="delete_tx_api">删除</button>
    <button id="clear_tx_api">清空全部</button>
    <script src="popup.js"></script>
  </body>
</html>
js 复制代码
function executeAction(actionType, value) {
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    chrome.scripting.executeScript({
      target: { tabId: tabs[0].id },
      func: (action, v) => {
        switch (action) {
          case "set":
            localStorage.setItem("tx_diy", v);
            break;
          case "delete":
            localStorage.removeItem("tx_diy");
            break;
          case "clear":
            localStorage.clear();
            break;
        }
        alert(`操作成功`);
      },
      args: [actionType, value],
    });
  });
}

document.getElementById("create_tx_api").addEventListener("click", () => {
  const value = document.getElementById("tx_api_value").value;
  if (value)
    executeAction(
      "set",
      JSON.stringify({
        tx_api: value,
      })
    );
});

document.getElementById("delete_tx_api").addEventListener("click", () => {
  executeAction("delete");
});

document.getElementById("clear_tx_api").addEventListener("click", () => {
  if (confirm("确认清空全部数据?")) executeAction("clear");
});

然后拓展程序管理,把这个文件夹加进去

然后就可以使用了

然后 localStorage 就加上了,然后刷新页面登录

这个时候就可以在本地或者测试环境,映射API连到正式环境了

最后

大概就这些吧,还有的已经 go 完了所以就写进来了。

重构本身是有意义的。但是在某些环境下(懂得都懂),重构只会增加工作负担。所以我衷心的祝愿你们都不会遇到这样的情况。

相关推荐
難釋懷1 小时前
Vue-Todo-list 案例
前端·vue.js·list
前端达人1 小时前
React 播客专栏 Vol.18|React 第二阶段复习 · 样式与 Hooks 全面整合
前端·javascript·react.js·前端框架·ecmascript
GISer_Jing1 小时前
Monorepo 详解:现代前端工程的架构革命
前端·javascript·架构
比特森林探险记2 小时前
Go Gin框架深度解析:高性能Web开发实践
前端·golang·gin
打小就很皮...4 小时前
简单实现Ajax基础应用
前端·javascript·ajax
wanhengidc5 小时前
服务器租用:高防CDN和加速CDN的区别
运维·服务器·前端
哆啦刘小洋6 小时前
HTML Day04
前端·html
再学一点就睡6 小时前
JSON Schema:禁锢的枷锁还是突破的阶梯?
前端·json
从零开始学习人工智能8 小时前
FastMCP:构建 MCP 服务器和客户端的高效 Python 框架
服务器·前端·网络
烛阴8 小时前
自动化测试、前后端mock数据量产利器:Chance.js深度教程
前端·javascript·后端