前言
今年的大环境比去年更加艰难,参加了诸多面试,对于一个接近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
、修改数据的方法 increment
、decrement
呢?"
" 这个得从 useModel
这个自定义的 hook 开始讲起 。" 我指着 src/.umi/plugin-model/index.tsx 中 useModel
的实现代码说。
研究一个 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
,这个 state
是 dispatcher.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>
}
可以发现 models
是 rawModels
改造而来,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
、修改数据的方法 increment
、decrement
联系起来了。
而 Context.Provider
组件的父级组件 Provider
在 ProviderWrapper
组件中使用,在 *runtime.tsx * 文件中使用 dataflowProvider
方法把这个组件包裹在页面根节点外层。
可以借助 React Developer Tools 调试功能来看一下 Context.Provider
组件所在位置及其属性数据。
这样在页面中的任意组件可以通过 useModel('counterModel')
获取到定义在 src/models/counterModel.ts 中的数据 counter
、修改数据的方法 increment
、decrement
。
为什么 dispatcher 不能定义在 Provider
组件内部呢
面试官:"为什么 dispatcher
不能定义在 Provider
组件内部呢?而且还在 Executor
组件中给 dispatcher
设值,绕这么一大圈。"
这么设计主要有以下几点原因:
-
避免不必要的重新渲染 :如果
dispatcher
在Provider
组件内部定义,那么每次Provider
组件重新渲染时,都会创建一个新的dispatcher
实例。由于Context.Provider
的value
属性值发生变化,所有消费该 Context 的子组件都会进行不必要的重新渲染,哪怕实际上dispatcher
的内容并没有发生任何变化。 -
保持状态的持久化 :将
dispatcher
定义在组件外部,可以确保其状态在整个应用的生命周期内保持持久化。如果在组件内部定义,每次组件卸载再挂载,dispatcher
的状态都会被重置。 -
单一数据源 :在组件外部定义
dispatcher
可以保证在应用中有一个单一的数据源。这样,无论Provider
组件被渲染多少次,或者在不同的地方被渲染,所有的消费者都能够访问到同一个dispatcher
实例,确保数据的一致性。 -
减少依赖 :如果
dispatcher
在Provider
内部定义,那么它的初始化和存在可能会依赖于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.current
是 Executor
组件 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
函数按 namespace
为 key
被添加到 dispatcher.callbacks
中,并在使用 useModel
的组件卸载后,主动从 dispatcher.callbacks
中删除对应的 handler
函数。在上文提到的 dispatcher.update
函数中执行 callbacks
中的函数,就是执行对应 handler
函数。
handler
函数的参数 data
是对应 model 中导出函数的返回的 state 和修改 state 方法。如果 useModel
有传第二参数 selector
,data
经过 selector
函数过滤后赋值给 currentState
。如果没有传第二参数 selector
,data
直接赋值给 currentState
。currentState
和上一次的 previousState
相对比一下,不同则调用修改 useModel
返回的 state
的 setState
方法,这样组件中使用 useModel
得到的 state 也跟着发生改变,从而导致组件更新。
为什么通过组件属性传递的函数不能直接用,都是用useRef包裹一下
面试官:"我看代码中所有通过组件属性传递的函数不能直接用吗?都是用useRef包裹一下,为什么?"
因为使用 useRef
包裹组件一个函数属性有以下两点好处:
- 减少重复的计算 :如果在一个组件中定义了一个函数属性,并在该组件的渲染方法中调用这个函数,那么每次渲染该组件时都会重新计算这个函数。如果使用
useRef
包裹这个函数属性, React 会将其缓存在内存中,从而避免了每次渲染时的重复计算。 - 避免无法预期的错误 :如果在组件的渲染方法中直接使用了一个函数属性,而该函数属性在后续被更新或者被 removed 时会导致无法预期的错误。如果使用
useRef
包裹这个函数属性,那么 React 会在缓存中保存该函数的值,从而避免了这些错误。
Namespaces
类型定义实现思路
面试官:"讲一讲 Namespaces
类型定义的实现思路?"
Namespaces
类型主要是限制 useModel
的第一参数的输入,如果输入的值不匹配 models 文件夹下的文件名称,则会提示报错。假设 models 文件夹下有 counterModel 和 userModel 两个文件,那么 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.onGenerateFiles
和 api.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.absTmpPath
和 plugin-${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概率并不高,或许是小公司所需要的技术水平三四年工作经验就可以满足吧。难!实在!路在何方,各位掘友有什么建议,欢迎留言,一起度过寒冬。