前语

本年的大环境比上一年愈加困难,参加了诸多面试,关于一个挨近35岁的大龄程序员面试一场比一场难。

最难忘是国庆后的一次面试。在面试中我介绍自己运用UmiJS4来搭建前端工程,面试最后那位小年青的面试官问我:“ useModel 有用过吧,那 useModel 怎样完成的”。我吧啦吧啦讲了一堆,结果面试官告诉我他听不明白,他没用过UmiJS4,我其时就愣住了。说完他就出去了,我还想是不是表达得不够明晰,要不要脱离,面试算失败了。

没过多久,他拿了一台笔记本进来对我说,找一下 useModel 完成的源码,对着源码跟我说。唉,真不知道面试官这是来学习的,仍是来面试的。算了,面试官便是爷,有要求,小的无所不从。于是从GitHub下载了UmiJS的源码,预备给这位爷报告报告。

预备工作

win R 快捷键输入cmd,翻开命令行窗口。输入 node -v,确保 node 版本是 14 或以上。

输入 pnpm -v,发现 pnpm 未装置。 输入 npm install -g pnpm 装置 pnpm 。

输入 D: 回车进入 D 盘,输入 mkdir react-demo,创立一个文件夹,输入 cd react-demo 进入该文件夹。

输入 pnpm dlx create-umi@latest 创立一个最简略的 react 项目。

在项目的 .umirc.ts 文件中增加以下代码,敞开 useModel

import { defineConfig } from "umi";
export default defineConfig({
  routes: [
    { path: "/", component: "index" },
    { path: "/docs", component: "docs" },
  ],
  npmClient: 'pnpm',
  plugins: [
   '@umijs/plugins/dist/model',
  ],
  model: {},
});

在项目中的 src 目录下创立 models 文件夹,这个文件夹称号 UmiJS 约定的。

models 文件夹中创立 counterModel.ts 文件,在里面增加以下代码。

  import { useState, useCallback } from 'react';
  export default function counterModel() {
    const [counter, setCounter] = useState(0);
    const increment = useCallback(() => setCounter((c) => c   1), []);
    const decrement = useCallback(() => setCounter((c) => c - 1), []);
    return { counter, increment, decrement };
 };

然后将 src/pages/index.tsx 文件内容清空后,在里面增加以下代码。

import { useModel } from 'umi';
const App = () => {
  const { add, minus, counter } = useModel('counterModel', (model) => ({
    counter: model?.counter,
    add: model?.increment,
    minus: model?.decrement,
  }));
  return (
    <div>
      <div>{counter}</div>
      <button onClick={add}>加 1</button>
      <button onClick={minus}>减 1</button>
    </div>
  );
}
export default App

履行 pnpm dev 发动项目。成功后在浏览器拜访 http://localhost:8000 。出现下图所示画面。说明预备工作现已完成,一个最简略的运用 useModel 的 demo 现已创立好了。预备给爷开端报告了。

useModel的完成源码在哪里

面试官问我:“ useModel的完成源码在哪里?”

我:“在 src/.umi/plugin-model/index.tsx 这儿。” 我手指着文件给面试官看。

2023年的面试如此困难 丨掘金年度征文

这些代码是 UmiJS4中自带的 plugin-model 插件生成的。

为什么运用useModel能够获取界说在models 文件夹下文件中的数据

面试官问我:“ 为啥能够在 src/pages/index.tsx 中,能够经过 useModel('counterModel') 获取到界说在 src/models/counterModel.ts 中的数据 counter、修正数据的办法 incrementdecrement 呢?”

“ 这个得从 useModel 这个自界说的 hook 开端讲起 。” 我指着 src/.umi/plugin-model/index.tsxuseModel 的完成代码说。

2023年的面试如此困难 丨掘金年度征文

研究一个 hook ,首要要看这个 hook 回来值是什么。先把 useModel 的代码简化一下,去除一些 Typescript 的界说。

export function useModel(namespace, selector) {
  const { dispatcher } = useContext(Context);
  const selectorRef = useRef(selector);
  selectorRef.current = selector;
  const [state, setState] = useState(() =>
    selectorRef.current
      ? selectorRef.current(dispatcher.data[namespace])
      : dispatcher.data[namespace],
  );
  return state;
}

可见 useModel 回来的是一个 state,这个 statedispatcher.data 目标中 [namespace] 特点的值,namespace 的值是咱们界说在* src/models* 文件夹下的文件名,比如 counterModel

dispatcher 又是用 useContext(Context) 获取的,useContext 是用来获取一个叫做 Context 的上下文 context,其回来值是调用 useModel 了的组件外层最近的 Context.Provider 组件的 value 特点值。假如没有这样的 provider 组件,那么回来值将会是为创立该上下文 context 传递给的默许值。所以咱们来找一下叫做 Context 的上下文 context 是哪里创立的?

src/.umi/plugin-model/index.tsx 中,能够很轻易找到。

const Context = React.createContext<{ dispatcher: Dispatcher }>(null);

会发现其默许值是 null ,那么要找找 Context.Provider 组件在哪里?

会发现 Context.Provider 组件在 Provider 组件中运用,但是 Provider 组件是被导出,在文件中并未运用。

export function Provider(props) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

再找找 Provider 组件在哪里运用?

src/.umi/plugin-model/runtime.tsx 中发现了 Provider 组件。

import React  from 'react';
import { Provider } from './';
import { models as rawModels } from './model';
function ProviderWrapper(props: any) {
  const models = React.useMemo(() => {
    return Object.keys(rawModels).reduce((memo, key) => {
      memo[rawModels[key].namespace] = rawModels[key].model;
      return memo;
    }, {});
  }, []);
  return <Provider models={models} {...props}>{ props.children }</Provider>
}
export function dataflowProvider(container, opts) {
  return <ProviderWrapper {...opts}>{ container }</ProviderWrapper>;
}

UmiJS4 中约定好的插件运行时会履行 runtime.tsx 文件中 export 出且约定好的办法。

dataflowProvider 办法能够在给 react-dom 烘托时的根组件外面包裹一层该办法所回来的组件。

借助 React Developer Tools 调试功能,能够很明晰的了解页面中的组件结构。

我翻开控制台面板,切到 Components 标签页,给面试官指出 dataflowProvider 办法回来的 ProviderWrapper 组件在下图红框所示。

2023年的面试如此困难 丨掘金年度征文

Context.Provider 组件找到了,要开端找 Context.Provider 组件上面的 value 特点值是从哪里来的。

来咱们看 src/.umi/plugin-model/index.tsx 中的 Provider 组件中。

export function Provider(props) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

能够看到 Context.Provider 组件上面的 value 特点值是 dispatcher,再找一下 dispatcher 的界说。

class Dispatcher {
  callbacks = {};
  data = {};
  update = namespace => {
    if (this.callbacks[namespace]) {
      this.callbacks[namespace].forEach((cb) => {
        try {
          const data = this.data[namespace];
          cb(data);
        } catch (e) {
          cb(undefined);
        }
      });
    }
  };
}
  const dispatcher = new Dispatcher();

dispatcher 是经过 Dispatcher 构造函数创立出来一个目标。在 useModel 中回来的 state 的值是 dispatcher.data 的值。再来找找 dispatcher.data 的值是哪里赋值的?

发现在 Executor 组件的 onUpdate 特点值中,该特点值是个函数,在这个函数中有对dispatcher.data 赋值的。

export function Provider(props) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

那么咱们来看一下 Executor 组件内部是怎样调用经过 onUpdate 特点传入的办法。

function Executor(props: ExecutorProps) {
  const { hook, onUpdate, namespace } = props;
  const updateRef = useRef(onUpdate);
  const initialLoad = useRef(false);
  let data: any;
  try {
    data = hook();
  } catch (e) {
    console.error(
      `plugin-model: Invoking '${namespace || 'unknown'}' model failed:`,
      e,
    );
  }
  useMemo(() => {
    updateRef.current(data);
  }, []);
  // React 16.13 后 update 函数用 useEffect 包裹
  useEffect(() => {
    if (initialLoad.current) {
     updateRef.current(data);
    } else {
      initialLoad.current = true;
    }
  });
  return null;
}

会发现经过 onUpdate 特点传入的办法被赋值给 updateRef.current 。履行updateRef.current(data) 相当调用经过 onUpdate 特点传入的办法。

可揣度出 dispatcher.data 的值是 data ,而 data 是履行 hook 获取的。咱们回到 Provider 组件中看一下 Executor 组件的 hook 特点值是什么?

export function Provider(props) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

props.models[namespace] ,咱们找一下 Provider 组件的 models 是哪里来的。回到 src/.umi/plugin-model/runtime.tsx 文件中。

import React  from 'react';
 import { Provider } from './';
 import { models as rawModels } from './model';
function ProviderWrapper(props: any) {
  const models = React.useMemo(() => {
    return Object.keys(rawModels).reduce((memo, key) => {
      memo[rawModels[key].namespace] = rawModels[key].model;
      return memo;
    }, {});
  }, []);
  return <Provider models={models} {...props}>{ props.children }</Provider>
}

能够发现 modelsrawModels 改造而来,models 是一个目标,其key 为 rawModels 每项中的namespace的值。而 rawModels 是从 src/.umi/plugin-model/model.ts 文件导入,其代码如下所示:

// src/.umi/plugin-model/model.ts
import model_1 from 'D:/project/react-demo/src/models/counterModel';
export const models = {
   model_1: { namespace: 'counterModel', model: model_1 },
};

借助 React Developer Tools 调试功能,能够看到 models 的值如下所示:

2023年的面试如此困难 丨掘金年度征文

能够看到 models 的值是一个目标,其 key 咱们界说在 src/models* 文件夹下的文件名,比如 counterModel , 其特点值是 src/models/counterModel.ts 文件中默许导出的办法。

页面初次烘托时,Executor 组件中,会履行以下代码:

useMemo(() => {
   updateRef.current(data);
}, []);

其间 data 的值是经过履行 hook() 获取,根据 Executor 组件特点 hook 的赋值,相当履行 src/models/counterModel.ts 文件中 counterModel 办法。

// src/models/counterModel.ts
import { useState, useCallback } from 'react';
export default function counterModel() {
  const [counter, setCounter] = useState(0);
  const increment = useCallback(() => setCounter((c) => c   1), []);
  const decrement = useCallback(() => setCounter((c) => c - 1), []);
  return { counter, increment, decrement };
};

能够揣度出 data 的值如下图所示:

2023年的面试如此困难 丨掘金年度征文

那么 dispatcher.data 的值如下图所示:

2023年的面试如此困难 丨掘金年度征文

这样 Context.Provider 组件的 value 特点值就和界说在 src/models/counterModel.ts 中的数据 counter、修正数据的办法 incrementdecrement 联络起来了。

Context.Provider 组件的父级组件 ProviderProviderWrapper 组件中运用,在 *runtime.tsx * 文件中运用 dataflowProvider办法把这个组件包裹在页面根节点外层。

能够借助 React Developer Tools 调试功能来看一下 Context.Provider 组件地点位置及其特点数据。

2023年的面试如此困难 丨掘金年度征文

这样在页面中的任意组件能够经过 useModel('counterModel') 获取到界说在 src/models/counterModel.ts 中的数据 counter、修正数据的办法 incrementdecrement

为什么 dispatcher 不能界说在 Provider 组件内部呢

面试官:“为什么 dispatcher 不能界说在 Provider 组件内部呢?而且还在 Executor 组件中给 dispatcher 设值,绕这么一大圈。”

这么规划主要有以下几点原因:

  • 防止不必要的从头烘托:假如 dispatcherProvider 组件内部界说,那么每次 Provider 组件从头烘托时,都会创立一个新的 dispatcher 实例。由于 Context.Providervalue 特点值产生改变,一切消费该 Context 的子组件都会进行不必要的从头烘托,哪怕实践上 dispatcher 的内容并没有产生任何改变。

  • 保持状况的耐久化:将 dispatcher 界说在组件外部,能够确保其状况在整个运用的生命周期内保持耐久化。假如在组件内部界说,每次组件卸载再挂载,dispatcher 的状况都会被重置。

  • 单一数据源:在组件外部界说 dispatcher 能够确保在运用中有一个单一的数据源。这样,不管 Provider 组件被烘托多少次,或者在不同的地方被烘托,一切的消费者都能够拜访到同一个 dispatcher 实例,确保数据的一致性。

  • 削减依靠:假如 dispatcherProvider 内部界说,那么它的初始化和存在可能会依靠于 Provider 组件的其他 props 或 state,这增加了组件间的耦合。将其界说在外部,能够使 dispatcher 独立于 Provider 组件,削减不必要的依靠。

所以将 dispatcher 界说在 Provider 组件外部,能够进步运用的性能,保持状况的耐久性和一致性,削减组件间的耦合。

为什么我点“加1”按钮,页面中的数字会产生改变

面试官:“为什么我点’加1’按钮,页面中的数字会产生改变?”

这个要害步骤,在 Executor 组件特点 hook={props.models[namespace]} 上,其间 props.models[namespace] 是界说在对应的 model 文件中默许导出的函数,该函数回来 state 和对应修正 state 办法。而在 Executor 组件中履行 data = hook() 获取了 state ,这样在运用 useModel 获取相同的 state 和对应修正 state 办法的组件中,调用修正 state 办法就会触发 Executor 组件更新。

不明白,没事下面举个简略的比如看了就明白。

import { useCallback, useState } from "react";
const NumberShow = props =>{
  const { hook } = props;
  const { counter, increment } = hook();
  return <button onClick={increment}>{counter}</button>
}
const Demo = () => {
  return (
    <NumberShow hook={() => {
      const [counter, setCounter] = useState(0);
      const increment = useCallback(() => setCounter((c) => c   1), []);
      return { counter, increment };
    }}/>
  )
}
export default Demo;

以上代码中,在NumberShow 组件中每次点击按钮会履行 increment 办法, increment 办法中调用修正 counter 这个 state 的办法 setCounter ,而 setCounter 办法相对 NumberShow 组件是外部的,NumberShow 组件更新,其按钮上面的数字加1。其实以上代码仅仅下面代码的另一种写法而已。

import { useCallback, useState } from "react";
const NumberShow = (props: any) => {
  const { counter, increment } = props;
  return <button onClick={increment}>{counter}</button>
}
const Demo = () => {
  const [counter, setCounter] = useState(0);
  const increment = useCallback(() => setCounter((c) => c   1), []);
  return (
    <NumberShow counter={counter} increment={increment} />
  )
}
export default Demo;

这样是不是就很好理解了。那么回到 Executor组件代码中:

function Executor(props: ExecutorProps) {
  const { hook, onUpdate, namespace } = props;
  const updateRef = useRef(onUpdate);
  const initialLoad = useRef(false);
  let data: any;
  try {
    data = hook();
  } catch (e) {
    console.error(
      `plugin-model: Invoking '${namespace || 'unknown'}' model failed:`,
      e,
    );
  }
  useMemo(() => {
    updateRef.current(data);
  }, []);
  useEffect(() => {
    if (initialLoad.current) {
      updateRef.current(data);
    } else {
      initialLoad.current = true;
    }
  });
  return null;
}

Executor组件更新时,会履行 updateRef.current(data) ,其间 data 经过 hook() 获取到更新后的 state 。updateRef.currentExecutor组件 onUpdate 特点的值。来看运用 Executor 组件的代码片段。

export function Provider(props: {
  models: Record<string, any>;
  children: React.ReactNode;
}) {
  return (
    <Context.Provider value={{ dispatcher }}>
      {Object.keys(props.models).map((namespace) => {
        return (
          <Executor
            key={namespace}
            hook={props.models[namespace]}
            namespace={namespace}
            onUpdate={(val) => {
              dispatcher.data[namespace] = val;
              dispatcher.update(namespace);
            }}
          />
        );
      })}
      {props.children}
    </Context.Provider>
  );
}

onUpdate 特点值函数中,先把更新后的数据 val 赋值给 dispatcher.data[namespace] 然后更新 dispatcher 的值。然后再履行 dispatcher.update(namespace),这一步是要害。来看一下 dispatcher.update 这个函数。

class Dispatcher {
  callbacks: Record<Namespaces, Set<Function>> = {};
  data: Record<Namespaces, unknown> = {};
  update = (namespace: Namespaces) => {
    if (this.callbacks[namespace]) {
      this.callbacks[namespace].forEach((cb) => {
        try {
          const data = this.data[namespace];
          cb(data);
        } catch (e) {
          cb(undefined);
        }
      });
    }
  };
}

发现 update 函数中是在履行 callbacks 中的函数,那要找一下在哪里给 callbacks 增加函数。发现是在 useModel 中增加的,来看一下 useModel 的代码片段。

export function useModel( namespace, selector){
  const { dispatcher } = useContext(Context);
  const selectorRef = useRef(selector);
  selectorRef.current = selector;
  const [state, setState] = useState(() =>
    selectorRef.current
      ? selectorRef.current(dispatcher.data[namespace])
      : dispatcher.data[namespace],
  );
  const stateRef = useRef(state);
  stateRef.current = state;
  const isMount = useRef(false);
  useEffect(() => {
    isMount.current = true;
    return () => {
      isMount.current = false;
    };
  }, []);
  useEffect(() => {
    const handler = data => {
      if (!isMount.current) {
        setTimeout(() => {
          dispatcher.data[namespace] = data;
          dispatcher.update(namespace);
        });
      } else {
        const currentState = selectorRef.current
          ? selectorRef.current(data)
          : data;
        const previousState = stateRef.current;
        if (!isEqual(currentState, previousState)) {
          stateRef.current = currentState;
          setState(currentState);
        }
      }
    };
    dispatcher.callbacks[namespace] ||= new Set();
    dispatcher.callbacks[namespace].add(handler);
    dispatcher.update(namespace);
    return () => {
      dispatcher.callbacks[namespace].delete(handler);
    };
  }, [namespace]);
  return state;
}

其间 dispatcher.callbacks 是个 Set 结构的数据 ,handler 函数按 namespacekey 被增加到 dispatcher.callbacks 中,并在运用 useModel 的组件卸载后,主动从 dispatcher.callbacks 中删去对应的 handler 函数。在上文说到的 dispatcher.update 函数中履行 callbacks 中的函数,便是履行对应 handler 函数。

handler 函数的参数 data 是对应 model 中导出函数的回来的 state 和修正 state 办法。假如 useModel 有传第二参数 selectordata 经过 selector 函数过滤后赋值给 currentState 。假如没有传第二参数 selectordata 直接赋值给 currentStatecurrentState 和上一次的 previousState 相对比一下,不同则调用修正 useModel 回来的 statesetState 办法,这样组件中运用 useModel 得到的 state 也跟着产生改变,然后导致组件更新。

为什么经过组件特点传递的函数不能直接用,都是用useRef包裹一下

面试官:“我看代码中一切经过组件特点传递的函数不能直接用吗?都是用useRef包裹一下,为什么?”

由于运用 useRef 包裹组件一个函数特点有以下两点优点:

  • 削减重复的计算:假如在一个组件中界说了一个函数特点,并在该组件的烘托办法中调用这个函数,那么每次烘托该组件时都会从头计算这个函数。假如运用 useRef 包裹这个函数特点, React 会将其缓存在内存中,然后防止了每次烘托时的重复计算。
  • 防止无法预期的错误:假如在组件的烘托办法中直接运用了一个函数特点,而该函数特点在后续被更新或者被 removed 时会导致无法预期的错误。假如运用 useRef 包裹这个函数特点,那么 React 会在缓存中保存该函数的值,然后防止了这些错误。

Namespaces 类型界说完成思路

面试官:“讲一讲 Namespaces 类型界说的完成思路?”

Namespaces 类型主要是约束 useModel 的榜首参数的输入,假如输入的值不匹配 models 文件夹下的文件称号,则会提示报错。假设 models 文件夹下有 counterModeluserModel 两个文件,那么 Namespaces 类型便是 type Namespaces = 'counterModel' | 'userModel',这种类型称为联合类型。

在实践场景中 models 文件夹下的文件的数量和称号是不固定,要动态获取。

那怎样动态获取呢?咱们来看一下 src/.umi/plugin-model/model.ts 中的代码:

import model_1 from 'D:/project/react-demo/src/models/counterModel';
export const models = {
  model_1: { namespace: 'counterModel', model: model_1 },
} as const

该代码是UmiJS主动生成的,models 中包含 models 文件夹下一切文件的默许导出。其间 models 每个特点值中的 namespace 特点值便是 models 文件夹下某个文件称号。

留意了,models 最后运用 as const 进行断言。

举个比如解释一下 as const 的作用。

const example = { key: 'value' } 中会将example的类型揣度为{ key: string },这意味着key的值能够是任何字符串。

而在 const example = { key: 'value' } as const 中会将 example 的类型会被揣度为{ readonly key: 'value' },这意味着key是只读的,且它的值只能是字面量'value'

所以能够运用 typeof 来获取 models 的类型,其间 namespace 的类型会被揣度字面量 'counterModel'

import type { models as rawModels } from '@@/plugin-model/model';
type Models = typeof rawModels;

Models 的值如下所示:

2023年的面试如此困难 丨掘金年度征文

接下来界说一个泛型类型 GetNamespaces<M>,用来从 Models 每个键值中提取一切namespace的类型并合并成联合类型。

首要运用索引类型查询(keyof M)获取模型目标M 每个键的集合,为遍历模型目标M 做预备。

然后运用映射类型([K in keyof M])遍历模型目标M 的一切键并将每个键K映射到一个新类型上,该新类型是运用索引拜访类型( M[K]['namespace'])获取模型目标M 每个键值中的键 namespace 的类型。

type GetNamespaces<M> = {
  [K in keyof M]: M[K]['namespace']
};

再运用类型守卫( M[K] extends { namespace: string }) 扫除 namespace 的类型不是字符串的,假如不是字符串回来 never 类型。

type GetNamespaces<M> = {
  [K in keyof M]: M[K] extends { namespace: string }
    ? M[K]['namespace']
    : never;
};

再运用索引拜访类型([keyof M])来创立一个联合类型,会过滤掉其间为 never 的类型。

type GetNamespaces<M> = {
  [K in keyof M]: M[K] extends { namespace: string }
    ? M[K]['namespace']
    : never;
}[keyof M];

最后把 Models 类型传入 GetNamespaces<M> 泛型类型得到 Namespaces

type Namespaces = GetNamespaces<Models>;

src/.umi/plugin-model/index.tsx 这个文件是怎样生成的

面试官:“src/.umi/plugin-model/index.tsx 这个文件是怎样生成的?”

这个文件是 UmiJS 提供的 model 插件生成的,其源码途径是 packages/plugins/src/model.ts,来看一下相关的代码片段:

import { readFileSync } from 'fs';
export default (api: IApi) => {
    api.onGenerateFiles(async () => {
      // index.tsx
      const indexContent = readFileSync(
        join(__dirname, '../libs/model.tsx'),
        'utf-8',
      ).replace(
          'fast-deep-equal',
          winPath(require.resolve('fast-deep-equal')
      ));
      api.writeTmpFile({
        path: 'index.tsx',
        content: indexContent,
      });
    });
})

其间 api.onGenerateFilesapi.writeTmpFile UmiJS 提供的插件Api。

  • api.onGenerateFiles :监听 .umi 文件夹生成时分触发。
  • api.writeTmpFile: 往 path 途径的文件写入 content 内容。

来看一下 content 的值 indexContent,运用 readFileSync 读取 packages/plugins/libs/model.tsx 文件的内容,而且其文件内容跟src/.umi/plugin-model/index.tsx 文件的内容除了一个地方不一样,其余地方一模一样,不一样的地方运用 replace 替换一下。

要替换内容如下所示:

import isEqual from 'fast-deep-equal';

替换内容如下所示:

import isEqual from 'D:/project/react-demo/node_modules/.pnpm/fast-deep-equal@3.1.3/node_modules/fast-deep-equal/index.js';

api.writeTmpFile的path参数是index.tsx,怎样确保它指向src/.umi/plugin-model/index.tsx

面试官:“api.writeTmpFile的path参数是index.tsx,怎样确保它指向src/.umi/plugin-model/index.tsx?”

这个问题要在这个packages/preset-umi/src/registerMethods.ts文件中找答案。来看一下代码片段;

const absPath = join(
  api.paths.absTmpPath,
  // @ts-ignore
  this.plugin.key && !opts.noPluginDir ? `plugin-${this.plugin.key}` : '',
  opts.path,
);
//...
writeFileSync(absPath, content!, 'utf-8');

终究的写入途径是用 api.paths.absTmpPathplugin-${this.plugin.key} 以及 opts.path 组成的,其间 api.paths.absTmpPath 假如没有设置默许是 .umi 文件地点的相对途径,model这个插件的key便是model,则 plugin-${this.plugin.key} 的值便是 plugin-model

一切 absPath 的值是 src/.umi/plugin-model/index.tsx

面试结果

面试官看我有点不耐烦了,就没持续诘问下去。确实我其时也是有点不耐烦了,一个面试2个小时了,没有谈到任何实质性的内容,马上就到中午,下午还有一场面试。

面试官:“能够看出你对React以及相关生态很熟悉,但是咱们这边大多都是用Vue开发的,不过咱们最近在谈一个项目,是运用React开发的,我现在曩昔请老板过来一下,你跟他谈一下待遇。”

终于到了谈薪阶段了,没过多久,老板夹着一根烟走进来,开口就问我会不会抽烟。

我说:“不会。”

“那不行,得学会抽烟。会喝酒吗,酒量咋样。”

“不会喝酒。” 上一年检查出胆囊息肉和脂肪肝的我下意识回答不会。

“咦,是这样的,公司这边预备接一个前端用React开发的系统,预备找个既会技能又会办理项目的,你做过项目办理吗?”

“嗯,有做过项目办理。” 我以为的项目办理便是管管研制进度和交付之类的。

“那标书应该会做吧,还要回款跟进也有经历吧?”

“我给你一个月5千底薪,百分十的项目收益提成,人员你自己招?” 没等我回答,老板持续说道。

“嗯,这个有做过。薪资方面我考虑一下。” 标书和回款跟进这些我一个研制那会接触这些呢,这不是商务的事情吗。

这话一说完,气氛有点冷场。“那你几天内给我答复。” 老板持续问道。

“明日给你答复。”

“那好,明日你跟HR联络,那今天就到这儿。” 说完老板就脱离了会议室。

终究结果,第二天我跟HR回复说:“岗位要求不符合自己的职业规划,抱歉了。”

感触

经历这么多场面试后,发现本年大点公司没有HC,小公司招聘大龄程序员,对其研制才能并不怎样垂青,更垂青其事务才能,说白了,假如你能为公司挣钱,则很简单拿到offer。不然,技能再好,相关于年青程序员性价比并不是很高,拿到offer概率并不高,或许是小公司所需求的技能水平三四年工作经历就能够满足吧。难!真实!路在何方,各位掘友有什么主张,欢迎留言,一起度过隆冬。