SegmentFault 香辣土豆条最新的文章
2020-07-24T11:57:28+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
复杂场景下的h5与小程序通信
https://segmentfault.com/a/1190000023360940
2020-07-24T11:57:28+08:00
2020-07-24T11:57:28+08:00
懒懒的技术宅
https://segmentfault.com/u/lanlandejishuzhai
17
<h3>复杂场景下的h5与小程序通信</h3>
<h4>一、背景</h4>
<blockquote>在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。<br>但在套壳小程序中,h5与小程序通信存在以下几个问题:</blockquote>
<ul>
<li>注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。</li>
<li>小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。</li>
<li>一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。</li>
</ul>
<h4>二、在业务内的实践</h4>
<ul><li>
<p>因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:</p>
<pre><code class="typescript">export function injectMiniAppScript() {
if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
const s = document.createElement('script');
s.src = 'https://appx/web-view.min.js';
s.onload = () => {
// 加载完成时触发自定义事件
const customEvent = new CustomEvent('myLoad', { detail:'' });
document.dispatchEvent(customEvent);
};
s.onerror = (e) => {
// 加载失败时上传日志
uploadLog({
tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
});
};
document.body.insertBefore(s, document.body.firstChild);
}
}
</code></pre>
<p>加载脚本完成后,我们就可以调用<code>my.postMessage</code>和<code>my.onMessage</code>进行通信(<strong>统一约定h5发送消息给小程序时,必须带<code>action</code>,小程序根据<code>action</code>处理业务逻辑,同时小程序处理完成的结果必须带<code>type</code>,h5在不同的业务场景下通过<code>my.onMessage</code>处理不同type的响应</strong>),比如典型的,h5调用小程序签到:<br> h5部分代码如下:</p>
<pre><code class="ts">// 处理扫脸签到逻辑
const faceVerify = (): Promise<AlipaySignResult> => {
return new Promise((resolve) => {
const handle = () => {
window.my.onMessage = (result: AlipaySignResult) => {
if (result.type === 'FACE_VERIFY_TIMEOUT' ||
result.type === 'DO_SIGN' ||
result.type === 'FACE_VERIFY' ||
result.type === 'LOCATION' ||
result.type === 'LOCATION_UNBELIEVABLE' ||
result.type === 'NOT_IN_ALIPAY') {
resolve(result);
}
};
window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
};
if (window.my) {
handle();
} else {
// 先记录错误日志
sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
// 监听load事件
document.addEventListener('myLoad', handle);
}
});
};</code></pre>
<p>实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdk<code>miniAppBus</code>,先来看看怎么用,还是上面的场景</p>
<pre><code class="ts">// 处理扫脸签到逻辑
const faceVerify = (): Promise<AlipaySignResult> => {
miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
return miniAppBus.subscribeAsync<AlipaySignResult>([
'FACE_VERIFY_TIMEOUT',
'DO_SIGN',
'FACE_VERIFY',
'LOCATION',
'LOCATION_UNBELIEVABLE',
'NOT_IN_ALIPAY',
])
};</code></pre>
<p>可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。</p>
</li></ul>
<h4>三、实现及背后的思考</h4>
<ul><li>
<p>为了满足不同场景和使用的方便,公开暴露的interface如下:</p>
<pre><code class="typescript">
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @memberof MiniAppEventBus
*/
subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise<void>}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise<void>;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise<unknown>}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise<unknown>;
}</code></pre>
<p><code>subscribe</code>:函数接收两个参数,<br> type:需要订阅的type,可以是字符串,也可以是数组。<br> callback:回调函数。<br><code>subscribeAsync</code>:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。<br><code>unsubscribe</code>:取消订阅。<br><code>postMessage</code>:postMessage替代,无需关注环境变量。</p>
<p>完整代码:</p>
<pre><code class="typescript"> import { injectMiniAppScript } from './tools';
/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/
interface MiniAppMessageBase {
type: string;
}
type MiniAppMessage<T extends unknown = {}> = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}
interface MiniAppMessageSubscriber<T extends unknown = {}> {
(params: MiniAppMessage<T>): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @memberof MiniAppEventBus
*/
subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise<void>}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise<void>;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise<unknown>}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise<unknown>;
}
class MiniAppEventBus implements MiniAppEventBus{
/**
* @description: 监听函数
* @type {Map<string, MiniAppMessageSubscriber[]>}
* @memberof MiniAppEventBus
*/
listeners: Map<string, MiniAppMessageSubscriber[]>;
constructor() {
this.listeners = new Map<string, Array<MiniAppMessageSubscriber<unknown>>>();
this.init();
}
/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}
this.startListen();
}
/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};
// 全局变量
if (window.my) {
promiseResolve();
}
document.addEventListener('myLoad', promiseResolve);
});
}
/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage<unknown>) => {
this.dispatch<unknown>(msg.type, msg);
};
}
private async startListen() {
return this.ensureEnv(this.listen);
}
/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};
resolve(this.ensureEnv(realPost));
});
}
/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber<T>} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber<T>) => {
let listeners = this.listeners.get(type) || [];
listeners.push(cb);
this.listeners.set(type, listeners);
};
this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}
private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}
for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];
cb(element);
}
}
}
/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise<MiniAppMessage<T>>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>> {
return new Promise((resolve, _reject) => {
this.subscribe<T>(type, resolve);
});
}
/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch<T = {}>(type: string, msg: MiniAppMessage<T>) {
let listeners = this.listeners.get(type) || [];
listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}
public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};
this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}
export default new MiniAppEventBus();</code></pre>
<p>class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。</p>
</li></ul>
<h4>四、小程序内部的处理</h4>
<ul><li>
<p>定义action handle,通过策略模式解耦:</p>
<pre><code class="typescript">const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {
return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}</code></pre>
<p>使用起来也是得心顺畅,舒服。</p>
</li></ul>
<h4>其他</h4>
<p>类型完备,使用时智能提示,方便快捷。</p>
Typescript 进阶
https://segmentfault.com/a/1190000020715919
2019-10-17T10:31:28+08:00
2019-10-17T10:31:28+08:00
懒懒的技术宅
https://segmentfault.com/u/lanlandejishuzhai
56
<h3>一、前言</h3>
<p>这是一篇自己总结的 <code>Typescript type</code>相关的进阶文章,适合有一定ts基础,并在<code>type</code>编写方面感到迷惑、感到绝望的同学,也给那些入门<code>Typescript</code>已久,却无法更上一层楼的童鞋一个方向。如果是<code>Typescript</code>小白,建议先看看基础知识,大神请忽略。不知道你有没有过这样一种错觉,<code>Typescript</code>使用也不算少了,各种<code>interface</code>各种<code>type</code>也写得得心应手。但他看起来重复又累赘,一点也不像别人家的<code>type</code>那么眉清目秀。年轻人,是时候进阶一波了,加油!相信你认真看完这篇文章,一定能够原地拔高3米!拔不高的当我没说~</p>
<p>另外,文章不足之处,还请各位大佬指正。</p>
<h3>二、进阶姿势---从内置工具类型说起</h3>
<p><code>Typescript</code>内置工具type</p>
<ul>
<li>
<p><code>Partial<T></code>,属性可选,使用频率:一般;</p>
<p>看起来十分简单,通过<code>keyof</code>拿到泛型T的全部<code>properties</code>,再给每个<code>property</code>加上可选标记?即可。</p>
<pre><code class="Typescript">type Partial<T> = {
[P in keyof T]?: T[P];
};</code></pre>
<p>举个例子:我们的用户信息包含<code>name</code>和<code>address</code>属性,但是有些用户很特殊,他们这两个属性可有可无。</p>
<pre><code class="Typescript">type User={
name:string;
address:string;
};
type OptionalUser=Partial<User></code></pre>
</li>
<li>
<p><code>Required<T></code>属性为required,使用频率:一般;</p>
<pre><code class="Typescript">type Required<T> = {
[P in keyof T]-?: T[P];
};</code></pre>
<p>还是上面的例子,现在我们倒过来,让<code>optional</code>的属性变成<code>required</code>:</p>
<pre><code class="Typescript">type OptionalUser=Partial<User>;
type RequiredUser=Required<User>;</code></pre>
<p>其实和User是一样的,默认就是<code>required</code>。</p>
</li>
<li>
<p><code>Readonly<T></code>属性只读,使用频率:一般;</p>
<p>确保属性是只读的,不可以被修改,常用于<code>react</code>组件的<code>props</code>和<code>state</code>。</p>
<pre><code class="Typescript">type Readonly<T> = {
readonly [P in keyof T]: T[P];
};</code></pre>
</li>
<li>
<code>Pick<T K extends keyof T></code> 选取部分属性生成新type,使用频率:较多;这个helper用的就比较多了。</li>
</ul>
<pre><code> type Pick<T, K extends keyof T> = {
[P in K]: T[P]
};</code></pre>
<p>还是之前的<code>User</code>,我们现在多了一种用户,他只有<code>name</code>属性。这时候我们又不想重新写一个差不多的<code>type</code>,怎么办呢?</p>
<pre><code class="typescript">type NameOnlyUser=Pick<User,'name'>;// type NameOnlyUser = {name: string;}</code></pre>
<ul>
<li>
<p><code>Record<K extends keyof any, T></code>使用频率:一般;</p>
<p>看起来就是创建一个具有同类型属性值的对象。没实际遇到使用的情况。</p>
<pre><code>type Record<K extends keyof any, T> = {[P in K]: T;};</code></pre>
</li>
<li>
<p><code>Exclude<T,U></code>使用频率:较多;</p>
<p>从类型T中剔除所有可以赋值给U的属性,然后构造一个类型。<br> 主要用在联合类型的exclude中</p>
<pre><code class="Typescript">type Exclude<T, U> = T extends U ? never : T;</code></pre>
</li>
<li>
<p><code>Extract<T,U></code>使用频率:一般;</p>
<p>基本同上,功能相反</p>
<pre><code class="Typescript">type Extract<T, U> = T extends U ? T : never;</code></pre>
</li>
<li>
<p><code>Omit<T, K extends keyof any></code>使用频率:较多;</p>
<pre><code class="Typescript">type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;</code></pre>
<p>主要用于剔除<code>interface</code>中的部分属性。还是之前的<code>User</code>,现在我们想剔除name属性,当然可以使用前述的方式</p>
<pre><code class="Typescript">type UserWithoutName=Pick<User,'address'>;// type NameOnlyUser = {address: string;}</code></pre>
<p>这样就很麻烦,特别是属性比较多的时候,更简便的就是直接用<code>Omit</code>:</p>
<pre><code class="Typescript">type UserWithoutName=Omit<User,'name'>;// type NameOnlyUser = {address: string;}</code></pre>
</li>
<li>
<p><code>NonNullable<T></code>使用频率:一般;</p>
<pre><code class="Typescript">type NonNullable<T> = T extends null | undefined ? never : T;</code></pre>
<p>主要用于过滤掉null和undefined两个基本类型的数据。</p>
</li>
<li>
<p><code>Parameters<T extends (...args: any) => any></code>使用频率:一般;</p>
<p>写到这里恰好就进入了type中比较有趣的地方了,为什么?因为之前的type都没有用到<code>infer</code>这个关键字,而之后的几个<code>type</code>全用到了,纯属巧合,不过infer确实是type进阶最重要的知识点之一。这个type获取了泛型T的函数参数:</p>
<pre><code class="Typescript">type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;</code></pre>
<p>具体来说,我们可以通过<code>react-router</code>里面常用的<code>withRoute</code>r函数举例,看看我们的<code>Parameters</code>返回结果(原理放在下一节):</p>
<pre><code class="Typescript">export function withRouter<P extends RouteComponentProps<any>>(component: React.ComponentType<P>): React.ComponentClass<Omit<P, keyof RouteComponentProps<any>>>;// 这是withRouter的函数签名,现在我们来拿他试试
type WithRouterParameters=Parameters<typeof withRouter>//type WithRouterParameters = [React.ComponentType<RouteComponentProps<any, StaticContext, any>>]</code></pre>
<p>这样,我们就顺利拿到了我们的参数type。</p>
</li>
<li>
<p><code>ConstructorParameters<T></code>获取构造函数参数类型,使用频率:一般;</p>
<pre><code class="Typescript">type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;</code></pre>
<p>这个看起来和上面的几乎一样,其实也差不多,原理是一模一样的。<br>我们还是用例子来说话,我们定义一个User的class,通过这个<code>type</code>来获取class的<code>constructor parameters</code></p>
<pre><code class="Typescript">class User {
static name: string
static address: string
static age: number
constructor(name: string, address: string, age: number) {
this.name = name;
this.address = address;
this.age = age
}
public sayHello() {
alert('hello from' + this.name)
}
};
type ConstructorParametersOfUser=ConstructorParameters<typeof User>//type ConstructorParametersOfUser = [string, string, number]</code></pre>
<p>轻松就拿到了实际的参数tuple:<code>[string, string, number]</code></p>
</li>
<li>
<p><code>ReturnType<T></code>获取函数返回值类型,使用频率:一般;</p>
<pre><code class="Typescript">type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;</code></pre>
</li>
<li>
<p><code>InstanceType<T></code>获取实例类型,使用频率:一般;</p>
<pre><code class="Typescript">type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;</code></pre>
</li>
</ul>
<h3>三、<code>extends</code> and <code>infer</code>
</h3>
<p>为什么要单独说说这两个关键字,仔细看过文章上面部分的同学会发现,几乎所有使用infer的地方,都有extends的身影。上面部分看完且理解的同学几乎不用继续看了,上面没看完,心态有点爆炸,脑子有点懵的同学,真是难为你们了,现在来讲 <em>重点</em>:</p>
<h4>3.1 extends(有条件类型)</h4>
<blockquote>TypeScript 2.8 introduces conditional types which add the ability to express non-uniform type mappings. A conditional type selects one of two possible types based on a condition expressed as a type relationship test:</blockquote>
<pre><code class="Typescript">T extends U ? X : Y</code></pre>
<p>大致意思就是:TypeScript 2.8引入了有条件类型,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一。再简化一点,若T能够赋值给U,那么类型是X,否则为Y。<br>这个好理解,我们经常<code>export class CustomComponent extends React.Component<any, any> { }</code>就是CustomComponent可赋值给React.Component(type)。</p>
<h4>3.2 infer(类型推断)</h4>
<blockquote>Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.</blockquote>
<p>文档意思是说,现在在有条件类型的<code>extends</code><strong>子语</strong>句中,允许出现<code>infer</code>声明,它会引入一个<em>待推断的类型变量</em>。 这个推断的类型变量可以在有条件类型的<code>true</code>分支中被引用。 允许出现<strong>多个</strong>同类型变量的infer。<br>还记得上面的<code>Parameters</code>、<code>ConstructorParameters</code>、<code>ReturnType</code>、<code>InstanceType</code>吗?他们正是利用了类型推断,获取各种待推断的类型变量。只要记住并理解了以下几点,你就已经完全掌握了infer:</p>
<ul>
<li>只能出现在有条件类型的<code>extends</code><strong>子语</strong>句中;</li>
<li>出现<code>infer</code>声明,会引入一个<em>待推断的类型变量</em>;</li>
<li>推断的类型变量可以在有条件类型的<code>true</code>分支中被引用;</li>
<li>允许出现<strong>多个</strong>同类型变量的infer。</li>
</ul>
<p>为了便于理解,我们先看这个小栗子:</p>
<pre><code class="Typescript">type GetTypeSimple<T>=T extends infer R ? R : never;</code></pre>
<p>emmmm?这是什么操作?<br>这个GetTypeSimple接收一个T作为参数来判断他的推断类型,显然,种瓜得瓜种豆得豆,传什么类型就是什么类型。</p>
<pre><code class="Typescript">type Test1 = GetTypeSimple<number>;//number
type Test2 = GetTypeSimple<string>;//string
type Test3 = GetTypeSimple<Array<number>>;//number[]
type Test4 = GetTypeSimple<typeof withRouter>;//<P extends RouteComponentProps<any, StaticContext, any>>(component: React.ComponentType<P>) => React.ComponentClass<Omit<P, "history" | "location" | "match" | "staticContext">, any></code></pre>
<p>不知各位看官有点感觉没有?<br>继续,来个稍微复杂点的栗子:<br>我们假设定义一个如下的类型,要求将他的实例属性type不为never的单独拿出来作为一个联合类型,使用<code>infer</code>来完成。</p>
<pre><code class="Typescript">class User {
public name: string
public address: string
public age: number;
public never:never;
constructor(name: string, address: string, age: number) {
this.name = name;
this.address = address;
this.age = age
}
public sayHello() {
alert('hello from' + this.name)
}
}</code></pre>
<p>期望的结果是返回:<code>string | number | (() => void)</code><br>看到这么个class,记性好的同学应该想到了上面的InstanceType,没错,这里我们借助它来推导返回的实例type;</p>
<pre><code class="Typescript">type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
type TypeOfUser=InstanceType<typeof User>//User</code></pre>
<p>然后对拿到的InstanceType进行属性类型的推导,最后过滤掉类型为never的属性即可</p>
<pre><code class="Typescript">type TypeOfUserWithoutNever={
[K in keyof TypeOfUser] : TypeOfUser[K] extends infer S ? S : never
}[keyof TypeOfUser]//`string | number | (() => void)`</code></pre>
<p>最后,我们把这两步合到一起:</p>
<pre><code class="Typescript">type GetUnionPropertiesWithoutNeverOfT<T extends new (args: any) => any> = T extends new (...args: infer R) => infer U
?
{
[K in keyof U]: U[K] extends infer S ? S : never
}[keyof U]
: never;
type TypeOfUserWithoutNever=GetUnionPropertiesWithoutNeverOfT<typeof User>//`string | number | (() => void)`</code></pre>
<p>看到这里的,基本上都能搞明白了,其实上面的<code>GetUnionPropertiesWithoutNeverOfT</code>我故意多写了一个推断类型R,验证了<em>允许出现<strong>多个</strong>同类型变量的<code>infer</code></em>。其实完全没用,去掉即可。<br>那么,是时候来检验一波你们到底有没有掌握上面的知识了。</p>
<p>下面我们掌声有请2018年12月份的一道来自<a href="https://link.segmentfault.com/?enc=FJZpyTA3IfR9NBWFEa0tww%3D%3D.wftyylQaXPWzHHMA5PN%2FqixswmN92KJS%2FUG5%2FdiCsdU%3D" rel="nofollow">LeetCode-OpenSource</a>的<a href="https://link.segmentfault.com/?enc=M1v5Itdc5KC%2FxvBbhkzXvA%3D%3D.1eEJw3MedkeKEITy1xJ%2FVXHv7CKIa9TCeI3Wrs8zW2mB%2Bvo%2FAm7uVk9JgSYinG5VyzAoCHfe4Ur5BwwNxECUW1vfgYuskrumpeqHIiK%2FZeM%3D" rel="nofollow">笔试题</a>:<br>假设有一个叫 EffectModule 的类</p>
<pre><code class="Typescript">class EffectModule {}</code></pre>
<p>这个对象上的方法只可能有两种类型签名:</p>
<pre><code class="Typescript">interface Action<T> {
payload?: T
type: string
}
asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>>
syncMethod<T, U>(action: Action<T>): Action<U></code></pre>
<p>这个对象上还可能有一些任意的非函数属性:</p>
<pre><code class="Typescript">interface Action<T> {
payload?: T;
type: string;
}
class EffectModule {
count = 1;
message = "hello!";
delay(input: Promise<number>) {
return input.then(i => ({
payload: `hello ${i}!`,
type: 'delay'
});
}
setMessage(action: Action<Date>) {
return {
payload: action.payload!.getMilliseconds(),
type: "set-message"
};
}
}</code></pre>
<p>现在有一个叫 <code>connect</code> 的函数,它接受 <code>EffectModule</code> 实例,将它变成另一个一个对象,这个对象上只有<code>EffectModule</code> 的同名方法,但是方法的类型签名被改变了:</p>
<pre><code class="Typescript">asyncMethod<T, U>(input: Promise<T>): Promise<Action<U>> // 变成了
asyncMethod<T, U>(input: T): Action<U> </code></pre>
<pre><code class="Typescript">syncMethod<T, U>(action: Action<T>): Action<U> //变成了
syncMethod<T, U>(action: T): Action<U></code></pre>
<p>connect 之后</p>
<pre><code class="Typescript">const effectModule = new EffectModule();
const connected: Connected = connect(effectModule);
type Connected = {
delay(input: number): Action<string>
setMessage(action: Date): Action<number>
};// 期望结果</code></pre>
<p>此处建议自己先试试解题,再来看我的思路和你的一不一样。</p>
<p>第一步,观察结果,发现除了函数属性,普通的属性都没了,看来我们要先过滤掉非函数属性</p>
<pre><code class="Typescript">const effectModule = new EffectModule();
const connected: Connected = connect(effectModule);
type PickFunctionProperties<T>={
[K in keyof T]:T[K] extends Function ? K:never
}[keyof T];
type FunctionProperties=PickFunctionProperties<EffectModule>//"delay" | "setMessage"</code></pre>
<p>然后通过Pick产出我们需要的只包含函数属性的类型</p>
<pre><code class="Typescript">type FunctionsLeftT<T>=Pick<T,PickFunctionProperties<T>>;
type FunctionLeftT1=FunctionLeftT<EffectModule>
<!-- type FunctionLeftT1 = {
delay: (input: Promise<number>) => Promise<{
payload: string;
type: string;
}>;
setMessage: (action: Action<Date>) => {
payload: number;
type: string;
};
} --></code></pre>
<p>紧接着,用我们今天所学,进行最关键的类型推导转换:<br>观察题目可以发现,两个函数签名参数和返回值都不太一样,所以我们需要先判断当前处理的函数是哪种类型,然后运用对应类型的转换规则就可以了,这里我写详细一些,方便大家真的搞懂这个点,<br>我们先来转换<code>delay</code>这个函数:</p>
<pre><code class="Typescript">type TransformDelay<T extends (args: any) => any> = T extends (input: Promise<infer S>) => Promise<Action<infer U>> ? (input: S) => Action<U> : never</code></pre>
<p>同理,我们再来转换<code>setMessage</code>这个函数:</p>
<pre><code class="Typescript">type TransformSetMessage<T extends (args: any) => any> = T extends (action: Action<infer V>) => Action<infer U> ? (action: V) => Action<U> : never</code></pre>
<p>然后,这两种类型的函数其实再来一个条件类型判断就完全可以写到一起,不信你看</p>
<pre><code class="Typescript">Method extends delay ? TransformDelay : TransformSetMessage</code></pre>
<p>于是我们可以写一个更加通用的type,假设我们命名为ConnectMethod</p>
<pre><code class="Typescript">type ConnectMethod<T extends (args: any) => any> =
T extends (input: Promise<infer S>) => Promise<Action<infer U>>
? (input: S) => Action<U>
: T extends (action: Action<infer V>) => Action<infer U>
? (action: V) => Action<U>
: never;</code></pre>
<p>最后处理掉<code>FunctionLeftT1</code>的函数属性就可以啦!</p>
<pre><code class="Typescript">type ConnectAllMethod<T> = {
[K in keyof T]: ConnectMethod<T[K]>
}</code></pre>
<p>所以最后,我们的Connect完成了,恭喜你通过面试(哈哈,别想太多)。</p>
<pre><code class="Typescript">type Connect<T> = ConnectAllMethod<FunctionLeftT<T>>
</code></pre>
<p>最后按照题目意思,我们的Connect应该长这样:</p>
<pre><code class="Typescript">type Connect = (module: EffectModule) => ConnectAllMethod<FunctionLeftT<EffectModule>>;
//具体使用
const connect:Connect// 省略函数具体实现。。。
//
const connected: Connected = connect(effectModule);</code></pre>
<p>完事儿,吃根辣条冷静会儿。</p>
<h3>四、总结</h3>
<p>到这儿基本上想说的都说完了,个人认为<code>Typescript</code>的类型掌握到这个程度,已经可以hold住大部分场景了,技多不压身,也许你现在不会用到条件类型判断不会用到类型推断,但是多掌握一点儿肯定是好事,万一就需要用了呢,react官方的types里面就有大约20处用到<code>infer</code>,<code>extends</code>自然是多不胜数了。</p>
React hooks实战总结
https://segmentfault.com/a/1190000020120456
2019-08-19T19:35:43+08:00
2019-08-19T19:35:43+08:00
懒懒的技术宅
https://segmentfault.com/u/lanlandejishuzhai
42
<h4>一、什么是hooks?</h4>
<p>react 于19年2月份在16.8版本中新增了hook这一特性,已经过去了半年多了,社区的各种文章解析页汗牛充栋,本文将结合自己的项目实践,对<code>react hooks</code>做一个全面的讲解,俗话说没吃过猪肉,还没见过猪跑吗?确实,可能大部分同学对hooks特性都有了自己的了解,但是在实际项目中使用又是另一回事了,实践出真知,这篇文章是自己对<code>react hooks</code>的理解,也是在上一个项目中使用<code>react hooks</code>的总结<br><strong>看着猪跑一千次,不如自己吃一次猪肉。</strong></p>
<ul>
<li>官方解释: <code>hook</code> 是 <code>React 16.8 </code>的新增特性。它可以让你在不编写 <code>class </code>的情况下使用 <code>state</code> 以及其他的 <code>React</code> 特性。</li>
<li>个人理解:让传统的函数组件<code>function component</code>有内部状态<code>state</code>的函数<code>function</code>。</li>
</ul>
<h4>二、为什么需要hooks?</h4>
<ul>
<li>在以往的react开发流程中,我们的自定义组件通常需要定义几个生命周期函数,在不同的生命周期处理各自的业务逻辑,有可能他们是重复的。</li>
<li>
<p>解决上一个问题我们通常通过 <code>mixins</code>(不推荐) 或者 <code>HOC </code>实现,在hooks出现之前,的确是非常好的解决途径,但是它不够好,为什么这么说呢?来看一下我们的一个具有中英文切换,主题切换同时<code>connect</code>一些<code>redux</code> 状态仓库里面的数据的全局组件<code>alert</code>:</p>
<pre><code>export default translate('[index,tips]')(withStyles(styles, { withTheme: true })(connect(mapStateToProps,mapDispatchToProps)(Alert)));</code></pre>
<pre><code>其实如果我们还可以将 `withTheme`也提取成一个高阶函数,那么我们的组件就将由现在的3层变成4层,实际使用的时候可能还有别的属性通过别的高阶函数添加,嵌套层级就会更深。给人明显的感觉就是不够直观。</code></pre>
</li>
<li>
<p>this指向问题,react绑定this有几种方式?哪一种方式性能相对来说好一些?</p>
<pre><code> 如果你答不上来,可以戳一下下面两个链接。</code></pre>
<ul>
<li>
<a href="https://link.segmentfault.com/?enc=CuNYy9XJctOCl8Wbmjc%2BiQ%3D%3D.GR13PIlWZxAh8vErU6ExJg2zU4lQP1u5swcK%2BkYoAjV2w4x%2F8Wu56UbUaLZwAIDIktJPGhrX8aOC1z2kkLs%2FUA%3D%3D" rel="nofollow">React事件处理</a>。</li>
<li>
<a href="https://link.segmentfault.com/?enc=F0lFCCVNIWORrMpigQRnlQ%3D%3D.MKn%2BB3e5EQ838DvvmNFPV6yrEnDz8S8ZLmVfRmtCxy6F4UY7CQpx4gFftjB3x0cn" rel="nofollow">React.js绑定this的5种方法</a>。</li>
</ul>
</li>
<li>
<p>hook 只能在FunctionComponent内部使用,而相比<code>ClassComponent</code>,传统的<code>FunctionComponent(FC)</code>具有更多的优势,具体体现在:</p>
<ul>
<li>FC 容易测试,相同的输入总是有相同的输出,</li>
<li>FC 其实就是普通的<code>javascript</code>函数,相比于<code>ClassComponent</code>,具有潜在的更好的性能。</li>
<li>FC 没有生命周期函数,更容易<code>debug</code>。</li>
<li>FC 具有更好的可重用性。</li>
<li>FC 可以减少代码耦合。</li>
<li>
<a href="https://link.segmentfault.com/?enc=FjzCCkmf2R0bx5VrjPQBxg%3D%3D.%2BKkxbXvmS3KAiD6couaIteiOSokkrV1mBgyicQokIl7TotzJWPCAxr5o9Oij1Y8XyRMckYW4bkiQ07B3TVEzLgIQBa8n3QqGchwKWCfsDsI%3D" rel="nofollow">September 10th, 2018 Comments React Functional or Class Components: Everything you need to know</a>。</li>
<li>
<a href="https://link.segmentfault.com/?enc=7R03CsXZugrfrexahJAnsA%3D%3D.fOSwzMupBnPb4EpzYMVcF%2FkIIWWk7%2FPrX4ve9L5tDEjV3LomTRnOpHcSsR1DZLRBGHrnd33UzhzScdr%2B%2BZq%2BQPKzmQHyoD4Ma1NbEcZIxIssMDkBehmvWkr70jewesrw" rel="nofollow">45% Faster React Functional Components, Now</a>。</li>
<li>
<code>FC</code>有更多的优势,但是他没有生命周期,也没有自己的内部状态,我们需要复杂的状态管理机制的时候,不得不转向<code>ClassComponent</code>。 FC现有的这些问题,我们能轻松结合<code>hook</code>解决。</li>
</ul>
</li>
</ul>
<h4>三、useState hook 的执行过程追踪</h4>
<ul>
<li>
<p>React目前官方支持的hook有三个基础Hook:<br><code>useState</code>,<br><code>useEffect</code>,<br><code>useContext</code>,<br> 和几个额外的 Hook:<br><code>useReducer</code>,<br><code>useCallback</code>,<br><code>useMemo</code>,<br><code>useRef</code>,<br><code>useImperativeHandle</code>,<br><code>useLayoutEffect</code>,<br><code>useDebugValue </code>,<br> 他们的作用各不相同,但是可以这么总结一下:<strong>让<code>Function Component</code>有状态(state)</strong>,流氓不可怕,就怕流氓有文化。当我们给比较有优势的FC 插上<code>state</code>的翅膀之后,他就要起飞了。原来<code>ClassComponent</code>能干的事情他也能干起来了,加上前文分析的优势,还干的更加有声有色。这里我们使用<code>useState</code>做一个全面的解析,<br> 首先我们来看一下一个简单的的计数器,点击click 按钮,state加1并渲染到页面上:</p>
<p><code>ClassComponent</code>实现:</p>
<pre><code class="javascript">import React from 'react';
interface ITestState {
count: number;
}
class Test extends React.Component<{}, ITestState> {
constructor(props: {}) {
super(props);
this.state = {
count: 0
};
}
public handleClick = () => {
const { count } = this.state;
this.setState({ count: count + 1 });
}
public render() {
return (
<>
<div>{this.state.count}</div>
<button onClick={this.handleClick}>click</button>
</>
);
}
}
export default Test;</code></pre>
<p><code>hooks</code>实现:</p>
<pre><code class="javascript">import React, { useState } from 'react';
const Test: React.FunctionComponent<{}> = () => {
const [count, setCount] = useState<number>(0);
return (
<>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>click</button>
</>
);
};
export default Test;</code></pre>
<ul><li>
<p>对比两种实现,直观感受是代码变少了,没错,也不用关心this指向了,<code>ClassComponent</code>里面通过<code>class fields</code>正确绑定回调函数的<code>this</code>指向,使得我们在handleClick函数中能正确的访问<code>this</code>,并调用<code>this.setState</code>方法更新<code>state</code>。</p>
<pre><code class="javascript">public handleClick = () => {
const { count } = this.state;
this.setState({ count: count + 1 });
}</code></pre>
</li></ul>
</li>
<li>
<p>深入源码分析hooks,这里我们以刚使用过的hook <code>useState</code>为例,看看他是怎么管理我们的FC state的。</p>
<pre><code class="javascript"> export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}</code></pre>
<p>这个函数接收一个参数<code>initialState: (() => S) | S</code>,初始state的函数或者我们的state初始值。<br> 然后调用<br><code>dispatcher.useState(initialState);</code>,这里我们看一下<code>dispatcher</code>是怎么来的:</p>
<pre><code class="javascript">function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
...
return dispatcher;
}</code></pre>
<p>发现是通过<code>ReactCurrentDispatcher.current</code>得到,那<code>ReactCurrentDispatcher</code>又是何方神圣呢?<br> 我们进一步看看它怎么来的</p>
<pre><code class="typescript">import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;</code></pre>
<p>根据type,我们可以判断dispatcher的类型是<code>react-reconciler/src/ReactFiberHooks</code>里面定义的<a href="https://link.segmentfault.com/?enc=p7mNcCLX5VMYhm0twZ%2B3nw%3D%3D.xFOLhcAynlFIKiTYPILCCF%2FtZ82N0bvBmDRbFNb%2BWnk97U4S2%2FxXheKvM%2BXtme0ThbXjqdFI9FuOE43wFsK%2FQVhjiJV8TZIOoNiHvIehq0u1ONE4xVQ4fb5g1ZMgn10KnOYyf7RTYRtVyzk7Y0fG1KSqehMdGt6DS7U1umCkJ00WwZ25T4BrgWRmt1InuJr1" rel="nofollow">Dispatcher</a>,可以看到这个current属性是个null。那它是什么时候被赋值的呢?<br> 我们来看看functionComponent的render过程<code>renderWithHooks</code>,</p>
<pre><code class="typescript">export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any{
....
if (__DEV__) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
} else {
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
}</code></pre>
<p>这里react源码根据<code>nextCurrentHook</code>做了一些判断,我移除掉了,只关注<code>ReactCurrentDispatcher.current</code>的值,可以看到它的取值分为两种,<code>HooksDispatcherOnMount</code>和 <code>HooksDispatcherOnUpdate</code>分别对应<code>mount/update</code>两个组件状态;这里我们先看<a href="https://link.segmentfault.com/?enc=2eIa9Raf%2B29hPjKJG%2FvIXQ%3D%3D.No6VVfhO%2Bq2x5FMWJAIQDAzrNtTya%2Ff9767f0LumwgFkYCUD%2FPa90wnE1Sg3LMKr7pO29FPwpwZskbDtYzMf2XxCXVOrN%2FD4r3jvqhzyx8xSBF%2B9vSZctveFgXE37WL07Jia3hsg%2Bo1Io44M4hfF%2F2xsunDJGxAKwfY8zxo8UljsBmshk4zpy4pcSe%2FHh%2FzW" rel="nofollow"><code>HooksDispatcherOnMount</code></a>:</p>
<pre><code class="typescript">const HooksDispatcherOnMount: Dispatcher = {
...
useState: mountState,
...
};</code></pre>
<p>这就是我们寻寻觅觅的<code>Dispatcher</code>的长相,最终我们<code>useState</code>在组件mount的时候执行的就是这个<a href="https://link.segmentfault.com/?enc=QB40Mm8PA5xajmQP9gcSpw%3D%3D.qkv4jm2JeSWXZVIwpqtBaLp9fsxng2BR3yXW6PwWCCPeZw110TDWLHtPgYqHwAwuOlfTJA1E4X%2FkHTtE%2FQxYmTQv32klHypu5YzJCLOUfamvFu%2ByNtShTAOALDVxZM836ncVw3W3dipQuF%2F%2FIbWUvFaJz%2BGEr4TCKRrzgHXxGMmOZiLFi1slWSYyIW6zR5zO" rel="nofollow"><code>mountState</code></a>了,那我们就迫不及待如饥似渴的来看看<code>mountState</code>又做了什么吧。</p>
<pre><code class="typescript">function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber),
queue,
): any));
return [hook.memoizedState, dispatch];
}</code></pre>
<p>进入这个函数首先执行的<a href="https://link.segmentfault.com/?enc=dufp8hTdPs0nBVeETA1LxQ%3D%3D.W%2FjRlMcd4HgCYDytGQi%2BQFzveziRu%2FPng0Ko6xZY3CQAdIYzBpYEslQ9T%2FH3DF50OhApGr9LmWuiUKZvDccbieZky839JOZ8aTXRgYE3Fpr2dBrojLWpts1cPtjteKlYzdwed0ldISVbWG7AohzLydP69OuMZE4eFgokQDQtyEC%2BnQC8fKzFlUPMVZ%2BxJXRx" rel="nofollow"><code>mountWorkInProgressHook()</code></a>获取到当前的<code>workInProgressHook</code>,看这个名字就知道他是和<code>workInProgress</code>分不开了,这个<code>workInProgress</code>代表了当前正在处理的fiber,<a href="https://link.segmentfault.com/?enc=Hqk%2BgLqt52UlxE13IDVRow%3D%3D.GA4S2tNB2PKZIvEc6ckRwBTmgHcy%2FYs6R%2FyUpm4cOQquMDZxL%2BHQWbioo8QFXtixvqJVZ3ybiYr%2B60jt6zkkiJXuifG0EQaxXo3rLbmz2pL2xaU1KNQ9ICpB5qphrforsNrnC4X4tDoFnafo2l0CYxtHCgpTwN9kVQS6R5gl%2BYcXstv4P8yG%2FF%2B%2FxITUqaih" rel="nofollow"><code>fiber</code></a>是当前组件的需要完成或者已经完成的work的对象,也可以理解为我们的这个正在执行mountState的组件的各种数据和状态的集合。我们来具体的看一下mountWorkInProgressHook的执行逻辑:</p>
<pre><code class="typescript">function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}</code></pre>
<p>判断当前<code>fiber</code>的<code>workInProgressHook</code>是不是<code>null</code>,如果是,将全新的hook赋值给全局的<code>workInProgressHook</code>和<code>firstWorkInProgressHook</code>,否则,将初始值赋值给<code>workInProgressHook</code>。相当于<code>mountState</code>里面的hook值就是</p>
<pre><code class="typescript"> const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};</code></pre>
<p>实际上,workInProgressHook是这样的一个链表结构,React里面广泛使用了这样的结构存储副作用。</p>
<pre><code class="typescript">{
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: {
...
next: {
...
next: {
next: {...},
...
},
},
}
}</code></pre>
<p>继续往下看:</p>
<pre><code class="typescript">if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;</code></pre>
<p>useState接收的参数类型如果是函数,这里就会执行传进来的函数获取<code>initialState</code>,赋值给<code>hook.memoizedState = hook.baseState</code>这两个属性,再往下,建立了当前<code>hook</code>的更新队列<code>queue:<UpdateQueue></code>,这个我们后续再讲,这里暂时不用知道。继续往下看,是我们修改state的回调函数,通常是setState,通过改变<code>dispatchAction</code>的this指向,将当前render的fiber和上面创建的queue作为参数传入,当我们执行<code>setState</code>的时候实际上调用的就是这里的<code>dispatchAction</code>,最后一行:<br><code>return [hook.memoizedState, dispatch];</code><br> 将<code>state</code>和<code>setState</code>以数组的形式返回,这也是我们使用<code>useState hook</code>的正确姿势。到这里相信大家都很清楚了,<code>useState</code>通过将我们的初始<code>state</code>暂存到<code>workInProgressHook</code>的<code>memoizedState</code>中,每次更新的时候通过<code>dispatchAction</code>更新<code>workInProgressHook</code>。<br> 我们回过头来再看看刚才没深入过的<code>queue</code>,通过类型我们可以知道他是<code><UpdateQueue></code>,具体看看<a href="https://link.segmentfault.com/?enc=3d%2FU%2FQd2H9iBOMNu2cw1KQ%3D%3D.W8JMf8JftwO4vhjSvWLNrCoV%2FVyhDwrANoDhIG8jWx0emSOh0dSzuZwpZpy1rxPCM7De%2FYHe8XVoS7MjB89kbxL9mo4qTNt%2FnX%2F1uyeaWtZL3aP3sEw7qmY3ZiHs9lhbaC%2FxGnMrknqxQnAZtiQksev81v8ZeQXBwqF4O2MocJoK0X%2BmXLK9Yh9EYcxebjEP" rel="nofollow"><code><UpdateQueue></code></a>的定义:</p>
<pre><code class="typescript">type UpdateQueue<S, A> = {
last: Update<S, A> | null,
dispatch: (A => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};</code></pre>
<p>看到这个结构,熟悉<code>react fiber</code>的同学已经心中有数了,它的last属性是一个链表,用来存储当前hook的变化信息,能够通过<a href="https://link.segmentfault.com/?enc=hLTSumnAVYyY7DzsasSiZg%3D%3D.%2FC9qgZNoT0Tt2LG7Z7XVCP457XhGXdSvCob5F8S2mZd6NVfORLc8kKlaA%2BGObOEzySPXDaQKREj199rnHIxOr7zTPD26%2FNkIlWaf59ewIWVrEOx6IpcrGH8Vhz0WcYKtLjzRtLicBOEYFmTBfVi2IXVx0Kizm0yh1WxAmJ4aJ7%2BRBB2uj6rQgDq7TDsJD8s6" rel="nofollow"><code>next</code></a>迭代处理所有变更信息和状态。这里我们就到此为止,感兴趣的同志可以自行深入琢磨,对于这个<code>hook</code>,掌握到这里已经够了,很多文章说<code>useState</code>和<code>useReducer</code>的基友关系,从这里我们就看出来了,<code>useState</code>最终使用的也是<code>useReducer</code>一致的api,通过类似<code>redux</code>的理念,通过<code>dispatchAction</code>修改<code>state</code>,有兴趣的同志可以看这里<a href="https://link.segmentfault.com/?enc=us7sjOACnzqPvqtXMmTAqg%3D%3D.4dLersuEHF%2BdvFL2bDCc2HhBDa0tHdcaLh53WjH8cemGKoNbyUbZsP7zMm6ijnnqwI3E%2ByauZ0R9MCZdJp9l1xyLMfxfTZlnl%2FpoCq2Y83qi5I6IiAnCsprM9oypGyJVG2LXcmLhnoZmmhXBYWy1jqS9eFEPNR39%2F5sb2ENuFNRaH0lnTHpoMVeiOkxhwpLe" rel="nofollow"><code>useReducer</code></a>源码;</p>
<ul><li>其他的hook就不展开了,感兴趣的同志可以去看看源码,欢迎交流探讨。</li></ul>
</li>
</ul>
<h4>四、自定义hooks</h4>
<p>阿西吧,东拉西扯的到了这块最有趣的地方。这块以项目中实际用到的几个hook来举例说明。先说一下,其实官方的hook已经很多很全了,状态我们可以<code>useState</code>,复杂多状态我们可以用<code>useReducer</code>,共享和传递状态可以使用<code>useContext</code>,引用组件、引用状态可以<code>useRef</code>,组件render完成之后的操作通过<code>useEffect</code>完成...还有其他几个hook,那么我们为什么还需要自定义hooks呢?</p>
<ul>
<li>其实,自定义hook也是基于官方的hook进行组合,逻辑复用,业务代码解耦抽象后进一步提炼出来的具备一定功能的函数。它应当具有一定条件下的的通用性,可移植性。</li>
<li>目前的hook可能并不十分契合我们的需求,我们需要进行二次加工,成为我们的业务hook, 官方推荐自定义hook命名以use开头。</li>
</ul>
<h5>useWindowLoad</h5>
<ul><li>
<p>在项目过程中有这样一个业务场景,许多个地方(几十到几百不等)需要监听window.onload事件,等待onload后执行特定的业务逻辑,如果window已经load,需要返回当前的,同时希望拿到window loaded的状态,处理后续的其他逻辑,这里我们将业务逻辑用这个函数表示一下:</p>
<pre><code class="javascript">const executeOnload:()=>{alert('alert after loaded')}</code></pre>
<p>传统的实现思路:</p>
<pre><code class="typescript">{
if(window.loaded)executeOnload();return;
const old = window.onload;
window.onload = () => {
window.loaded = true;
executeOnload();
old && old();
};
}</code></pre>
<p>在使用我们的自定义hook useWindowLoad之后</p>
<pre><code class="typescript">const isWindowLoaded= useWindowLoad(executeOnload)</code></pre>
<p>每一处需要监听的地方都变得十分简单有没有,话不多说,直接上码:</p>
<pre><code class="typescript">export default function useWindowLoad(func?: (params?: any) => any): boolean {
useEffect(() => {
let effect: (() => void) | null = null;
const old = window.onload;
window.onload = () => {
effect = func && func();
old && old();
window.loaded = true;
};
return () => {
if (typeof effect === 'function') {
effect();
}
};
});
return window.loaded;
})</code></pre>
<p>最后,我们返回load状态。这里我们主要使用了useEffect这个hook,并在接受的参数的返回值中清除了对应的副作用。useEffect在每次组件render完成后执行,具体使用参考<a href="https://link.segmentfault.com/?enc=2W0bI2jhKR1%2FxMaharskhQ%3D%3D.0JJO9%2B7lJHKyHP8ypAXgM9NKetc%2B8aHt7LSx7gJznyEs54k9sL65gu%2FFGdAxIawpnkSt47%2FxK%2FgqXxLnNYQSFg%3D%3D" rel="nofollow">文档</a>。注意,<strong>副作用的清除很重要,因为我们不能保证传入的回调函数不会带来副作用,所以使用时应该传递return一个函数的函数作为参数</strong></p>
</li></ul>
<h5>useMessage</h5>
<pre><code>这样一个场景:我们需要一个全局的消息提示,已经写好了一个全局组件,并通过redux管理状态控制Message的显示和隐藏,这其实是一个很常见的功能,在使用hook之前,我们的实现可能是这样的:
</code></pre>
<pre><code class="typescript">
import React from 'react';
import { connect } from 'react-redux';
import { message } from './actions';
import Errors from './lib/errors';
interface IDemoProps {
message(params: Message): void;
}
const mapStateToProps = (state: IStore) => ({});
const mapDispatchToProps = (dispatch: any) => ({
message: (params: Message) =>dispatch(message(params))
});
class Demo extends React.Component<IDemoProps, {}> {
public handleClick() {
this.props.message({ content: Errors.GLOBAL_NETWORK_ERROR.message, type: 'error', duration: 1600, show: true });
}
public render() {
return <button className='demo' onClick={this.handleClick}>click alert message</button>;
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Demo);
</code></pre>
<pre><code>每次我们要使用就得mapDispatchToProps,引入action,connect,...繁琐至极,我们也可以用**高阶组件**包装一下,透传一个message函数给需要的子组件,这里我们使用自定义hook来解决,先看看最终达到的效果:</code></pre>
<pre><code class="typescript"> import React from 'react';
import Errors from './lib/errors';
const Demo: React.FC<{}> = () => {
const message = useMessage();
const handleClick = () => {
message.info(content: Errors.GLOBAL_NETWORK_ERROR.message);
};
return <button className='demo' onClick={handleClick}>alert message</button>;
};
export default Demo;
</code></pre>
<pre><code>简单了许多,每次需要全局提示的地方,我们只需要通过`const message = useMessage();`
然后再组件内部任何地方使用`message.info('content')`,`message.error('content')`,`message.success('content')`,`message.warn('content')`即可,再也不关心action,redux connect等一系列操作。
我们来看看这个逻辑如何实现的:</code></pre>
<pre><code class="typescript"> import { useDispatch } from 'react-redux';
import { message as alert } from '../actions/index';
/**
* @param {type}
* @return:
*/
export default function useMessage() {
const dispatch = useDispatch();
const info = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'info' }));
const warn = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'warn' }));
const error = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'error' }));
const success = (content: Partial<Message>['content']) => dispatch(alert({ content, duration: 3000, show: true, type: 'success' }));
const tmpMessage = {
success,
info,
warn,
error
};
// 注意,不能直接返回tmpMessage,会导致有依赖message的effect重复执行(每次useMessage返回了不同的引用地址)。
const [message] = useState(tmpMessage);
return message;
}</code></pre>
<pre><code>我们内部使用useDispatch拿到dispatch,封装了四个不同功能的函数,直接对外提供封装好的对象,就实现使用上了类似antd message组件的功能,哪里需要哪里useMessage就可以开心的玩耍了。
- 项目中还有其他的自定义hook,但是思路很上面两个一致,提取共性,消除副作用。 这里给大家推荐一个自定义的hook的一个[站点](https://usehooks.com)。我从这里吸收了一些经验。
</code></pre>
<h4>五、总结</h4>
<ul>
<li>文章写得杂乱,各位多多包含,有不对的地方欢迎指正。限于篇幅太长,其他hook就不一一细说了,有兴趣,有问题的同学欢迎交流探讨。</li>
<li>距离hook提出大半年了,很多第三方库也逐渐支持hook写法,现在使用起来遇到坑的机会不多了。总体写起来比<code>class</code>写法舒服,不过对几个基础<code>hook</code>,特别是<code>useState</code>,<code>useEffect</code>的掌握十分重要,结合setTimeout,setInterval往往会有意料之外的惊喜,网上文章也很多。本项目还没写完,目前看来,选择React hook是对的,过程中也学习了不少知识。趁年轻,折腾吧!</li>
</ul>
H5拍照、选择图片上传组件核心
https://segmentfault.com/a/1190000017564833
2018-12-29T16:57:10+08:00
2018-12-29T16:57:10+08:00
懒懒的技术宅
https://segmentfault.com/u/lanlandejishuzhai
26
<h2>背景</h2>
<p>前段时间项目重构,改成SSR的项目,但之前用的图片选择上传组件不支持SSR(server-side-render)。遂进行了调研,发现很多的工具。但有的太大,有的使用麻烦,有的不满足使用需求。决定自己写一个h5移动端图片上传组件。图片上传是一个比较普遍的需求,PC端还好,移动端就不是特别好做了。下面将过程中一些重点的问题进行简单的记录。</p>
<h2>重点</h2>
<h3>1.关于input</h3>
<hr>
<ul><li>
<p>选择功能使用<code><input></code>标签实现。属性<code>accept='image/*'</code>,:capture表示,可以捕获到系统默认的设备,比如:<code>camera</code>--照相机;<code>camcorder</code>--摄像机;<code>microphone</code>--录音。如果设置了capture="camera",那么默认使用相机,存在部分机型无法调用相机的问题,我们这里不设置。允许多选<code>multiple</code>,加上onchange事件的回调函数。最终input大概长这个样子:</p>
<pre><code><input type='file'
className={classes.picker}
accept='image/*'
multiple
capture="camera"
onChange={this.onfileChange} />
</code></pre>
<pre><code>当然,这个input很丑,我们可以通过设置`opacity:0`,通过定位将我们需要的选择按钮样式覆盖上去。让它更加迷人一些。
</code></pre>
</li></ul>
<h3>2.关于选择预览功能</h3>
<hr>
<ul><li>
<p>选择图片后能预览是一个常见的功能,这里抛开样式,只说代码实现。在onchange的回调函数中,我们能通过<code>e.target.files</code>拿到所选择的文件,但是文件是无法展示在页面上的,通常的做法是使用<code>reader.readAsDataURL(file)</code>转为<code>base64</code>然后展示在页面上。我这边采用九宫格展示,每个图片是一个<code>canvas</code>。考虑到不同图片宽高比的问题,我先通过<code>reader.readAsDataURL(file)</code>拿到base64文件。然后创建一个通过九宫格的canvas宽高比绘制图像,使图片内容在不失真的情况下铺满整个canvas。</p>
<pre><code>fileToCanvas (file, index) {//文件
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (event) => {
let image = new Image();
image.src = event.target.result;
image.onload = () => {
let imageCanvas = this['canvas' + index].getContext('2d');
let canvas = { width: imageCanvas.canvas.scrollWidth * 2, height: imageCanvas.canvas.scrollHeight * 2 };
let ratio = image.width / image.height;
let canvasRatio = canvas.width / canvas.height;
let xStart = 0; let yStart = 0; let renderableWidth; let renderableHeight;
if (ratio > canvasRatio) {
// 横向过大,以高为准,缩放宽度
let hRatio = image.height / canvas.height;
renderableHeight = image.height;
renderableWidth = canvas.width * hRatio;
xStart = (image.width - renderableWidth) / 2;
}
if (ratio < canvasRatio) {
// 横向过小,以宽为准,缩放高度
let wRatio = image.width / canvas.width;
renderableWidth = image.width;
renderableHeight = canvas.height * wRatio;
yStart = (image.height - renderableHeight) / 2;
}
imageCanvas.drawImage(image, xStart, yStart, renderableWidth, renderableHeight, 0, 0, canvas.width * 2, canvas.height);
};
};
}</code></pre>
</li></ul>
<h3>3.文件上传的扩展名获取</h3>
<hr>
<ul><li>部分机型拍照时文件通过onchange事件拿到的文件是<code>blob</code>(小米6等)此时通过<code>blob.type</code>手动判断扩展名。</li></ul>
<h3>4.ios拍照方向获取</h3>
<hr>
<ul><li>
<p>当ios拍照上传后发现文件被旋转了,本地文件确是正常的,这个问题的原因这里不作详细解释。有兴趣的可以搜一下。所以我们需要检测orientation,并将图像旋转回正常方向。获取orientation有现成的很多库如<a href="https://link.segmentfault.com/?enc=DUjSP8WoaTahsLlVi68G0Q%3D%3D.ty%2Bj%2BsllGid4loX8ivft9%2BpbB%2F%2BNGX4TGKXi%2F846JXNEIQmtinvg7YNIyExQqFo8" rel="nofollow">Exif.js</a>。但是这个库有些大,为了这个小需求引入似乎不太值得。<a href="https://link.segmentfault.com/?enc=zNyuWLvXFDnDYLoH5uydKw%3D%3D.N8vk5Qf%2BCfv2r8uDq9D8KNdXp%2FZNUZlQnwvoPW6%2FakFXUvLuHKiuS9MxMx3kKfD9N9K%2FcZHSkTpjB%2F67vJaQa2Ujy2Ja%2BHcYu0HFvyNJZttqiJPpJQ1DdXuj5eTS1KdNrOFI8VVOvacJmnR5Dqk%2Fjg%3D%3D" rel="nofollow">stackoverflow</a>上有很多现成的获取图片方向的代码。<br> 稍微改了下:</p>
<pre><code>getOrientation (file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = function (e) {
//e.target.result为base64编码的文件
let view = new DataView(e.target.result);
if (view.getUint16(0, false) !== 0xffd8) {
return resolve(-2);
}
let length = view.byteLength;
let offset = 2;
while (offset < length) {
let marker = view.getUint16(offset, false);
offset += 2;
if (marker === 0xffe1) {
let tmp = view.getUint32(offset += 2, false);
if (tmp !== 0x45786966) {
return resolve(-1);
}
let little = view.getUint16(offset += 6, false) === 0x4949;
offset += view.getUint32(offset + 4, little);
let tags = view.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++) {
if (view.getUint16(offset + i * 12, little) === 0x0112) {
return resolve(view.getUint16(offset + i * 12 + 8, little));
}
}
} else if ((marker & 0xff00) !== 0xff00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
return resolve(-1);
};
reader.readAsArrayBuffer(file.slice(0, 64 * 1024));
});
}</code></pre>
<p>//返回值:1--正常,-2--非jpg,-1--undefined</p>
</li></ul>
<h3>5.ios照片方向修正</h3>
<hr>
<p>正常的图像<code>orientation</code>应该是1,于是我们将<code>file</code>转为<code>canvas</code>,使用<code>canvas</code>的<code>transform</code>方法对canvas进行变换, <a href="https://link.segmentfault.com/?enc=TrljkiOAwIiXBV6zMnlMgQ%3D%3D.MmtMwLhbv42VtvnCbKCU0h1yjvXcN8cGZH5%2BaFeLwCtka0wnYHIaAThL2ErzQxuQcS%2BCmvaJqK0CHQCngQ%2B5R%2FtAhcFJFWHR8iItrne8AlFyxGkwwQjIyh6vQLey0fOLlAHo6yRGoUyxzB8YIoWaJA%3D%3D" rel="nofollow">参考</a>。最后通过<code>canvas.toDataURL('')</code>拿到base64编码的方向正常的base64图片,再将base64转为blob进行上传;</p>
<pre><code>
//重置文件orientation
resetOrientationToBlob (file, orientation) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (event) => {
let image = new Image();
image.src = event.target.result;
image.onload = () => {
let width = image.width;
let height = image.height;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
if (orientation > 4 && orientation < 9) {
canvas.width = height;
canvas.height = width;
} else {
canvas.width = width;
canvas.height = height;
}
switch (orientation) {
case 2:
ctx.transform(-1, 0, 0, 1, width, 0);
break;
case 3:
ctx.transform(-1, 0, 0, -1, width, height);
break;
case 4:
ctx.transform(1, 0, 0, -1, 0, height);
break;
case 5:
ctx.transform(0, 1, 1, 0, 0, 0);
break;
case 6:
ctx.transform(0, 1, -1, 0, height, 0);
break;
case 7:
ctx.transform(0, -1, -1, 0, height, width);
break;
case 8:
ctx.transform(0, -1, 1, 0, 0, width);
break;
default:
ctx.transform(1, 0, 0, 1, 0, 0);
}
ctx.drawImage(image, 0, 0, width, height);
let base64 = canvas.toDataURL('image/png');
let blob = this.dataURLtoBlob(base64);
resolve(blob);
};
};
});</code></pre>
<p>}</p>
<h2>最后</h2>
<ul>
<li>图片上传,这部分应该比较easy。通过FormData的形式将文件上传即可。以上代码仅是部分功能的伪代码,不是所有功能的最终实现。</li>
<li>能折腾就折腾一下,最后你会发现,学到了很多东西,但还是别人的轮子好用2333。</li>
</ul>