面试反思:我详解了useModel源码,为何仍未获得offer?

前言

今年的大环境比去年更加艰难,参加了诸多面试,对于一个接近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

diff 复制代码
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 文件,在里面添加以下代码。

diff 复制代码
+ 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 文件内容清空后,在里面添加以下代码。

ts 复制代码
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 这里。" 我手指着文件给面试官看。

这些代码是 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 的实现代码说。

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

diff 复制代码
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 中,可以很轻易找到。

tsx 复制代码
const Context = React.createContext<{ dispatcher: Dispatcher }>(null);

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

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

diff 复制代码
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 组件。

diff 复制代码
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 组件在下图红框所示。

Context.Provider 组件找到了,要开始找 Context.Provider 组件上面的 value 属性值是从哪里来的。

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

diff 复制代码
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 的定义。

diff 复制代码
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 赋值的。

diff 复制代码
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 属性传入的方法。

diff 复制代码
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 属性值是什么?

diff 复制代码
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 文件中。

diff 复制代码
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 文件导入,其代码如下所示:

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 的值如下所示:

可以看到 models 的值是一个对象,其 key 我们定义在 src/models* 文件夹下的文件名,比如 counterModel , 其属性值是 src/models/counterModel.ts 文件中默认导出的方法。

页面首次渲染时,Executor 组件中,会执行以下代码:

ts 复制代码
useMemo(() => {
   updateRef.current(data);
}, []);

其中 data 的值是通过执行 hook() 获取,根据 Executor 组件属性 hook 的赋值,相当执行 src/models/counterModel.ts 文件中 counterModel 方法。

ts 复制代码
// 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 的值如下图所示:

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

这样 Context.Provider 组件的 value 属性值就和定义在 src/models/counterModel.ts 中的数据 counter、修改数据的方法 incrementdecrement 联系起来了。

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

可以借助 React Developer Tools 调试功能来看一下 Context.Provider 组件所在位置及其属性数据。

这样在页面中的任意组件可以通过 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 组件更新。

不懂,没事下面举个简单的例子看了就明白。

tsx 复制代码
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。其实以上代码只是下面代码的另一种写法而已。

tsx 复制代码
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 组件代码中:

tsx 复制代码
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 组件的代码片段。

tsx 复制代码
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 这个函数。

ts 复制代码
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 的代码片段。

js 复制代码
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 中的代码:

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'

ts 复制代码
import type { models as rawModels } from '@@/plugin-model/model';

type Models = typeof rawModels;

Models 的值如下所示:

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

首先使用索引类型查询(keyof M)获取模型对象 M 每个键的集合,为遍历模型对象 M 做准备。

然后使用映射类型([K in keyof M])遍历模型对象 M 的所有键并将每个键 K 映射到一个新类型上,该新类型是使用索引访问类型( M[K]['namespace'])获取模型对象 M 每个键值中的键 namespace 的类型。

ts 复制代码
type GetNamespaces<M> = {
  [K in keyof M]: M[K]['namespace']
};

再使用类型守卫( M[K] extends { namespace: string }) 排除 namespace 的类型不是字符串的,如果不是字符串返回 never 类型。

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

再使用索引访问类型([keyof M])来创建一个联合类型,会过滤掉其中为 never 的类型。

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

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

ts 复制代码
type Namespaces = GetNamespaces<Models>;

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

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

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

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 替换一下。

要替换内容如下所示:

ts 复制代码
import isEqual from 'fast-deep-equal';

替换内容如下所示:

ts 复制代码
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文件中找答案。来看一下代码片段;

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概率并不高,或许是小公司所需要的技术水平三四年工作经验就可以满足吧。难!实在!路在何方,各位掘友有什么建议,欢迎留言,一起度过寒冬。

相关推荐
F-2H20 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
gqkmiss1 小时前
Chrome 浏览器插件获取网页 iframe 中的 window 对象
前端·chrome·iframe·postmessage·chrome 插件
m0_748247553 小时前
Web 应用项目开发全流程解析与实战经验分享
开发语言·前端·php
m0_748255024 小时前
前端常用算法集合
前端·算法
真的很上进4 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web130933203984 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2344 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
测试老哥4 小时前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
如若1235 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~6 小时前
npm error code ETIMEDOUT
前端·npm·node.js