SegmentFault 随笔最新的文章
2023-10-07T08:34:43+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
聊聊前端框架的未来 Signals
https://segmentfault.com/a/1190000044279252
2023-10-07T08:34:43+08:00
2023-10-07T08:34:43+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<blockquote>Signals 在目前前端框架的选型中遥遥领先!</blockquote><p>国庆节前最后一周在 Code Review 新同学的 React 代码,发现他想通过 memo 和 useCallback 只渲染被修改的子组件部分。事实上该功能在 React 中是难以做到的。因为 React 状态变化后,会重新执行 render 函数。也就是在组件中调用 setState 之后,整个函数将会重新执行一次。</p><p>React 本身做不到。但是基于 Signals 的框架却不会这样,它通过自动状态绑定和依赖跟踪使得当前状态变化后仅仅只会重新执行用到该状态代码块。</p><p>个人当时没有过多的解释这个问题,只是匆匆解释了一下 React 的渲染机制。在这里做一个 Signals 的梳理。</p><h2>优势</h2><p>对比 React,基于 Signals 的框架状态响应粒度非常细。这里以 Solid 为例:</p><pre><code class="js">import { createSignal, onCleanup } from "solid-js";
const CountingComponent = () => {
// 创建一个 signal
const [count, setCount] = createSignal(0);
// 创建一个 signal
const [count2] = createSignal(666);
// 每一秒递增 1
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 组件销毁时清除定时器
onCleanup(() => clearInterval(interval));
return (
<div>
<div>
count: {count()}
{console.log("count is", count())}
</div>
<div>
count2: {count2()}
{console.log("count2 is", count2())}
</div>
</div>
);
};</code></pre><p>上面这段代码在 count 单独变化时,只会打印 count,压根不会打印 count2 数据。</p><p>控制台打印如下所示:</p><ul><li>count is 0</li><li>count2 is 666</li><li>count is 1</li><li>count is 2</li><li>...</li></ul><p>从打印结果来看,Solid 只会在最开始执行一次渲染函数,后续仅仅只会渲染更改过的 DOM 节点。这在 React 中是不可能做到的,React 是基于视图驱动的,状态改变会重新执行整个渲染函数,并且 React 完全无法识别状态是如何被使用的,开发者甚至可以通过下面的代码来实现 React 的重新渲染。</p><pre><code class="js">const [, forceRender] = useReducer((s) => s + 1, 0);</code></pre><p>除了更新粒度细之外,使用 Signals 的框架心智模型也更加简单。其中最大的特点是:开发者完全不必在意状态在哪定义,也不在意对应状态在哪渲染。如下所示:</p><pre><code class="js">import { createSignal } from "solid-js";
// 把状态从过组件中提取出来
const [count, setCount] = createSignal(0);
const [count2] = createSignal(666);
setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 子组件依然可以使用 count 函数
const SubCountingComponent = () => {
return <div>{count()}</div>;
};
const CountingComponent = () => {
return (
<div>
<div>
count: {count()}
{console.log("count is", count())}
</div>
<div>
count2: {count2()}
{console.log("count2 is", count2())}
</div>
<SubCountingComponent />
</div>
);
};</code></pre><p>上述代码依然可以正常运行。因为它是基于状态驱动的。开发者在组件内使用 Signal 是本地状态,在组件外定义 Signal 就是全局状态。</p><p>Signals 本身不是那么有价值,但结合派生状态以及副作用就不一样了。代码如下所示:</p><pre><code class="js">import {
createSignal,
onCleanup,
createMemo,
createEffect,
onMount,
} from "solid-js";
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 计算缓存
const doubleCount = createMemo(() => count() * 2);
// 基于当前缓存
const quadrupleCount = createMemo(() => doubleCount() * 2);
// 副作用
createEffect(() => {
// 在 count 变化时重新执行 fetch
fetch(`/api/${count()}`);
});
const CountingComponent = () => {
// 挂载组件时执行
onMount(() => {
console.log("start");
});
// 销毁组件时执行
onCleanup(() => {
console.log("end");
});
return (
<div>
<div>Count value is {count()}</div>
<div>doubleCount value is {doubleCount()}</div>
<div>quadrupleCount value is {quadrupleCount()}</div>
</div>
);
};</code></pre><p>从上述代码可以看到,派生状态和副作用都不需要像 React 一样填写依赖项,同时也将副作用与生命周期分开(代码更好阅读)。</p><h2>实现机制</h2><p>细粒度,高性能,同时还没有什么限制。不愧被誉为前端框架的未来。那么它究竟是如何实现的呢?</p><p>本质上,Signals 是一个在访问时跟踪依赖、在变更时触发副作用的值容器。</p><p>这种基于响应性基础类型的范式在前端领域并不是一个特别新的概念:它可以追溯到十多年前的 Knockout observables 和 Meteor Tracker 等实现。Vue 的选项式 API 也是同样的原则,只不过将基础类型这部分隐藏在了对象属性背后。依靠这种范式,Vue2 基本不需要优化就有非常不错的性能。</p><h3>依赖收集</h3><p>React useState 返回当前状态和设置值函数,而 Solid 的 createSignal 返回两个函数。即:</p><pre><code class="TypeScript">type useState = (initial: any) => [state, setter];
type createSignal = (initial: any) => [getter, setter];</code></pre><p>为什么 createSignal 要传递 getter 方法而不是直接传递对应的 state 值呢?这是因为框架为了具备响应能力,Signal 必须要收集谁对它的值感兴趣。仅仅传递状态是无法提供 Signal 任何信息的。而 getter 方法不但返回对应的数值,同时执行时创建一个订阅,以便收集所有依赖信息。</p><h3>模版编译</h3><p>要保证 Signals 框架的高性能,就不得不结合模版编译实现该功能,框架开发者通过模版编译实现动静分离,配合依赖收集,就可以做到状态变量变化时点对点的 DOM 更新。所以目前主流的 Signals 框架没有使用虚拟 DOM。而基于虚拟 DOM 的 Vue 目前依靠编译器来实现类似的优化。</p><p>下面我们先看看 Solid 的模版编译:</p><pre><code class="js">const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};</code></pre><p>对应编译后的的组件代码。</p><pre><code class="js">const _tmpl$ = /*#__PURE__*/ _$template(`<div>Count value is `);
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
onCleanup(() => clearInterval(interval));
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_$insert(_el$, count, null);
return _el$;
})();
};</code></pre><ul><li>执行 \_tmpl$ 函数,获取对应组件的静态模版</li><li>提取组件中的 count 函数,通过 \_$insert 将状态函数和对应模版位置进行绑定</li><li>调用 setCount 函数更新时,比对一下对应的 count,然后修改对应的 \_el$ 对应数据</li></ul><h2>其他</h2><p>大家可以看一看使用 Signals 的主流框架:</p><ul><li><a href="https://link.segmentfault.com/?enc=gxj7rjegpY5%2Ffaas%2B%2FuH6Q%3D%3D.KVbaobq5zhv4iUu4M8rVR2HFvpoURWpFuHDzAzIQS1cPcO%2BTPZRHWZqHNFRbf0C5O5HOLyoAda%2Fd2ytrQR%2FV%2BQ%3D%3D" rel="nofollow">Vue Ref</a></li><li><a href="https://link.segmentfault.com/?enc=tWEi8WoZCc7J%2FojtvYoNHQ%3D%3D.CDNKaUw9GlFSJhYUyE7vr303RgpE5H3Ha7Hd9kTJlYsfyrwXp4XNQdEwD48Wkcro" rel="nofollow">Angular Signals</a></li><li><a href="https://link.segmentfault.com/?enc=JtYm0ESfG88Ut2Rr0vz%2BuQ%3D%3D.ysmmD1NrGCS46HC1wQNcQhsWr3fD8%2BjnqBIgCeD4370Fk3kvdVEDMPX3hnVvwDOa" rel="nofollow">Preact Signals</a></li><li><a href="https://link.segmentfault.com/?enc=ZYrGify%2Bnyutd3q%2Bki%2BXeQ%3D%3D.gL1Lg5ZI8RpalYnzcIWzE4dktbs6Ll4ZZ%2Bl3VslhNO3fzcbIMIn%2Fm7vRkpbYAOTp7d%2F%2FJPtbQu%2BJ9B6bvMY0HA%3D%3D" rel="nofollow">Solid Signals</a></li><li><a href="https://link.segmentfault.com/?enc=o%2Brx2vGnXXkQDxdgnYsQKQ%3D%3D.zlylWz3zDSxo8wPO8pZod8vcd7FvhuG85d0ksH2QRpXoCcfG%2FHboT%2BYCHKoye6EYfnS6%2FlZTP2Ag8ZdAyjL4jg%3D%3D" rel="nofollow">Qwik Signals</a></li><li><a href="https://link.segmentfault.com/?enc=WXz2lkhQ5GzEQb8QBKqMSA%3D%3D.W7Ot%2FXVE9hSKHj5GZJtSqz4lrPOhKmbUmDjdow%2BT98I%3D" rel="nofollow">Svelte 5(即将推出)</a></li></ul><p>不过目前来看 React 团队可能不会使用 Signals。</p><ul><li>Signals 性能很好,但不是编写 UI 代码的好方式</li><li>计划通过编译器来提升性能</li><li>可能会添加类似 Signals 的原语</li></ul><p>PREACT 作者编写了 <a href="https://link.segmentfault.com/?enc=5SmqY7%2FGoCMugYoyo2XoEA%3D%3D.7VEeD%2FSMPhZAEHcp8ch3zO%2FM9y09VfONmus8s00gQv8Y649QkjyxPXpuPtvnxYpfluxwLyOjLb9US8JUjpF%2FfQ%3D%3D" rel="nofollow">@preact/signals-react</a> 为 React 提供了 Signals。不过个人不建议在生产环境使用。</p><p>篇幅有限,后续个人会解读 <a href="https://link.segmentfault.com/?enc=oA0HgpcfSnLiNAeSzJyE6g%3D%3D.2huq1GqSp4txPXtjndVwrLltdmj1ccI0to9UxyBTqnN%2BAuIpwfzFDl2VvB5Pu2NV3AB5BXbmiO1Qo%2FNQSTHwxA%3D%3D" rel="nofollow">@preact/signals-core</a> 的源码。</p><h2>参考资料</h2><ul><li><a href="https://link.segmentfault.com/?enc=l222NN5POq91wUK2Wp1nsw%3D%3D.vXKjXEaiT6WBMHo1rWb%2B8QjN9cHZOYKAfVOdzHq7cTVYMQlhF%2FbjHMFV4fxAAMxC2xJH1MN0MlqVQlF9F%2Bi2KwGTyreDQMAElt1rJPhxG%2BQmajiHdVXi3%2FlGEcrSWVv0" rel="nofollow">精读《SolidJS》</a></li><li><a href="https://link.segmentfault.com/?enc=8SeDJJgF%2FG%2BcQahllD1euA%3D%3D.pxIweJs2aXRdBGQY8kXeZucB2MI4l%2FNk9idej6oenek%3D" rel="nofollow">Solid.js</a></li><li><a href="https://link.segmentfault.com/?enc=wQEm0G8hjBJNx6pvk%2FQkzw%3D%3D.bY%2FhgOWUOipD1EF33CzZtHQhctWs20w%2BAUx67gEwyR4%3D" rel="nofollow">Introducing runes</a></li></ul><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=DtGLmBnPZ%2Bd%2FXzoUJ3ahBw%3D%3D.ieHI19rR1k826jkV9keVBN9effIIlKylrhlYu11RbCdPq9pk%2B%2FIFIXj8gNdizKX2" rel="nofollow">博客地址</a></p>
前端持久化缓存优化
https://segmentfault.com/a/1190000044256023
2023-09-24T23:14:52+08:00
2023-09-24T23:14:52+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>缓存是提升 web 应用程序有效方法之一,尤其是用户受限于网速的情况下。提升系统的响应能力,降低网络的消耗。当然,内容越接近于用户,则缓存的速度就会越快,缓存的有效性则会越高。</p><p>之前个人写过 <a href="https://link.segmentfault.com/?enc=M3B4AtIWRHf3W%2FUBsrrhng%3D%3D.svjQgZe1bz8jhnqoTuqefs0dgNKttsgNjbK6prx0Lj0%2B4TUPOI7KdAvKNfANjzQ3" rel="nofollow">前端 api 请求缓存方案</a>。介绍的了内存中的缓存以及过期逻辑。后续也写过 <a href="https://link.segmentfault.com/?enc=f7%2Bmpt8O8DYiKjWxzuPfDQ%3D%3D.tU3gaxaBvxWBjIDyZ2cZGF%2F6Gb2QaLqwfLbBYNgKG50dJeLY4yDo0CnzPp1s1cc2qAs76xCdUfLuQffdVaLSHQ%3D%3D" rel="nofollow">手写一个前端存储工具库</a>,该工具利用了适配器处理了不同的存储介质(内存,IndexedDB, localStorage 等)。</p><p>不过,在某些特定场景下缓存还需要优化,例如:用户需要在登录或者填写表单时需要通过某些接口获取必要数据,而这些接口是由第三方平台提供的。这些接口可能会出现错误或超时的情况。如果当前数据有很强实时性,开发者就必须重试或者联系第三方平台来处理对应的错误。如果数据的实时性不强,当前就可以使用本地缓存。</p><p>一般来说,当获取时效性缓存时候,我们会检查并删除当前数据。代码简写如下所示:</p><pre><code class="javascript">// 缓存对应的的模块以及功能
const EXTRA_INFO_CACHE_KEY = 'xxx.xxx.xxx';
// 缓存时长为 7 天
const CACHE_TIME = 7 * 24 * 60 * 60 * 1000;
const getCachedExtraInfo = () => {
const cacheStr = localStorage.getItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`);
if (!cacheStr) {
return null;
}
let cache = null;
try {
cache = JSON.parse(cacheStr);
} catch () {
return null;
}
if (!cache) {
return null;
}
// 缓存过期了,直接返回 null
if ((cache.expiredTime ?? 0) < new Date().getTime()) {
return null;
}
return cache.data;
}
const getExtraInfo = () => {
const cacheData = getCachedExtraInfo();
if (cacheData) {
return Promise.resolve(cacheData);
}
return getExtraInfoApi().then(res => {
localStorage.setItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`, {
data: res,
expiredTime: (new Data()).getTime() + CACHE_TIME,
});
return res;
});
}</code></pre><p>如果这时候接口出现了访问错误问题,很多数据到期的用户就无法正常使用功能了,这时候添加重试功能可能会解决某些错误。这时候我们先不考虑重试的逻辑。</p><p>考虑到绝大部份用户对应数据不会进行修改的情况下,对应代码就可以不进行数据删除。而是返回超时标记。</p><pre><code class="javascript">const EXTRA_INFO_CACHE_KEY = 'xxx.xxx.xxx';
const CACHE_TIME = 7 * 24 * 60 * 60 * 1000;
const getCachedExtraInfo = () => {
const cacheStr = localStorage.getItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`);
if (!cacheStr) {
return null;
}
let cache = null;
try {
cache = JSON.parse(cacheStr)
} catch () {
return null;
}
if (!cache) {
return null;
}
if ((cache.expiredTime ?? 0) < new Date().getTime()) {
return {
data: cache.data,
// 数据已经超时了
isOverTime: true,
};
}
return {
data: cache.data,
// 数据没有超时
isOverTime: false,
};
}
const getExtraInfo = () => {
const cacheInfo = getCachedExtraInfo();
// 数据没有超时才返回对应数据
if (cacheInfo && !cacheInfo.isOverTime) {
return Promise.resolve(cacheInfo.data);
}
return getExtraInfoApi().then(res => {
localStorage.setItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`, {
data: res,
expiredTime: (new Data()).getTime() + CACHE_TIME,
});
return res;
}).catch(err => {
// 有数据,才返回,否则继续抛出错误
if (cacheInfo) {
return cacheInfo.data;
}
throw err;
})
}</code></pre><p>这样的话,我们可以保证绝大多数用户是可以继续正常使用的。但如果对应的接口不稳定,会让用户等待很长时间才能继续使用。</p><p>这时候开发者可以考虑完全抛弃异步代码,同时减少缓存时间。</p><pre><code class="javascript">const EXTRA_INFO_CACHE_KEY = 'xxx.xxx.xxx';
// 将缓存时效减少为 5 天
const CACHE_TIME = 5 * 24 * 60 * 60 * 1000;
const getCachedExtraInfo = () => {
const cacheStr = localStorage.getItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`);
if (!cacheStr) {
return null;
}
let cache = null;
try {
cache = JSON.parse(cacheStr)
} catch () {
return null;
}
if (!cache) {
return null;
}
if ((cache.expiredTime ?? 0) < new Date().getTime()) {
return {
data: cache.data,
isOverTime: true,
};
}
return {
data: cache.data,
isOverTime: false,
};
}
const getExtraInfo = () => {
const cacheInfo = getCachedExtraInfo();
// 如果超时了,就去获取,下一次再使用即可
if (cacheInfo.isOverTime) {
getExtraInfoApi().then(res => {
localStorage.setItem(`${EXTRA_INFO_CACHE_KEY}.${userId}`, {
data: res,
expiredTime: (new Data()).getTime() + CACHE_TIME,
})
})
}
return cacheInfo.data
}</code></pre><h2>参考文档</h2><p><a href="https://link.segmentfault.com/?enc=GhjoWhFp54mW9QEyQjba0Q%3D%3D.35ck5R9RPdcPnWBoQFGFTeCqERYohJdC9aOvviiFhG6qm7gb96nvyufNLCrO7Wvy" rel="nofollow">前端 api 请求缓存方案</a></p><p><a href="https://link.segmentfault.com/?enc=p3juNjT66Pijhgj8aWpWFg%3D%3D.N0rflSd%2BHYCEM4NFRStjsNP%2FBG3LQp3eARkloJRkmEqudEf5LzJLCjZ%2BbCv563dSj2zIAVuSSwmgqHvLv6MwOg%3D%3D" rel="nofollow">手写一个前端存储工具库</a></p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=6scwPM4GXJWbK9eTHELDxA%3D%3D.1t3b22ZLzrue56YWfHYEU3NZR7J8z71PxojsB9J72TCpQ2RdV4J%2BMovZoV43HQy6" rel="nofollow">博客地址</a></p>
利用中介模式开发全局控制器
https://segmentfault.com/a/1190000044202167
2023-09-10T21:58:56+08:00
2023-09-10T21:58:56+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<blockquote>中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。</blockquote><p>在实际的项目中,程序由许多对象组成,对象间的交流错综复杂。</p><p><img width="633" height="327" src="/img/bVc9Daj" alt="" title=""></p><p>随着应用程序的规模增大,对象越来愈多,他们之间的关系也越来复杂。对象间很容易出现相互引用而导致程序无法运行。同时开发者需要改变或者删除某一个对象时候,需要查找并且改造所有引用到它的对象。这样一来,改造的成本会变的非常高。</p><p>但中介者模式可以让各个对象之间得以解耦。</p><p><img width="632" height="341" src="/img/bVc9Dak" alt="" title=""></p><p>之前的场景下,如果 A 发生了改变,开发者需要修改 B、D、E、F 4 个对象,而通过中介者模式,我们只需要修改中介者这一个对象即可。</p><p>中介者的好处是简化了对象之间的交互,坏处则是中介类有可能会变成大而复杂的“上帝类”(God Class)。所以,在使用中介者模式时候,设计上一定要非常克制。</p><h2>实际使用</h2><p>在前端项目开发的过程中,有很多业务无关的功能,但这些功能会散落在各个业务中,难以管理,我们利用中介者模式的思想来构建一个的控制器。</p><h3>基础控制器</h3><pre><code class="typescript">// axios 请求工具
import axios from "axios";
// mitt 微型发布订阅工具
import mitt from "mitt";
const createApi = ($controller) => {
const api = axios.create({
baseURL: "/api",
timeout: 10000,
});
api.interceptors.request.use(() => {
// 可以通过 $controller.tmpCache 缓存一些数据
});
return api;
};
const createBus = () => mitt();
class Controller {
// 临时缓存,也可以添加更复杂的缓存
tmpCache: Record<string, any> = {};
// 事件总线
bus = createBus();
constructor() {
this.api = createApi(this);
}
static instance: Controller | null = null;
static getInstance() {
if (!Controller.instance) {
Controller.instance = new Controller();
}
return Controller.instance;
}
}
export default Controller;</code></pre><p>此时控制器中有一个极简的缓存 tmpCache,发布订阅工具。以及服务端请求方法 api。开发者只需要导入 Controller 类即可使用。如果后续需要更加复杂的缓存或者改造 api(如切换为 fetch)请求方法。开发者可以很快的进行替换。而无需改造对应文件。</p><pre><code class="typescript">import axios from "redaxios";
const createApi = ($controller) => {
const api = axios.create({
baseURL: "/api",
});
const getOld = api.get;
api.get = (...params) => {
// 如果出发缓存直接返回
// if (xxx) {
// return $controller.tmpCache
// }
return getOld(...params);
};
return api;
};</code></pre><h3>添加用户类</h3><p>登录的用户信息以及对应操作可以说是对象交互的核心,将其放入控制器中。</p><pre><code class="typescript">class User {
readonly $controller: Controller;
user: User | null = null;
constructor($controller: Controller) {
this.$controller = $controller;
}
getData(key?: string) {
return key ? this.user[key] : this.user;
}
// 登录
login(params) {
$controller.api.post("/login", params).then((response) => {
// 处理授权以及 user 信息
});
}
// 退出
logout() {
$controller.api.post("/logout").then(() => {
// 清理对应数据
$controller.tmpCache = {};
this.user = null;
});
}
// 设置用户配置
setSetting(params) {
return $controller.api.post("/setting", params).then(() => {
this.user.setting = params;
});
}
}
class Controller {
constructor() {
this.api = createApi(this);
this.user = new User(this);
}
logout() {
this.user.logout();
}
}</code></pre><p>此时控制器已经具备前端开发中“大部分”功能了,如数据缓存,数据请求,登录用户信息处理等。大部分情况下,中介类也基本够用了。至于其他的工具类,除非 80% 的业务都会用到,否则不建议添加到此类中去。但对于这些工具做一层简单的封装也是必要的(考虑后续成本)。</p><h3>添加业务类</h3><p>此时控制器有了用户信息,多种缓存,请求方法等。我们可以通过注入来实现其他业务。</p><pre><code class="typescript">class BasicService {
constructor($controller) {
this.api = $controller.api;
this.bus = $controller.bus;
}
}
class OrderService extends BasicService {
static xxxKey = "xxxx";
constructor($controller) {
super($controller);
// 使用全局缓存
this.cache = $controller.xxxCache;
// 或者构建一个新的 cache 挂载到 controller 上
this.cache = $controller.getCreatedCache("order", XXXCache);
}
getOrders = async () => {
if (this.cache.has(OrderService.xxxKey)) {
return this.cache.get(OrderService.xxxKey);
}
let orders = await this.api.get("xxxxx");
// 业务处理,包括其他的业务开发
order = order.map();
this.cache.set(OrderService.xxxKey, orders);
// 可以发送全局事件
this.bus.emit("getOrdersSuccess", orders);
return orders;
};
clear() {
// 如果构建一个新的缓存对象,直接 clear 即可 this.cache.clear();
this.cache.delete(OrderService.xxxKey);
}
}
class Controller {
// 临时缓存,也可以添加更复杂的缓存
tmpCache: Record<string, any> = {};
// 事件总线
bus = createBus();
services: Record<string, BasicService> = {};
getService(serviceCls) {
// 同一个类 name 为相同的名称
const { name } = serviceCls;
if (!this.services[name]) {
this.services[name] = new serviceCls(Controller.instance);
}
return this.services[name];
}
}</code></pre><p>开发者可以在具体的代码中这样使用。</p><pre><code class="typescript">import Controller from "../../controller";
import OrderService from "./order-service";
// A 页面
const orderService = Controller.getInstance().getService(OrderService);
const priceService = Controller.getInstance().getService(PriceService);
// B 页面都会使用同一个服务
const orderService = Controller.getInstance().getService(OrderService);</code></pre><p>有同学可能会奇怪,为什么 Controller 不能直接导入或者自动注入 OrderService 呢。这样使用的原因是往往 Controller 和 BasicService 在基础类中,具体的业务类分布在各个业务代码中。</p><p>这里没有使用任何依赖注入的框架,原因是因为如果引入了依赖注入会大大增加复杂度。同时前端的业务都是分散的,没有必要都放在一起。在组件需要使用到服务候才将其装载起来。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=jB%2BjC7BpEmss1oisUhuzGg%3D%3D.hG70rXUBvCH9RjF%2BfMZAEhP2lrJuba4KToerBUjSI2ZLByqE%2FIJKpFsgpMvS9Efb" rel="nofollow">博客地址</a></p>
聊聊版本号的作用与价值
https://segmentfault.com/a/1190000044202160
2023-09-10T21:57:01+08:00
2023-09-10T21:57:01+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>在项目开发和运行的过程中,总是少不了各类升级。例如某个功能组件需要更高的依赖库、数据项需要进行兼容等等问题。遇到此类问题开发者需要使用版本号来解决。今天我们就来分析一下项目迭代过程中会遇到的各类升级问题以及如何使用版本号来解决。</p><p>通常来说升级会涉及到三个点:</p><ul><li>向下兼容</li><li>协商升级</li><li>拒绝服务</li></ul><h2>依赖升级</h2><p>开发者在产品演进的过程中会不断的升级工具依赖,以 npm 版本为例。版本号通常由三部分组成: 主版本号、次版本号和修订版本号。</p><pre><code>Major.Minor.Patch</code></pre><p>其中,Major 表示主版本,当你做了不兼容的 API 修改时,就需要升级这个版本号。Minor 表示次版本,当你做了向下兼容的功能性新增时,就需要升级这个版本号。而 Patch 表示修订版本,当你做了向下兼容的问题修正时,就需要升级这个版本号。</p><p>每次在使用 npm install 时都会下载<br>package.json 中的依赖。而在在依赖中有 ^ 和 ~ 符号。其中 ^ 代表次版本兼容,~ 是修订版本兼容。</p><pre><code class="json">{
"devDependencies": {
"lib1": "0.15.3",
"lib2": "^0.15.3",
"lib3": "~0.15.3"
}
}</code></pre><p>如果当前三个库都有几个高版本,如:</p><ul><li>0.15.3</li><li>0.15.4</li><li>0.16.1</li><li>1.0.0</li></ul><p>在项目下载后执行 install ,下载的对应版本则是</p><ul><li>lib1 0.15.3</li><li>lib2 0.16.1</li><li>lib3 0.15.4</li></ul><p>虽然 ^ 和 ~ 都不会升级破坏性依赖,但版本号只是“君子协议”。还是建议大家不要使用这些符号。同时之前也遇到过组件库在某个修订版本中出现了 bug。虽然很快修复好了。但是定位问题还是需要花费一定时间的。</p><h2>数据缓存</h2><p>很多情况,开发者为了减少网络请求都会使用数据缓存。如果是一个较为稳定的数据。我们可以添加版本号进行缓存(同时添加一个足够长的过期时间方便重新获获取)。</p><p>以 localStorage 为例,代码如下所示:</p><pre><code class="ts">interface Store<T> {
/** 存储数据 */
data: T;
/**
* 当前版本数据,可以是一个数字或一个日期字符串 '220101-1'
* 后续的 -1 是为了当天发布多个版本而准备的。
*/
version: string | number;
/**
* 过期时间
* 可以使用 时间戳(天数),天数 dayjs 等
*/
expries: string | number;
}
/**
* 实际存储 key 值
*/
const XXX_STORAGE_KEY = 'storageKey';
const isNeedUpgrade = async <T>(): Promise<boolean> => {
const storeJSONStr = localStorage.getItem(XXX_STORAGE_KEY);
// 没有存储 JSON 字符串
if (storeJSONStr === null) {
return true;
}
let store: Partial<Store<T>> = {};
try {
store = JSON.parse(storeJSONStr)
} catch (e) {
// JSON 字符串解析失败
return true;
}
const { expries, version: localVersion } = store;
// 没有过期时间获取当前时间超过过期时间
if (!expries || isOverTime(expries)) {
return true;
}
// 没有缓存本地版本
if (!localVersion) {
return true;
}
const currentVersion = await getCurrentVersionForXXXStore();
// 版本不一致
if (currentVersion !== localVersion) {
return true;
}
// 无需升级
return false;
}</code></pre><p>当前代码其实就涉及到了上述所说的协商升级。</p><h2>使用版本号进行 api 维护</h2><p>随着业务的发展,数据结构不可避免会发生一定的改变,如果仅仅只是增加一个数据,开发者可以直接在服务端做一下向下兼容即可。但有些时候我们可能需要做出一系列的调整,诸如前一个版本处理传递上来的 A 和 C 数据,但是后一个版本需要处理 A 和 D 数据。这时候我们可能就无法通过数据传输来确定如何使用。因为我们无法保证服务端与客户端能够同步升级。</p><p>此时我们不得不借助版本号。新版本前端调用新版本的 API ,旧版本前端调用旧版本的 API。</p><pre><code class="ts">/**
* 2019-11-12 版本 15 兼容处理了 xxxx, xxxx
* 2018-12-10 版本 14 xxxxxx
*/
const api = initRequest({
// 全局 api 版本号
apiVersion: 15,
});
const queryXXX = () => {
return api({
// 可以使用 api 版本号,不传递默认使用全局版本号
apiVersion: 3,
});
};</code></pre><p>使用或者不使用全局版本号都有各自的优点。使用全局版本号一个版本可以同时进行多处修改,方便开发者维护。但是如果 api 兼容过多的话,apiVersion 也会升级的很快。最终反而不利于维护。大家可以酌情处理,如果是互联网项目,大家可以考虑使用 api 独立版本号,如果是企业服务则优先使用全局版本号。</p><p>大部分情况下服务端都可以兼容之前的代码。</p><pre><code class="typescript">@Controller({
path: "user",
version: "2",
})
export class UserController {
@Get()
@Version("2")
findAll() {
return this.userService.findAll();
}
@Get()
@Version("1")
findAllOld() {
return this.userService.findAllOld();
}
}</code></pre><p>极少数情况下,服务端代码难以兼容,或者需要付出极大代价,这时候就可以拒绝服务。</p><pre><code class="typescript">@Controller({
path: "user",
version: "2",
})
export class UserController {
@Get()
@Version("1")
findAllOld() {
// 抛出版本不支持异常
throw BusinessException.throwVersionNotSupport();
}
}</code></pre><p>老的前端代码中后端拒绝服务。这样的话就不需要在服务端维护多个版本。代码如下所示:</p><pre><code class="ts">export const handleVersionError(err) {
// 版本不支持和
if (err.errCode !== 'versionNotSupport') {
return;
}
this.$confirm('当前版本过低,无法正常使用此功能。', '温馨提示', {
confirmButtonText: '刷新页面使用最新版本',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
location.reload();
})
}</code></pre><p>如果使用小程序开发,也可以通过小程序 API updateManager 重启升级。代码如下所示:</p><pre><code class="ts">const updateManager = Taro.getUpdateManager();
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
console.log(res.hasUpdate);
});
updateManager.onUpdateReady(function () {
Taro.showModal({
title: "更新提示",
content: "新版本已经准备好,是否重启应用?",
success: function (res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(function () {
// 新的版本下载失败
});</code></pre><p>当然,针对小程序开发者还可以存储当前页面和获取信息,如果重启小程序后,直接打开对应界面并删除信息(添加超时机制)。</p><h2>乐观锁</h2><p>乐观锁也是利用了版本号来实现的。</p><p>当一些可变数据无法隔离时候,开发者可以用两种不同的控制策略:乐观锁策略和悲观锁策略。乐观锁用于冲突检测,悲观锁用于冲突避免。</p><p>悲观者策略非常简单,当 A 用户获取到用户信息时系统把当前用户信息给锁定,然后 B 用户在获取用户信息时就会被告知别人正在编辑。等到 A 员工进行了提交,系统才允许 B 员工获取数据。此时 B 获取的是 A 更新后的数据。</p><p>乐观者策略则不对获取进行任何限制,它可以在用户信息中添加版本号来告知用户信息已被修改。乐观锁要求每条数据都有一个版本号,同时在更新数据时候就会更新版本号,如 A 员工在更新用户信息时候提交了当前的版本号。系统判断 A 提交的时候的版本号和该条信息版本号一致,允许更新。然后系统就会把版本号修改为新的版本号,B 员工来进行提交时携带的是之前版本号,此时系统判定失败。</p><p>业务也可以根据情况添加用户问询,询问用户是否需要强制更新,在用户选择“是”时可以添加额外参数并携带之前的版本号以方便日志信息存储。</p><p>当然了,如果当前业务对时间要求没有那么高的情况下,开发者也可以直接利用数据的更新时间作为这条数据的版本号。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=g1pF%2FkNzgbs%2Fr14i67rRMw%3D%3D.SOWFvGwH1ufkVxjZPYu1Su%2FQYP9FpcnHrf6eMW1vhB7F9IOzOOjSU%2FXe5m322zMk" rel="nofollow">博客地址</a></p>
聊聊存储引擎的实现要素
https://segmentfault.com/a/1190000043942028
2023-06-27T18:16:02+08:00
2023-06-27T18:16:02+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<p>众所周知,MySQL 的 InnoDB 存储引擎使用了 B+ 树作为索引实现,那么为什么不使用其他的数据结构呢?数组、链表或者哈希表。实现存储引擎究竟需要什么条件呢?</p><p>我们现在先以存储最简单的数据为例,这里的数据类似于 json 对象。有 key 和 value。</p><pre><code class="json">{
"0": "value1",
"1": "value2"
}</code></pre><p>最简单的存储引擎必须实现以下三个方法:</p><ul><li>read: (key: number) => value 查找 key 并返回 value</li><li>write: (key: number, value) => void 查找并插入 key 以及 value</li><li>scan: (begin: number, end: number) => value[] 查找返回 key 范围内数据</li></ul><h2>简单数据结构</h2><p>对于开发项目来说,能使用最简单的数据结构完成项目是非常棒的,这意味着更少的 bug 和更少的时间。</p><h3>有序数组</h3><p>如果当前有序数组的位置和存储的 key 可以一一对应的话,也就是数组 index 对应 key(没有对应也就是稀疏数组),我们的 read 和 write 方法的时间复杂度会是 O(1),scan 方法也是 O(1)。但数据量稍大就扛不住了。</p><p>退而求其次,不存在位置对应主键的情况下,有序数组紧密存储,这样可以通过二分查找,read 和 scan 方法的时间复杂度为 O(log2n)。但 write 方法成本会高到离谱。</p><p>综上所属,有序数组是在数据量少的情况下可以用来做存储引擎的。</p><h3>哈希表</h3><p>不考虑空间是不可能的,那么直接舍弃 scan 方法呢?在某些业务场景下是可以不使用 scan 方法的。</p><p>哈希表使用一对多的组织方式来实现 read 和 write。先对 key 进行 hash 运算然后再寻址,性能基本接近于 O(1)。</p><p>综上所属,哈希表在不考虑 scan 方法的情况下是可以用来做存储引擎的。</p><h3>二叉平衡树</h3><p>二叉平衡树相对 hash 和有序数据来说是一个折衷方案。该数据结构是通过链表实现的,所以不需要大块内存。它的 read 和 write 都是 O(log2n),虽然 scan 遍历慢的难以忍受,但是它能够实现这三个方法了。</p><p>综上所属,二叉平衡树是可以用来做存储引擎的,但有一定的局限性。</p><h2>要素分析</h2><p>在分析上面几种数据结构后,我们不难得出结论。</p><ul><li>有序性是实现 scan 方法的前提条件</li><li>局部性是提升 scan/read 方法性能的必要条件</li></ul><p>这里我们提到了局部性,那么局部性究竟是什么呢?</p><p>通常来说,良好的计算机程序需要良好的局部性,局部性主要有:</p><ul><li>时间局部性 :指的是同一个内存位置,从时间维度来看,它能够在较短时间内被多次引用</li><li>空间局部性 :指的是同一个内存位置,从空间维度来看,它附近的内存位置能够被引用</li></ul><p>仔细分析一下,scan 方法和空间局部性有关。如果使用平衡二叉树来作为查询的数据结构。scan 的性能是非常差的,但是使用有序数组来作为数据结构 scan 可以直接遍历获取两者之间的数据,性能非常高。</p><p>同时,局部性也和 read 性能有很大关系。使用二分法来查询数据。局部性较低的情况下,read 需要多次从磁盘加载数据。如果局部性高,直接一次加载数据即可。</p><p>那是不是局部性越高越好呢?不是这样的。一方面局部性高会占用较高的内存。另一方面,局部性过高会导致 write 方法变慢,因为局部性高了,write 方法需要移动的数据也就多了。</p><p>平衡二叉树是唯一能在现实世界中实现 3 个方法的数据结构,局部性是提升 scan 方法性能的必要条件。那么把两者结合呢?把平衡二叉树的结点构造成一个个有序数组,这样就可以得到两个方案的优点了。</p><ul><li>对于有序数组来说,通过拆分数组,使得在 write 方法的成本大大减少</li><li>对于平衡二叉树来说,通过节点替换,大大增加了局部性,让 scan 方法性能成本大大减少</li></ul><p>事实上,只要能够低成本且高效的维持数据有序的数据结构都可以作为存储引擎。无论是 B 树, B+ 树或者 跳表。同时每个数据结构都有其对应的侧重点。只要抓住这几个点,就不难分析出为什么当前存储引擎使用该数据结构作为索引了。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=f6jlH89oAOpJbRMMDpwxbQ%3D%3D.me4aopL9rTIEFHF3H8k32qpZQ%2BMcLKpbN1C6N0aQqoX2Znnl69nfBaoru%2FFTO2%2FZ" rel="nofollow">博客地址</a></p>
让 React 拥有更快的虚拟 DOM
https://segmentfault.com/a/1190000043913624
2023-06-18T13:44:50+08:00
2023-06-18T13:44:50+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p><a href="https://link.segmentfault.com/?enc=UrKroeD2HiQKlMC2Me0Z1Q%3D%3D.lsHOy0ar3RpxtKqcah58UdBJ6axdpgKklf6BhnjHx0s%3D" rel="nofollow">Million.js</a> 是一个非常快速和轻量级的 ( <4kb) 虚拟 DOM。框架可以通过包装 React 组件来提升性能(该框架目前版本只兼容 React 18 及以上版本)。</p><p>先说结论:Million.js 适应的场景极其有限,但在特定场景下也大放异彩。</p><h3>如何使用</h3><p>Million.js 集成 React 中使用非常简单。先进行安装和编译器配置。</p><h4>安装与配置</h4><pre><code class="bash">npm install million</code></pre><p>当前是 webpack 的配置文件。如果有使用其他的构建工具,可以自行参考 <a href="https://link.segmentfault.com/?enc=Vv23fSyEuKArHO761spIkQ%3D%3D.MDNzKYdpe4Hmn032BqUVdblLww1oulBZeLVeX5C%2F65%2FAmzsClywlVIfJv0xMmZU1" rel="nofollow">安装 Million.js</a>。</p><pre><code class="js">const million = require('million/compiler');
module.exports = {
plugins: [
million.webpack(),
],
}</code></pre><h4>使用 block 和 For 组件</h4><pre><code class="tsx">import { block as quickBlock } from "million/react";
// million block 是一个 HOC
const LionQuickBlock = quickBlock(function Lion() {
return <img src="https://million.dev/lion.svg" />;
});
// 直接使用
export default function App() {
return (
<div>
<h1>mil + LION = million</h1>
<LionQuickBlock />
</div>
);
}</code></pre><p>当前是数组的情况下</p><pre><code class="tsx">import { block as quickBlock, For } from "million/react";
const RowBlock = quickBlock(function Row({ name, age, phone }) {
return (
<tr>
<td>{name}</td>
<td>{age}</td>
<td>{phone}</td>
</tr>
);
});
// 使用 For 组件优化
export default function App() {
return (
<div>
<For each={data}>
{({ adjective, color, noun }) => (
<RowBlock
adjective={adjective}
color={color}
noun={noun}
/>
)}
</For>
</div>
);
}</code></pre><h3>Block Virtual DOM</h3><p>Million.js 引入了 Block Virtual DOM 来进行优化。</p><p>Block Virtual DOM 采用不同的方法进行比较,可以分为两部分:</p><ul><li>静态分析(分析虚拟 DOM 以将 DOM 动态部分搜集起来,放入 Edit Map 或者 edits(列表) 中去)</li><li>脏检查(比较状态(不是虚拟 DOM 树)来确定发生了什么变化。如果状态发生变化,DOM 将直接通过 Edit Map 进行更新)</li></ul><p>这种方式大部分情况下要比 React 的虚拟 DOM 要快,因为它比较数据而并非 DOM,将树遍历从 O(tree) 变为 Edit Map O(1)。同时我们也可以看出 Million.js 也会通过编译器对原本的 React 组件进行修改。</p><h3>适用场景</h3><p>但所有的事情都不是绝对的,Block Virtual DOM 在某些情况下甚至要比虚拟 DOM 要慢。</p><h4>静态内容多,动态内容少</h4><p>block virtual DOM 会跳过 virtual DOM 的静态部分。</p><pre><code class="tsx">// ✅ Good
<div>
<div>{dynamic}</div>
Lots and lots of static content...
</div>
// ❌ Bad
<div>
<div>{dynamic}</div>
<div>{dynamic}</div>
<div>{dynamic}</div>
<div>{dynamic}</div>
<div>{dynamic}</div>
</div></code></pre><h4>稳定的 UI 树</h4><p>因为 Edit Map 只创建一次,不需要在每次渲染时都重新创建。所以稳定的 UI 树是很重要的。</p><pre><code class="tsx">// ✅ Good
return <div>{dynamic}</div>
// ❌ Bad
return Math.random() > 0.5 ? <div>{dynamic}</div> : <p>sad</p>;</code></pre><h4>细粒度使用</h4><p>初学者犯的最大错误之一是到处使用 Block virtual DOM。这是个坏主意,因为它不是灵丹妙药。开发者应该识别块虚拟 DOM 更快的某些模式,并仅在这些情况下使用它。</p><h3>规则</h3><p>以下是一些要遵循的一般准则</p><ul><li>嵌套数据:块非常适合呈现嵌套数据。Million.js 将树遍历从O(tree)变为O(1),允许快速访问和更改。</li><li>使用 For 组件而不是 Array.map:For 组件会做针对性优化</li><li><p>使用前需要先声明:编译器需要进行分析,没有申明将无法进行分析</p><pre><code class="tsx">// ✅ Good
const Block = block(<div />)
// ❌ Bad
console.log(block(<div />))
export default block(<div />)</code></pre></li><li><p>传递组件而不是 JSX</p><pre><code class="tsx">// ✅ Good
const GoodBlock = block(App)
// ❌ Bad
const BadBlock = block(<Component />)</code></pre></li><li>确定的返回值:返回必须是“确定性的”,这意味着在返回稳定树的块末尾只能有一个返回语句(组件库,Spread attributes 都有可能造成不确定的返回值而导致性能下降)</li></ul><h3>其他</h3><p>million 源码中有非常多的缓存优化。同时在它最开始就拆分传入的 dom 节点,将其分成多个可变量,放入数组,patch 和 mount 时仅遍历数组数据对比(这也是需要确定的返回值原因),较为新颖。源代码也较为简单明了。大家可以自行阅读源码学习。</p><ul><li><a href="https://link.segmentfault.com/?enc=f7SrJ%2FOdT6GxLcDejQRmbQ%3D%3D.rK6ZRHpmqSx%2Fk1L%2FERLkqF0%2F7ynJOdURkAuqyHp%2BIjDOLdYU7%2FsEvI2S1HTpzI2nsQsmojbvfPVeAJbN24Ko4XfXCwBQmImTLa5exmD77hA%3D" rel="nofollow">million react/block.ts</a></li><li><a href="https://link.segmentfault.com/?enc=2R0SEHStFnZVhHcgQYRO1w%3D%3D.JJDLoSUIEnrXSffzrmSylzvqvjBd1WzXs8W3MKpZn08W2oPJgSrgk6gAQyjLMFPx96z2UMRIFhncsB8M8hpK8EgeBsE0Sc3waZydGqF9AvU%3D" rel="nofollow">million template.ts</a></li><li><a href="https://link.segmentfault.com/?enc=JBAaw1fnKYbBWv7U7bDj3A%3D%3D.p56jaiw9TwZ6kN1mGhbyVHhGBPzAD48m1X%2B5Lw%2BDVQ0cemjjeFeU6at54AQH%2BDByU0G9DDZDWxvEwsqanRg9%2FgtptawdSzmUiZ%2FeNnYC3IQ%3D" rel="nofollow">million block.ts</a> .</li></ul><p>million 的 Block Virtual DOM 的思路来源于 <a href="https://link.segmentfault.com/?enc=zTNYfILj3uICGvlI6%2FYw5Q%3D%3D.6tsTspZFLe2Rmu86NcfxWRsnp%2FWNXI66ZjWTF%2Bvg%2BB0ZSEdE46NEic2Q6NVIbGKq" rel="nofollow">blockdom</a>, blockdom 是一个较低级别的抽象层。这个框架同时提供了制作框架的教程 <a href="https://link.segmentfault.com/?enc=C%2FyuAx5ouf6y5wWBNCvuJg%3D%3D.lVFOaiEbtBYaJ56B7yYF17ow7IYiMSPdzSnZP97W732TwWQjU2qGSptnSCkkmrsZisejFrEa3kWpZAdFJOU5%2FEMU9S2gU8r%2FqP220ZdICzv38M1zaHNWrdJbmbTNI4y%2F" rel="nofollow">制作自己的框架</a>。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=TaaQjmQHjNuXBt1XQyPUlw%3D%3D.%2FM8Qa4jEEsnuHcftB1xje8j99c8X4rf2StDF1NH8H%2Fkr3jGuALtv39kPHBdNLpX5" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=pmD394oMigD8PG13qttQ4A%3D%3D.RwDpieF0XuR5T2Px%2FHw0Xuno0ru68ap3v%2F5GWu9mykQ%3D" rel="nofollow">Million.js</a></p><p><a href="https://link.segmentfault.com/?enc=i4%2FHppguk8UBPxMbI6RgYQ%3D%3D.BQRuk4MnfSCyBXY%2BtSMxu07bKOo%2FArJFXmg8DwAgAugfkN6g98A9PJG2b1s%2BE%2FLG" rel="nofollow">blockdom</a></p>
手写一个前端存储工具库
https://segmentfault.com/a/1190000043420823
2023-02-14T02:07:04+08:00
2023-02-14T02:07:04+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
5
<p>在项目开发的过程中,为了减少提高性能,减少请求,开发者往往需要将一些不易改变的数据放入本地缓存中。如把用户使用的模板数据放入 localStorage 或者 IndexedDB。代码往往如下书写。</p><pre><code class="ts">// 这里将数据放入内存中
let templatesCache = null;
// 用户id,用于多账号系统
const userId: string = '1';
const getTemplates = ({
refresh = false
} = {
refresh: false
}) => {
// 不需要立即刷新,走存储
if (!refresh) {
// 内存中有数据,直接使用内存中数据
if (templatesCache) {
return Promise.resolve(templatesCache)
}
const key = `templates.${userId}`
// 从 localStorage 中获取数据
const templateJSONStr = localStroage.getItem(key)
if (templateJSONStr) {
try {
templatesCache = JSON.parse(templateJSONStr);
return Promise.resolve(templatesCache)
} catch () {
// 解析失败,清除 storage 中数据
localStroage.removeItem(key)
}
}
}
// 进行服务端掉用获取数据
return api.get('xxx').then(res => {
templatesCache = cloneDeep(res)
// 存入 本地缓存
localStroage.setItem(key, JSON.stringify(templatesCache))
return res
})
};</code></pre><p>可以看到,代码非常冗余,同时这里的代码还没有处理数据版本、过期时间以及数据写入等功能。如果再把这些功能点加入,代码将会更加复杂,不易维护。</p><p>于是个人写了一个小工具 <a href="https://link.segmentfault.com/?enc=t5W3sO1VKo13G8pANrwrkQ%3D%3D.HTfQrGzLv1FlwNjB511HIXTfmI8QwIj0jTOsUIX007Oi4lUULuJTIzC781sBUguo" rel="nofollow">storage-tools</a> 来处理这个问题。</p><h2>使用 storage-tools 缓存数据</h2><p>该库默认使用 localStorage 作为数据源,开发者从库中获取 StorageHelper 工具类。</p><pre><code class="ts">import { StorageHelper } from "storage-tools";
// 当前用户 id
const userId = "1";
// 构建模版 store
// 构建时候就会获取 localStorage 中的数据放入内存
const templatesStore = new StorageHelper({
// 多账号用户使用 key
storageKey: `templates.${userId}`,
// 当前数据版本号,可以从后端获取并传入
version: 1,
// 超时时间,单位为 秒
timeout: 60 * 60 * 24,
});
// 从内存中获取数据
const templates = templatesStore.getData();
// 没有数据,表明数据过期或者没有存储过
if (templates === null) {
api.get("xxx").then((val) => {
// 存储数据到内存中去,之后的 getData 都可以获取到数据
store.setData(val);
// 闲暇时间将当前内存数据存储到 localStorage 中
requestIdleCallback(() => {
// 期间内可以多次掉用 setData
store.commit();
});
});
}</code></pre><p>StorageHelper 工具类支持了其他缓存源,代码如下:</p><pre><code class="ts">import { IndexedDBAdaptor, StorageAdaptor, StorageHelper } from "storage-tools";
// 当前用户 id
const userId = "1";
const sessionStorageStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入 sessionStorage
adapter: sessionStorage,
});
const indexedDBStore = new StorageHelper({
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入 IndexedDBAdaptor
adapter: new IndexedDBAdaptor({
dbName: "userInfo",
storeName: "templates",
}),
});
// IndexedDB 只能异步构建,所以现在只能等待获取构建获取完成
indexedDBStore.whenReady().then(() => {
// 准备完成后,我们就可以 getData 和 setData 了
const data = indexedDBStore.getData();
// 其余代码
});
// 只需要有 setItem 和 getItem 就可以构建 adaptor
class MemoryAdaptor implements StorageAdaptor {
readonly cache = new Map();
// 获取 map 中数据
getItem(key: string) {
return this.cache.get(key);
}
setItem(key: string, value: string) {
this.cache.set(key, value);
}
}
const memoryStore = new StorageHelper({
// 配置同上
storageKey: `templates.${userId}`,
version: 1,
timeout: 60 * 60 * 24,
// 适配器,传入携带 getItem 和 setItem 对象
adapter: new MemoryAdaptor(),
});</code></pre><p>当然了,我们还可以继承 StorageHelper 构建业务类。</p><pre><code class="ts">// 也可以基于 StorageHelper 构建业务类
class TemplatesStorage extends StorageHelper {
// 传入 userId 以及 版本
constructor(userId: number, version: number) {
super({
storageKey: `templates.${userId}`,
// 如果需要运行时候更新,则可以动态传递
version,
timeout: 60 * 60 * 24,
});
}
// TemplatesStorage 实例
static instance: TemplatesStorage;
// 如果需要版本信息的话,
static version: number = 0;
static getStoreInstance() {
// 获取版本信息
return getTemplatesVersion().then((newVersion) => {
// 没有构建实例或者版本信息不相等,直接重新构建
if (
newVersion !== TemplatesStorage.version || !TemplatesStorage.instance
) {
TemplatesStorage.instance = new TemplatesStorage("1", newVersion);
TemplatesStorage.version = newVersion;
}
return TemplatesStorage.instance;
});
}
/**
* 获取模板缓存和 api 请求结合
*/
getTemplates() {
const data = super.getData();
if (data) {
return Promise.resolve(data);
}
return api.get("xxx").then((val) => {
this.setTemplates(val);
return super.getData();
});
}
/**
* 保存数据到内存后提交到数据源
*/
setTemplats(templates: any[]) {
super.setData(templates);
super.commit();
}
}
/**
* 获取模版信息函数
*/
const getTemplates = () => {
return TemplatesStorage.getStoreInstance().then((instance) => {
return instance.getTemplates();
});
};</code></pre><p>针对于某些特定列表顺序需求,我们还可以构建 ListStorageHelper。</p><pre><code class="ts">import { ListStorageHelper, MemoryAdaptor } from "../src";
// 当前用户 id
const userId = "1";
const store = new ListStorageHelper({
storageKey: `templates.${userId}`,
version: 1,
// 设置唯一键 key,默认为 'id'
key: "searchVal",
// 列表存储最大数据量,默认为 10
maxCount: 100,
// 修改数据后是否移动到最前面,默认为 true
isMoveTopWhenModified: true,
// 添加数据后是否是最前面, 默认为 true
isUnshiftWhenAdded: true,
});
store.setItem({ searchVal: "new game" });
store.getData();
// [{
// searchVal: 'new game'
// }]
store.setItem({ searchVal: "new game2" });
store.getData();
// 会插入最前面
// [{
// searchVal: 'new game2'
// }, {
// searchVal: 'new game'
// }]
store.setItem({ searchVal: "new game" });
store.getData();
// 会更新到最前面
// [{
// searchVal: 'new game'
// }, {
// searchVal: 'new game2'
// }]
// 提交到 localStorage
store.commit();</code></pre><h2>storage-tools 项目演进</h2><p>任何项目都不是一触而就的,下面是关于 storage-tools 库的编写思路。希望能对大家有一些帮助。</p><h3>StorageHelper 支持 localStorage 存储</h3><p>项目的第一步就是支持本地储存 localStorage 的存取。</p><pre><code class="ts">// 获取从 1970 年 1 月 1 日 00:00:00 UTC 到用户机器时间的秒数
// 后续有需求也会向外提供时间函数配置,可以结合 sync-time 库一起使用
const getCurrentSecond = () => parseInt(`${new Date().getTime() / 1000}`);
// 获取当前空数据
const getEmptyDataStore = (version: number): DataStore<any> => {
const currentSecond = getCurrentSecond();
return {
// 当前数据的创建时间
createdOn: currentSecond,
// 当前数据的修改时间
modifiedOn: currentSecond,
// 当前数据的版本
version,
// 数据,空数据为 null
data: null,
};
};
class StorageHelper<T> {
// 存储的 key
private readonly storageKey: string;
// 存储的版本信息
private readonly version: number;
// 内存中数据,方便随时读写
store: DataStore<T> | null = null;
constructor({ storageKey, version }) {
this.storageKey = storageKey;
this.version = version || 1;
this.load();
}
load() {
const result: string | null = localStorage.getItem(this.storageKey);
// 初始化内存信息数据
this.initStore(result);
}
private initStore(storeStr: string | null) {
// localStorage 没有数据,直接构建 空数据放入 store
if (!storeStr) {
this.store = getEmptyDataStore(this.version);
return;
}
let store: DataStore<T> | null = null;
try {
// 开始解析 json 字符串
store = JSON.parse(storeStr);
// 没有数据或者 store 没有 data 属性直接构建空数据
if (!store || !("data" in store)) {
store = getEmptyDataStore(this.version);
} else if (store.version !== this.version) {
// 版本不一致直接升级
store = this.upgrade(store);
}
} catch (_e) {
// 解析失败了,构建空的数据
store = getEmptyDataStore(this.version);
}
this.store = store || getEmptyDataStore(this.version);
}
setData(data: T) {
if (!this.store) {
return;
}
this.store.data = data;
}
getData(): T | null {
if (!this.store) {
return null;
}
return this.store?.data;
}
commit() {
// 获取内存中的 store
const store = this.store || getEmptyDataStore(this.version);
store.version = this.version;
const now = getCurrentSecond();
if (!store.createdOn) {
store.createdOn = now;
}
store.modifiedOn = now;
// 存储数据到 localStorage
localStorage.setItem(this.storageKey, JSON.stringify(store));
}
/**
* 获取内存中 store 的信息
* 如 modifiedOn createdOn version 等信息
*/
get(key: DataStoreInfo) {
return this.store?.[key];
}
upgrade(store: DataStore<T>): DataStore<T> {
// 获取当前的秒数
const now = getCurrentSecond();
// 看起来很像 getEmptyDataStore 代码,但实际上是不同的业务
// 不应该因为代码相似而合并,不利于后期扩展
return {
// 只获取之前的创建时间,如果没有使用当前的时间
createdOn: store?.createdOn || now,
modifiedOn: now,
version: this.version,
data: null,
};
}
}</code></pre><h3>StorageHelper 添加超时机制</h3><p>添加超时机制很简单,只需要在 getData 的时候检查一下数据即可。</p><pre><code class="ts">class StorageHelper<T> {
// 其他代码 ...
// 超时时间,默认为 -1,即不超时
private readonly timeout: number = -1;
constructor({ storageKey, version, timeout }: StorageHelperParams) {
// 传入的数据是数字类型,且大于 0,就设定超时时间
if (typeof timeout === "number" && timeout > 0) {
this.timeout = timeout;
}
}
getData(): T | null {
if (!this.store) {
return null;
}
// 如果小于 0 就没有超时时间,直接返回数据,事实上不可能小于0
if (this.timeout < 0) {
return this.store?.data;
}
// 修改时间加超时时间大于当前时间,则表示没有超时
// 注意,每次 commit 都会更新 modifiedOn
if (getCurrentSecond() < (this.store?.modifiedOn || 0) + this.timeout) {
return this.store?.data;
}
// 版本信息在最开始时候处理过了,此处直接返回 null
return null;
}
}</code></pre><h3>StorageHelper 添加其他存储适配</h3><p>此时我们可以添加其他数据源适配,方便开发者自定义 storage。</p><pre><code class="ts">/**
* 适配器接口,存在 getItem 以及 setItem
*/
interface StorageAdaptor {
getItem: (key: string) => string | Promise<string> | null;
setItem: (key: string, value: string) => void;
}
class StorageHelper<T> {
// 其他代码 ...
// 非浏览器环境不具备 localStorage,所以不在此处直接构造
readonly adapter: StorageAdaptor;
constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) {
// 此处没有传递 adapter 就会使用 localStorage
// adapter 对象必须有 getItem 和 setItem
// 此处没有进一步判断 getItem 是否为函数以及 localStorage 是否存在
// 没有办法限制住所有的异常
this.adapter = adapter && "getItem" in adapter && "setItem" in adapter
? adapter
: localStorage;
this.load();
}
load() {
// 此处改为 this.adapter
const result: Promise<string> | string | null = this.adapter.getItem(
this.storageKey,
);
}
commit() {
// 此处改为 this.adapter
this.adapter.setItem(this.storageKey, JSON.stringify(store));
}
}</code></pre><h3>StorageHelper 添加异步获取</h3><p>如有些数据源需要异步构建并获取数据,例如 IndexedDB 。这里我们先建立一个 IndexedDBAdaptor 类。</p><pre><code class="ts">import { StorageAdaptor } from "../utils";
// 把 indexedDB 的回调改为 Promise
function promisifyRequest<T = undefined>(
request: IDBRequest<T> | IDBTransaction,
): Promise<T> {
return new Promise<T>((resolve, reject) => {
// @ts-ignore
request.oncomplete = request.onsuccess = () => resolve(request.result);
// @ts-ignore
request.onabort = request.onerror = () => reject(request.error);
});
}
/**
* 创建并返回 indexedDB 的句柄
*/
const createStore = (
dbName: string,
storeName: string,
upgradeInfo: IndexedDBUpgradeInfo = {},
): UseStore => {
const request = indexedDB.open(dbName);
/**
* 创建或者升级时候会调用 onupgradeneeded
*/
request.onupgradeneeded = () => {
const { result: store } = request;
if (!store.objectStoreNames.contains(storeName)) {
const { options = {}, indexList = [] } = upgradeInfo;
// 基于 配置项生成 store
const store = request.result.createObjectStore(storeName, { ...options });
// 建立索引
indexList.forEach((index) => {
store.createIndex(index.name, index.keyPath, index.options);
});
}
};
const dbp = promisifyRequest(request);
return (txMode, callback) =>
dbp.then((db) =>
callback(db.transaction(storeName, txMode).objectStore(storeName))
);
};
export class IndexedDBAdaptor implements StorageAdaptor {
private readonly store: UseStore;
constructor({ dbName, storeName, upgradeInfo }: IndexedDBAdaptorParams) {
this.store = createStore(dbName, storeName, upgradeInfo);
}
/**
* 获取数据
*/
getItem(key: string): Promise<string> {
return this.store("readonly", (store) => promisifyRequest(store.get(key)));
}
/**
* 设置数据
*/
setItem(key: string, value: string) {
return this.store("readwrite", (store) => {
store.put(value, key);
return promisifyRequest(store.transaction);
});
}
}</code></pre><p>对 StorageHelper 类做如下改造</p><pre><code class="ts">type CreateDeferredPromise = <TValue>() => CreateDeferredPromiseResult<TValue>;
// 劫持一个 Promise 方便使用
export const createDeferredPromise: CreateDeferredPromise = <T>() => {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {
currentPromise: promise,
resolve,
reject,
};
};
export class StorageHelper<T> {
// 是否准备好了
ready: CreateDeferredPromiseResult<boolean> = createDeferredPromise<
boolean
>();
constructor({ storageKey, version, adapter, timeout }: StorageHelperParams) {
this.load();
}
load() {
const result: Promise<string> | string | null = this.adapter.getItem(
this.storageKey,
);
// 检查一下当前的结果是否是 Promise 对象
if (isPromise(result)) {
result
.then((res) => {
this.initStore(res);
// 准备好了
this.ready.resolve(true);
})
.catch(() => {
this.initStore(null);
// 准备好了
this.ready.resolve(true);
});
} else {
// 不是 Promise 直接构建 store
this.initStore(result);
// 准备好了
this.ready.resolve(true);
}
}
// 询问是否做好准备
whenReady() {
return this.ready.currentPromise;
}
}</code></pre><p>如此,我们就完成了 StorageHelper 全部代码。</p><h3>列表辅助类 ListStorageHelper</h3><p>ListStorageHelper 基于 StorageHelper 构建,方便特定业务使用。</p><pre><code class="ts">// 数组最大数量
const STORE_MAX_COUNT: number = 10;
export class ListStorageHelper<T> extends StorageHelper<T[]> {
// 主键,默认为 id
readonly key: string = "id";
// 存储最大数量,默认为 10
readonly maxCount: number = STORE_MAX_COUNT;
// 是否添加在最前面
readonly isUnshiftWhenAdded: boolean = true;
// 修改后是否放入最前面
readonly isMoveTopWhenModified: boolean = true;
constructor({
maxCount,
key,
isMoveTopWhenModified = true,
isUnshiftWhenAdded = true,
storageKey,
version,
adapter,
timeout,
}: ListStorageHelperParams) {
super({ storageKey, version, adapter, timeout });
this.key = key || "id";
// 设置配置项
if (typeof maxCount === "number" && maxCount > 0) {
this.maxCount = maxCount;
}
if (typeof isMoveTopWhenModified === "boolean") {
this.isMoveTopWhenModified = isMoveTopWhenModified;
}
if (typeof this.isUnshiftWhenAdded === "boolean") {
this.isUnshiftWhenAdded = isUnshiftWhenAdded;
}
}
load() {
super.load();
// 没有数据,设定为空数组方便统一
if (!this.store!.data) {
this.store!.data = [];
}
}
getData = (): T[] => {
const items = super.getData() || [];
// 检查数据长度并移除超过的数据
this.checkThenRemoveItem(items);
return items;
};
setItem(item: T) {
if (!this.store) {
throw new Error("Please complete the loading load first");
}
const items = this.getData();
// 利用 key 去查找存在数据索引
const index = items.findIndex(
(x: any) => x[this.key] === (item as any)[this.key],
);
// 当前有数据,是更新
if (index > -1) {
const current = { ...items[index], ...item };
// 更新移动数组数据
if (this.isMoveTopWhenModified) {
items.splice(index, 1);
items.unshift(current);
} else {
items[index] = current;
}
} else {
// 添加
this.isUnshiftWhenAdded ? items.unshift(item) : items.push(item);
}
// 检查并移除数据
this.checkThenRemoveItem(items);
}
removeItem(key: string | number) {
if (!this.store) {
throw new Error("Please complete the loading load first");
}
const items = this.getData();
const index = items.findIndex((x: any) => x[this.key] === key);
// 移除数据
if (index > -1) {
items.splice(index, 1);
}
}
setItems(items: T[]) {
if (!this.store) {
return;
}
this.checkThenRemoveItem(items);
// 批量设置数据
this.store.data = items || [];
}
/**
* 多添加一个方法 getItems,等同于 getData 方法
*/
getItems() {
if (!this.store) {
return null;
}
return this.getData();
}
checkThenRemoveItem = (items: T[]) => {
if (items.length <= this.maxCount) {
return;
}
items.splice(this.maxCount, items.length - this.maxCount);
};
}</code></pre><p>该类继承了 StorageHelper,我们依旧可以直接调用 commit 提交数据。如此我们就不需要维护复杂的 storage 存取逻辑了。</p><p>代码都在 <a href="https://link.segmentfault.com/?enc=qhitvhTz%2BT%2BF7Zlcms6TxQ%3D%3D.rkQrQbbz44ZrHqNo7X04kvkQNeQM1oXIG5bVpixODo1dmZ9Oo6JBHF%2B83yh1zP50" rel="nofollow">storage-tools</a> 中,欢迎各位提交 issue 以及 pr。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=2kAu2zyQkDISzR3ubTEIuA%3D%3D.s3%2B%2BAYPxgGts%2FCucmlz0jlWoWE2kVQqWx6pSpE5BKGaVqi3%2F8FOqseHMulN5n0Oa" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=EWCYMnJTnDYFfDHjDFgqxQ%3D%3D.ws9Z4zNTV0OlusVsJTUuh8nFImYwXD%2FD1chOzb6knfxxAXrFqmb4ibGuHToOIjDZ" rel="nofollow">storage-tools</a></p><p><a href="https://link.segmentfault.com/?enc=GvAQyfKFwDPU9JP1jtE9Hw%3D%3D.uKPgr9sYqtHUFhDc9LEA50GJ6NA0ir55D2zf%2F4Kd3Us8OVLUdOIKpYqYd6Ivn4Cq" rel="nofollow">sync-time</a></p>
从 await-to-js 到 try-run-js
https://segmentfault.com/a/1190000043103112
2022-12-18T20:38:43+08:00
2022-12-18T20:38:43+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<p>之前在做 code review 时候发现有同事使用 try catch 包装了一堆异步代码,于是个人就觉得很奇怪,难道不应该只 catch 可能出问题的代码吗?同事告诉我说 try catch 太细的话会出现内外作用域不一致,需要提前声明变量。</p><pre><code class="ts">let res: Data[] = [];
try {
res = await fetchData();
} catch (err) {
// 错误操作或者终止
// return
}
// 继续执行正常逻辑</code></pre><p>的确,一方面开发者不应该大范围包裹非异常代码,另一方面提前声明变量会让代码不连贯同时也会打断思路。其中一个方式是直接使用原生 Promie 而不是 async。</p><pre><code class="ts">fetchData().then((res) => {
}).catch((err) => {
});</code></pre><p>这样对于单个异步请求当然没有任何问题,如果是具有依赖性的异步请求。虽然可以再 Promise 中返回另外的 Promise 请求,但是这样处理 catch 却只能有一个。</p><pre><code class="ts">fetchData().then((res) => {
// 业务处理
return fetchData2(res);
}).then((res) => {
// 业务处理
}).catch((err) => {
// 只能做一个通用的错误处理了
});</code></pre><p>如果需要多个 catch 处理,我们就需要这样写。</p><pre><code class="ts">fetchData().then((res) => {
// 业务处理
return fetchData2(res);
}).catch((err) => {
// 错误处理并且返回 null
return null;
}).then((res) => {
if (res === null) {
return;
}
// 业务处理
}).catch((err) => {
// 错误处理
});</code></pre><p>这时候开发者也要考虑 fetchData2 会不会返回 null 的问题。于是个人开始找一些方法来帮助我们解决这个问题。</p><h2>await-to-js</h2><p><a href="https://link.segmentfault.com/?enc=xKhsdvvBThki%2F9%2BLMsP7oQ%3D%3D.3oqO0zUSaeNK9eAAjPjEbt8RzC0US36I4RuaTHdZCO5fs7jYzQ3wRyYDvGDEyOLb" rel="nofollow">await-to-js</a> 是一个辅助开发者处理异步错误的库。我们先来看看该库是如何解决我们问题的。</p><pre><code class="ts">import to from "await-to-js";
const [fetch1Err, fetch1Result] = await to(fetchData());
if (fetch1Err) {
// 错误操作或者终止
// return
}
const [fetch2Err, fetch1Result] = await to(fetchData2(fetch1Result));
if (fetch2Err) {
// 错误操作或者终止
// return
}</code></pre><p>源码非常简单。</p><pre><code class="js">export function to(
promise,
errorExt,
) {
return promise
.then((data) => [null, data])
.catch((err) => {
if (errorExt) {
const parsedError = Object.assign({}, err, errorExt);
return [parsedError, undefined];
}
return [err, undefined];
});
}</code></pre><h2>使用 try-run-js</h2><p>看到 await-to-js 将错误作为正常流程的一部分,于是个人想到是不是能通过 try catch 解决一些异步代码问题呢?</p><p>我立刻想到了需要获取 DOM 节点的需求。现有框架都使用了数据驱动的思路,但是 DOM 具体什么时候渲染是未知的,于是个人想到之前代码,Vue 需要获取 ref 并进行回调处理。</p><pre><code class="ts">function resolveRef(refName, callback, time: number = 1) {
// 超过 10 次跳出递归
if (time > 10) throw new Error(`cannot find ref: ${refName}`);
//
const self = this;
// 获取 ref 节点
const ref = this.$refs[refName];
if (ref) {
callback(ref);
} else {
// 没有节点就下一次
this.$nextTick(() => {
resolveRef.call(self, refName, callback, time + 1);
});
}
}</code></pre><p>当然了,上述代码的确可以解决此类的问题,在处理此类问题时候我们可以替换 ref 和 nextTick 的代码。于是 await-to-js 的逻辑下,个人开发了 <a href="https://link.segmentfault.com/?enc=itdTAo08ZSGGsnZcnGZnKA%3D%3D.Nr%2BXw4vz1wNii%2F2%2FAUZbDHdYxsj4qRAfiEEBq51uKGdRj%2BJXDEVzC0z09lsoMK4R" rel="nofollow">try-run-js</a> 库。我们先看一下该库如何使用。</p><pre><code class="ts">import tryRun from "try-run-js";
tryRun(() => {
// 直接尝试使用正常逻辑代码
// 千万不要添加 ?.
// 代码不会出错而不会重试
this.$refs.navTree.setCurrentKey("xxx");
}, {
// 重试次数
retryTime: 10,
// 下次操作前需要的延迟时间
timeout: () => {
new Promise((resolve) => {
this.$nextTick(resolve);
});
},
});</code></pre><p>我们也可以获取错误数据和结果。</p><pre><code class="ts">import tryRun from "try-run-js";
const getDomStyle = async () => {
// 获取一步的
const { error: domErr, result: domStyle } = await tryRun(() => {
// 返回 dom 节点样式,不用管是否存在 ppt
// 千万不要添加 ?.
// 代码不会出错而返回 undefined
return document.getElementById("domId").style;
}, {
// 重试次数
retryTime: 3,
// 返回数字的话,函数会使用 setTimeout
// 参数为当前重试的次数,第一次重试 100 ms,第二次 200
timeout: (time) => time * 100,
// 还可以直接返回数字,不传递默认为 333
// timeout: 333
});
if (domErr) {
return {};
}
return domStyle;
};</code></pre><p>当然了,该库也是支持返回元组以及 await-to-js 的 Promise 错误处理的功能的。</p><pre><code class="ts">import { tryRunForTuple } from "try-run-js";
const [error, result] = await tryRunForTuple(fetchData());</code></pre><h2>try-run-js 项目演进</h2><p>try-run-js 核心在于 try catch 的处理,下面是关于 try-run-js 的编写思路。希望能对大家有一些帮助</p><h3>支持 await-to-js</h3><pre><code class="ts">const isObject = (val: any): val is Object =>
val !== null &&
(typeof val === "object" || typeof val === "function");
const isPromise = <T>(val: any): val is Promise<T> => {
// 继承了 Promise
// 拥有 then 和 catch 函数,对应手写的 Promise
return val instanceof Promise || (
isObject(val) &&
typeof val.then === "function" &&
typeof val.catch === "function"
);
};
const tryRun = async <T>(
// 函数或者 promise
promiseOrFun: Promise<T> | Function,
// 配置项目
options?: TryRunOptions,
): Promise<TryRunResultRecord<T>> => {
// 当前参数是否为 Promise
const runParamIsPromise = isPromise(promiseOrFun);
const runParamIsFun = typeof promiseOrFun === "function";
// 既不是函数也不是 Promise 直接返回错误
if (!runParamIsFun && !runParamIsPromise) {
const paramsError = new Error("first params must is a function or promise");
return { error: paramsError } as TryRunResultRecord<T>;
}
if (runParamIsPromise) {
// 直接使用 await-to-js 代码
return runPromise(promiseOrFun as Promise<T>);
}
};</code></pre><h3>执行错误重试</h3><p>接下来我们开始利用 try catch 捕获函数的错误并且重试。</p><pre><code class="ts">// 默认 timeout
const DEFAULT_TIMEOUT: number = 333
// 异步等待
const sleep = (timeOut: number) => {
return new Promise<void>(resolve => {
setTimeout(() => {
resolve()
}, timeOut)
})
}
const tryRun = async <T>(
promiseOrFun: Promise<T> | Function,
options?: TryRunOptions,
): Promise<TryRunResultRecord<T>> => {
const { retryTime = 0, timeout = DEFAULT_TIMEOUT } = {
...DEFAULT_OPTIONS,
...options,
};
// 当前第几次重试
let currentTime: number = 0;
// 是否成功
let isSuccess: boolean = false;
let result;
let error: Error;
while (currentTime <= retryTime && !isSuccess) {
try {
result = await promiseOrFun();
// 执行完并获取结果后认为当前是成功的
isSuccess = true;
} catch (err) {
error = err as Error;
// 尝试次数加一
currentTime++;
// 注意这里,笔者在这里犯了一些错误
// 如果没有处理好就会执行不需要处理的 await
// 1.如果当前不需要重新请求(重试次数为 0),直接跳过
// 2.最后一次也失败了(重试完了)也是要跳过的
if (retryTime > 0 && currentTime <= retryTime) {
// 获取时间
let finalTimeout: number | Promise<any> = typeof timeout === "number"
? timeout
: DEFAULT_TIMEOUT;
// 如果是函数执行函数
if (typeof timeout === "function") {
finalTimeout = timeout(currentTime);
}
// 当前返回 Promise 直接等待
if (isPromise(finalTimeout)) {
await finalTimeout;
} else {
// 如果最终结果不是 number,改为默认数据
if (typeof finalTimeout !== "number") {
finalTimeout = DEFAULT_TIMEOUT;
}
// 这里我尝试使用了 NaN、 -Infinity、Infinity
// 发现 setTimeout 都进行了处理,下面是浏览器的处理方式
// If timeout is an Infinity value, a Not-a-Number (NaN) value, or negative, let timeout be zero.
// 负数,无穷大以及 NaN 都会变成 0
await sleep(finalTimeout);
}
}
}
}
// 成功或者失败的返回
if (isSuccess) {
return { result, error: null };
}
return { error: error!, result: undefined };
};</code></pre><p>这样,我们基本完成了 try-run-js.</p><h3>添加 tryRunForTuple 函数</h3><p>这个就很简单了,直接直接 tryRun 并改造其结果:</p><pre><code class="ts">const tryRunForTuple = <T>(
promiseOrFun: Promise<T> | Function,
options?: TryRunOptions): Promise<TryRunResultTuple<T>> => {
return tryRun<T>(promiseOrFun, options).then(res => {
const { result, error } = res
if (error) {
return [error, undefined] as [any, undefined]
}
return [null, result] as [null, T]
})
}</code></pre><p>代码都在 <a href="https://link.segmentfault.com/?enc=OryKkii7retdpc1QMhLHbQ%3D%3D.d3pTP6hhhxIh7GXeW3%2FsHmZS3j9OIDxaPQ5g3eZ8JGGCyAuH1xhucHUrT5%2BeTmhw" rel="nofollow">try-run-js</a> 中,大家还会在什么情况下使用 try-run-js 呢?同时也欢迎各位提交 issue 以及 pr。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=QP7bq11KE1GZjEZCmDx5%2BQ%3D%3D.%2BwKvJONaFmRzalSZcga4O%2Bt53wqDFiDlgJ67D0TpUhbiAuMeJpipKXffv1SjzHbN" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=Xp9R3CxhkoBQBUb8KofIFQ%3D%3D.WAhnyTpHEAT%2FmEC70KnH2o%2BKbrGN9FZHBnS4vcDy5ITeuNAgjuNogjk%2BAHcahZFq" rel="nofollow">await-to-js</a></p><p><a href="https://link.segmentfault.com/?enc=HBKA4zEn4%2B5ls0yrMUj5kw%3D%3D.HgmHITpjb6tRnlJrvrCK%2B9ohwghxygkDgBpxOcv8oKat9%2BpcXe%2Ber4KryjS8KT0t" rel="nofollow">try-run-js</a></p>
手写一个 React 动画组件
https://segmentfault.com/a/1190000043048394
2022-12-13T00:29:25+08:00
2022-12-13T00:29:25+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<p>在项目开发的过程中,设计师不免会做一些动画效果来提升用户体验。如果当前效果不需要交互,只是用来展示的话,我们完全可以利用 GIF 或者 APNG 来实现效果。不了解 APNG 小伙伴可以看看这篇博客 <a href="https://link.segmentfault.com/?enc=MeM5R5XQNu05pLGvP%2FqWiw%3D%3D.uUZjKaP9LLsYcz5BxmGcV2Tf2OViTdL0yLAZFZEAzyfCJobML1RwqBRVBAjxzhIrBNzKFaTo23XAJ8AGGSkOYHjSm20u0sRh8dXFP7V00UnDkUAMMHdqQYZWDk51qrDe" rel="nofollow">APNG 历史、特性简介以及 APNG 制作演示</a>。</p><p><img src="/img/bVc4M05" alt="" title=""></p><p>但是如果当前动画除了展示还需要其他交互,甚至是一个组件需要动画效果,使用图片格式就不合理了。于是我写一个极其简单的 css 动画库 <a href="https://link.segmentfault.com/?enc=MBD4mBHVQzc%2FAcN2%2FhpHjw%3D%3D.ID4kw80jtTks1E8AmVYH45aXm3oWV1x03HoDTLms6Tz2m72cuv2ktpVc3f3MI4Q9" rel="nofollow">rc-css-animate</a>。这里直接使用 <a href="https://link.segmentfault.com/?enc=xkCXchgC1FC4zagWVe9ggw%3D%3D.PiT8nW9NcDToURlFwaCm9rIJIEz1QyiRXhl3Z67Wg%2Fk%3D" rel="nofollow">animate.css</a> 作为 css 动画的依赖库。 animate.css 不但提供了很多交互动画样式类,也提供了动画运行速度,延迟,以及重复次数等样式类。</p><p>可以看到,默认的 animate.css 构建动画都需要携带前缀 “animate__”。</p><pre><code class="html"><h1 class="animate__animated animate__bounce">An animated element</h1></code></pre><p>当然,该库是对 css 动画进行了一层封装,依然支持其他动画库以及自己手写的 css 动画,但如果开发者需要对动画进行各种复杂控制,不推荐使用此库。</p><h2>使用</h2><p>可以利用如下方式使用:</p><pre><code class="tsx">import React, { useRef } from "react";
import ReactCssAnimate from "rc-css-animate";
// 引入 animate.css 作为动画依赖
import "animate.css";
function App() {
const animateRef = useRef(null);
return (
<div className="App">
<ReactCssAnimate
// 定义当前展示动画的组件
// 默认使用 div
tag="div"
// 当前组件的 className
className=""
// 当前组件的 style
style={{}}
// 当前组件的 ref
ref={animateRef}
// 动画前缀
clsPrefix="animate__"
// 当前动画的 className
animateCls="animated backInDown infinite"
// 动画开始时候是否处于展示状态
initialVisible={false}
// 获取动画结束是否处理展示状态
getVisibleWhenAnimateEnd={(cls) => {
// 如果当前 animateCls 中有 Out
// 返回 false 则会在动画结束后不再显示
if (cls.includes("Out")) {
return false;
}
return true;
}}
// 动画结束回调
onAnimationEnd={() => {
console.log("done");
}}
>
<div>
测试动画
</div>
</ReactCssAnimate>
</div>
);
}</code></pre><p>ReactCssAnimate 使用了 React hooks,但是也提供了兼容的类组件。同时也提供了全局的前缀设置。</p><pre><code class="tsx">import React from "react";
import {
// 使用类组件兼容之前的版本
CompatibleRAnimate as ReactCssAnimate,
setPrefixCls,
} from "rc-css-animate";
// 引入 animate.css 作为动画依赖
import "animate.css";
// 设置全局 prefix,会被当前组件覆盖
setPrefixCls("animate__");
/** 构建动画块组件 */
function BlockWrapper(props) {
// 需要获取并传入 className, children, style
const { className, children, style } = props;
return (
<div
className={className}
style={{
background: "red",
padding: 100,
...style,
}}
>
{children}
</div>
);
}
function App() {
return (
<div className="App">
<ReactCssAnimate
tag={BlockWrapper}
// 当前动画的 className
animateCls="animated backInDown infinite"
>
<div>
测试动画
</div>
</ReactCssAnimate>
</div>
);
}</code></pre><h2>源码解析</h2><p>源代码较为简单,是基于 createElment 和 forwardRef 构建完成。其中 forwardRef 会将当前设置的 ref 转发到内部组件中去。对于 forwardRef 不熟悉的同学可以查看一下官网中关于 <a href="https://link.segmentfault.com/?enc=EsVLn2T1xK4BYBZC4p2zkw%3D%3D.vIaCOkn9C8E7OueeJG8Y%2Bjr8waI13oE01%2BOTWg3hIEw%2BguxtUZRkizQU9RWO6W9AmKw%2BdeG03xlnPk7ZI6GxTQ%3D%3D" rel="nofollow">Refs 转发的文档</a>。</p><pre><code class="tsx">import React, {
createElement,
forwardRef,
useCallback,
useEffect,
useState,
} from "react";
import { getPrefixCls } from "./prefix-cls";
import { AnimateProps } from "./types";
// 全局的动画前缀
let prefixCls: string = "";
const getPrefixCls = (): string => prefixCls;
// 设置全局的动画前缀
export const setPrefixCls = (cls: string) => {
if (typeof cls !== "string") {
return;
}
prefixCls = cls;
};
const Animate = (props: AnimateProps, ref: any) => {
const {
tag = "div",
clsPrefix = "",
animateCls,
style,
initialVisible,
onAnimationEnd,
getVisibleWhenAnimateEnd,
children,
} = props;
// 通过 initialVisible 获取组件的显隐,如果没有则默认为 true
const [visible, setVisible] = useState<boolean>(initialVisible ?? true);
// 当前不需要展示,返回 null 即可
if (!visible) {
return null;
}
// 没有动画类,直接返回子组件
if (!animateCls || typeof animateCls !== "string") {
return <>{children}</>;
}
useEffect(() => {
// 当前没获取请求结束的设置显示隐藏,直接返回,不进行处理
if (!getVisibleWhenAnimateEnd) {
return;
}
const visibleWhenAnimateEnd = getVisibleWhenAnimateEnd(animateCls);
// 如果动画结束后需要展示并且当前没有展示,直接进行展示
if (visibleWhenAnimateEnd && !visible) {
setVisible(true);
}
}, [animateCls, visible, getVisibleWhenAnimateEnd]);
const handleAnimationEnd = useCallback(() => {
if (!getVisibleWhenAnimateEnd) {
onAnimationEnd?.();
return;
}
// 当前处于展示状态,且动画结束后需要隐藏,直接设置 visible 为 false
if (visible && !getVisibleWhenAnimateEnd(animateCls)) {
setVisible(false);
}
onAnimationEnd?.();
}, [getVisibleWhenAnimateEnd]);
let { className = "" } = props;
if (typeof className !== "string") {
className = "";
}
let animateClassName = animateCls;
// 获取最终的动画前缀
const finalClsPrefix = clsPrefix || getPrefixCls();
// 没有或者动画前缀不是字符串,不进行处理
if (!finalClsPrefix || typeof finalClsPrefix !== "string") {
animateClassName = animateCls.split(" ").map((item) =>
`${finalClsPrefix}${item}`
).join(" ");
}
// 创建并返回 React 元素
return createElement(
tag,
{
ref,
onAnimationEnd: handleAnimationEnd,
// 将传递的 className 和 animateClassName 合并
className: className.concat(` ${animateClassName}`),
style,
},
children,
);
};
// 利用 forwardRef 转发 ref
// 第一个参数是 props,第二个参数是 ref
export default forwardRef(Animate);</code></pre><p>以上代码全部在 <a href="https://link.segmentfault.com/?enc=CK17QITPz7e3hShoEUZnYQ%3D%3D.2YBfRHQz4NF1LmzjOi7ixeMh6LHHPaqwE5PBdT7EHbLvAeNGyrqJzzQN1WrYbFmn" rel="nofollow">rc-css-animate</a> 中。这里也欢迎各位小伙伴提出 issue 和 pr。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=mXDXzBkiCwTy4JB%2Bbe3pqg%3D%3D.Mb07gx4spS27DeXBgCfDFfbGli22jpQGPGQJjHsjy4%2BYNcW4Y9C6IAfBRxOfHEHG" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=f4v6a55OTrGJpT4xkaxn5w%3D%3D.UEnQ7Hn%2BolE%2BksX6H3DBJIr%2Fl7vMq%2BEcicoftqsCJU0j5nuUAzH13m2i8s9A2n2pLQNMHTnAFah0P%2Fl1WZTYfviOySptday2bpOjTUhTwTg2N0f11oIuyIOv%2FjthaQEt" rel="nofollow">APNG 历史、特性简介以及 APNG 制作演示</a></p><p><a href="https://link.segmentfault.com/?enc=5%2FFZJlJM%2BRoja%2BM5PnKxyw%3D%3D.83%2Bk2TkivyKRVETAsWvCwnoOO%2BvUl6MalaMWjGYYubyKHJGpCOwqch0uNk2AdJ2Q" rel="nofollow">rc-css-animate</a></p><p><a href="https://link.segmentfault.com/?enc=ATfrh86tsefBTxCg496pEQ%3D%3D.1sjKnXG4BL1N5jCs52oGe34Ohi1sy4%2FEjK3ztv%2FW5kM%3D" rel="nofollow">animate.css</a></p>
手写一个同步服务端时间的小工具
https://segmentfault.com/a/1190000043003926
2022-12-08T01:17:52+08:00
2022-12-08T01:17:52+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
2
<p>在前端开发的过程中,开发者经常会用到 new Date() 来获取当前时间,但是 new Date() 是获取的当前操作系统的时间,由于用户可以修改当前电脑时间,所以它是不准确的。</p><p>大部分情况下,用户修改当前电脑时间都没有什么问题,但是当我们需要根据服务端传递的数据时间与当前时间进行计算时,前端展示就会出错。同时,需要过期时间的数据(时间)存入前端缓存( localStorage, IndexedDB )中也是会出现问题。</p><p>这时候我们考虑使用服务器提供的时间,而不是前端时间。服务器每次进行数据交互时都会在响应头提供时间数据。我们可以通过该数据修正前端时间。</p><p><img src="/img/bVc4BrR" alt="sync-time" title="sync-time"></p><p>于是个人写了一个小工具 <a href="https://link.segmentfault.com/?enc=mnvInrMOXfSxbQn2QmMlow%3D%3D.VmprH1W3lGr8U9PWmfkepgnRDnbEZjJZ9%2FtMcQzOAB0GKHWE%2BVdcsyX%2FJz9IKop4" rel="nofollow">sync-time</a> 。以 fetch 为例子:</p><pre><code class="ts">import { sync, time, date } from 'sync-time'
async function getJSON() {
let url = 'https://www.npmjs.com/search?q=';
let response
try {
response = await fetch(url);
// 响应头部通常会有 date 数据
console.log(response.headers.get('date'))
// 把响应头时间作为服务器时间,调用 sync 同步数据
sync(response.headers.get('date'))
} catch (error) {
}
return response.body
}
getJson()
// => 返回数字,即修正好的毫秒 getTime
time()
// 1670345143730
// 返回 Date,new Date(time())
date()
// Wed Dec 07 2022 00:46:47 GMT+0800 (中国标准时间)</code></pre><p>源代码如下所示:</p><pre><code class="ts">let diffMillisecond: number = 0
// 获取前端时间
const getCurrentTime = (): number => (new Date()).getTime();
// 同步时间
const sync = (time: Date | string): void => {
// 没有传递时间,直接使用前端时间
if (!time) {
diffMillisecond = 0
return
}
// 获取 UNIX 时间戳
const syncTime = time instanceof Date ? time.getTime() : Date.parse(time)
// 当前是 NaN,直接返回
if (Number.isNaN(syncTime)) {
return
}
// 获取两个时间的差值
diffMillisecond = syncTime - getCurrentTime()
}
// 补差值并获取 UNIX 时间戳
const time = (): number => getCurrentTime() + diffMillisecond
const date = (): Date => new Date(time())
export {
sync,
time,
date
}</code></pre><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=DtrD5OuOgpfNhmCi4gZXWA%3D%3D.bK65slWV9RZZlkFEUJcGNqe3zmWtuH5%2Fzdd7AotZToDsUU8mYfLz72kl27mEMJrK" rel="nofollow">博客地址</a></p>
不可变数据工具库 immutability-helper
https://segmentfault.com/a/1190000042984915
2022-12-06T02:46:22+08:00
2022-12-06T02:46:22+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>之前学习函数式编程语言的过程中,有 3 比较重要的特性:</p><ul><li>函数是一等公民</li><li>数据不可变</li><li>惰性求值</li></ul><p>JavaScript 虽然具有函数式语言的特性,但是很可惜,它还是没有具备不可变数据这一大优势。</p><p>在开发复杂系统的情况下,不可变性具有两个非常重要的特性:不可修改 (减少错误的发生) 以及结构共享(节省空间)。不可修改也意味着数据容易回溯,易于观察。</p><p>当前端开发谈到不可变性数据时候,第一个一定会想到 <a href="https://link.segmentfault.com/?enc=DTbhuGdVZ0O8v6TAgRrJeg%3D%3D.ACusAgVkdS8OOSpdaCrbQ7mmwEYjniOp6fXmivOkvdX%2BXhZoY6CV1T40xrm0wpfb" rel="nofollow">Immer</a> 库,Immer 利用<br>ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构。React 也通过不可变数据结构结合提升性能。不过 Immer<br>还是有一定侵入性。那么有没有较好且没有侵入的解决方案呢?本文将介绍另一个工具 <a href="https://link.segmentfault.com/?enc=WNuW9lrvwsvlx%2FP07n%2FXmA%3D%3D.ULZscAs5ijv%2FzTDwp1ZUNywxFZxzXROBl0QL7jyG8TujLgFBVGFY9Fr4cJP0pBJA" rel="nofollow">immutability-helper</a>,该库也在 <a href="https://link.segmentfault.com/?enc=07%2BsgrkWjd0Z%2FHmvXnS7OQ%3D%3D.c4uaXxsuLzMMDeCrKn9Wau355vhETa2lsmQsWLR%2FrG4n8SXxXU1c0MGpSV2CAE4GlzerKd3i91ialgnsOsPteFWHnjemYoEjeI9O4%2FnlusEiVhACXcYnvXPBSE27psfJ" rel="nofollow">React 性能优化</a> 有所描述。</p><h2>浅拷贝实现不可变数据</h2><p>最简单的不可变数据结构就是深拷贝了。</p><pre><code class="ts">const newUser = JSON.parse(JSON.stringify(user));
newUser[key] = value;</code></pre><p>但这对于大部分的场景来说是无法接受的,它大量消耗了时间与空间,会让复杂的系统变得不可用。</p><p>事实上,开发中完全可以利用浅拷贝来实现不可变数据结构的,这也是 immutability-helper 所使用的方案。我们先来构造以下数据:</p><pre><code class="ts">const user = {
name: "wsafight",
company: {
name: "测试公司",
otherInfo: {
owner: "测试公司老板",
},
},
schools: [
{ name: "测试小学" },
{ name: "测试初中" },
{ name: "测试高中" },
],
};</code></pre><p>我们怎么才能在不改变原有数据的情况下改变 user.company.name 呢?代码如下</p><pre><code class="ts">// 修改公司名称
const newUser = {
...user,
company: {
...user.company,
name: "升级测试公司",
},
};
user === newUser;
// false
user.company === newUser.company;
// false
user.company.otherInfo === newUser.company.otherInfo;
// true
newUser.schools === user.schools;
// true</code></pre><p>我们并没有改变原有的 user 数据,同时获取了共用其他数据结构的 newUser。同时,如果当前功能需要数据回溯,即使将当前对象直接存入一个数组中,内存占用也不会出现非常大的情况。当然,<a href="https://link.segmentfault.com/?enc=2YH3zczo0jhL%2BHs4rcGBbQ%3D%3D.RQiKK7q3CF%2FqChHH5TPyDu9gm8i7mq%2BvTlHHVlQ9CsnD%2Fiecdf8rEZ6Je2iQOAZJ" rel="nofollow">Immer Patches</a> 对于回溯的处理更优,后续个人也会继续解读不可变结构的其他工具库。</p><h2>immutability-helper 用法</h2><p>使用浅拷贝来实现不可变数据结构是不错,但是编写起来过于复杂。当开发者面对复杂的数据结构,未免捉襟见肘。还很容易写出 bug。</p><p>于是 kolodny 出手编写了 immutability-helper 来帮助我们构建不可变的数据结构。</p><pre><code class="ts">import update from "immutability-helper";
// 修改公司名称
const newUser = update(user, {
company: {
name: {
$set: "升级测试公司",
},
},
});</code></pre><p>我们可以看到 update 函数传入之前的数据以及一个对象结构,得到了新的数据。$set 是替换目前的数据的意思。除此之外,还有其他的命令。</p><p>针对数组的操作</p><ul><li>{ $push: any[] } 针对当前数组数据 push 一些数组</li><li>{ $unshift: any[] } 针对当前数组数据 unshift 一些数组</li><li>{ $splice: {start: number, deleteCount: number, ...items: T[]}[] }<br>使的参数调用目标上的每个项目,注意顺序</li></ul><pre><code class="ts">// 添加了用户的学校
const newUser = update(user, {
schools: {
$push: [
{ name: "测试大学" },
],
},
});
const newUser = update(user, {
schools: {
$unshift: [
{ name: "测试幼儿园" },
],
},
});
// 排序操作
const sourceItem = user[sourceIndex];
const newUser = update(user, {
schools: {
$splice: [
[sourceIndex, 1],
[targetIndex, 0, sourceItem!],
],
},
});
const newUser = update(user, {
schools: {
// 也可以同时放入命令进行操作
$unshift: [
{ name: "测试幼儿园" },
],
$push: [
{ name: "测试幼儿园" },
],
$splice: [],
},
});</code></pre><p>还有一个可以基于当前数据进行操作的 $apply.</p><pre><code class="ts">// 每次更新都基于当前的数据来计算
const newUser = update(user, {
name: {
$apply: (name) => `${name} change`,
},
});</code></pre><p>该库还有针对对象的 $set, $unset, $merge 以及针对 Map,Set 的 $add, $remove。甚至我们还可以自定义指令。这些就不一一介绍了,大家遇到了就自行查阅一下文档。</p><h2>添加辅助函数</h2><p>对比之前的写法无疑对我们已经有很大的帮助了。但是针对当前操作还是非常难受。还是需要编写复杂的数据结构。</p><p>编写如下函数:</p><pre><code class="ts">export const convertImmutabilityByPath = (
// 对象路径
path: string,
// 当前操作
actions: Record<string, any>,
) => {
// 路径 path 没有或者不是字符串,直接返回空对象
if (!path || typeof path !== "string") {
return {};
}
// actions 没有或者不是对象,直接返回空对象
if (
!actions || Object.prototype.toString.call(actions) !== "[object Object]"
) {
return {};
}
// 简单替换 [ 和 ] 为 . 和 空字符串,没有做太多逻辑处理
// 请不要建立奇怪的对象路径,否则可能出现未知错误
const keys = path.replace(/\[/g, ".")
.replace(/\]/g, "")
.split(".")
.filter(Boolean);
const result: Record<string, any> = {};
let current = result;
const len = keys.length;
// 根据路径一步步构建对象
keys.forEach((key: string, index: number) => {
current[key] = index === len - 1 ? actions : {};
current = current[key];
});
return result;
};</code></pre><p>当前代码在 <a href="https://link.segmentfault.com/?enc=kEvCb8jB2nDCkXkTiptiOA%3D%3D.kqOho%2BPqLFlOrFsv%2F94nINFP%2Fe8DvWxK0es4m7omvoZ2DiMlSD7GXoqt5xwKCkPM" rel="nofollow">val-path-helper</a> 中,该库还有其他的功能,目前还在编写中。</p><p>如此一来我们就可以直接编辑数据了。</p><pre><code class="ts">convertImmutabilityByPath(
"schools[0].name",
{ $set: "试试小学" },
);
// 也可以使用 'schools.0.name' 'schools.[0].name'
// 甚至 'schools[0.name' 也行
// 我们也可以使用这种方式操作数据中对象
convertImmutabilityByPath(
`schools[${index}].${key}`,
{ $set: value },
);</code></pre><h2>实测 React</h2><p>这里我们开始实测 immutability-helper 对于 react 渲染的帮助。代码利用 <a href="https://link.segmentfault.com/?enc=fIjk%2FmF0qFOPTu4hOVtKXA%3D%3D.4CZZQ9jsxNAgMDQx%2B%2FSp4CDsa73EUcU6K2jmjileNDcW%2BdUYxLPrf0GYKD4IXOfG" rel="nofollow">Profiler API</a> 来查看渲染代价。</p><pre><code class="ts">function App() {
const [user, setUser] = useState({
name: "wsafight",
company: {
name: "测试公司",
},
schools: [
{ name: "测试小学", start: "1998-01-02", end: "2004-01-02" },
{ name: "测试高中", start: "2005-01-02", end: "2007-01-02" },
],
});
/**
* Profiler 组件,可以查看渲染
*/
const renderCallback = (...info) => {
console.log("渲染原因", info[1]);
console.log("本次更新 committed 花费的渲染时间", info[2]);
};
const handleSchoolsChange = () => {
user.schools[0].name = "测试小学1";
setUser({ ...user });
};
const handleSchools2 = () => {
// immutability-helper
const newUser = update(
user,
convertImmutabilityByPath("schools[0].name", {
$set: "测试小学2",
}),
);
setUser(newUser);
};
const handleSchools3 = () => {
user.schools[0].name = "测试小学3";
// 深拷贝
const newUser = JSON.parse(JSON.stringify(user));
setUser(newUser);
};
// 使用 useMemo 优化性能,也可以使用 memo 或者 shouldComponentUpdate
// 如果 user.schools 不变,则不会重新渲染
const renderSchools = useMemo(() => {
return (
<div>
{user.schools.map((item) => {
return (
<div key={item.name}>
{item.name}
{item.start}
{item.end}
</div>
);
})}
</div>
);
}, [user.schools]);
return (
<div className="App">
<Profiler id="render" onRender={renderCallback}>
<header className="App-header">
{user.name}
<button onClick={handleSchools}>修改学校1</button>
<button onClick={handleSchools2}>修改学校2</button>
<button onClick={handleSchools3}>修改学校3</button>
<div>{renderSchools}</div>
</header>
</Profiler>
</div>
);
}</code></pre><p>我们来看一下结果会怎么样。</p><p>测试按钮 1:</p><ul><li>点击 修改学校1,触发 handleSchools 函数</li><li>渲染原因 update,本次更新 committed 花费的渲染时间 0.8999999999068677</li><li>渲染失败,由于 user.schools 没有改变,renderSchools 不会重新渲染</li><li>再次点击 修改学校1,触发 handleSchools 函数</li><li>渲染原因 update,本次更新 committed 花费的渲染时间 0.10000000009313226</li></ul><p>测试按钮 2:</p><ul><li>点击 修改学校2,触发 handleSchools 函数</li><li>渲染原因 update,本次更新 committed 花费的渲染时间 1.6000000000931323</li><li>渲染成功</li><li>再次点击 修改学校2,触发 handleSchools 函数</li><li>没有进行任何修改,同时也没有触发 renderCallback</li></ul><p>测试按钮 3:</p><ul><li>点击 修改学校3,触发 handleSchools 函数</li><li>渲染原因 update,本次更新 committed 花费的渲染时间 1.300000000745058</li><li>渲染成功</li><li>再次点击 修改学校3,触发 handleSchools 函数</li><li>渲染原因 update,本次更新 committed 花费的渲染时间 0.5</li></ul><p>根据上述条件,我们可以看到 immutability-helper 的第二个好处,如果当前数据没有改变,将不会改变对象,从而不会触发渲染。</p><p>这里尝试把 schools 数据长度增加到 10002,再做一下测试。发现花费的渲染时间没有太多改变,均在 40 ms 左右,此时我们用 console.time 测试一下深拷贝和 immutability-helper 的时间差距。</p><pre><code class="ts">const handleSchools2 = () => {
console.time("浅拷贝");
const newUser = update(
user,
convertImmutabilityByPath("schools[0].name", {
$set: "测试小学2",
}),
);
console.timeEnd("浅拷贝");
setUser(newUser);
};
const handleSchools3 = () => {
user.schools[0].name = "测试小学3";
console.time("深拷贝");
const newUser = JSON.parse(JSON.stringify(user));
console.timeEnd("深拷贝");
setUser(newUser);
};</code></pre><p>得出的结果如下所示</p><ul><li>浅拷贝: 1.807861328125 ms</li><li>浅拷贝: 0.165771484375 ms(第二次调用)</li><li>深拷贝: 8.59716796875 ms</li></ul><p>测试下来有 4 倍的性能差距,再尝试在数据中添加 4 个 schools 大小的数据.</p><ul><li>浅拷贝: 3.60302734375 ms</li><li>浅拷贝: 0.10107421875 ms(第二次调用)</li><li>深拷贝: 28.789794921875 ms</li></ul><p>可以看到,随着数据的增大,耗费的时间差距也变得非常恐怖。</p><h2>源代码分析</h2><p>immutability-helper 仅有几百行代码。实现也非常简单。我们一起来看看作者是如何开发这个工具库的。</p><p>先是工具函数(保留核心,环境判断,错误警告等逻辑去除):</p><pre><code class="ts">// 提取函数,大量使用时有一定性能优势
const hasOwnProperty = Object.prototype.hasOwnProperty;
const splice = Array.prototype.splice;
const toString = Object.prototype.toString;
// 检查类型
function type<T>(obj: T) {
return (toString.call(obj) as string).slice(8, -1);
}
// 浅拷贝,使用 Object.assign,如果没有就手写一个
const assign = Object.assign || /* istanbul ignore next */
(<T, S>(target: T & any, source: S & Record<string, any>) => {
getAllKeys(source).forEach((key) => {
if (hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
});
return target as T & S;
});
// 获取对象 key
const getAllKeys = typeof Object.getOwnPropertySymbols === "function"
? (obj: Record<string, any>) =>
Object.keys(obj).concat(Object.getOwnPropertySymbols(obj) as any)
: /* istanbul ignore next */
(obj: Record<string, any>) => Object.keys(obj);
// 所有类型的拷贝函数
// 如果不是数组,Map,Set,对象,直接返回 拷贝值
function copy<T, U, K, V, X>(
object: T extends ReadonlyArray<U> ? ReadonlyArray<U>
: T extends Map<K, V> ? Map<K, V>
: T extends Set<X> ? Set<X>
: T extends object ? T
: any,
) {
return Array.isArray(object)
? assign(object.constructor(object.length), object)
: (type(object) === "Map")
? new Map(object as Map<K, V>)
: (type(object) === "Set")
? new Set(object as Set<X>)
: (object && typeof object === "object")
? assign(Object.create(Object.getPrototypeOf(object)), object) as T
: /* istanbul ignore next */
object as T;
}</code></pre><p>然后是核心代码(同样保留核心) :</p><pre><code class="ts">export class Context {
// 导入所有指令
private commands: Record<string, any> = assign({}, defaultCommands);
// 添加扩展指令(指令不要和对象中数据 key 相同)
public extend<T>(directive: string, fn: (param: any, old: T) => T) {
this.commands[directive] = fn;
}
// 功能核心
public update<T, C extends CustomCommands<object> = never>(
object: T,
$spec: Spec<T, C>,
): T {
// 增强健壮性,如果操作命令是函数,修改为 $apply
const spec = (typeof $spec === "function") ? { $apply: $spec } : $spec;
// 返回对象(数组)
let nextObject = object;
// 遍历对象,获取数据项和指令
getAllKeys(spec).forEach((key: string) => {
// 传入的是一个对象,如果当前 key 是指令的话,就进行操作
if (hasOwnProperty.call(this.commands, key)) {
// 性能优化,遍历过程中,如果 object 还是当前之前数据
const objectWasNextObject = object === nextObject;
// 用指令修改对象
nextObject = this.commands[key](
(spec as any)[key],
nextObject,
spec,
object,
);
// 修改后,两者使用传入函数计算,还是相等的情况下,直接使用之前数据
// 这样的话,数据没有修改,对象也不会改变
if (objectWasNextObject && this.isEquals(nextObject, object)) {
nextObject = object;
}
} else {
// 不在指令集中,做其他操作
// 类似于 update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// 解析对象规则后继续递归调用 update, 不断递归,不断返回
const nextValueForKey = type(object) === "Map"
? this.update((object as any as Map<any, any>).get(key), spec[key])
: this.update(object[key], spec[key]);
const nextObjectValue = type(nextObject) === "Map"
? (nextObject as any as Map<any, any>).get(key)
: nextObject[key];
// 内部数据有改变的情况下,进行 copy 操作
if (
!this.isEquals(nextValueForKey, nextObjectValue) ||
typeof nextValueForKey === "undefined" &&
!hasOwnProperty.call(object, key)
) {
if (nextObject === object) {
nextObject = copy(object as any);
}
if (type(nextObject) === "Map") {
(nextObject as any as Map<any, any>).set(key, nextValueForKey);
} else {
nextObject[key] = nextValueForKey;
}
}
}
});
// 返回对象
return nextObject;
}
}</code></pre><p>最后是通用指令的解析</p><pre><code class="ts">const defaultCommands = {
$push(value: any, nextObject: any, spec: any) {
// 数组添加,返回 concat 新数组
return value.length ? nextObject.concat(value) : nextObject;
},
$unshift(value: any, nextObject: any, spec: any) {
return value.length ? value.concat(nextObject) : nextObject;
},
$splice(value: any, nextObject: any, spec: any, originalObject: any) {
// 循环 splice 调用
value.forEach((args: any) => {
if (nextObject === originalObject && args.length) {
nextObject = copy(originalObject);
}
splice.apply(nextObject, args);
});
return nextObject;
},
$set(value: any, _nextObject: any, spec: any) {
// 直接替换当前数值
return value;
},
$toggle(targets: any, nextObject: any) {
const nextObjectCopy = targets.length ? copy(nextObject) : nextObject;
// 当前对象或者数组切换
targets.forEach((target: any) => {
nextObjectCopy[target] = !nextObject[target];
});
return nextObjectCopy;
},
$unset(value: any, nextObject: any, _spec: any, originalObject: any) {
// 拷贝后循环删除
value.forEach((key: any) => {
if (Object.hasOwnProperty.call(nextObject, key)) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
delete nextObject[key];
}
});
return nextObject;
},
$add(values: any, nextObject: any, _spec: any, originalObject: any) {
if (type(nextObject) === "Map") {
values.forEach(([key, value]) => {
if (nextObject === originalObject && nextObject.get(key) !== value) {
nextObject = copy(originalObject);
}
nextObject.set(key, value);
});
} else {
values.forEach((value: any) => {
if (nextObject === originalObject && !nextObject.has(value)) {
nextObject = copy(originalObject);
}
nextObject.add(value);
});
}
return nextObject;
},
$remove(value: any, nextObject: any, _spec: any, originalObject: any) {
value.forEach((key: any) => {
if (nextObject === originalObject && nextObject.has(key)) {
nextObject = copy(originalObject);
}
nextObject.delete(key);
});
return nextObject;
},
$merge(value: any, nextObject: any, _spec: any, originalObject: any) {
getAllKeys(value).forEach((key: any) => {
if (value[key] !== nextObject[key]) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
nextObject[key] = value[key];
}
});
return nextObject;
},
$apply(value: any, original: any) {
// 传入函数,直接调用函数修改
return value(original);
},
};</code></pre><p>根据上述代码,我们终于了解到了为什么作者需要传递一个对象来进行处理,同时我们也可以看出来如果当前数据路径的 key 值和指令相同就会出现错误。</p><h2>其他</h2><pre><code class="ts">convertImmutabilityByPath(
`schools[${index}].name`,
{ $set: "试试小学" },
);</code></pre><p>大家在看到如上代码会想到什么呢?就是个人之前在 <a href="https://link.segmentfault.com/?enc=aOq226lJKHxceaZFYvpirA%3D%3D.hGE4Gz45if6UUOXYU3J%2B6KGG%2FqTO76l6UlII6cdbwyrhgLaAZfY%2FU9QKfGIYdIBslSmxyWZD1DhMJWbU4FDp4g%3D%3D" rel="nofollow">手写一个业务数据比对库</a> 中推荐的 <a href="https://link.segmentfault.com/?enc=xfbl1ae3by3BBpR4SVjeMg%3D%3D.iJqAWzQo2zFyZi3f26CwzAgyclz7bG15%2FNjRr1bMD9fbMJ%2BGxt7wHdTvoWuCHVhq" rel="nofollow">westore</a> diff 函数。</p><pre><code class="ts">const result = diff({
a: 1,
b: 2,
c: "str",
d: { e: [2, { a: 4 }, 5] },
f: true,
h: [1],
g: { a: [1, 2], j: 111 },
}, {
a: [],
b: "aa",
c: 3,
d: { e: [3, { a: 3 }] },
f: false,
h: [1, 2],
g: { a: [1, 1, 1], i: "delete" },
k: "del",
});
// 结果
{
"a": 1,
"b": 2,
"c": "str",
"d.e[0]": 2,
"d.e[1].a": 4,
"d.e[2]": 5,
"f": true,
"h": [1],
"g.a": [1, 2],
"g.j": 111,
"g.i": null,
"k": null
}</code></pre><p>后续个人会结合 diff 以及 immutability-helper 开发一些有趣的工具。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=L1hkO3nSzHqNpalRphjmRQ%3D%3D.R9zOfFZZ8Y%2FKyiLNfqNe%2B9BLq0u%2Br1jGwUir%2FWsVzafOSJO4JjCDPgL75GWfDhu9" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=Om290QS597uKzqLeg3fg7w%3D%3D.jPkumL7PHCueMbT4rE1UnVsPZ6GfBp6MzNbC8J7SgciSPpTh8Wg%2FbSH1s3jtRcmP" rel="nofollow">immutability-helper</a></p><p><a href="https://link.segmentfault.com/?enc=xGlsngEe14uv1eKeBIecdA%3D%3D.sMPEFPt64jfWFgjeZBfysfNSFHSh7Qd9BjwAs1P9wg28N%2BmAnlfV5hdeDpvdkdB8" rel="nofollow">val-path-helper</a></p><p><a href="https://link.segmentfault.com/?enc=XSk1hOktO10r5sl53aglbQ%3D%3D.w6DaL4CRIbbozuimNHUruuT3Ab03z%2FvesxrdxsNOAZY3lO%2F6F7zw8DowmPY8FNCoigD%2FVlRQjenYNi5cIYqbtg%3D%3D" rel="nofollow">immutability-helper实践与优化</a></p>
手写一个业务数据比对库
https://segmentfault.com/a/1190000042950835
2022-12-01T02:35:45+08:00
2022-12-01T02:35:45+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<p>在开发 web 应用程序时,性能都是必不可少的话题。同时通用 web 应用程序离不开数据的增删改查,虽然用户大部分操作都是在查询,但是我们也不可以忽略更改数据对于系统的影响。于是个人写了一个业务数据比对库 <a href="https://link.segmentfault.com/?enc=hE61Qjhc3QTYpLKf0wUT5A%3D%3D.32uCbjzENRuxY4iCMTSJpEb5WJcHtMkwfUWqlVz6ZGSEGigToA26u7mN45iUWF28" rel="nofollow">diff-helper</a>。方便开发者在前端提交数据到服务端时候去除不必要的信息,优化网络传输和服务端性能。</p><h2>项目演进</h2><p>任何项目都不是一触而就的,下面是关于 diff-helper 库的编写思路。希望能对大家有一些帮助。</p><h3>简单对象比对</h3><p>前端提交 JSON 对象数据时,很多情况下都是对象一层数据比对。在不考虑对象中还有复杂数据(嵌套对象和数组)的情况下,编写如下代码</p><pre><code class="ts">// newVal 表示新数据,oldVal 表示老数据
const simpleObjDiff = ({
newVal,
oldVal,
}): Record<string, any> => {
// 当前比对的结果
const diffResult: Record<string, any> = {};
// 已经检查过的数据项,可以优化遍历性能
const checkedKeys: Set<string> = new Set();
// 遍历最新的对象属性
Object.keys(newVal).forEach((key: string) => {
// 将新数据的 key 记录一下
checkedKeys.add(key);
// 如果当前新的数据不等于老数据,直接把新的比对结果放入
if (newVal[key] !== oldVal[key]) {
diffResult[key] = newVal[key];
}
});
// 遍历之前的对象属性
Object.keys(oldVal).forEach((key) => {
// 如果已经检查过了,不在进行处理
if (checkedKeys.has(key)) {
return;
}
// 新的数据有,但是老数据没有可以认为数据已经不存在了
diffResult[key] = null;
});
return diffResult;
};</code></pre><p>此时我们就可以使用该函数进行一系列简单数据操作了。</p><pre><code class="ts">const result = simpleObjDiff({
newVal: {
a: 1,
b: 1,
},
oldVal: {
a: 2,
c: 2,
},
});
// => 返回结果为
result = {
a: 1,
b: 1,
c: null,
};</code></pre><h4>添加复杂属性比对</h4><p>当前函数在面对对象内部有复杂类型时候就没办法判断了,即使没有更改的情况下,结果也会包含新数据属性,但是考虑到提交到服务端的表单数据一般不需要增量提交,所以这里试一试 JSON.stringify 。</p><p>诸如:</p><pre><code class="ts">JSON.stringify("123");
// '"123"'
JSON.stringify(123);
// '123'
JSON.stringify(new Date());
// '"2022-11-29T15:16:46.325Z"'
JSON.stringify([1, 2, 3]);
// '[1,2,3]'
JSON.stringify({ a: 1, b: 2 });
// '{"b":2,"a":1}'
JSON.stringify({ b: 2, a: 1 });
// '{"b":2,"a":1}'
JSON.stringify({ b: 2, a: 1 }, ["a", "b"]);
// '{"a":1,"b":2}'
JSON.stringify({ b: 2, a: 1 }, ["a", "b"]) === JSON.stringify({ a: 1, b: 2 });
// true</code></pre><p>对比上述结果,我们可以看到,JSON.stringify 如果不提供 replacer 可能会对对象类型数据的生成结果产生“误伤”。但从系统实际运行上来说,对象内部属性不太会出现排序变化的情况。直接进行以下改造:</p><pre><code class="ts">const simpleObjDiff = ({
newVal,
oldVal,
}): Record<string, any> => {
// ... 之前的代码
// 遍历最新的对象数据
Object.keys(newVal).forEach((key: string) => {
// 当前已经处理过的对象 key 记录一下
checkedKeys.add(key);
// 先去查看类型,判断相同类型后再使用 JSON.stringify 获取字符串结果进行比对
if (
typeof newVal[key] !== typeof oldVal[key] ||
JSON.stringify(newVal[key]) !== JSON.stringify(oldVal[key])
) {
diffResult[key] = newVal[key];
}
});
// ... 之前的代码
};</code></pre><p>这时候尝试一下复杂数据类型</p><pre><code class="ts">const result = simpleObjDiff({
newVal: {
a: 1,
b: 1,
d: [1, 2, 3],
},
oldVal: {
a: 2,
c: 2,
d: [1, 2, 3],
},
});
// => 返回结果为
result = {
a: 1,
b: 1,
c: null,
};</code></pre><h4>添加自定义对象属性比对</h4><p>如果只使用 JSON.stringify 话,函数就没有办法灵活的处理各种需求,所以笔者开始追加函数让用户自行适配。</p><pre><code class="ts">const simpleObjDiff = ({
newVal,
oldVal,
options,
}): Record<string, any> => {
// ... 之前的代码
// 获取用户定义的 diff 函数
const { diffFun } = { ...DEFAULT_OPTIONS, ...options };
// 判断当前传入数据是否是函数
const hasDiffFun = typeof diffFun === "function";
// 遍历最新的对象数据
Object.keys(newVal).forEach((key: string) => {
// 当前已经处理过的对象 key 记录一下
checkedKeys.add(key);
let isChanged = false;
if (hasDiffFun) {
// 把当前属性 key 和对应的新旧值传入从而获取结果
const diffResultByKey = diffFun({
key,
newPropVal: newVal[key],
oldPropVal: oldVal[key],
});
// 返回了结果则写入 diffResult,没有结果认为传入的函数不处理
// 注意是不处理,而不是认为不变化
// 如果没返回就会继续走 JSON.stringify
if (
diffResultByKey !== null &&
diffResultByKey !== undefined
) {
diffResult[key] = diffResultByKey;
isChanged = true;
}
}
if (isChanged) {
return;
}
if (
typeof newVal[key] !== typeof oldVal[key] ||
JSON.stringify(newVal[key]) !== JSON.stringify(oldVal[key])
) {
diffResult[key] = newVal[key];
}
});
// ... 之前的代码
};</code></pre><p>此时我们尝试传入 diffFun 来看看效果:</p><pre><code class="ts">const result = simpleObjDiff({
newVal: {
a: [12, 3, 4],
b: 11,
},
oldVal: {
a: [1, 2, 3],
c: 22,
},
options: {
diffFun: ({
key,
newPropVal,
oldPropVal,
}) => {
switch (key) {
// 处理对象中的属性 a
case "a":
// 当前数组新旧数据都有的数据项才会保留下来
return newPropVal.filter((item: any) => oldPropVal.includes(item));
}
// 其他我们选择不处理,使用默认的 JSON.stringify
return null;
},
},
});
// => 结果如下所示
result = {
a: [3],
b: 11,
c: null,
};</code></pre><p>通过 diffFun 函数,开发者不但可以自定义属性处理,还可以利用 <a href="https://link.segmentfault.com/?enc=HoPS5aTsinu52jTSr7uefQ%3D%3D.WtcNSVjlJPcfYZZwFdUkxl9%2BPb2Bd88nXYLmfNukd3EVhx0CHlKlBEFzG6WZV7VY" rel="nofollow">fast-json-stringify</a> 来优化内部属性处理。该库通过 JSON schema 预先告知对象内部的属性类型,在提前知道数据类型的情况下,针对性处理会让 fast-json-stringify 性能非常高。</p><pre><code class="ts">import fastJson from "fast-json-stringify";
const stringify = fastJson({
title: "User Schema",
type: "object",
properties: {
firstName: {
type: "string",
},
lastName: {
type: "string",
},
age: {
description: "Age in years",
type: "integer",
},
},
});
stringify({
firstName: "Matteo",
lastName: "Collina",
age: 32,
});
// "{\"firstName\":\"Matteo\",\"lastName\":\"Collina\",\"age\":32}"
stringify({
lastName: "Collina",
age: 32,
firstName: "Matteo",
});
// "{\"firstName\":\"Matteo\",\"lastName\":\"Collina\",\"age\":32}"</code></pre><p>可以看到,利用 fast-json-stringify 同时无需考虑对象属性的内部顺序。</p><h4>添加其他处理</h4><p>这时候开始处理其他问题:</p><pre><code class="ts">// 添加异常错误抛出
const invariant = (condition: boolean, errorMsg: string) => {
if (condition) {
throw new Error(errorMsg);
}
};
// 判断是否是真实的对象
const isRealObject = (val: any): val is Record<string, any> => {
return Object.prototype.toString.call(val) === "[object Object]";
};
simpleObjDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams): Record<string, any> => {
// 添加错误传参处理
invariant(!isRealObject(newVal), "params newVal must be a Object");
invariant(!isRealObject(oldVal), "params oldVal must be a Object");
// ...
const { diffFun, empty } = { ...DEFAULT_OPTIONS, ...options };
// ...
Object.keys(oldVal).forEach((key) => {
// 如果已经检查过了,直接返回
if (checkedKeys.has(key)) {
return;
}
// 设定空数据,建议使用 null 或 空字符串
diffResult[key] = empty;
});
};</code></pre><p>简单对象比对函数就基本完成了。有兴趣的同学也可以直接阅读 <a href="https://link.segmentfault.com/?enc=vlrr1agtK4iKwjqBgSO%2FOA%3D%3D.lMDnud9VZIVqFzNSVslnN1cgg9cfZHQwHAoC4S4rAYAyhBNuyQlX%2BOywSfU8Q%2Bd1eoEgWmRDro6RDlsXVVlIonDvO0O2k7mGKhE7FxvxYeY%3D" rel="nofollow">obj-diff 源码</a> 。</p><h3>简单数组对比</h3><p>接下来就开始处理数组了,数组的比对核心在于数据的主键识别。代码如下:</p><pre><code class="ts">const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {
const opts = { ...DEFAULT_OPTIONS, ...options };
// 获取当前的主键 key 数值,不传递 key 默认为 'id'
const { key, getChangedItem } = opts;
// 增删改的数据
const addLines = [];
const deletedLines = [];
const modifiedLines = [];
// 添加检测过的数组主键,ListKey 是数字或者字符串类型
const checkedKeys: Set<ListKey> = new Set<ListKey>();
// 开始进行传入数组遍历
newVal.forEach((newLine) => {
// 根据主键去寻找之前的数据,也有可能新数据没有 key,这时候也是找不到的
let oldLine: any = oldVal.find((x) => x[key] === newLine[key]);
// 发现之前没有,走添加数据逻辑
if (!oldLine) {
addLines.push(newLine);
} else {
// 更新的数据 id 添加到 checkedKeys 里面去,方便删除
checkedKeys.add(oldLine[key]);
// 传入函数 getChangedItem 来获取结果
const result = getChangedItem!({
newLine,
oldLine,
});
// 没有结果则认为当前数据没有改过,无需处理
// 注意,和上面不同,这里返回 null 则认为数据没有修改
if (result !== null && result !== undefined) {
modifiedLines.push(result);
}
}
});
oldVal.forEach((oldLine) => {
// 之前更新过不用处理
if (checkedKeys.has(oldLine[key])) {
return;
}
// 剩下的都是删除的数据
deletedLines.push({
[key]: oldLine[key],
});
});
return {
addLines,
deletedLines,
modifiedLines,
};
};</code></pre><p>此时我们就可以使用该函数进行一系列简单数据操作了。</p><pre><code class="ts">const result = simpleListDiff({
newVal: [{
id: 1,
cc: "bbc",
},{
bb: "123",
}],
oldVal: [{
id: 1,
cc: "bb",
}, {
id: 2,
cc: "bdf",
}],
options: {
// 传入函数
getChangedItem: ({
newLine,
oldLine,
}) => {
// 利用对象比对 simpleObjDiff 来处理
const result = simpleObjDiff({
newVal: newLine,
oldVal: oldLine,
});
// 发现没有改动,返回 null
if (!Object.keys(result).length) {
return null;
}
// 否则返回对象比对过的数据
return { id: newLine.id, ...result };
},
key: "id",
},
});
// => 返回结果为
result = {
addedLines: [{
bb: "123",
}],
deletedLines: [{
id: 2,
}],
modifiedLines: [{
id: 1,
cc: "bbc",
}],
};</code></pre><p>函数到这里就差不多可用了,我们可以传入参数然后拿到比对好的结果发送给服务端进行处理。</p><h4>添加默认对比函数</h4><p>这里就不传递 getChangedItem 的逻辑,函数将做如下处理。如此我们就可以不传递 getChangedItem 函数了。</p><pre><code class="ts">const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {
const opts = { ...DEFAULT_OPTIONS, ...options };
// 获取当前的主键 key 数值,不传递 key 默认为 'id'
const { key } = opts;
let { getChangedItem } = opts;
// 如果没有传递 getChangedItem,就使用 simpleObjDiff 处理
if (!getChangedItem) {
getChangedItem = ({
newLine,
oldLine,
}) => {
const result = simpleObjDiff({
newVal: newLine,
oldVal: oldLine,
});
if (!Object.keys(result).length) {
return null;
}
return { [key]: newLine[key], ...result };
};
}
//... 之前的代码
};</code></pre><h4>添加排序功能</h4><p>部分表单提交不仅仅只需要增删改,还有排序功能。这样的话即使用户没有进行过增删改,也是有可能修改顺序的。此时我们在数据中添加序号,做如下改造:</p><pre><code class="ts">const simpleListDiff = ({
newVal,
oldVal,
options,
}: SimpleObjDiffParams) => {
const opts = { ...DEFAULT_OPTIONS, ...options };
// 此时传入 sortName,不传递则不考虑排序问题
const { key, sortName = "" } = opts;
// 判定是否有 sortName 这个配置项
const hasSortName: boolean = typeof sortName === "string" &&
sortName.length > 0;
let { getChangedItem } = opts;
if (!getChangedItem) {
//
}
const addLines = [];
const deletedLines = [];
const modifiedLines = [];
// 添加 noChangeLines
const noChangeLines = [];
const checkedKeys: Set<ListKey> = new Set<ListKey>();
newVal.forEach((newLine, index: number) => {
// 这时候需要查询老数组的索引,是利用 findIndex 而不是 find
let oldLineIndex: any = oldVal.findIndex((x) => x[key] === newLine[key]);
// 没查到
if (oldLineIndex === -1) {
addLines.push({
...newLine,
// 如果有 sortName 这个参数,我们就添加当前序号(索引 + 1)
...hasSortName && { [sortName]: index + 1 },
});
} else {
// 通过索引来获取之前的数据
const oldLine = oldVal[oldLineIndex];
// 判定是否需要添加顺序参数,如果之前的索引和现在的不同就认为是改变的
const addSortParams = hasSortName && index !== oldLineIndex;
checkedKeys.add(oldLine[key]);
const result = getChangedItem!({
newLine,
oldLine,
});
if (result !== null && result !== undefined) {
modifiedLines.push({
...result,
// 更新的数据同时添加排序信息
...addSortParams && { [sortName]: index + 1 },
});
} else {
// 这里是没有修改的数据
// 处理数据没改变但是顺序改变的情况
if (addSortParams) {
noChangeLines.push({
[key!]: newLine[key!],
[sortName]: index + 1,
});
}
}
}
});
//... 其他代码省略,删除不用考虑顺序了
return {
addLines,
deletedLines,
modifiedLines,
// 返回不修改的 line
...hasSortName && {
noChangeLines,
},
};
};</code></pre><p>开始测试一下:</p><pre><code class="ts">simpleListDiff({
newVal: [
{ cc: "bbc" },
{ id: 1, cc: "bb" }
],
oldVal: [
{ id: 1, cc: "bb" }
],
options: {
key: "id",
sortName: "sortIndex",
},
});
// 同样也支持为新增和修改的数据添加 sortIndex
result = {
addedLines: [
{
cc: "bbc",
// 新增的数据目前序号为 1
sortIndex: 1,
},
],
// id 为 1 的数据位置变成了 2,但是没有发生数据的改变
noChangeLines: [{
id: 1,
sortIndex: 2,
}],
deletedLines: [],
modifiedLines: [],
};</code></pre><p>简单数组比对函数就基本完成了。有兴趣的同学也可以直接阅读 <a href="https://link.segmentfault.com/?enc=lTSVY29PBCukc9Uqg0q7VQ%3D%3D.Me0%2B2okYyAMxUO4oewv8F5g3C7tLkXYpK2KPel4M6IVIkvUBYlTTy7AnJJSQplOnq2HYg2QxLdqZhLVtnekoIYso0mqjUmTO0mfkSOI1Cmk%3D" rel="nofollow">list-diff 源码</a> 。</p><p>以上所有代码都在 <a href="https://link.segmentfault.com/?enc=9fc97j16EVXtOcp5e1CMiA%3D%3D.j6PGdZFHK%2BqGSpjzBnYQybO%2B%2FVFCAuhNUTAsVW5%2BWS3BGV7wG7YkJb%2FoeGL7d2%2BH" rel="nofollow">diff-helper</a> 中,针对复杂的服务端数据请求,可以通过传参使得两个函数能够嵌套处理。同时也欢迎大家提出 issue 和 pr。</p><h2>其他</h2><p>针对形形色色需求,上述两种函数处理方案也是不够用的,我们来看看其他的对比方案。</p><h3>数据递归比对</h3><p>当前库也提供了一个对象或者数组的比对函数 commonDiff。可以嵌套的比对函数,可以看一下实际效果。</p><pre><code class="ts">import { commonDiff } from "diff-helper";
commonDiff({
a: {
b: 2,
c: 2,
d: [1, 3, 4, [3333]],
},
}, {
a: {
a: 1,
b: 1,
d: [1, 2, 3, [223]],
},
});
// 当前结果均是对象,不过当前会增加 type 帮助识别类型
result = {
type: "obj",
a: {
type: "obj",
a: null,
b: 1,
c: 2,
d: {
type: "arr",
// 数组第 2 个数据变成了 3,第 3 数据变成了 4,以此类推
1: 3,
2: 4,
3: {
type: "arr",
0: 223,
},
},
},
};</code></pre><h3>westore 比对函数</h3><p><a href="https://link.segmentfault.com/?enc=WLEAgyuRBUMAfEhhEMK8ww%3D%3D.XV1yee4KgEwsHQ7ZGntQsSu2rNJxdbLJrTwaDxPWfGygoZkx6u1n5w3d1SRW0MLV" rel="nofollow">westore</a> 是个人使用过最好用的小程序工具,兼顾了性能和可用性。其中最为核心的则是它的比对函数,完美的解决了小程序 setData 时为了性能需要建立复杂字符串的问题。</p><p>以下代码是实际的业务代码中出现的:</p><pre><code class="ts">// 更新表单项数据,为了性能,不建议每次都传递一整个 user
this.setData({ [`user.${name}`]: value });
// 设置数组里面某一项数据
this.setData({ [`users[${index}].${name}`]: value });</code></pre><p>这里就不介绍 westore 的用法了,直接看一下 westore diff 的参数以及结果:</p><pre><code class="ts">const result = diff({
a: 1,
b: 2,
c: "str",
d: { e: [2, { a: 4 }, 5] },
f: true,
h: [1],
g: { a: [1, 2], j: 111 },
}, {
a: [],
b: "aa",
c: 3,
d: { e: [3, { a: 3 }] },
f: false,
h: [1, 2],
g: { a: [1, 1, 1], i: "delete" },
k: "del",
});
// 结果
{
"a": 1,
"b": 2,
"c": "str",
"d.e[0]": 2,
"d.e[1].a": 4,
"d.e[2]": 5,
"f": true,
"h": [1],
"g.a": [1, 2],
"g.j": 111,
"g.i": null,
"k": null
}</code></pre><p>不过这种增量比对不适合通用场景,大家有需求可以自行查阅代码。笔者也在考虑上面两个比对函数是否有其他的使用场景。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=OD1ahnyfgywzlbBTIZaSmA%3D%3D.T%2FfcahnCilDnVPtG8SvPFhRmMk%2FIIBoTxb1gHjU60nZYgUxX%2BPh1LleSetfzsNPs" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=wg8zvHSqP0LppYGuYCWjQw%3D%3D.VWDQ7iV3YReCenWibkZsPA%2Bw4gsj61%2BmIcNG%2F1T4k43MWZU%2F9fGZLXlXEPEepQsz" rel="nofollow">fast-json-stringify</a></p><p><a href="https://link.segmentfault.com/?enc=0MFGoZTw3RLtPd8BkGOI0A%3D%3D.hUWrNCY3m%2B2uEpFEO2xWB6D2XtetET04FsSgHxulPuW6QFj%2B%2FzAxeaGF8iBT3ply" rel="nofollow">westore</a></p><p><a href="https://link.segmentfault.com/?enc=k%2FCDfTkzzhuZrOrKHyEU4g%3D%3D.71UD1RwzkFcjs7H6HJIB3TE4F9gt1sWbH1XLMr5M6KCacKhLuCsumuZ5eVyzcvXp" rel="nofollow">diff-helper</a></p>
使用 normalizr 进行复杂数据转换
https://segmentfault.com/a/1190000042216483
2022-07-25T01:27:16+08:00
2022-07-25T01:27:16+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
5
<p>笔者曾经开发过一个数据分享类的小程序,分享逻辑上类似于百度网盘。当前数据可以由被分享者加工然后继续分享(可以控制数据的过期时间、是否可以加工数据以及继续分享)。 </p><p>分享的数据是一个深度嵌套的 json 对象。在用户读取分享数据时存入小程序云数据库中(分享的数据和业务数据有差异,没使用业务服务器进行维护)。如果拿到数据就直接存储的话,很快云数据库就会变得很大,其次我们也没办法分析各项和检索各项子数据给予分享者。</p><p>这时候需要进行数据转换以便拆分和维护。我们可以使用 <a href="https://link.segmentfault.com/?enc=NYrp51jvS3lJcLAfHZrxmA%3D%3D.U9KKT3l9oM%2FYPH8etZjqYqa0khlD8Tez551t6sddxP0%3D" rel="nofollow">redux</a> 作者 Dan Abramov 编写的 <a href="https://link.segmentfault.com/?enc=odDLaxm15gnfYNjQnPiV8g%3D%3D.Qo61DKPt2R3sKG%2FaAZ%2BeTnp76Mb7Pt12FyRAQQdyxj%2BR5rotZzZ0imyFE729evag" rel="nofollow">normalizr</a> 来处理数据。</p><p>normalizr 创立的初衷是处理深层,复杂的嵌套的对象。</p><h2>如何使用</h2><p>稍微修改一下官方的例子,假定获取到如下书籍的数据:</p><pre><code class="js">{
id: "1",
title: "JavaScript 从入门到放弃",
// 作者
author: {
id: "1",
name: "chc"
},
// 评论
comments: [
{
id: "1",
content: "作者写的太好了",
commenter: {
id: "1",
name: "chc"
}
},
{
id: "2",
content: "楼上造假数据哈",
commenter: {
id: "2",
name: "dcd"
}
},
]
}</code></pre><p>这时候我们可以写出 3 个主体: 书籍信息、评论以及用户。我们先从基础的数据来构造模式:</p><pre><code class="js">import { normalize, schema } from 'normalizr';
// 构造第一个实体 用户信息
const user = new schema.Entity('users');
// 构造第二个实体 评论
const comment = new schema.Entity('comments', {
// 评价者是用户
commenter: user
});
// 构造第三个实体 书籍
const book = new schema.Entity('books', {
// 作者
author: user,
// 评论
comments: [comment]
});
// 传入数据以及当前最大的 schema 信息
const normalizedData = normalize(originalData, book);</code></pre><p>先来看一下最终数据。</p><pre><code class="js">{
"entities": {
"users": {
"1": {
"id": "1",
"name": "chc"
},
"2": {
"id": "2",
"name": "dcd"
}
},
"comments": {
"1": {
"id": "1",
"content": "作者写的太好了",
"commenter": "1"
},
"2": {
"id": "2",
"content": "楼上造假数据哈",
"commenter": "2"
}
},
"books": {
"1": {
"id": "1",
"title": "JavaScript 从入门到放弃",
"author": "1",
"comments": [
"1",
"2"
]
}
}
},
"result": "1"
}</code></pre><p>去除其他信息,我们可以看到获取了 3 个不同的实体对象, users,comments,books。对象的键为当前 id,值为当前平铺的数据结构。这时候我们就可以使用对象或者数组(Object.values) 来新增和更新数据。</p><h2>解析逻辑</h2><p>看到这里,大家可能是很懵的。先不管代码实现,这里先分析一下库是如何解析我们编写的 schema 的,以便大家可以在实际场景中使用,再看一遍数据和 schema 定义:</p><p>数据结构</p><pre><code class="js">{
id: "1",
title: "JavaScript 从入门到放弃",
// 作者
author: {
id: "1",
name: "chc"
},
// 评论
comments: [
{
id: "1",
content: "作者写的太好了",
commenter: {
id: "1",
name: "chc"
}
},
{
id: "2",
content: "楼上造假数据哈",
commenter: {
id: "2",
name: "dcd"
}
},
]
}</code></pre><ul><li><p>书籍信息是第一层对象,数据中有 id, title, author, comments,对应 schema 如下</p><pre><code class="js">const book = new schema.Entity('books', {
// 作者
author: user,
// 一本书对应多个评论,所以这里使用数组
comments: [comment]
});</code></pre><p>其中 id ,title 是 book 本身的属性,无需关注,把需要解析的数据结构写出来。books 字符串与解析无关,对应 entities 对象的 key。</p></li><li><p>再看 user</p><pre><code class="js">const user = new schema.Entity('users');</code></pre><p>user 没有需要解析的信息,直接定义实体即可。</p></li><li><p>最后是评论信息</p><pre><code class="js">const comment = new schema.Entity('comments', {
// 评价者是用户
commenter: user
});
{
id: "1",
content: "作者写的太好了",
commenter: {
id: "1",
name: "chc"
}
}</code></pre><p>把 comments 从原本的数据结构中拿出来,实际也就很清晰了。</p></li></ul><h2>高阶用法</h2><h3>处理数组</h3><p>normalizr 可以解析单个对象,那么如果当前业务传递数组呢?类似于 comment 直接这样使用即可:</p><pre><code class="js">[
{
id: '1',
title: "JavaScript 从入门到放弃"
// ...
},
{
id: '2',
// ...
}
]
const normalizedData = normalize(originalData, [book]);</code></pre><h3>反向解析</h3><p>我们只需要拿到刚才的 normalizedData 中的 result 以及 entities 就可以获取之前的信息了。</p><pre><code class="js">import { denormalize, schema } from 'normalizr';
//...
denormalize(normalizedData.result, book, normalizedData.entities);</code></pre><h3>Entity 配置</h3><p>开发中可以根据配置信息重新解析实体数据。</p><pre><code class="js">const book = new schema.Entity('books', {
// 作者
author: user,
// 一本书对应多个评论,所以这里使用数组
comments: [comment]
}, {
// 默认主键为 id,否则使用 idAttribute 中的数据,如 cid,key 等
idAttribute: 'id',
// 预处理策略, 参数分别为 实体的输入值, 父对象
processStrategy: (value, parent, key) => value,
// 遇到两个id 相同数据的合并策略,默认如下所示,我们还可以继续修改
mergeStrategy: (prev, prev) => ({
...prev,
...next,
// 是否合并过,如果遇到相同的,就会添加该属性
isMerge: true
}),
});
// 看一下比较复杂的例子,以 user 为例子
const user = new schema.Entity('users', {
}, {
processStrategy: (value, parent, key) => {
// 增加父对象的属性
// 例如 commenter: "1" => commenterId: "1" 或者 author: "2" => "authorId": "2"
// 但是目前还无法通过 delete 删除 commenter 或者 author 属性
parent[`${key}Id`] = value.id
// 如果是从评论中获取的用户信息就增加 commentIds 属性
if (key === 'commenter') {
return {
...value,
commentIds: [parent.id]
}
}
// 不要忘记返回 value, 否则不会生成 user 数据
return {
...value,
bookIds: [parent.id]
};
}
mergeStrategy: (prev, prev) => ({
...prev,
...next,
// 该用户所有的评论归并到一起去
commentIds: [...prev.commentIds, ...next.commentIds],
// 该用户所有的书本归并到一起去
bookIds: [...prev.bookIds, ...next.bookIds],
isMerge: true
}),
})
// 最终获取的用户信息为
{
"1": {
"id": "1",
"name": "chc"
// 用户 chc 写了评论和书籍,但是没有进行过合并
"commentIds": ["1"],
"bookIds": ["1"],
},
"2": {
"id": "2",
"name": "dcd",
// 用户 dcd 写了 2 个评论,同时进行了合并处理
"commentIds": [
"2",
"3"
],
"isMerge": true
}
}</code></pre><p>当然了,该库也可以进行更加复杂的数据格式化,大家可以通过 <a href="https://link.segmentfault.com/?enc=Q2oPpQMBILqCjy7QhA85mw%3D%3D.JYPUodwt%2BX1IHW0khx3iflRxokZqsiQPyhfU5kOkA0Y%2FN9PV7ePtjW%2FuIActRi8Ju5mwCBNMAcdoyFywwZ1umni39SyWuKAlcRi6v3b0p4M%3D" rel="nofollow">api 文档</a> 来进一步学习和使用。</p><h2>其他</h2><p>当然了,normalizr 使用场景毕竟有限,开源负责人也早已换人。目前主库已经无人维护了(issue 也也已经关闭)。当然了,normalizr 代码本身也是足够稳定。</p><p>笔者也在考虑一些新的场景使用并尝试为 normalizr 添加一些新的功能(如 id 转换)和优化(ts 重构),如果您在使用 normalizr 的过程中遇到什么问题,也可以联系我,存储库目前在 <a href="https://link.segmentfault.com/?enc=yFR0%2BpzB%2FO4E3qwiTpF2BA%3D%3D.oSVsjCMVT%2FJKGkuj9IcPFz%2BwWTlYphoVAT53712%2B9Ld3A0oXiI%2BzDz0bdCwlcCQg" rel="nofollow">normalizr-helper</a> 中。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=3CLH2KxJQs7%2FhSzWkMjgyw%3D%3D.TmNbrjRDQ6GEmcN7UBhlaeuLExwbnNOTRkEtCw%2FHA%2BbB3vYzPjto%2BIvRULwzFndP" rel="nofollow">博客地址</a></p>
确保从列表中获取可用值
https://segmentfault.com/a/1190000042042442
2022-06-28T09:25:36+08:00
2022-06-28T09:25:36+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
0
<p>对于很多项目来说,某些配置项或查询条件是必需的。当用户丢失配置数据或项目下线配置项,都会导致项目发生错误而造成不可用的问题,这时候,开发需要提供一些兜底策略,如当前列表数据查询不到时默认使用第一项。</p><h2>ensure-get-list-val</h2><p>这些东西每次都写一下又很麻烦,所以这里进行了一次封装,代码如下:</p><pre><code class="ts">interface EnsureGetValFromListParams<ItemType, ValueType> {
/** 列表数据 **/
items: ItemType[]
value?: ValueType | undefined
/** 列表中数据值的提取方法 **/
getVal?: (item: ItemType) => ValueType
/** 查询不到数据时候返回值的位置 **/
pos?: 'frist' | 'last'
}
// ValueType = ItemType
// 如果不提供 ValueType, 则 ValueType 默认为 ItemType
const ensureGetValFromList = <ItemType, ValueType = ItemType>({
items,
value,
getVal = item => item as unknown as ValueType,
pos = 'frist'
}: EnsureGetValFromListParams<ItemType, ValueType>): ValueType | null => {
// 当前不是数组直接返回 null
if (!Array.isArray(items)) {
return null
}
const count = items.length
// 当前为空数组直接返回 null
if (count === 0) {
return null;
}
// 没有传递数值或者当前列表长度为1,直接返回列表唯一数据
if (!value || count === 1) {
return getVal(items[0])
}
// 查询列表,是否有数值等于传入数值
if (items.some(item => getVal(item) === value)) {
return value
}
// 返回列表第一条还是最后一条数据
const index = pos === 'frist' ? 0 : count - 1
return getVal(items[index])
}</code></pre><p>代码在 <a href="https://link.segmentfault.com/?enc=ZFv6IuZmc3orq7prSXGSuQ%3D%3D.DLZmMd2xZQwQLxhELKHs5n2a7o%2FfONsB0V6%2BYoua5WJvtDjvG0%2F4FWwvVBZFxp0w" rel="nofollow">ensure-get-list-val</a> 中。也可以使用 npm 等工具进行安装和使用。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=Yweuy2SGV%2FNhx7we1fvt6Q%3D%3D.haMg5ZtgrPZ4skmi1H2oj9HSJMbVozU8RI4m8bsAFVuSEdsuGW7XPkFoPKWLBubr" rel="nofollow">博客地址</a></p>
玩转 AbortController 控制器
https://segmentfault.com/a/1190000042001788
2022-06-18T16:35:13+08:00
2022-06-18T16:35:13+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
6
<p>绝大部分情况,网络请求都是先请求先响应。但是某些情况下,由于未知的一些问题,可能会导致先请求的 api 后返回。最简单的解决方案就是添加 loading 状态,在所有请求都完成后才能进行下一次请求。</p><p>但不是所有的业务都可以采用这种方式。这时候开发者就需要对其进行处理以避免渲染错误数据。</p><h2>使用“版本号”</h2><p>我们可以使用版本号来决策业务处理以及数据渲染:</p><pre><code class="ts">const invariant = (condition: boolean, errorMsg: string) => {
if (condition) {
throw new Error(errorMsg)
}
}
let versionForXXXQuery = 0;
const checkVersionForXXXQuery = (currentVersion: number) => {
// 版本不匹配,就抛出错误
invariant(currentVersion !== versionForXXXQuery, 'The current version is wrong')
}
const XXXQuery = async () => {
// 此处只能使用 ++versionForXXXQuery 而不能使用 versionForXXXQuery++
// 否则版本永远不对应
const queryVersion = ++versionForXXXQuery;
// 业务请求
checkVersion(queryVersion)
// 业务处理
// ?界面渲染
// 业务请求
checkVersion(queryVersion)
// 业务处理
// ?界面渲染
}</code></pre><p>如此,先请求的 api 后返回就会被错误中止执行,但最终渲染到界面上的只有最新版本的请求。但是该方案对业务的侵入性太强。虽然我们可以利用 class 和 AOP 来简代码和逻辑。但对于开发来说依旧不友好。这时候我们可以使用 AbortController。</p><h2>使用 AbortController</h2><h3>AbortController 取消之前请求</h3><p>话不多说,先使用 AbortController 完成上面相同的功能。</p><pre><code class="ts">let abortControllerForXXXQuery: AbortController | null = null
const XXXQuery = async () => {
// 当前有中止控制器,直接把上一次取消
if (abortControllerForXXXQuery) {
abortControllerForXXXQuery.abort()
}
// 新建控制器
abortControllerForXXXQuery = new AbortController();
// 获取信号
const { signal } = abortControllerForXXXQuery
const resA = await fetch('xxxA', { signal });
// 业务处理
// ?界面渲染
const resB = await fetch('xxxB', { signal });
// 业务处理
// ?界面渲染
}</code></pre><p>我们可以看到:代码非常简单,同时得到了性能增强,浏览器将提前停止获取数据(注:服务器依旧会处理多次请求,只能通过 loading 来降低服务器压力)。</p><h3>AbortController 移除绑定事件</h3><p>虽然代码很简单,但是为什么需要这样添加一个 AbortController 类而不是直接通过添加 api 来进行中止网络请求操作呢?这样不是增加了复杂度吗?笔者开始也是这样认为的。到后面才发现。AbortController 类虽然较为复杂了,但是它是通用的,因此 AbortController 可以被其他 Web 标准和 JavaScript 库使用。</p><pre><code class="ts">const controller = new AbortController()
const { signal } = controller
// 添加事件并传递 signal
window.addEventListener('click', () => {
console.log('can abort')
}, { signal })
window.addEventListener('click', () => {
console.log('click')
});
// 开始请求并且添加 signal
fetch('xxxA', { signal })
// 移除第一个 click 事件同时中止未完成的请求
controller.abort()</code></pre><h3>通用的 AbortController</h3><p>既然它是通用的,那是不是也可以终止业务方法呢。答案是肯定的。先来看看 AbortController 到底为啥能够通用呢?</p><p>AbortController 提供了一个信号量 signal 和中止 abort 方法,通过这个信号量可以获取状态以及绑定事件。</p><pre><code class="ts">const controller = new AbortController();
// 获取信号量
const { signal } = controller;
// 获取当前是否已经执行过 abort,目前返回 false
signal.aborted
// 添加事件
signal.addEventListener('abort', () => {
console.log('触发 abort')
})
// 添加事件
signal.addEventListener('abort', () => {
console.log('触发 abort2')
})
// 中止 (不可以解构直接执行 abort,有 this 指向问题)
// 控制台打印 触发 abort,触发 abort2
controller.abort()
// 当前是否已经执行过 abort,返回 ture
signal.aborted
// 控制台无反应
controller.abort();</code></pre><p>无疑,上述的事件添加了 abort 事件的监听。综上,笔者简单封装了一下 AbortController。Helper 类如下所示:</p><pre><code class="ts">class AbortControllerHelper {
private readonly signal: AbortSignal
constructor(signal: AbortSignal) {
this.signal = signal
signal.addEventListener('abort', () => this.abort())
}
/**
* 执行调用方法,只需要 signal 状态的话则无需在子类实现
*/
abort = (): void => {}
/**
* 检查当前是否可以执行
* @param useBoolean 是否使用布尔值返回
* @returns
*/
checkCanExecution = (useBoolean: boolean = false): boolean => {
const { aborted } = this.signal
// 如果使用布尔值,返回是否可以继续执行
if (useBoolean) {
return !aborted
}
// 直接抛出异常
if (aborted) {
throw new Error('abort has already triggered');
}
return true
}
}</code></pre><p>如此,开发者可以添加子类继承 AbortControllerHelper 并放入 signal。然后通过一个 AbortController 中止多个乃至多种不同事件。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=AtPyhOyU%2F%2F2Ptsob6msvhA%3D%3D.laKtYLVD4d4yICTqQge%2F9wTG0HCFMD1wEW%2B%2FtdkSXHFYTA%2F1d4r%2FSU9PAukZ57Cz" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=4TZYrzopC1fL85jbb75ExQ%3D%3D.L8X0yMogmX1%2FrOMvFC0o0L4CBPUggAnd3sU9GFlb1NXHkOUIJOrwS238lzFf1%2Fi5tztA9Ju%2FsOpXqlHmkW68yCDupGHeVq6vIrQMBsA6kjM%3D" rel="nofollow">AbortController MDN</a></p>
聊聊并发控制锁
https://segmentfault.com/a/1190000041813879
2022-05-08T23:59:53+08:00
2022-05-08T23:59:53+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
2
<p>对于企业应用来说,完全不涉及到并发的问题,基本是不可能的。因为对于一个应用中很多的事情都是同时进行的。并发可能发生在数据获取,服务调用乃至于用户交互中。并发问题有两个重要的解决方案,一个是隔离,另一个是不变性。</p><p>并发问题会发生在多个执行单元同时访问同一资源的时候,此时,一个好的方法就是分好“蛋糕”,让每一个执行单元都能访问到各自的资源。好的并发设计就是:找到创建好隔离区的办法,然后通过分析工作流让隔离区能够完成尽可能多的任务。</p><p>在共享数据可以改变的情况下,并发问题就有可能发生。从实际的场景出发,同时有两个客户询问两位服务员是否还有某一货品时,两位服务员各自去查看了一下系统并回复客户还有一份,两位客户中一定有一位会失望。那么这件事情的解决方案就是添加隔离区(购物车),服务员把当前货品放入客户的购物车成功后告知用户,然后失败的一方就可以告知用户货品已经销售一空。虽然存在已购用户退货的可能,但无疑比前一结果要好太多。这也就是下文中所说的悲观锁。</p><p>下面我们开始介绍两种并发控制策略:</p><h2>乐观和悲观并发控制</h2><p>在某个系统中,同时有两个企业员工 A 和 B 想要编辑同一个用户信息。此时 A 和 B 都获取到了用户的信息数据。然后他们两个进行了修改,A 员工先完成了操作并且进行了提交。然后 B 员工完成了操作也进行了提交。此时系统中的这个用户信息只保留了 B 提供的数据,而丢弃了 A 员工的数据。这可能会造成一些难以预料的问题,甚至有可能导致他们丢掉工作。虽然可以通过操作日志来追溯到是哪个员工操作了数据,但这个信息没有任何意义,因为系统并没有让任何员工得知修改这一情况。</p><p>当一些可变数据无法隔离时候,我们可以用两种不同的控制策略:乐观锁策略和悲观锁策略。乐观锁用于冲突检测,悲观锁用于冲突避免。</p><p>悲观者策略非常简单,当 A 用户获取到用户信息时系统把当前用户信息给锁定,然后 B 用户在获取用户信息时就会被告知别人正在编辑。等到 A 员工进行了提交,系统才允许 B 员工获取数据。此时 B 获取的是 A 更新后的数据。</p><p>乐观者策略则不对获取进行任何限制,这时候我们可以在用户信息中添加版本号来告知用户信息已被修改。乐观锁要求每条数据都有一个版本号,同时在更新数据时候就会更新版本号,如 A 员工在更新用户信息时候提交了当前的版本号。系统判断 A 提交的时候的版本号和该条信息版本号一致,允许更新。然后系统就会把版本号修改掉,B 员工来进行提交时携带的是之前版本号,此时系统判定失败,要求 B 重新获取数据和版本号,然后再一次进行提交。</p><p>乐观锁和悲观锁进行选择的标准是: 冲突的频率和严重性。如果冲突的结果对于用户是难以接受的,我们只能采用悲观锁策略。如果冲突的结果不会很严重,或者频率也较低,我们就可以选择乐观锁,它更容易实现,也具有更好的并发性。</p><p>当然,我们也可以对乐观锁进行一些优化,把更新时间(作为版本号)和更新用户添加到信息中,如此以来,系统就可以告知 B 员工该条信息被修改过,以及在何时何人操作。系统还可以提供给 B 新的更新时间以及是否强制更新的选择。当然,甚至可以基于业务需求以及日志信息等来告知 B 员工之前具体修改的信息。</p><h2>死锁</h2><p>使用悲观锁技术有一个特别的问题就是死锁,即用户在已经获取锁的情况下还想要获取更多的锁。以最早的两个客户的问题来说,就是水果蛋糕需要获取水果和蛋糕,两个用户各有其中一种,并期望获取对方东西。</p><p>解决死锁的方法是检测处理和超时控制。</p><p>检查处理会检测出死锁发生并且会选择一个“牺牲者”,让他放弃他所拥有的已保证另外一个客户可以获取水果蛋糕。而超时控制则是给每个锁添加一个超时时间,一旦达到了超时时间,当前的购物车里面的物品就被清掉。</p><p>超时控制和检测机制用于已经发生了死锁的情况,而另外的方法则是避免死锁的发生。防止死锁的方法就是在用户获取锁的时候就获取所有可能需要的锁,粗力度锁(这很保守,但很有效),即水果蛋糕不是由两个货品组合而成的。</p><p>粗力度锁是覆盖多个资源的单个锁,这样会简化多个锁带来的复杂性。这其实也会发生在乐观锁的过程中,例如用户和用户相关地址信息,如果用户地址信息修改后也会更改用户信息,这样如何获取和设置乐观锁呢?我们需要寻找到一组资源的核心。</p><p>同时,找到一组资源的核心也会使得开发的代码逻辑更加清晰。大家不妨想一下,在数据库层面的操作中,是选择先更新子表然后再去更新主表这样的逻辑顺序更好,还是以主表为入口进行更新修改更好呢?</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=vJs4cnKjGZpDUcoCm99LNg%3D%3D.pjPR9JvDHKVqPht3AXJHSPszO3ISsPStNPcL9m0NJco6b6s8N4cOXjI3pyZFsvFc" rel="nofollow">博客地址</a></p>
使用 better-queue 管理复杂的任务
https://segmentfault.com/a/1190000040563076
2021-08-24T01:59:53+08:00
2021-08-24T01:59:53+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
6
<p>队列,在数据结构中是一种线性表,其特性为必须从一端插入,然后从另一端删除数据。但笔者今天重点不是如何实现该数据结构,我们可以看一看如何借助队列管理复杂的任务。</p><p>队列在实际开发中应用的场景非常广泛。因为在一个复杂的系统中,总是会有一些非常耗时的处理。在这种时候开发者不能要求系统提供实时处理、实时响应的能力。这时候我们就可以通过队列来解决此类问题。</p><p>开发者可以不停地往队列塞入数据,并为其生成唯一值(进行跟踪),同时根据当前系统的处理能力不断的从队列取出数据进行业务处理,以此来减轻在同一时间内进行大量复杂业务处理,以便增强系统的处理能力。</p><p>服务端通常可以借助队列来进行异步处理、系统解耦、数据同步以及流量削峰。</p><p>如果需求较为简单,开发者可以直接借助数组来进行处理。对于较为复杂的需求,可以使用 <a href="https://link.segmentfault.com/?enc=FilD0wBSQQA3ctKfwup7xA%3D%3D.ytXlw2jPMH0RR6gsV0uOAsXI3YVL9dXVT2DPSKnBd2MowO%2FVBhY9QUFOx%2BYMdy2N" rel="nofollow">better-queue</a> 来解决问题。</p><p>better-queue 进一步扩展了队列,让其有很多好用的功能。诸如:</p><ul><li>并行化处理</li><li>持久(和可扩展)存储</li><li>批处理</li><li>优先队列</li><li>合并、过滤任务</li><li>任务统计</li></ul><h2>使用方法</h2><p>让我们开始看一看 better-queue 如何使用。</p><h3>代码风格</h3><pre><code class="ts">import BetterQueue from "better-queue";
// 创建队列并且提供任务处理的回调函数
// 当调用回调意味该数据已经从队列中删除
// 然后从队列中取出下一条数据继续处理
const q = new BetterQueue(function (input, cb) {
// 从队列中取出数据并进行处理...
const result = 'xxxx'
try {
// 如果成功则调用回调并且返回结果
cb(null, result);
} catch (err) {
// 否则返回错误
cb(err)
}
})
q.push(1)
q.push({x: 1})</code></pre><p>我们可以看到,该库的代码风格还是采用了 Node 早期版本的回调风格,如果在执行期间发生了错误,会把错误作为回调的第一个参数传递到回调函数中。类似于:</p><pre><code class="ts">fs.readFile(filePath, (err, data) => {
if (err) {
console.log(err)
return
}
console.log(data)
})</code></pre><h3>队列生成与使用</h3><p>首先我们可以构建存储结构和请求的数据结构 Job。</p><pre><code class="ts">// 任务数据
interface Job<T> {
// 任务的唯一值,唯一确定当前任务
id: string;
// 当前任务的状态:等待中,已成功,已失败
status: 'waiting' | 'succeeded' | 'failed';
// 任务的请求参数,可以是 id,也可以是其他数据
queryArgs?: any;
// 任务的返回结果
result: T;
// 任务错误信息
err: Error;
}</code></pre><p>然后开发队列的回调函数以及新建任务队列:</p><pre><code class="ts">// 异步处理逻辑
async function asyncProcess<T>(job: Job<T>, cb: Function) {
const req = job.queryArgs || job.id
try {
// await 异步请求处理,数据库访问,或者生成文件等耗时任务
const result = await query('/xxx/xxx', req)
cb(null, result)
} catch (error) {
// 生成错误
cb(error)
}
}
// 创建队列
const betterQueue = new BetterQueue(asyncProcess)
// 对象存储,因为队列只会进行任务处理,并不包括数据的存储
// 也可以使用 map
const jobById = {}
// 创建队列数据
for (let i = 0; i < 10000; i++) {
// 建立 job
const asyncJob: Job = {
id: `${id}`,
queryArgs: {},
status: 'waiting'
}
// 存储 job,通过 id 追踪数据
jobById[asyncJob.id] = asyncJob
betterQueue.push(asyncJob)
// 取出数据并且完成请求后调用 cb(null, result) 会进入这里
.on('finish', (result) => {
// 修改任务状态,并存储任务结果
job.status = 'succeeded'
job.result = result
})
// 失败调用 cb(err) 会进入这里
.on('failed', (error: Error) => {
// 修改任务状态,并存储错误信息
job.status = 'failed'
job.err = error
})
}
// 获取任务,如果队列没有处理,会返回 wait 状态
// 队列已经处理,会返回 succeeded 或者 failed
function getJob(id: string) {
return jobById[id]
}</code></pre><p>在存储完任务之后,我们可以在前端或者服务端根据 id 来获取整个任务信息。</p><h3>并发处理</h3><p>此时任务队列就会一个接一个进行业务处理,在上一个异步任务完成(成功或者失败)后进行下一个任务。但这样就太慢了。同时也没有发挥出系统应有的处理能力。这时候我们可以直接添加配置项 concurrent。</p><pre><code class="ts">// 创建队列
const betterQueue = new BetterQueue(asyncProcess, {
concurrent: 10
}) </code></pre><p>这样的话,系统可以依次且同时处理多条任务。大大减少了所有任务的处理时长。</p><h3>任务状态</h3><p>我们还可以通过 getStats() 获取当前任务状态,这是 getStats 返回的信息:</p><pre><code class="ts"> interface QueueStats {
total: number; // 处理的任务总数
average: number; // 平均处理时间
successRate: number; // 成功率,在 0 和 1 之间
peak: number; // 大多数任务在任何给定时间点排队
}</code></pre><pre><code class="ts">function cb() {
// 获取当前队列的状态并打印完成数据对比。
// 如: 1/10 2/10
const stats = betterQueue.getStats()
console.log(`${stats.total}/10000`)
}
betterQueue.push(asyncJob)
.on('finish', (result) => {
// ...
// 完成时候进行回调
cb()
})
.on('failed', (error: Error) => {
// ...
// 完成时候进行回调
cb()
})</code></pre><p>这时候我们可以借助 getStats 向前端展示当前任务状态。</p><h3>队列控制</h3><p>better-queue 提供了强大的队列控制能力。</p><p>我们可以通过任务 id 直接取消某一个任务。</p><pre><code class="ts">// 直接取消任务
betterQueue.cancel(jobId)</code></pre><p>我们还可以通过 cancelIfRunning 设置为 true 来控制之前队列中之前的任务取消。</p><pre><code class="ts">// 创建队列
const betterQueue = new BetterQueue(asyncProcess, {
cancelIfRunning: true
})
betterQueue.push({id: 'xxx'});
// 如果之前的 id 在队列中,取消前一个任务,执行后一个任务
betterQueue.push({id: 'xxx'});</code></pre><p>我们也可以轻松控制队列暂停、恢复以及销毁。</p><pre><code class="ts">// 暂停队列运行
betterQueue.pause()
// 恢复队列运行
betterQueue.resume()
// 销毁队列,清理数据
betterQueue.destroy()</code></pre><p>同时,开发者也可以通过新建队列的回调函数中传出一个对象来自行控制。如:</p><pre><code class="ts">const betterQueue = new BetterQueue(function (file: File, cb: Function) {
var worker = someLongProcess(file);
return {
cancel: function () {
// 取消文件上传
},
pause: function () {
// 暂停文件处理
},
resume: function () {
// 恢复文件上传
}
}
})
betterQueue.push('/path/to/file.pdf')
betterQueue.pause()
betterQueue.resume()</code></pre><h3>重试与超时</h3><p>对于异步任务来说,如果出现了执行失败,better-queue 也提供了重试机制。</p><pre><code class="ts">const betterQueue = new BetterQueue(asyncProcess, {
// 当前任务失败了可以重新请求,最大为 10 次,超过 10 次宣告任务失败
maxRetries: 10,
// 重试等待时间 1s
retryDelay: 1000,
// 超时时间 5s,当前异步任务处理超过 5s 则认为任务失败
maxTimeout: 5000,
}) </code></pre><h3>持久化</h3><p>当前任务队列存储到内存中,但在开发服务端时候,仅放入内存可能不是那么安全,我们可以通过传入 store 配置项来持久化队列数据。</p><pre><code class="ts">// 此时队列的插入和删除都会和数据库进行交互
const betterQueue = new BetterQueue(asyncProcess, {
store: {
type: 'sql',
dialect: 'sqlite',
path: '/path/to/sqlite/file'
}
})
// 或者使用 use
betterQueue.use({
type: 'sql',
dialect: 'sqlite',
path: '/path/to/sqlite/file'
})
</code></pre><p>该库目前支持 SQLite 和 PostgreSQL,同时项目也提供了定制支持。</p><pre><code class="ts">betterQueue.use({
connect: function (cb) {
// 连接你的数据库
},
getTask: function (taskId, cb) {
// 查询任务
},
putTask: function (taskId, task, priority, cb) {
// 保存任务同时携带优先级
},
takeFirstN: function (n, cb) {
// 删除前 n 项(根据优先级和传入顺序排序)
},
takeLastN: function (n, cb) {
// 删除后 n 项(根据优先级和传入顺序排序)
}
})</code></pre><h3>先进后出</h3><p>better-queue 不仅仅提供了先进先出的逻辑,甚至提供了先进后出的逻辑,只需要在配置中添加 filo。</p><pre><code class="ts">// 创建队列
const betterQueue = new BetterQueue(asyncProcess, {
filo: true
})</code></pre><h3>任务过滤、合并以及调整优先级</h3><p>我们可以在业务处理中过滤某些任务,只需要添加 filter 函数。</p><pre><code class="ts">const betterQueue = new BetterQueue(asyncProcess, {
// 在推送任务前执行过滤
filter: async function (job: Job, cb: Function) {
// 在执行业务处理前预处理,验证数据,数据库查找等较为有用
// 异步处理验证失败
if (filterFail) {
cb('not_allowed')
return
}
// 为 job 前置处理
cb(null, job)
}
})</code></pre><p>对于有相同 id 的任务,better-queue 提供了合并函数:</p><pre><code class="ts">const betterQueue = new BetterQueue(function (task, cb) {
console.log("I have %d %ss.", task.count, task.id);
cb();
}, {
merge: function (oldTask, newTask, cb) {
oldTask.count += newTask.count;
cb(null, oldTask);
}
})
betterQueue.push({ id: 'apple', count: 2 })
betterQueue.push({ id: 'apple', count: 1 })
betterQueue.push({ id: 'orange', count: 1 })
betterQueue.push({ id: 'orange', count: 1 })
// 这时候会打印出
// I have 3 apples.
// I have 2 oranges.
// 而不是
// I have 1 apples.
// I have 1 oranges.</code></pre><p>优先级对于队列也是非常重要的配置。</p><pre><code class="ts">const betterQueue = new BetterQueue(asyncProcess, {
// 决定先处理那些任务
priority: function (job: Job, cb: Function) {
if (job.queryArgs === 'xxxxx') {
cb(null, 10)
return
}
if (job.queryArgs === 'xxx'){
cb(null, 5)
return
}
cb(null, 1);
}
})</code></pre><h3>批处理与批处理前置</h3><p>批处理同样也可以增强系统处理能力。使用批处理不会立即处理任务,而是将多个任务合并为一个任务处理。</p><p>批处理不同于 concurrent,该配置是当前队列内存储的数据达到批处理配置后才会进行数据处理。</p><pre><code class="ts">const betterQueue = new BetterQueue<(function (batch, cb) {
// batch 中是一个数组,最多为 3 个
// [job1, job2, job3]
cb()
}, {
// 批处理大小
batchSize: 3,
// 5 秒内等待队列拥有 3 个项目,或者 3 秒内没有添加新的任务
// 直接处理队列
batchDelay: 5000,
batchDelayTimeout: 3000
})
// 当前也会触发,不过要等 3 秒没有添加新任务
// 如开始时放入 1 条数据,等待 2.5 s 后放入第二条数据,则在 5s 后也会执行
betterQueue.push(job1)
betterQueue.push(job2)
// 在 1s 内推入第三条数据到队列中
// 队列数据达到 3 了,开始处理
betterQueue.push(job3)</code></pre><p>我们也可以通过添加前置条件判断是否执行下一个批处理。</p><pre><code class="ts">const betterQueue = new BetterQueue<(function (batch, cb) {
// batch 中是一个数组,最多为 3 个
// [job1, job2, job3]
cb()
}, {
precondition: function (cb) {
// 当前是否是联网状态
isOnline(function (err, ok) {
if (ok) {
// 返回 true,进行下一次批处理
cb(null, true);
} else {
// 继续执行直到为 true
cb(null, false);
}
})
},
// 每 10 秒执行一次 precondition 函数
preconditionRetryTimeout: 10 * 1000
})</code></pre><p>当然,better-queue 提供了更多的参数与配置,我们可以进一步学习,以便基于现有业务管理复杂的任务。让负责的任务变得更加可控。同时也可以提升系统处理业务的能力。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=4A0H0aqnsthT%2Ft1e7djx5A%3D%3D.ukZ0q2W6RrNl5Hjdj%2FXnCwJXUgIRcqgK6Rj3Polr1rld745tnT07xkghbaNkfLc%2B" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=VL5weVVxxZCxNVmYCrcjcw%3D%3D.M4XJPqgvpHRpB88rvhpZBFHxQLLafmghISSLg%2Bt8jmZlpSsNsyl6A2yOFjMcbv8K" rel="nofollow">better-queue</a></p>
利用增量构建工具 Preset 打造自己的样板库
https://segmentfault.com/a/1190000040038661
2021-05-21T00:24:18+08:00
2021-05-21T00:24:18+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
2
<p>你是如何开始一个项目呢?是基于当前技术栈提供的脚手架还是从 <strong>npm init</strong> 开始呢?</p><p>以前我没得选,必须面向搜索引擎。基于 webpack 或 rollup 来一步步构建项目,在开发过程中还有可能发生很多错误。但现在我只想专注于当前业务,挑选合适的脚手架之后迅速构建自己的项目,这样的话,就可以把大量维护性的工作交给开源作者。</p><p>当然,知名的脚手架工具(Vue CLI,Umi,Vite 等)自不必说,这里我推荐几个顺手的工具。</p><ul><li><a href="https://link.segmentfault.com/?enc=aEkcsrplANcw%2Fi4CPI3sQw%3D%3D.XjqyY4d4lnKCqiL1WRRjuO1ZpJnh4UB7t2ToINzgun9rJb1zSThSIWQXKEIjXar2" rel="nofollow">microbundle-crl</a> 专注于 React 组件的构建</li><li><a href="https://link.segmentfault.com/?enc=FJ3oq1KyvUev987RUWYNBw%3D%3D.bMlwEvFsqwT1Z7oK1AcWVxnwayxqaROiWbKu2ZF3YnQ%3D" rel="nofollow">tsdx</a> 专注于 TypeScript 库的构建</li><li><a href="https://link.segmentfault.com/?enc=FoVSH2%2FG7zottAhl3YFoHQ%3D%3D.jj3q1zcFTsJbIljtwz0%2BoXaFbwq8irHANkIU9RDRjno%3D" rel="nofollow">crateApp</a> 根据当前选项配置生成项目包 (多个基础构建工具 Webpack,Parcel,Snowpack)</li></ul><p>但无论是哪一个样板库或者脚手架,都不会完全符合当前业务的需求,开发者需要基于当前的样板进行修改。比如说需要在项目中要添加开源协议,修改项目名称,以及为项目添加不同的依赖。</p><p>从构建来说,目前有两个问题:</p><ul><li>大量重复性操作</li></ul><p>如果生成项目的工作频率很高的话,例如一周写一个业务性组件。虽然每次在项目中要添加开源协议,修改项目名称,添加特定依赖都是一些小活,但频率高起来也是一件麻烦的事情。</p><ul><li>底层依赖无法直接升级</li></ul><p>如果开发者修改了当前样板,那么脚手架出现破坏性更新时候就无法直接升级(这种问题当然也比较少)。虽然开发过程中会记录一些修改。但随着时间的偏移,开发者不会确切知道需要编辑或删除哪些文件才能使升级后的项目正常工作。</p><p>话不多说,我们来看一看工具 <a href="https://link.segmentfault.com/?enc=KPn3xsXPjtg2WBtQUViL7w%3D%3D.3BlhJ8r1Wn2ojH7Su3Fgor3qLZJvE8yGcifJXJke1BM%3D" rel="nofollow">Preset</a> 是如何解决这一系列的问题的。</p><h2>使用 Preset</h2><p>首先建立一个项目,以 vite 为例子,package.json 如下所示</p><pre><code class="json">{
"name": "vite-preset",
"version": "0.0.1",
"author": "jump-jump",
"license": "MIT",
"preset": "preset.ts",
"prettier": {
"printWidth": 80,
"tabWidth": 2,
"trailingComma": "all",
"singleQuote": true,
"arrowParens": "always",
"useTabs": false,
"semi": true
},
"devDependencies": {
"apply": "^0.2.15"
}
}</code></pre><p>执行下面的操作,我们会的到 my-vue-app 文件。</p><pre><code class="bash"># npm 6.x
npm init @vitejs/app my-vue-app --template vue</code></pre><p>拿到了当前命令生成的结果之后我们把当前生成文件拷贝到 vite-preset 根目录下的 templates 中(即 templates/vite ) 文件夹下。</p><p>然后我们通过 preset.ts(对应 package.json 中的 preset": "preset.ts" ) 编写 Preset 命令。</p><pre><code class="ts">import {Preset, color} from 'apply'
// 当前编写项目的名称,会在控制台中展示
Preset.setName('jump-jump vite preset')
// 从 templates/vite 中提取所有文件,并携带以 . 开头的文件 如 .gitignore 等
Preset.extract('vite')
.withDots()
// 更新当前 package.json 文件,添加依赖 tailwindcss,移除依赖 sass
Preset.editNodePackages()
.add('tailwindcss', '^2.0')
.remove('sass')
// 安装所有依赖
Preset.installDependencies()
// 运行提示
Preset.instruct([
`Run ${color.magenta('yarn dev')} to start development.`,
]).withHeading("What's next?");</code></pre><p>完成了!</p><p>我们可以来试试效果,我寻找一个合适的文件夹,然后运行指令:</p><pre><code class="bash">// 解析 vite-preset 项目
npx apply C:\re-search\vite-preset</code></pre><p><img src="/img/bVcR920" alt="Preset.png" title="Preset.png"></p><p>之前保存的 vite 样板文件夹被解压到当前文件夹下,此时依赖也被替换掉了,当然,我们也可以指定文件夹下安装,如</p><pre><code class="bash">npx apply C:\re-search\vite-preset vite-demo</code></pre><p>vite 样板板被解压到当前文件夹下的 vite-demo 文件夹中去了。</p><p>我们不但可以使用本地路径,当然,我们也可以使用 github 路径。如:</p><pre><code class="bash">npx apply git@github.com:useName/projectName.git
// 等同于
npx apply username/projectName</code></pre><p>目前来看,效果勉强还可以,实际上我们能够操作的远不止上述展示的,那么我开始逐个解读一下 Preset 的各个命令。</p><h2>玩转 Preset</h2><h3>setName 工程名设置</h3><p>正如上面图片展示的那样,该命令设置成功后会显示在控制台中。</p><pre><code class="ts">Preset.setName('jump-jump preset')</code></pre><h3>setTemplateDirectory 样板目录设置</h3><p>此操作会修改提取根路径,不使用则默认选项为 templates。</p><pre><code class="ts">// 文件提取根路径被改为了 stubs 而不是 templates
Preset.setTemplateDirectory('stubs');</code></pre><h3>extract 文件夹提取</h3><p>此操作允许将文件从预设的样板目录提取到目标目录。在大多数情况下,这个命令已经可以解决绝大部分问题。</p><pre><code class="ts">// 当前会提取整个根样板 即 templates 或者 stubs
Preset.extract();
// 当前会提取 templates/vite 文件夹到根目录
Preset.extract('vite');
// 先提取 templates/presonal,然后提取 templates/presonal 文件夹
Preset.extract('vite');
Preset.extract('presonal');
// 等同于 Preset.extract('vite')
Preset.extract().from('vite');
// 提取到根路径下的 config 文件夹
Preset.extract().to('config');
// 遇到文件已存在的场景 [ask 询问, override 覆盖, skip 跳过]
// 注意:如果询问后拒绝,将会中止当前进度
Preset.extract().whenConflict('ask');
// 在业务中,我们往往这样使用,是否当前式交互模式?
// 是则询问,否则覆盖
Preset.extract().whenConflict(Preset.isInteractive() ? 'ask' : 'override')
// 如果没有此选项,以 .开头的文件(如 .gitignore .vscode) 文件将被忽略。
// 注意:建议在样板中使用 .dotfile 结尾。
// 如: gitignore.dotfile => .gitignore
Preset.extract().withDots();</code></pre><h3>editJson 编辑 JSON 文件</h3><p>使用 editJson 可以覆盖和删除 JSON 文件中的内容。</p><pre><code class="TS">// 编辑 package.json 深度拷贝数据
Preset.editJson('package.json')
.merge({
devDependencies: {
tailwindcss: '^2.0'
}
});
// 编辑 package.json 删除 开发依赖中的 bootstrap 和 sass-loader
Preset.editJson('package.json')
.delete([
'devDependencies.bootstrap',
'devDependencies.sass-loader'
]);</code></pre><p>当然,Preset 为 node 项目提供了简单的控制项 editNodePackages 。</p><pre><code class="TS">Preset.editNodePackages()
// 会删除 bootstrap
// 无论是 dependencies, devDependencies and peerDependencies
.remove('bootstrap')
// 添加 dependencies
.add('xxx', '^2.3.0')
// 添加 devDependencies
.addDev('xxx', '^2.3.0')
// 添加 peerDependencies
.addPeer('xxx', '^2.3.0')
// 设置键值对
.set('license', 'MIT')
.set('author.name', 'jump-jump')</code></pre><h3>installDependencies 安装依赖</h3><p>在搭建项目的同时我们需要安装依赖,这里通过 installDependencies 完成。</p><pre><code class="ts">// 安装依赖,默认为 node,也支持 PHP
Preset.installDependencies();
// 询问用户是否安装
Preset.installDependencies('php')
.ifUserApproves();</code></pre><h3>instruct 引导</h3><p>该命令可以添加标语来一步步引导用户进行下一步操作,还可以添加各种颜色。</p><pre><code class="ts">import { Preset, color } from `apply`;
Preset.instruct([
`Run ${color.magenta('yarn dev')} to start development.`,
]).withHeading("What's next?");</code></pre><h3>options 设置配置</h3><p>开发者想要添加多个样板,是否需要开发多个项目呢?答案是否定的,我们通过 options 获取参数即可。</p><pre><code>npx apply C:\re-search\vite-preset vite-demo --useEsbuild</code></pre><p>当前数据会被设置到 Preset.options 中。</p><pre><code class="TS">// 默认设置 useEsbuild 为 true
Preset.option('useEsbuild', true);
// 默认设置 use 为字符串 esbuild
Preset.option('use', 'esbuild');
// 如果配置项 useEsbuild 为 ture 解压 templates/esbuild
// 也有 ifNotOption 取反
Preset.extract('esbuld').ifOption('useEsbuild');
// use 严格相等于 esbuild 解压 templates/esbuild
Preset.extract('esbuld').ifOptionEquals('use','esbuild');
Preset.extract((preset) => {
// 如果配置项 useEsbuild 为 ture 解压 templates/esbuild
if (preset.options.useEsbuild) {
return 'esbuild';
}
return 'vite';
});</code></pre><p>我们可以在执行 npx 是添加配置项,如下所示</p><table><thead><tr><th>标志</th><th>价值观</th></tr></thead><tbody><tr><td><code>--auth</code></td><td><code>{ auth: true }</code></td></tr><tr><td><code>--no-auth</code></td><td><code>{ auth: false }</code></td></tr><tr><td><code>--mode auth</code></td><td><code>{ mode: 'auth' }</code></td></tr></tbody></table><h3>input confirm 交互设置</h3><p>Preset 设置配置项很棒。但就用户体验来说,通过交互设置则更好。这样我们无需记忆各个配置项。通过人机交互来输入数据,当前数据会被添加到 Preset.prompt 中。</p><pre><code class="TS">// 第一个参数将传入 Preset.prompt
Preset.input('projectName', 'What is your project name?');
// 第三个是可选的上下文字符串,用于定义提示的默认值。
// 如果预设是在非交互模式下启动的,它将被使用。
Preset.input('projectName', 'What is your project name?', 'jump project');
// 编辑脚本
Preset.editNodePackages()
.set('name', Preset.prompt.projectName)
.set('license', 'MIT')
.set('author.name', 'jump-jump')
// 第一个参数将传入 Preset.prompt
// 第三个是可选的上下文布尔值,用于定义提示的默认值。
// 如果预设是在非交互模式下启动的,它将被使用。
Preset.confirm('useEsLint', 'Install ESLint?', true);</code></pre><h3>delete edit 修改文件</h3><p>删除生成文件夹中的文件直接使用 delete</p><pre><code class="TS">Preset.delete('resources/sass');</code></pre><p>编辑文件</p><pre><code class="TS">// 替换文本字符串
Preset.edit('config/app.php').update((content) => {
return content.replace('en_US', 'fr_FR');
});
// 替换 README.md 文件中的字符串 {{ projectName }}
// {{prejectName}} => prompts.name ?? 'Preset'
Preset.edit('README.md').replaceVariables(({ prompts }) => ({
projectName: prompts.name ?? 'Preset',
}));</code></pre><h3>execute 执行 bash 命令</h3><p>如果之前的命令都不能满足你,那只能执行 bash 命令了吧!Preset 也提供了这个功能,结合 hooks 可添加各种参数。</p><pre><code class="TS">// 利用钩子将数据存储到 context 中
Preset.hook(({ context, args, options }) => {
const allowedOptions = ['auth', 'extra'];
context.presetName = args[2];
context.options = Object.keys(options)
.filter((option) => allowedOptions.includes(option))
.map((option) => `--${option}`);
});
// 第一个参数是程序或者命令名称,后面是参数,从 context 中读取
Preset.execute('php')
.withArguments(({ context }) => [
'artisan',
'ui',
context.presetName,
...context.options
])
// 修改当前标题,当前执行时会在控制台打印如下字符串,而不是默认字符串
.withTitle(({ context }) => `Applying ${context.presetName}`);</code></pre><h2>进一步思考</h2><p>通过对 Preset 库的学习,我们可以看到 Preset 具备非常不错的设计风格与强大的功能。Preset 没有从底层构建项目,反而是帮助开发者通过一些命令衍生出自己的工具,同时还可以记录开发者绝大部分对于项目的修改。</p><h3>增量思想</h3><p>在使用 Preset 构建样板的过程中,开发者没有对原本的样板进行修改,这样使得开发者升级原始样本变得非常简单。在构建 Preset 项目过程其实也就是修改样板增量。</p><p>我们应该进一步在开发中使用增量思想,在这里「增量」这个概念的对立面是「全量」。增量会根据比对当前与过去之间的差异,只关注差异性所带来的影响。</p><p>增量有很多实际的意义,我们可以看到:</p><ul><li>前后端交互时候前端只提交变化的数据</li><li>rsync 增量同步文件</li><li>网盘增量上传文件</li><li>数据库增量备份</li><li>增量代码检查、构建、打包</li></ul><h3>链式调用</h3><p>随着前端框架带来了数据驱动,JQuery 逐渐退出历史舞台(Bootstrap 5 去除了 JQuery)。ES 不断升级也给与用户大量帮助,用户无需自行构建对象进行链式调用了。但这并不意味链式调用不重要。</p><p>因为链式调用可以优雅的记录时序,开发者可以依赖当前调用来进行分析。</p><p>大多数工具都会提供不同的配置项。此时我们可以直接传入配置项来使用工具。</p><p>如果当前操作有时序性(先后顺序决定最终结果),构建对象进行链式调用则更有效。当然你可以说我们添加一个数组配置来决定顺序。但面对复杂的顺序,优秀的函数命名可以让用户更简单的理解代码。</p><p>又如果,我们在面对复杂的图形结构时,构建对象来进行节点的选择与操作一定会更加简单。如果有需求,我们甚至需要根据链式调用来生成 sql 语句。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=RhoVTRGsF9wGVK01dM3WpA%3D%3D.zGXbZHJIkb29RxCqW2a3k5kp66wHtx0swMd6QbOtm0Z6WUtk80%2FiQOtov4vVl%2FBT" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=UZYiLp%2FEW4BP95DcLqiVGQ%3D%3D.OuQ%2FEeD0xjqggLXmF5F3m%2FlZkJPJtIkFqbAwefDjSOM%3D" rel="nofollow">Preset</a></p><p><a href="https://link.segmentfault.com/?enc=HBaqnANL07Oiu2igodokNw%3D%3D.Lp3Ca%2BdOeo4cSqAFh7qTFQYpPgwCcUwGGBbDW77Vc4DO8fHIGjhxeroRZk7uhQfR" rel="nofollow">microbundle-crl</a></p><p><a href="https://link.segmentfault.com/?enc=FGF8rL9FW4fae8%2FVv2oYaA%3D%3D.mUAqx%2BIox2R6PkpsSavOSOxh%2BzQcZaS2in9kDO0EESc%3D" rel="nofollow">tsdx</a></p><p><a href="https://link.segmentfault.com/?enc=AzcMhcih35NTL11DCRRjeA%3D%3D.mD%2FJjiDFkCLxdTwf2Ci2WXevGhixshm%2B%2BD15j%2BcLra4%3D" rel="nofollow">crateApp</a></p>
从 CSS 开始学习数据可视化
https://segmentfault.com/a/1190000039962299
2021-05-07T23:53:38+08:00
2021-05-07T23:53:38+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<blockquote>一图胜千言</blockquote><p>可视化领域是目前(泛)前端中最火热的分支之一。无论是面向普通用户的可视化大屏展示数据信息,还是企业服务中数据统计概览或者调度服务,乃至于国家大力推动的智慧建设(智慧大脑,智慧城市)等项目,都重度使用了数据可视化技术。</p><p>以下图片来自于 <a href="https://link.segmentfault.com/?enc=eq0uFX32BaVk2thAGp17Kg%3D%3D.Gcs9yx00NwCdI1s3oz0npdWZVRVv710%2BhWTFpGKcq1UGLVbImmDiX8dSVwDHDJqp" rel="nofollow"><strong>前端搞可视化</strong></a> 菜鸟体验体验技术 其歌 的分享 《如何融合数据可视化与物理世界》。我们可以看到:可视化结合硬件也有很大的用武之地。</p><p><img src="/img/bVcRQbe" alt="25b917de747e4a2188ad3227ed7471f.png" title="25b917de747e4a2188ad3227ed7471f.png"></p><h2>可视化是什么</h2><p>当然,我曾经一度认为可视化就是绘制各种图表,学习可视化就是学习 echarts, D3 等库,然后利用这些工具绘制饼图、折线图、柱状图这类图表。然而,大部分情况下,我们是可以借助这些库来进行可视化项目的开发。但这些库是通用的解决方案。特定条件下,如在短时间内同时渲染大量元素,通用的解决方案就无法使用,此时我们就需要选择更加底层的方案(如利用 WebGL 自行控制 GPU 渲染图像)。</p><p>可视化的源头是数据。我们需要拿到有用的数据,然后通过转化以及整合数据生成用户所需要的结构,最终以图形的方式展现出来。可视化一定是与当前业务高度结合的。可视化工程师需要根据当前的业务以及产品需求,选择合适当前业务的技术栈并生成对用户有用的图像。</p><p>可视化的目的是提升用户对数据的认知能力,解放用户的大脑,从而让数据更有价值。</p><h2>用 css 做数据可视化</h2><p>通常来说,SVG 易于交互,Canvas2D 性能更好。基本上会根据当前交互和计算量来确定使用 SVG 或者 Canvas 。 如果遇到大量像素计算,甚至还需要通过 WebGL 深入 GPU 编程(自行控制 CPU 并行计算) 。</p><p>但如果我们做官网首页的图表呢?如果当前的图表很简单,不需要变化呢?我们还需要引入 ECharts 这种库?或者说手动写一个图表。</p><p>实际上,随着浏览器的发展,CSS 的表现能力愈发强大,完全可以实现常规的图表。如柱状图和饼图等。使用网格布局(Grid Layout)加上线性渐变(Linear-gradient)可以直接生成柱状图。</p><pre><code class="html"><style>
.bargraph {
margin: 0 auto;
display: grid;
width: 250px;
height: 200px;
padding: 10px;
transform: scaleY(3);
grid-template-columns: repeat(5, 20%);
}
.bargraph div {
margin: 0 5px;
}
.bargraph div:nth-child(1) {
/** 从上到下(to bottom 默认,可不写),75% 全透明, 25% 红色, **/
background: linear-gradient(to bottom, transparent 75%, red 0);
}
.bargraph div:nth-child(2) {
background: linear-gradient(transparent 74%, yellow 0);
}
.bargraph div:nth-child(3) {
background: linear-gradient(transparent 60%, blue 0);
}
.bargraph div:nth-child(4) {
background: linear-gradient(transparent 55%, green 0);
}
.bargraph div:nth-child(5) {
/** 也可以多种颜色渐变 **/
background: linear-gradient(transparent 32%, #37c 0, #37c 63%, #3c7 0);
}
</style>
<body>
<div class="bargraph">
<div></div>
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</body></code></pre><p><img src="/img/bVcRQbf" alt="image-20210507221406584.png" title="image-20210507221406584.png"></p><p>我们还可以使用圆锥渐变 <strong>conic-gradient</strong> 实现饼图,以及使用 div + <strong>transform</strong> 实现折线图。</p><p>当然,用 CSS 进行图表绘制优点在于不需要学习额外的库和 api。但缺点也很明显:</p><ul><li>对应关系复杂,无法直观的修改当前代码以快速替换当前图标样式(换算往往需要 JavaScript )</li><li>属于 DOM 树的一员,性能往往难以把控(作为稳定的的首页图表,不会由太大问题)</li></ul><h2>图表库 Chart.css</h2><p>在没有遇到 <a href="https://link.segmentfault.com/?enc=iQXtj3AJYUrj2%2BZfv7QA9w%3D%3D.gDQFOUQqsKVSgRrSVRqaq5zhggG6SUM8PnoaZi%2BDwJ8%3D" rel="nofollow">Charts.css</a> 之前, 我认为图表是离不开 JavaScript 计算的。但看到该库时候,我也是非常的欣喜。<strong>Charts.css</strong> 是一个 CSS 框架。它使用 CSS3 将 HTML 元素设置为图表样式,同时该库其中一个设计原则就是不会使用 JavaScript 代码(如果无法使用CSS完成,则不会成为框架的一部分 )。当然,用户可以自行决定是否使用 JavaScript 。</p><p>拿出最简单的表格为例:</p><pre><code class="html"><table border="1">
<caption> Front End Developer Salary </caption>
<tbody>
<tr>
<td> $40K </td>
</tr>
<tr>
<td> $60K </td>
</tr>
<tr>
<td> $75K </td>
</tr>
<tr>
<td> $90K </td>
</tr>
<tr>
<td> $100K </td>
</tr>
</tbody>
</table></code></pre><p>如图所显:</p><p><img src="/img/bVcRQbh" alt="image-20210428000546653.png" title="image-20210428000546653.png"></p><p>使用 Chart.css 之后:</p><pre><code class="html"><table style="width: 400px;height: 400px" class="charts-css column">
<caption> Front End Developer Salary </caption>
<tbody>
<tr>
<td style="--size: calc( 40 / 100 )"> $40K </td>
</tr>
<tr>
<td style="--size: calc( 60 / 100 )"> $60K </td>
</tr>
<tr>
<td style="--size: calc( 75 / 100 )"> $75K </td>
</tr>
<tr>
<td style="--size: calc( 90 / 100 )"> $90K </td>
</tr>
<tr>
<td style="--size: calc( 100 / 100 )"> $100K </td>
</tr>
</tbody>
</table> </code></pre><p><img src="/img/bVcRQbi" alt="image-20210428000851714.png" title="image-20210428000851714.png"></p><p>非常棒!</p><p>我们可以看到其中最重要的修改是使用了 CSS 变量,CSS 虽然不像 JavaScript 是通用编程语言,但 CSS 变量却是一个桥梁,让其拥有了与其他元素沟通的能力(HTML, JavaScript),其次借助 CSS 中的计算属性 calc 。同时也可以参考我之前写的的博客 <a href="https://link.segmentfault.com/?enc=oOFhspEPuQ5S6u3SOT6H6w%3D%3D.hAjGlSrDnJQer1v1zAV16KzrKmwaU%2BECVcHv18KbkrHdEva81o%2FfFuXfW3c%2BkYdcMYPj232VHp9XDBtU3gVXpw%3D%3D" rel="nofollow">玩转 CSS 变量</a> 和 <a href="https://link.segmentfault.com/?enc=AjLG8dgPwnrkgV1nr9vC9A%3D%3D.vKFqfZD7iXhQ7hqdbNp%2BRdk0%2BCKmeGineni9ctRyPHosXTj9PK1FMK13uQ3S1ULB" rel="nofollow">CSS 扫雷游戏</a> 。</p><pre><code class="css">/** 图表 css 中会有很多这种计算代码 **/
height: calc(100% * var(--end, var(--size, 1)));</code></pre><p>当然,该库目前可以描述水平条形图(bar)、柱状图(column)、面积图 (area)、折线图(line)。饼图,雷达图等还在开发中。当然该库也可以实现混合图表:</p><p><img src="/img/bVcRQbj" alt="image-20210507232115185.png" title="image-20210507232115185.png"></p><p>Charts.css 同时支持用户使用 CSS3 为当前图表添加各种效果,详情见 <a href="https://link.segmentfault.com/?enc=jv3x6PPZHAHdUdcinZ9ysQ%3D%3D.Gt6h9qLOpraaxp7J7okwSW%2F5LWuZxXBJ1fKIh5pZx%2BSbn7k3zlDjxwItgep3IFuD" rel="nofollow">定制化</a> 。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=7xmnf07V2RgDDGTO8H%2FWKA%3D%3D.cZydnl7vmQ8DbxXyW4KTk%2BxhSp0YDccSQnHEjwc5tc75vqjzpr6%2BO32bQjuFb0LT" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p>《如何融合数据可视化与物理世界》</p><p><a href="https://link.segmentfault.com/?enc=dIIrBr3ZEeOsGhhRzyB6ng%3D%3D.3NBLOqaw9VazEjBw7wnabvlfTtx%2BvZIm4Ms6EffPya8QiWDtiaFt2y8D6WCW1rvs" rel="nofollow">跟月影学可视化</a></p><p><a href="https://link.segmentfault.com/?enc=mQYJaC%2F0la7aI9W2v97jmQ%3D%3D.VjRPQAYLLyWjoAY3EDgvcg5USDmAidCygCn4Y%2FuvTfU%3D" rel="nofollow">Charts.css</a></p>
从组件 boolean 值属性谈谈分层架构
https://segmentfault.com/a/1190000039813726
2021-04-13T01:25:58+08:00
2021-04-13T01:25:58+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>在刚入行的时候,我从事的是企业服务,在当前业务下开发组件或者页面的时候遇到需要表示 boolean 值属性的时候,往往以 can 作为变量前缀来表示组件是否可以执行某一类或者某一个操作。这种命名习惯跟随了我很久。</p><p>直到有一天,我去了另一家公司开发拖拽设计器的时候,领导告诉我:虽然 can 开头表示 boolean 值是没什么问题,但是对于组件开发来说,应该是 able 结束或者 is 开头更合适。然后我在查看了市面上较为知名的几个的组件库,它们在表示 boolean 值时候都是以 able 结尾,我就默默的把当前的变量修改了一下。</p><p>可惜的是:当时没有仔细分析这个问题以及问题背后的逻辑。但是做一件事总是要有一个理由的,即使这个理由无法说服别人,但至少也要说服自己。下面我就来分析一下,两者的异同。</p><h2>属性实意</h2><p>我们来看一看 can,able 这两者的实际意思,is 后面再聊。</p><ul><li>can (表示有能力做或能够发生)能,会;(表示知道如何做) 懂得; 与动词feel、hear、see、smell、taste 连用</li><li>able 能;能够;有才智的;有才能的</li></ul><p>以编辑为例子: canEdit 表示可以编辑,而 editable 表示可编辑的。前者着重表示能够实现某事,后者着重表示具备能力实现某事。</p><p>在不同的命名下,我们可以看出决定当前命名的内在逻辑在于当前工作的重点是什么:在之前的工作中,我们是开发业务系统,而对于当时的业务系统来说,有大量的权限控制。而在后来的工作中,我们更多开发的是配置(基础)组件。</p><p>我们不妨再来看一看 is 前缀,is 前缀可能更接近 boolean 的意思。如 isInternalStaff 表示是否是内部员工。 但同样,对比前两个单词的意思,它能够表示的粒度太粗。在没有实际深入分析业务前,你无法通过该变量分析出任何实际意义。</p><h2>分层模式</h2><p>分层模式是最常用的架构模式。大部分软件系统都是由多人开发的。根据某种方式或规则可以将系统分层,方便开发人员协同工作。分层实现了层间低耦合和层内高内聚,提升了系统的可维护性。</p><h2>特点</h2><p>从规则上来看,分层的所有代码必须属于某一层内,上层代码可以使用下一层,但这种关系必须是单向的。不可以进行循环依赖。当然,从浏览器运行实际分析,依次加载和执行 js 无疑是分层模式的天然应用场景。</p><p>当然,分层模式的优势和劣势也是较为明显的,优势无疑是把基础,不易变化的单独提取出来。提高系统的可复用性,可测试性。更容易修改。同时系统分层非常容易实施。但同时,每一次分层都引入了额外的抽象,增加了系统的复杂度,并且有可能会影响性能(清晰的结构比那一点点性能损失要有用的多),同时也可能导致开发有一定的痛苦。</p><p>分层模式有许多变种,但无论分为多少层,它的关系,使用规则是不变的。</p><h3>效率提升</h3><blockquote>Flux 架构就像眼镜:您自会知道什么时候需要它。</blockquote><p>对待任何系统,都有符合自身的代码架构。但对于分层来说,它太棒了。除非你在进行 demo 测试,否则我一定会推荐你使用分层架构。</p><p>分层模式还有一个特点是知识屏蔽(封装),分层可以减少不相关事务间的影响,对于一个成熟的开发团队来说,一定会有人才梯度设计。当团队进入新人的情况下,成熟的分层可以让开发人员不清楚下层细节的情况下,依然可以利用下层技术文档以及 cv 大法进行系统开发。但如果所有的代码都在一个层内,所有人面向同样的问题。虽然我们已经很努力的让新人处理简单的问题,但是错综复杂的调用依然会降低实际开发效率。所以分层模式会帮助团队提效。同时也起到一定代码保护的作用。</p><p>分层模式也可以帮你分析实际问题,当你清楚这个问题属于谁,其实问题已经解决了一大半了。我们当然需要具有主人翁精神,但事实上,找到更了解它的人无疑是更有效的方案。</p><p>从架构上来说,我们也尽可能的从下层解决问题,因为下层的代码有强大的复用能力。虽然越接近代码细节,修改越有效,性能提升越高。但是对于系统架构来说,细节的解决方案反而是最后考虑的。</p><h2>实际分析</h2><p>我们再回头看一下 boolean 值属性。able 适合与基础组件设计,用于表示基础组件拥有的能力。</p><p>can 表明权限控制,在业务块(business block)中使用,利用基础组件,但往往有一定的业务属性,但还可以提炼出一套通用的逻辑。例如 <a href="https://link.segmentfault.com/?enc=B5iuHKGzqVYFMfzwndBZxA%3D%3D.C4xZ4eUiMeJKnWXbBb9X2fmrkMlSqucGi%2F1KJP%2BbFF4oTSLjL6WYPX2DLMnXul5NkkmVLQHUXMFFmz5JHckh3A%3D%3D" rel="nofollow">Vant 地址选择</a> 这种,有添加,删除,排序以及修改默认地址的业务逻辑。</p><p>最后的 is 适合于是业务系统(页面),我们可以基于不同的角色等构建不同的业务,利用基础组件和业务块来构建。同时我们也可以看出,我们不应该在基础组件和业务块中使用 redux 等状态管理库,以避免耦合。</p><p>不过在业务系统上,我们更要分析出当前的代码重复是否是知识的重复。在很多团队编程规则中,都会列举 DRY 原则,甚至 CI 系统中,会指出代码重复,并且禁止你提交代码。这时候,你也需要告诉他们你的理由,代码的确相同,但是当前代码所表示的知识并不相同,这仅仅是一个巧合罢了。</p><p>在实际项目开发中,我们可以逐步前进,先在代码中使用 components 文件夹封装组件和块。发展到一定阶段后,然后再利用 monorepo (多项目一个仓库管理)去管理当前系统,最终在稳定之后,提取各个层级形成 multirepo 以便新的项目复用。</p><p>分层架构简单但是十分强大,不过想要用好可不是一件简单的事。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=rzjC%2BIoyRqYJ4IakCe7KKQ%3D%3D.XZrrEDS%2Bm0iYWvt%2B3WZxtz%2FybLcFpJniDLUoV17Z31TlLs%2Bc9fm0QngY5cH%2Bk9yl" rel="nofollow">博客地址</a></p>
手写一个基于 Proxy 的缓存库
https://segmentfault.com/a/1190000039217566
2021-02-17T12:21:27+08:00
2021-02-17T12:21:27+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
40
<p>两年前,我写了一篇关于业务缓存的博客 <a href="https://link.segmentfault.com/?enc=p3hoQHWuwk0GGMctJy6yHg%3D%3D.XFd9PPyb2jxw2LaeeVYKIBH80%2BwfqUxmZ%2Bx4sDZ%2FtQ7fL6BKPxc13MJi3jGLih6e" rel="nofollow">前端 api 请求缓存方案</a>, 这篇博客反响还不错,其中介绍了如何缓存数据,Promise 以及如何超时删除(也包括如何构建修饰器)。如果对此不够了解,可以阅读博客进行学习。</p><p>但之前的代码和方案终归还是简单了些,而且对业务有很大的侵入性。这样不好,于是笔者开始重新学习与思考代理器 Proxy。</p><p>Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。关于 Proxy 的介绍与使用,建议大家还是看阮一峰大神的 <a href="https://link.segmentfault.com/?enc=K%2FDyI2UlZ9B%2F8P36O8lMRA%3D%3D.6xhoACI5aP5iVbIdDTSJJ4b%2FdVbVK%2F0S97shgCXxrD%2FxYhYeV59MAsN24dPzgy7T" rel="nofollow">ECMAScript 6 入门 代理篇</a>。</p><h2>项目演进</h2><p>任何项目都不是一触而就的,下面是关于 Proxy 缓存库的编写思路。希望能对大家有一些帮助。</p><h3>proxy handler 添加缓存</h3><p>当然,其实代理器中的 handler 参数也是一个对象,那么既然是对象,当然可以添加数据项,如此,我们便可以基于 Map 缓存编写 memoize 函数用来提升算法递归性能。</p><pre><code class="ts">type TargetFun<V> = (...args: any[]) => V
function memoize<V>(fn: TargetFun<V>) {
return new Proxy(fn, {
// 此处目前只能略过 或者 添加一个中间层集成 Proxy 和 对象。
// 在对象中添加 cache
// @ts-ignore
cache: new Map<string, V>(),
apply(target, thisArg, argsList) {
// 获取当前的 cache
const currentCache = (this as any).cache
// 根据数据参数直接生成 Map 的 key
let cacheKey = argsList.toString();
// 当前没有被缓存,执行调用,添加缓存
if (!currentCache.has(cacheKey)) {
currentCache.set(cacheKey, target.apply(thisArg, argsList));
}
// 返回被缓存的数据
return currentCache.get(cacheKey);
}
});
}
</code></pre><p>我们可以尝试 memoize fibonacci 函数,经过了代理器的函数有非常大的性能提升(肉眼可见):</p><pre><code class="ts">const fibonacci = (n: number): number => (n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2));
const memoizedFibonacci = memoize<number>(fibonacci);
for (let i = 0; i < 100; i++) fibonacci(30); // ~5000ms
for (let i = 0; i < 100; i++) memoizedFibonacci(30); // ~50ms</code></pre><h3>自定义函数参数</h3><p>我们仍旧可以利用之前博客介绍的的函数生成唯一值,只不过我们不再需要函数名了:</p><pre><code class="ts">const generateKeyError = new Error("Can't generate key from function argument")
// 基于函数参数生成唯一值
export default function generateKey(argument: any[]): string {
try{
return `${Array.from(argument).join(',')}`
}catch(_) {
throw generateKeyError
}
}</code></pre><p>虽然库本身可以基于函数参数提供唯一值,但是针对形形色色的不同业务来说,这肯定是不够用的,需要提供用户可以自定义参数序列化。</p><pre><code class="ts">// 如果配置中有 normalizer 函数,直接使用,否则使用默认函数
const normalizer = options?.normalizer ?? generateKey
return new Proxy<any>(fn, {
// @ts-ignore
cache,
apply(target, thisArg, argsList: any[]) {
const cache: Map<string, any> = (this as any).cache
// 根据格式化函数生成唯一数值
const cacheKey: string = normalizer(argsList);
if (!cache.has(cacheKey))
cache.set(cacheKey, target.apply(thisArg, argsList));
return cache.get(cacheKey);
}
});</code></pre><h3>添加 Promise 缓存</h3><p>在之前的博客中,提到缓存数据的弊端。同一时刻多次调用,会因为请求未返回而进行多次请求。所以我们也需要添加关于 Promise 的缓存。</p><pre><code class="ts">if (!currentCache.has(cacheKey)){
let result = target.apply(thisArg, argsList)
// 如果是 promise 则缓存 promise,简单判断!
// 如果当前函数有 then 则是 Promise
if (result?.then) {
result = Promise.resolve(result).catch(error => {
// 发生错误,删除当前 promise,否则会引发二次错误
// 由于异步,所以当前 delete 调用一定在 set 之后,
currentCache.delete(cacheKey)
// 把错误衍生出去
return Promise.reject(error)
})
}
currentCache.set(cacheKey, result);
}
return currentCache.get(cacheKey);</code></pre><p>此时,我们不但可以缓存数据,还可以缓存 Promise 数据请求。</p><h3>添加过期删除功能</h3><p>我们可以在数据中添加当前缓存时的时间戳,在生成数据时候添加。</p><pre><code class="ts">// 缓存项
export default class ExpiredCacheItem<V> {
data: V;
cacheTime: number;
constructor(data: V) {
this.data = data
// 添加系统时间戳
this.cacheTime = (new Date()).getTime()
}
}
// 编辑 Map 缓存中间层,判断是否过期
isOverTime(name: string) {
const data = this.cacheMap.get(name)
// 没有数据(因为当前保存的数据是 ExpiredCacheItem),所以我们统一看成功超时
if (!data) return true
// 获取系统当前时间戳
const currentTime = (new Date()).getTime()
// 获取当前时间与存储时间的过去的秒数
const overTime = currentTime - data.cacheTime
// 如果过去的秒数大于当前的超时时间,也返回 null 让其去服务端取数据
if (Math.abs(overTime) > this.timeout) {
// 此代码可以没有,不会出现问题,但是如果有此代码,再次进入该方法就可以减少判断。
this.cacheMap.delete(name)
return true
}
// 不超时
return false
}
// cache 函数有数据
has(name: string) {
// 直接判断在 cache 中是否超时
return !this.isOverTime(name)
}</code></pre><p>到达这一步,我们可以做到之前博客所描述的所有功能。不过,如果到这里就结束的话,太不过瘾了。我们继续学习其他库的功能来优化我的功能库。</p><h3>添加手动管理</h3><p>通常来说,这些缓存库都会有手动管理的功能,所以这里我也提供了手动管理缓存以便业务管理。这里我们使用 Proxy get 方法来拦截属性读取。</p><pre><code class="ts"> return new Proxy(fn, {
// @ts-ignore
cache,
get: (target: TargetFun<V>, property: string) => {
// 如果配置了手动管理
if (options?.manual) {
const manualTarget = getManualActionObjFormCache<V>(cache)
// 如果当前调用的函数在当前对象中,直接调用,没有的话访问原对象
// 即使当前函数有该属性或者方法也不考虑,谁让你配置了手动管理呢。
if (property in manualTarget) {
return manualTarget[property]
}
}
// 当前没有配置手动管理,直接访问原对象
return target[property]
},
}
export default function getManualActionObjFormCache<V>(
cache: MemoizeCache<V>
): CacheMap<string | object, V> {
const manualTarget = Object.create(null)
// 通过闭包添加 set get delete clear 等 cache 操作
manualTarget.set = (key: string | object, val: V) => cache.set(key, val)
manualTarget.get = (key: string | object) => cache.get(key)
manualTarget.delete = (key: string | object) => cache.delete(key)
manualTarget.clear = () => cache.clear!()
return manualTarget
}</code></pre><p>当前情况并不复杂,我们可以直接调用,复杂的情况下还是建议使用 <a href="https://link.segmentfault.com/?enc=69%2FgWWHnkBYnW4Tv2J174w%3D%3D.%2F7BSFaGa4IZOywsEqs8XlADKH6fq87%2BN8SkDTlGIoldpMXBilTLpNRY6R%2F9vG0%2Bc" rel="nofollow">Reflect</a> 。</p><h3>添加 WeakMap</h3><p>我们在使用 cache 时候,我们同时也可以提供 WeakMap ( WeakMap 没有 clear 和 size 方法),这里我提取了 BaseCache 基类。</p><pre><code class="ts">export default class BaseCache<V> {
readonly weak: boolean;
cacheMap: MemoizeCache<V>
constructor(weak: boolean = false) {
// 是否使用 weakMap
this.weak = weak
this.cacheMap = this.getMapOrWeakMapByOption()
}
// 根据配置获取 Map 或者 WeakMap
getMapOrWeakMapByOption<T>(): Map<string, T> | WeakMap<object, T> {
return this.weak ? new WeakMap<object, T>() : new Map<string, T>()
}
}</code></pre><p>之后,我添加各种类型的缓存类都以此为基类。</p><h3>添加清理函数</h3><p>在缓存进行删除时候需要对值进行清理,需要用户提供 dispose 函数。该类继承 BaseCache 同时提供 dispose 调用。</p><pre><code class="ts">export const defaultDispose: DisposeFun<any> = () => void 0
export default class BaseCacheWithDispose<V, WrapperV> extends BaseCache<WrapperV> {
readonly weak: boolean
readonly dispose: DisposeFun<V>
constructor(weak: boolean = false, dispose: DisposeFun<V> = defaultDispose) {
super(weak)
this.weak = weak
this.dispose = dispose
}
// 清理单个值(调用 delete 前调用)
disposeValue(value: V | undefined): void {
if (value) {
this.dispose(value)
}
}
// 清理所有值(调用 clear 方法前调用,如果当前 Map 具有迭代器)
disposeAllValue<V>(cacheMap: MemoizeCache<V>): void {
for (let mapValue of (cacheMap as any)) {
this.disposeValue(mapValue?.[1])
}
}
}</code></pre><p>当前的缓存如果是 WeakMap,是没有 clear 方法和迭代器的。个人想要添加中间层来完成这一切(还在考虑,目前没有做)。如果 WeakMap 调用 clear 方法时,我是直接提供新的 WeakMap 。</p><pre><code class="ts">clear() {
if (this.weak) {
this.cacheMap = this.getMapOrWeakMapByOption()
} else {
this.disposeAllValue(this.cacheMap)
this.cacheMap.clear!()
}
}</code></pre><h3>添加计数引用</h3><p>在学习其他库 <a href="https://link.segmentfault.com/?enc=9Cgojrl1DBQ9qWIMqQUbwQ%3D%3D.ClNsvmkkaXdlcoX2p4yel2tE%2FIwLdw7ca6nO4V8eybtHAn9u8%2BqPIy7Qed0YA72E" rel="nofollow">memoizee</a> 的过程中,我看到了如下用法:</p><pre><code class="js">memoized = memoize(fn, { refCounter: true });
memoized("foo", 3); // refs: 1
memoized("foo", 3); // Cache hit, refs: 2
memoized("foo", 3); // Cache hit, refs: 3
memoized.deleteRef("foo", 3); // refs: 2
memoized.deleteRef("foo", 3); // refs: 1
memoized.deleteRef("foo", 3); // refs: 0,清除 foo 的缓存
memoized("foo", 3); // Re-executed, refs: 1</code></pre><p>于是我有样学样,也添加了 RefCache。</p><pre><code class="ts">export default class RefCache<V> extends BaseCacheWithDispose<V, V> implements CacheMap<string | object, V> {
// 添加 ref 计数
cacheRef: MemoizeCache<number>
constructor(weak: boolean = false, dispose: DisposeFun<V> = () => void 0) {
super(weak, dispose)
// 根据配置生成 WeakMap 或者 Map
this.cacheRef = this.getMapOrWeakMapByOption<number>()
}
// get has clear 等相同。不列出
delete(key: string | object): boolean {
this.disposeValue(this.get(key))
this.cacheRef.delete(key)
this.cacheMap.delete(key)
return true;
}
set(key: string | object, value: V): this {
this.cacheMap.set(key, value)
// set 的同时添加 ref
this.addRef(key)
return this
}
// 也可以手动添加计数
addRef(key: string | object) {
if (!this.cacheMap.has(key)) {
return
}
const refCount: number | undefined = this.cacheRef.get(key)
this.cacheRef.set(key, (refCount ?? 0) + 1)
}
getRefCount(key: string | object) {
return this.cacheRef.get(key) ?? 0
}
deleteRef(key: string | object): boolean {
if (!this.cacheMap.has(key)) {
return false
}
const refCount: number = this.getRefCount(key)
if (refCount <= 0) {
return false
}
const currentRefCount = refCount - 1
// 如果当前 refCount 大于 0, 设置,否则清除
if (currentRefCount > 0) {
this.cacheRef.set(key, currentRefCount)
} else {
this.cacheRef.delete(key)
this.cacheMap.delete(key)
}
return true
}
}</code></pre><p>同时修改 proxy 主函数:</p><pre><code class="ts">if (!currentCache.has(cacheKey)) {
let result = target.apply(thisArg, argsList)
if (result?.then) {
result = Promise.resolve(result).catch(error => {
currentCache.delete(cacheKey)
return Promise.reject(error)
})
}
currentCache.set(cacheKey, result);
// 当前配置了 refCounter
} else if (options?.refCounter) {
// 如果被再次调用且当前已经缓存过了,直接增加
currentCache.addRef?.(cacheKey)
}
</code></pre><h3>添加 LRU</h3><p>LRU 的英文全称是 Least Recently Used,也即最不经常使用。相比于其他的数据结构进行缓存,LRU 无疑更加有效。</p><p>这里考虑在添加 maxAge 的同时也添加 max 值 (这里我利用两个 Map 来做 LRU,虽然会增加一定的内存消耗,但是性能更好)。</p><p>如果当前的此时保存的数据项等于 max ,我们直接把当前 cacheMap 设为 oldCacheMap,并重新 new cacheMap。</p><pre><code class="ts">set(key: string | object, value: V) {
const itemCache = new ExpiredCacheItem<V>(value)
// 如果之前有值,直接修改
this.cacheMap.has(key) ? this.cacheMap.set(key, itemCache) : this._set(key, itemCache);
return this
}
private _set(key: string | object, value: ExpiredCacheItem<V>) {
this.cacheMap.set(key, value);
this.size++;
if (this.size >= this.max) {
this.size = 0;
this.oldCacheMap = this.cacheMap;
this.cacheMap = this.getMapOrWeakMapByOption()
}
}</code></pre><p>重点在与获取数据时候,如果当前的 cacheMap 中有值且没有过期,直接返回,如果没有,就去 oldCacheMap 查找,如果有,删除老数据并放入新数据(使用 _set 方法),如果都没有,返回 undefined.</p><pre><code class="ts">get(key: string | object): V | undefined {
// 如果 cacheMap 有,返回 value
if (this.cacheMap.has(key)) {
const item = this.cacheMap.get(key);
return this.getItemValue(key, item!);
}
// 如果 oldCacheMap 里面有
if (this.oldCacheMap.has(key)) {
const item = this.oldCacheMap.get(key);
// 没有过期
if (!this.deleteIfExpired(key, item!)) {
// 移动到新的数据中并删除老数据
this.moveToRecent(key, item!);
return item!.data as V;
}
}
return undefined
}
private moveToRecent(key: string | object, item: ExpiredCacheItem<V>) {
// 老数据删除
this.oldCacheMap.delete(key);
// 新数据设定,重点!!!!如果当前设定的数据等于 max,清空 oldCacheMap,如此,数据不会超过 max
this._set(key, item);
}
private getItemValue(key: string | object, item: ExpiredCacheItem<V>): V | undefined {
// 如果当前设定了 maxAge 就查询,否则直接返回
return this.maxAge ? this.getOrDeleteIfExpired(key, item) : item?.data;
}
private getOrDeleteIfExpired(key: string | object, item: ExpiredCacheItem<V>): V | undefined {
const deleted = this.deleteIfExpired(key, item);
return !deleted ? item.data : undefined;
}
private deleteIfExpired(key: string | object, item: ExpiredCacheItem<V>) {
if (this.isOverTime(item)) {
return this.delete(key);
}
return false;
} </code></pre><h3>整理 memoize 函数</h3><p>事情到了这一步,我们就可以从之前的代码细节中解放出来了,看看基于这些功能所做出的接口与主函数。</p><pre><code class="ts">// 面向接口,无论后面还会不会增加其他类型的缓存类
export interface BaseCacheMap<K, V> {
delete(key: K): boolean;
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): this;
clear?(): void;
addRef?(key: K): void;
deleteRef?(key: K): boolean;
}
// 缓存配置
export interface MemoizeOptions<V> {
/** 序列化参数 */
normalizer?: (args: any[]) => string;
/** 是否使用 WeakMap */
weak?: boolean;
/** 最大毫秒数,过时删除 */
maxAge?: number;
/** 最大项数,超过删除 */
max?: number;
/** 手动管理内存 */
manual?: boolean;
/** 是否使用引用计数 */
refCounter?: boolean;
/** 缓存删除数据时期的回调 */
dispose?: DisposeFun<V>;
}
// 返回的函数(携带一系列方法)
export interface ResultFun<V> extends Function {
delete?(key: string | object): boolean;
get?(key: string | object): V | undefined;
has?(key: string | object): boolean;
set?(key: string | object, value: V): this;
clear?(): void;
deleteRef?(): void
}
</code></pre><p>最终的 memoize 函数其实和最开始的函数差不多,只做了 3 件事</p><ul><li>检查参数并抛出错误</li><li>根据参数获取合适的缓存</li><li>返回代理</li></ul><pre><code class="ts">export default function memoize<V>(fn: TargetFun<V>, options?: MemoizeOptions<V>): ResultFun<V> {
// 检查参数并抛出错误
checkOptionsThenThrowError<V>(options)
// 修正序列化函数
const normalizer = options?.normalizer ?? generateKey
let cache: MemoizeCache<V> = getCacheByOptions<V>(options)
// 返回代理
return new Proxy(fn, {
// @ts-ignore
cache,
get: (target: TargetFun<V>, property: string) => {
// 添加手动管理
if (options?.manual) {
const manualTarget = getManualActionObjFormCache<V>(cache)
if (property in manualTarget) {
return manualTarget[property]
}
}
return target[property]
},
apply(target, thisArg, argsList: any[]): V {
const currentCache: MemoizeCache<V> = (this as any).cache
const cacheKey: string | object = getKeyFromArguments(argsList, normalizer, options?.weak)
if (!currentCache.has(cacheKey)) {
let result = target.apply(thisArg, argsList)
if (result?.then) {
result = Promise.resolve(result).catch(error => {
currentCache.delete(cacheKey)
return Promise.reject(error)
})
}
currentCache.set(cacheKey, result);
} else if (options?.refCounter) {
currentCache.addRef?.(cacheKey)
}
return currentCache.get(cacheKey) as V;
}
}) as any
}</code></pre><p>完整代码在 <a href="https://link.segmentfault.com/?enc=Ohyrh3w1ORnVZdOWXGh6EA%3D%3D.qTu1CQ41LckGezcMTAWV%2F4i9kL2p1p%2B5N%2BUSbkYNvSrNN5chkPGiHFseT4JpAflF" rel="nofollow">memoizee-proxy</a> 中。大家自行操作与把玩。</p><h2>下一步</h2><h3>测试</h3><p>测试覆盖率不代表一切,但是在实现库的过程中,<a href="https://link.segmentfault.com/?enc=WWMs5j%2Ful2w%2F8sAkEJ1GOg%3D%3D.NwByA%2BcSRruo9M13XBjlbSU34XV1GzY%2Fv0%2FcqLiaRDk%3D" rel="nofollow">JEST</a> 测试库给我提供了大量的帮助,它帮助我重新思考每一个类以及每一个函数应该具有的功能与参数校验。之前的代码我总是在项目的主入口进行校验,对于每个类或者函数的参数没有深入思考。事实上,这个健壮性是不够的。因为你不能决定用户怎么使用你的库。</p><h3>Proxy 深入</h3><p>事实上,代理的应用场景是不可限量的。这一点,ruby 已经验证过了(可以去学习《ruby 元编程》)。</p><p>开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。</p><p>当然,Proxy 虽然来自于 ES6 ,但该 API 仍需要较高的浏览器版本,虽然有 <a href="https://link.segmentfault.com/?enc=Drxt4iC7K4dpaNdFQNMItg%3D%3D.Pb4%2F6jk4ZGLm5%2Fi56MWzBU5OY2laHeH37pqzE3NaOvVQrCUik4JImEGFSNcopNgs" rel="nofollow">proxy-pollfill</a> ,但毕竟提供功能有限。不过已经 2021,相信深入学习 Proxy 也是时机了。</p><h3>深入缓存</h3><p>缓存是有害的!这一点毋庸置疑。但是它实在太快了!所以我们要更加理解业务,哪些数据需要缓存,理解那些数据可以使用缓存。</p><p>当前书写的缓存仅仅只是针对与一个方法,之后写的项目是否可以更细粒度的结合返回数据?还是更往上思考,写出一套缓存层?</p><h3>小步开发</h3><p>在开发该项目的过程中,我采用小步快跑的方式,不断返工。最开始的代码,也仅仅只到了添加过期删除功能那一步。 </p><p>但是当我每次完成一个新的功能后,重新开始整理库的逻辑与流程,争取每一次的代码都足够优雅。同时因为我不具备第一次编写就能通盘考虑的能力。不过希望在今后的工作中,不断进步。这样也能减少代码的返工。</p><h2>其他</h2><h3>函数创建</h3><p>事实上,我在为当前库添加手动管理时候,考虑过直接复制函数,因为函数本身是一个对象。同时为当前函数添加 set 等方法。但是没有办法把作用域链拷贝过去。</p><p>虽然没能成功,但是也学到了一些知识,这里也提供两个创建函数的代码。</p><p>我们在创建函数时候基本上会利用 new Function 创建函数,但是浏览器没有提供可以直接创建异步函数的构造器,我们需要手动获取。</p><pre><code class="ts">AsyncFunction = (async x => x).constructor
foo = new AsyncFunction('x, y, p', 'return x + y + await p')
foo(1,2, Promise.resolve(3)).then(console.log) // 6</code></pre><p>对于全局函数,我们也可以直接 fn.toString() 来创建函数,这时候异步函数也可以直接构造的。</p><pre><code class="ts">function cloneFunction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
return new Function('return '+ fn.toString())();
}</code></pre><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=as%2Bq1n%2BFlpFJreNBKwVkxQ%3D%3D.EvWitCy3r9VqAHTRi7VbMxQZGcXJID7qEbJk9hUwKXhsW7IYXWt9Qg%2BQgWg2P2qs" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=liWQ%2F2xV9ZfSib%2FiBI3F0w%3D%3D.xpHVxlvfwAZyinfIoqFiw8Vlf08gz2Yv5UHrMLFuRXQJHECzOkRihdFycukTVadu" rel="nofollow">前端 api 请求缓存方案</a></p><p><a href="https://link.segmentfault.com/?enc=m2bZWQrHS1plf81MfQOp2A%3D%3D.9SFXwWJ8KaVTZWxvVU4VKFAkKQHx14gyZVMuEWIHPRnF2wqxxRAHUTGv3%2BkT8kQ4" rel="nofollow">ECMAScript 6 入门 代理篇</a></p><p><a href="https://link.segmentfault.com/?enc=XObOMaqKEQrgyg1CtgNbRQ%3D%3D.ZTAykjSLIxY4pp6qYlPFVUzMXMCy2%2BxdZ6pelnx4OlyYlh1PSTBsm3eBrYbhc7xe" rel="nofollow">memoizee</a> </p><p><a href="https://link.segmentfault.com/?enc=RlL5axndBoJY1xEku1ycVw%3D%3D.L2TMKOEE%2FFY5D4ex30puZuVogM5CZZ7BvLje%2B5gByLwj48%2F9wblyGBI3CS2zdXv1" rel="nofollow">memoizee-proxy</a></p>
聊聊不可变数据结构
https://segmentfault.com/a/1190000039071447
2021-01-25T08:45:00+08:00
2021-01-25T08:45:00+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<p>三年前,我接触了 <a href="https://link.segmentfault.com/?enc=h1WGcAOrLyGx7QdtRM6KLw%3D%3D.YuHt36YJgwoLYRCEzYgzZRQ2le%2BB4MAHl7LrDw9B2vHTKoFPHfhe8N0Up%2Br4%2Fjee" rel="nofollow">Immutable</a> 库,体会到了不可变数据结构的利好。</p><p>Immutable 库具有两个最大的优势: 不可修改以及结构共享。</p><ul><li>不可修改(容易回溯,易于观察。减少错误的发生)</li></ul><pre><code class="ts">let obj = { a: 1 };
handleChange(obj);
// 由于上面有 handleChange,无法确认 obj 此时的状态
console.log(obj)</code></pre><ul><li>结构共享( 复用内存,节省空间,也就意味着数据修改可以直接记录完整数据,其内存压力也不大,这样对于开发复杂交互项目的重做等功能很有用)</li></ul><p><img src="/img/remote/1460000039071449" alt="" title=""></p><p>当然,由于当时还在重度使用 Vue 进行开发,而且 受益于 Vue 本身的优化以及业务抽象和系统的合理架构,项目一直保持着良好的性能。同时该库的侵入性和难度都很大,贸然引入项目也未必是一件好事。</p><p>虽然 Immutable 库没有带来直接的收益,但从中学到一些思路和优化却陪伴着我。</p><h2>浅拷贝 assign 胜任 Immutable</h2><p>当我们不使用任何库,我们是否就无法享受不可变数据的利好?答案是否定的。</p><p>当面临可变性数据时候,大部分情况下我们会使用深拷贝来解决两个数据引用的问题。</p><pre><code class="ts">const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);</code></pre><p>不幸的是,深度拷贝是昂贵的,在有些情况下更是不可接受的。深拷贝占用了大量的时间,同时两者之间没有任何结构共享。但我们可以通过仅复制需要更改的对象和重用未更改的对象来减轻这种情况。如 Object.assign 或者 ... 来实现结构共享。</p><p>大多数业务开发中,我们都是先进行深拷贝,再进行修改。但是我们真的需要这样做吗?事实并非如此。从项目整体出发的话,我们只需要解决一个核心问题 “深层嵌套对象”。当然,这并不意味着我们把所有的数据都放在第一层。只需要不嵌套可变的数据项即可。</p><pre><code>const staffA = {
name: 'xx',
gender: 'man',
company: {},
authority: []
}
const staffB = {...staffA}
staffB.name = 'YY'
// 不涉及到 复杂类型的修改即可
staffA.name // => 'xx'
const staffsA = [staffA, staffB]
// 需要对数组内部每一项进行浅拷贝
const staffsB = staffsA.map(x => ({...x}))
staffsB[0].name = 'gg'
staffsA[0].name // => 'xx'</code></pre><p>如此,我们就把深拷贝变为了浅拷贝。同时实现了结构共享 (所有深度嵌套对象都被复用了) 。但有些情况下,数据模型并不是容易修改的,我们还是需要修改深度嵌套对象。那么就需要这样修改了。</p><pre><code class="ts">const newData = Object.assign({}, myData, {
x: Object.assign({}, myData.x, {
y: Object.assign({}, myData.x.y, {z: 7}),
}),
a: Object.assign({}, myData.a, {b: myData.a.b.concat(9)})
});</code></pre><p>这对于绝大部份的业务场景来说是相当高效的(因为它只是浅拷贝,并重用了其余的部分) ,但是编写起来却非常痛苦。</p><h2>immutability-helper 库辅助开发</h2><p><a href="https://link.segmentfault.com/?enc=NMR2Su2D%2BqSaEXNKMaWj9g%3D%3D.kJL8fCphj9uUwrLroLxHEnJJ9BJoLYn%2BzSHfTPGuiakQ4nKE0nb5rXgvQYkAiO8K" rel="nofollow">immutability-helper</a> (语法受到了 MongoDB 查询语言的启发 ) 这个库为 Object.assign 方案提供了简单的语法糖,使得编写浅拷贝代码更加容易:</p><pre><code class="ts">import update from 'immutability-helper';
const newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
const initialArray = [1, 2, 3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]
initialArray // => [1, 2, 3]</code></pre><h3>可用命令</h3><ul><li>$push (类似于数组的 push,但是提供的是数组)</li><li>$unshift (类似于数组的 unshift,但是提供的是数组)</li><li>$splice (类似于数组的 splice, 但提供数组是一个数组, $splice: [ [1, 1, 13, 14] ] )</li></ul><p>注意:<em>数组中的项目是顺序应用的,因此顺序很重要。目标的索引可能会在操作过程中发生变化。</em></p><ul><li>$toggle (字符串数组,切换目标对象的布尔数值)</li><li>$set (完全替换目标节点, 不考虑之前的数据,只用当前指令设置的数据)</li><li>$unset (字符串数组,移除 key 值(数组或者对象移除))</li><li>$merge (合并对象)</li></ul><pre><code class="ts">const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}</code></pre><ul><li>$add(为 Map 添加 [key,value] 数组)</li><li>$remove (字符串对象,为 Map 移除 key)</li><li>$apply (应用函数到节点)</li></ul><pre><code class="ts">const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
const newObj2 = update(obj, {b: {$set: obj.b * 2}});
// => {a: 5, b: 6}</code></pre><p>后面我们解析源码时,可以看到不同指令的实现。</p><h3>扩展命令</h3><p>我们可以基于当前业务去扩展命令。如添加税值计算:</p><pre><code class="ts">import update, { extend } from 'immutability-helper';
extend('$addtax', function(tax, original) {
return original + (tax * original);
});
const state = { price: 123 };
const withTax = update(state, {
price: {$addtax: 0.8},
});
assert(JSON.stringify(withTax) === JSON.stringify({ price: 221.4 }));</code></pre><p>如果您不想弄脏全局的 <code>update</code> 函数,可以制作一个副本并使用该副本,这样不会影响全局数据:</p><pre><code class="ts">import { Context } from 'immutability-helper';
const myContext = new Context();
myContext.extend('$foo', function(value, original) {
return 'foo!';
});
myContext.update(/* args */);</code></pre><h3>源码解析</h3><p>为了加强理解,这里我来解析一下源代码,同时该库代码十分简洁强大:</p><p>先是工具函数(保留核心,环境判断,错误警告等逻辑去除):</p><pre><code class="ts">// 提取函数,大量使用时有一定性能优势,且简明(更重要)
const hasOwnProperty = Object.prototype.hasOwnProperty;
const splice = Array.prototype.splice;
const toString = Object.prototype.toString;
// 检查类型
function type<T>(obj: T) {
return (toString.call(obj) as string).slice(8, -1);
}
// 浅拷贝,使用 Object.assign
const assign = Object.assign || /* istanbul ignore next */ (<T, S>(target: T & any, source: S & Record<string, any>) => {
getAllKeys(source).forEach(key => {
if (hasOwnProperty.call(source, key)) {
target[key] = source[key] ;
}
});
return target as T & S;
});
// 获取对象 key
const getAllKeys = typeof Object.getOwnPropertySymbols === 'function'
? (obj: Record<string, any>) => Object.keys(obj).concat(Object.getOwnPropertySymbols(obj) as any)
/* istanbul ignore next */
: (obj: Record<string, any>) => Object.keys(obj);
// 所有数据的浅拷贝
function copy<T, U, K, V, X>(
object: T extends ReadonlyArray<U>
? ReadonlyArray<U>
: T extends Map<K, V>
? Map<K, V>
: T extends Set<X>
? Set<X>
: T extends object
? T
: any,
) {
return Array.isArray(object)
? assign(object.constructor(object.length), object)
: (type(object) === 'Map')
? new Map(object as Map<K, V>)
: (type(object) === 'Set')
? new Set(object as Set<X>)
: (object && typeof object === 'object')
? assign(Object.create(Object.getPrototypeOf(object)), object) as T
/* istanbul ignore next */
: object as T;
}
</code></pre><p>然后是核心代码(同样保留核心) :</p><pre><code class="ts">export class Context {
// 导入所有指令
private commands: Record<string, any> = assign({}, defaultCommands);
// 添加扩展指令
public extend<T>(directive: string, fn: (param: any, old: T) => T) {
this.commands[directive] = fn;
}
// 功能核心
public update<T, C extends CustomCommands<object> = never>(
object: T,
$spec: Spec<T, C>,
): T {
// 增强健壮性,如果操作命令是函数,修改为 $apply
const spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec;
// 数组(数组) 检查,报错
// 返回对象(数组)
let nextObject = object;
// 遍历指令
getAllKeys(spec).forEach((key: string) => {
// 如果指令在指令集中
if (hasOwnProperty.call(this.commands, key)) {
// 性能优化,遍历过程中,如果 object 还是当前之前数据
const objectWasNextObject = object === nextObject;
// 用指令修改对象
nextObject = this.commands[key]((spec as any)[key], nextObject, spec, object);
// 修改后,两者使用传入函数计算,还是相等的情况下,直接使用之前数据
if (objectWasNextObject && this.isEquals(nextObject, object)) {
nextObject = object;
}
} else {
// 不在指令集中,做其他操作
// 类似于 update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}});
// 解析对象规则后继续递归调用 update, 不断递归,不断返回
// ...
}
});
return nextObject;
}
}</code></pre><p>最后是通用指令:</p><pre><code class="ts">const defaultCommands = {
$push(value: any, nextObject: any, spec: any) {
// 数组添加,返回 concat 新数组
return value.length ? nextObject.concat(value) : nextObject;
},
$unshift(value: any, nextObject: any, spec: any) {
return value.length ? value.concat(nextObject) : nextObject;
},
$splice(value: any, nextObject: any, spec: any, originalObject: any) {
// 循环 splice 调用
value.forEach((args: any) => {
if (nextObject === originalObject && args.length) {
nextObject = copy(originalObject);
}
splice.apply(nextObject, args);
});
return nextObject;
},
$set(value: any, _nextObject: any, spec: any) {
// 直接替换当前数值
return value;
},
$toggle(targets: any, nextObject: any) {
const nextObjectCopy = targets.length ? copy(nextObject) : nextObject;
// 当前对象或者数组切换
targets.forEach((target: any) => {
nextObjectCopy[target] = !nextObject[target];
});
return nextObjectCopy;
},
$unset(value: any, nextObject: any, _spec: any, originalObject: any) {
// 拷贝后循环删除
value.forEach((key: any) => {
if (Object.hasOwnProperty.call(nextObject, key)) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
delete nextObject[key];
}
});
return nextObject;
},
$add(values: any, nextObject: any, _spec: any, originalObject: any) {
if (type(nextObject) === 'Map') {
values.forEach(([key, value]) => {
if (nextObject === originalObject && nextObject.get(key) !== value) {
nextObject = copy(originalObject);
}
nextObject.set(key, value);
});
} else {
values.forEach((value: any) => {
if (nextObject === originalObject && !nextObject.has(value)) {
nextObject = copy(originalObject);
}
nextObject.add(value);
});
}
return nextObject;
},
$remove(value: any, nextObject: any, _spec: any, originalObject: any) {
value.forEach((key: any) => {
if (nextObject === originalObject && nextObject.has(key)) {
nextObject = copy(originalObject);
}
nextObject.delete(key);
});
return nextObject;
},
$merge(value: any, nextObject: any, _spec: any, originalObject: any) {
getAllKeys(value).forEach((key: any) => {
if (value[key] !== nextObject[key]) {
if (nextObject === originalObject) {
nextObject = copy(originalObject);
}
nextObject[key] = value[key];
}
});
return nextObject;
},
$apply(value: any, original: any) {
// 传入函数,直接调用函数修改
return value(original);
},
};</code></pre><p>就这样,作者写了一个简洁而强大的浅拷贝辅助库。</p><h2>优秀的 Immer 库</h2><p><a href="https://link.segmentfault.com/?enc=h%2Br8NkV6feEsx7SBcndkRA%3D%3D.Zjjwdz%2FUsFs5Ba7MhImv1FS5StKxHGXUcJkr54Vggdk5OJyKV7cvWHKteHXXgk%2BH6eL%2B1n1Oz6pjjIsDZQZ%2Bmg%3D%3D" rel="nofollow">Immer</a> 是一个非常优秀的不可变数据库,利用 proxy 来解决问题。不需要学习其他 api,开箱即用 ( gzipped 3kb )</p><pre><code class="ts">import produce from "immer"
const baseState = [
{
todo: "Learn typescript",
done: true
},
{
todo: "Try immer",
done: false
}
]
// 直接修改,没有任何开发负担,心情美美哒
const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})</code></pre><p>关于 immer 性能优化请参考 <a href="https://link.segmentfault.com/?enc=hVwKFSKPzdePcy9IPNcVCg%3D%3D.J3GyiPfpBuEb9M1rqnw7dRAtTdsTcXTB94lcz676j815zWAdL2baRHlGsIYJ1qKj%2Fd6pjikVDRHa0VnDpn3%2FNA%3D%3D" rel="nofollow">immer performance</a>。</p><h3>核心代码分析</h3><p>该库的核心还是在 proxy 的封装,所以不全部介绍,仅介绍代理功能。</p><pre><code class="ts">export const objectTraps: ProxyHandler<ProxyState> = {
get(state, prop) {
// PROXY_STATE是一个symbol值,有两个作用,一是便于判断对象是不是已经代理过,二是帮助proxy拿到对应state的值
// 如果对象没有代理过,直接返回
if (prop === DRAFT_STATE) return state
// 获取数据的备份?如果有,否则获取元数据
const source = latest(state)
// 如果当前数据不存在,获取原型上数据
if (!has(source, prop)) {
return readPropFromProto(state, source, prop)
}
const value = source[prop]
// 当前代理对象已经改回了数值或者改数据是 null,直接返回
if (state.finalized_ || !isDraftable(value)) {
return value
}
// 创建代理数据
if (value === peek(state.base_, prop)) {
prepareCopy(state)
return (state.copy_![prop as any] = createProxy(
state.scope_.immer_,
value,
state
))
}
return value
},
// 当前数据是否有该属性
has(state, prop) {
return prop in latest(state)
},
set(
state: ProxyObjectState,
prop: string /* strictly not, but helps TS */,
value
) {
const desc = getDescriptorFromProto(latest(state), prop)
// 如果当前有 set 属性,意味当前操作项是代理,直接设置即可
if (desc?.set) {
desc.set.call(state.draft_, value)
return true
}
// 当前没有修改过,建立副本 copy,等待使用 get 时创建代理
if (!state.modified_) {
const current = peek(latest(state), prop)
const currentState: ProxyObjectState = current?.[DRAFT_STATE]
if (currentState && currentState.base_ === value) {
state.copy_![prop] = value
state.assigned_[prop] = false
return true
}
if (is(value, current) && (value !== undefined || has(state.base_, prop)))
return true
prepareCopy(state)
markChanged(state)
}
state.copy_![prop] = value
state.assigned_[prop] = true
return true
},
defineProperty() {
die(11)
},
getPrototypeOf(state) {
return Object.getPrototypeOf(state.base_)
},
setPrototypeOf() {
die(12)
}
}
// 数组的代理,把当前对象的代理拷贝过去,再修改 deleteProperty 和 set
const arrayTraps: ProxyHandler<[ProxyArrayState]> = {}
each(objectTraps, (key, fn) => {
// @ts-ignore
arrayTraps[key] = function() {
arguments[0] = arguments[0][0]
return fn.apply(this, arguments)
}
})
arrayTraps.deleteProperty = function(state, prop) {
if (__DEV__ && isNaN(parseInt(prop as any))) die(13)
return objectTraps.deleteProperty!.call(this, state[0], prop)
}
arrayTraps.set = function(state, prop, value) {
if (__DEV__ && prop !== "length" && isNaN(parseInt(prop as any))) die(14)
return objectTraps.set!.call(this, state[0], prop, value, state[0])
}</code></pre><h3>其他</h3><p>开发过程中,我们往往会在 React 函数中使用 useReducer 方法,但是 useReducer 实现较为复杂,我们可以用 <a href="https://link.segmentfault.com/?enc=4zlIcXkmixa7hcG5nJi61w%3D%3D.gtaH75IaBnKD1UVv6QaqRIA9HQ6ijmZl5bKZTpDUDDCCkUh%2FU28xPj1lLdfiVIS%2F" rel="nofollow">useMethods</a> 简化代码。useMethods 内部就是使用 immer (代码十分简单,我们直接拷贝 index.ts 即可)。</p><p>不使用 useMethods 情况下:</p><pre><code class="ts">const initialState = {
nextId: 0,
counters: []
};
const reducer = (state, action) => {
let { nextId, counters } = state;
const replaceCount = (id, transform) => {
const index = counters.findIndex(counter => counter.id === id);
const counter = counters[index];
return {
...state,
counters: [
...counters.slice(0, index),
{ ...counter, count: transform(counter.count) },
...counters.slice(index + 1)
]
};
};
switch (action.type) {
case "ADD_COUNTER": {
nextId = nextId + 1;
return {
nextId,
counters: [...counters, { id: nextId, count: 0 }]
};
}
case "INCREMENT_COUNTER": {
return replaceCount(action.id, count => count + 1);
}
case "RESET_COUNTER": {
return replaceCount(action.id, () => 0);
}
}
};</code></pre><p>对比使用 useMethods :</p><pre><code class="ts">import useMethods from 'use-methods';
const initialState = {
nextId: 0,
counters: []
};
const methods = state => {
const getCounter = id => state.counters.find(counter => counter.id === id);
return {
addCounter() {
state.counters.push({ id: state.nextId++, count: 0 });
},
incrementCounter(id) {
getCounter(id).count++;
},
resetCounter(id) {
getCounter(id).count = 0;
}
};
};</code></pre><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=Mkn46u1z2uSb8FmNSRsRoA%3D%3D.Y2uJcSDncGLIlDh16mAxhXt%2FOpn9yWtqX6lUqjMZUm0MCqY3DJJOdt6w0csSOIlZ" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=m2wxZto5JMUsjg97s6v13g%3D%3D.YkCNiUs0HwKCiP18szvqHUHGHhemTjYj7L9oa5278fqNcafVZYOviIDJ7Y8tGTV3" rel="nofollow">immutability-helper</a></p><p><a href="https://link.segmentfault.com/?enc=ZTPyfSRBplRqkEIxsaeSsA%3D%3D.GESWuFep3LuKzuR6wnbF3xfWmUADxECQV1Abm13Jrm%2Fh0Y2q1bZZ1NNxrkR41qbv3GKjT4PJbQ1SVEBHzy8vKA%3D%3D" rel="nofollow">Immer</a> </p><p><a href="https://link.segmentfault.com/?enc=3RxTSwbeJK4Abs9P150%2FfA%3D%3D.nD1Cd3pRye9EEMnJ9ssqVc0al7lIM%2BDT0JauiFffj3XeTi6%2FwklrK0EQK6Hty%2Fc6" rel="nofollow">useMethods</a></p>
使用 AVIF 图片格式
https://segmentfault.com/a/1190000038961291
2021-01-13T01:36:15+08:00
2021-01-13T01:36:15+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<blockquote>文字需要翻译,图片不用。在图片的世界,不管是中国人、印度人、美国人、英国人的笑,全世界的人都能明白那是在笑。图片所承载的情感是全球通明的。</blockquote><p>众所周知,一图胜千言,图片对于视觉的冲击效果远大于文字。但对于我们的互联网而言,传输与解析一张图片的代价要远比"千言"大的多的多(目前上亿像素已经成为主流)。</p><p>面对动辄 10 多 M 的大型图片,使用优化的图像来节省带宽和加载时间无疑是性能优化中的重头戏,无论对于用户还是公司都有巨大的意义。因为对于用户来说,可以更早的看到图片,对于公司而言,更加省钱。</p><p>在不使用用户提供的图片时,最简单就可以使用 <a href="https://link.segmentfault.com/?enc=AT7mD1AuD%2F6niwjeQDA5jg%3D%3D.jdyrJIxBheHHg4Pm90tTxa%2BQkPtHSc5P3fNta2zjp9c%3D" rel="nofollow">tinypng</a> 网站针对各个图片进行图像压缩与优化。在减少了近 50% 大小的同时做到了肉眼无法区分,收益是非常大的。</p><h2>AVIF 介绍</h2><p>当然,目前最值得关注的新型图片格式是 AVIF(AV1 Image File Format,AV1图像文件格式,是业界最新的开源视频编码格式AV1的关键帧衍生而来的一种新的图像格式。AVIF 来自于 Netflix(著名的流媒体影视公司), 在 2020 年情人节公布。</p><p>当遇到新的技术时候,我们总是要考虑兼容问题,话不多说,我们打开 <a href="https://link.segmentfault.com/?enc=xffRbEZUsvOk%2Bz1g4gZoTw%3D%3D.ph5y3MBytmUZfsmhC98M6iVa6X8ETP%2FEcX70NS264UV7x5ZEGk9ze9EcGg%2BQcVfL" rel="nofollow">caniuse</a> 。</p><p><img src="/img/bVcNDL3" alt="image" title="image"></p><p>就这?就这?是的,虽然当前的浏览器支持情况堪忧,但是开发者为了浏览器提供了 4kb 的 polyfill:</p><p>在使用 <a href="https://link.segmentfault.com/?enc=k%2F4zT%2BdDz3brcUHZOEJBBg%3D%3D.cWvrIxi5OIBbUQoG1y6zw6Un68Wv3JLnJhfRMSeDi5wM81qfIz5EiomaJeyxE4oJ" rel="nofollow">avif</a> 后,我们可以使用的浏览器版本:</p><ul><li>Chrome 57+</li><li>Firefox 53+</li><li>Edge 17+</li><li>Safari 11+</li></ul><p>该格式的优势在于:</p><ul><li>权威 <br>AVIF 图片格式由开源组织 AOMedia 开发,Netflix、Google 与 Apple 均是该组织的成员, 所以该格式的未来也是非常明朗的。</li><li>压缩能力强 <br>在对比中发现 AVIF 图片格式压缩很棒,基本上大小比 JPEG 小 10 倍左右且具有相同的图像质量。</li><li>polyfill <br>面对之前浏览器无力情况提供 polyfill,为当前状况下提供了可用性</li></ul><p>如果是技术性网站或某些 Saas 产品就可以尝试使用。</p><h2>使用 Sharp 生成 AVIF</h2><p><a href="https://link.segmentfault.com/?enc=yFhSIb00boLt9xBfRDFMIQ%3D%3D.VoOC5W6eqs6vCFtH6gld8iWQr20kZd7VNzdnT6s9lEEVUnnYtNQWYaaz41Fna6U0" rel="nofollow">Sharp</a> 是一个转换格式的 node 工具库, 最近该库提供了对 AVIF 的支持。</p><p>我们可以在 node 中这样使用:</p><pre><code class="js">const sharp = require("sharp");
const fs = require("fs");
fs.readFile("xxx.jpeg", (err, inputBuffer) => {
if (err) {
console.error(err);
return;
}
// WebP
sharp(inputBuffer)
.webp({ quality: 50, speed: 1 })
.toFile("xxx.webp");
// AVIF 转换, 速度很慢
sharp(inputBuffer)
.avif({quality: 50, speed: 1})
.toFile("xxx.avif");
});</code></pre><p>在后端传入 jpg,png 等通用格式,这样我们便可以在浏览器中直接使用 AVIF。</p><p>虽然 AVIF 是面向未来的图片格式,但是就目前来说,在开发需要大量图片的业务时,使用专业的 OSS 服务和 CDN 才是更好的选择。 </p><p>由于 OSS 服务支持jpg、png、bmp、gif、webp、tiff等格式的转换,以及缩略图、剪裁、水印、缩放等多种操作,这样就可以更简单的根据不同设备(分辨率)提供不同的图片。同时 CDN 也可以让用户更快的获取图片。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=UF3y09cw99LFCSlH%2BpHKeA%3D%3D.Am8nAo11JTbBohGWY0IYIZHG2x%2B5iSLoknRAJnqs5A4%2FnGmP%2BSjOH3fcroENTcBN" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=SQWVIUQ5Mnd06LLqhQv5Bg%3D%3D.A3MCW3suNSHzJEOXH%2F9hxEuDIrr0t6RrwXyMaSRoJuYdoBes2FbCFlBhgrUJvv%2Bv" rel="nofollow">node-avif</a></p><p><a href="https://link.segmentfault.com/?enc=pnyb%2BIkIIQyrWRhNkTb06w%3D%3D.4kJc%2FqZ6UuDVnvRBP%2F%2FU%2BCAqmUYme89CoqjrOKHKRlc%3D" rel="nofollow">tinypng</a></p><p><a href="https://link.segmentfault.com/?enc=kG4QCCihFFf2y%2BrMNkE7jQ%3D%3D.brgLazQmbwWS949gjh4ZnfjWyFhm6qkaoJrALCMeQQfOzqVYDSpyjQDPU5t20wno" rel="nofollow">Sharp</a></p>
组织和管理 CSS
https://segmentfault.com/a/1190000038419764
2020-12-08T23:50:20+08:00
2020-12-08T23:50:20+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<p>在项目开发的过程中,基于有限的时间内保质保量的完成开发任务无疑是一场挑战。在这场挑战中我们不但要快速处理自己的问题,还需要与别人协同合作,以避免两者之间的冲突。</p><p>针对于大型项目的开发,CSS 如何组织和管理才能让我们用更少的时间写出更强大的功能。这篇文章来表述一些我对 CSS 组织和管理的理解。当然,对于 ToC(面向个人) 应用,出于细节和动画的把控。再加上这种网页生命周期较短,往往复用性较差,但是针对于 ToB(面向企业) 应用,统一风格往往会赢得客户的青睐。行列间距,主题样式等都应该结合统一,而不是每个页面不同设计。基于此,我们需要组织与管理我们的 css,而不仅仅只是是靠 css in js 来为每个组件单独设计。</p><h2>BEM 命名约定</h2><p>BEM 是一种相当知名的命名约定,BEM 的意思就是块(block)、元素(element)、修饰符(modifier),是由 <a href="https://link.segmentfault.com/?enc=idXiZtXyajqQTqj0t3w6Yw%3D%3D.cJk0Y5Y3djypPygCn6IUdSSKI7lYZnb9EKIObzqtri0%3D" rel="nofollow">Yandex</a> 团队提出的一种前端命名方法论。这种巧妙的命名方法让你的CSS类对其他开发者来说更加透明而且更有意义。BEM 命名约定更加严格,而且包含更多的信息,它用于一个团队开发一个耗时的大项目。</p><p>如 我们在书写伙伴卡片组件 代码风格如下:</p><pre><code class="css">.partner-card {
}
.partner-card__name {
}
.partner-card__content {
}
.partner-card__content {
}
.partner-card__content--md {
}</code></pre><p>根据上述代码,我们很容易看出当前开发的意图,同时也很难遇到代码重复的问题。当然,我们可以利用 <a href="https://link.segmentfault.com/?enc=AeI7nVTIISykGhZuFkrekw%3D%3D.Dyeiy8X8yPrSyyy%2FnsX%2BVHgL8jMUF7AmIwqyUejrBKk%3D" rel="nofollow">Less</a>、<a href="https://link.segmentfault.com/?enc=4qSxbtyEaloSIJXU%2FFnxbQ%3D%3D.TJVdmmk6S4QgWpktA1myzpVUAuBQ3zWppM11LEqzCW8%3D" rel="nofollow">Sass</a>、<a href="https://link.segmentfault.com/?enc=IcW9ysYmn46ESNW7gaVQ3Q%3D%3D.%2FFhdZoX67Big4bJsISvXEQx%2F3qT7Y4UYRLgcZvAbuyI%3D" rel="nofollow">Stylus</a> 这些 css 处理器辅助开发,这里不再赘述。</p><p>计算科学中最难的两件事之一就是命名,日常开发中如果没有一些约定,就很容易发生命名冲突,BEM 恰恰解决了这一痛点,我们只需要外层样式名是一个有意义且独立唯一,就无需考虑太多。</p><p>与 BEM 相对应的还有 OOCSS SMACSS。而这两种不是直接可见的命名约定,而是提供了一系列的目标规则。这里不再详细阐述,大家如果想要了解,可以去看一看 <a href="https://link.segmentfault.com/?enc=NRzmLcalw3lKutA%2F4DBPEg%3D%3D.RKp04p8hgZblYhRP3On9i1RAkT7Pf6LtJCPz3WCVTvIMzCbw5nnDaGcYInyDaKXzmbLVKAkNgacM5qoAdspVLA%3D%3D" rel="nofollow">值得参考的css理论:OOCSS、SMACSS与BEM</a>。当然了,真正的组织与管理必然也离不开这些目标规则。</p><p>同时,使用 BEM 而不是 CSS 后代选择器进行项目也会有一定性能优势(可以忽略不计),这是因为浏览器解析 css 时是逆向解析,之前对 css 解析有一定误区,由于书写是从前往后,所以认为解析也一定是从前往后,但是大部分情况下,css 解析都是从后往前。</p><p>如果规则正向解析,例如「div div p em」,我们首先就要检查当前元素到 html 的整条路径,找到最上层的 div,再往下找,如果遇到不匹配就必须回到最上层那个 div,往下再去匹配选择器中的第一个 div,<strong>回溯</strong>若干次才能确定匹配与否,效率很低。逆向匹配则不同,如果当前的 DOM 元素是 div,而不是 selector 最后的 em 元素,那只要<strong>一步</strong>就能排除。只有在匹配时,才会不断向上找父节点进行验证与过滤。无需回溯,效率较高。。</p><h2>Atomic CSS</h2><p><a href="https://link.segmentfault.com/?enc=IdyLYiO85BmXopHbkenaqA%3D%3D.SPGyykLIrE1QbBPb4uaLmf7WcoH68Q%2FbYm6vr7NBNT0%3D" rel="nofollow">ACSS</a> 表示的是原子化 CSS(Atomic CSS),是 Yahoo 提出来的一种独特的 CSS 代码组织方式,应用在 Yahoo 首页和其他产品中。ACSS 的独特性在于它的理念与一般开发人员的理解有很大的不同,并挑战了传统意义上编写 CSS 的最佳实践,也就是关注点分离原则。ACSS 认为关注点分离原则会导致冗余、繁琐和难以维护的 CSS 代码。</p><p>代码风格如下所示:</p><pre><code class="css">/** 背景色 颜色 内边距 */
<div class="Bgc(#0280ae.5) C(#fff) P(20px)">
Lorem ipsum
</div>
<div>
<div class="Bgc(#0280ae.5) H(90px) IbBox W(50%) foo_W(100%)"></div>
<div class="Bgc(#0280ae) H(90px) IbBox W(50%) foo_W(100%)"></div>
</div>
<hr>
<div class="foo">
<div class="Bgc(#0280ae.5) H(90px) IbBox W(50%) foo_W(100%)"></div>
<div class="Bgc(#0280ae) H(90px) IbBox W(50%) foo_W(100%)"></div>
</div></code></pre><p>计算科学中最难的两件事之一就是命名,而 Atomic CSS 直接舍弃了命名。一个类只做一件事。yahoo 利用这种方案减轻了很多代码。</p><p>很多人会抗拒 Atomic CSS,原因在于需要记住一堆的类名,感觉会看起来比较乱。但是事实上,你不需要考虑它的结构等因素,而是它需要什么样式就直接提供好了。把脑力运动变成机械的体力运动。</p><p>原子 CSS 的优势的确有很多:</p><ul><li>变化是可以预见的由于单一职责原则(一个类==一种样式),很容易预测删除或添加一个类会做什么。</li><li>CSS是精益的,几乎没有冗余,也没有自重(所有样式都与项目相关)。</li><li>范围有限,不依赖于后代/上下文选择器-样式是在“ <a href="https://link.segmentfault.com/?enc=XkBQCviOQ4H0NAJR6wgsRA%3D%3D.UdXvwAoN2fkkHN9AvLiQrGicSgGR9tCH8owqNnxe5I7hlP0sPL9N9NVNXFcku7XpAqCGz5QvMrHgFpLlzd0MqFbX0vha%2FLXf3zPI3vrD3AE%3D" rel="nofollow">特异性层</a> ” 内部完成的。</li><li>初学者友好,原子 CSS 在设计好的情况下,甚至不需要编写样式表。 对于 css 不够擅长的开发人员更友好(这个也不一定是一件好事,css 学习是必需的)</li><li>越大型的系统,对当前设计越熟悉,对库开发越熟练的开发人员,编写代码的时间和代码量就会越少。</li></ul><p>如果一件事情只有利好而没有弊病那也是不可能的:</p><ul><li>需要记住一堆没有意义的原子类,对于不同的团队,类名难以重用。</li><li>对于初学者有一定影响,可能只会记得原子类</li><li>没有结合设计意图,原子类太细。</li></ul><h3>tailwind</h3><p>如果 ACSS 这个框架令人难以接受,那么不久前拿到 200w 投资的 <a href="https://link.segmentfault.com/?enc=IllNwfvkEcKaDBHydtGABg%3D%3D.Q2ZrXNIeLx0YmZ4Atan%2FdUy6wdEhGNG0as4mUvtUUcs%3D" rel="nofollow">tailwind</a> 框架则是更优秀的选择。虽然该库也是原子化 CSS,但是它更具可读性。</p><pre><code class="html"><div class="md:flex bg-white rounded-lg p-6">
<img class="h-16 w-16 md:h-24 md:w-24 rounded-full mx-auto md:mx-0 md:mr-6" src="avatar.jpg">
<div class="text-center md:text-left">
<h2 class="text-lg">Erin Lindford</h2>
<div class="text-purple-500">Product Engineer</div>
<div class="text-gray-600">erinlindford@example.com</div>
<div class="text-gray-600">(555) 765-4321</div>
</div>
</div></code></pre><p>如果你重度使用 Bootstrap,那么我认为直接上手 tailwind 没有什么问题。 对比于 BootStrap,他做的更少,不会提供组件,仅仅提供样式。</p><ul><li>自适应前置,我们在使用其他 UI 库书写自适应前端网页时,往往会携带 -md -xs 诸如此类的类。而 Tailwind 则以 md:text-left lg:bg-teal-500 开头布局响应式风格。在书写时候,让代码更加自然。</li><li>代码量可控,虽然 Tailwind CSS 的开发版本未压缩为1680.1K,但是它可以轻易与 webpack 结合剔出没有使用的 css。</li><li>结合设计意图</li></ul><pre><code class="js"> // tailwind 配置项
module.exports = {
theme: {
screens: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
fontFamily: {
display: ['Gilroy', 'sans-serif'],
body: ['Graphik', 'sans-serif'],
},
borderWidth: {
default: '1px',
'0': '0',
'2': '2px',
'4': '4px',
},
extend: {
colors: {
cyan: '#9cdbff',
},
spacing: {
'96': '24rem',
'128': '32rem',
}
}
}
}</code></pre><p>如果将来某一天个人需要从头编写自己的组件库,我一定会使用它。相信该库会给我带来更好的开发体验。</p><h2>MVP.css</h2><p><a href="https://link.segmentfault.com/?enc=tr7XG%2BdQgGs0hLi8FE3ShQ%3D%3D.msjuz955071TrSEq6T%2Bw%2BTG3QODlypzsOlDkHmOStvRKHt%2BweoUOaETJpwCZAq3S" rel="nofollow">Mvp.css</a> 是一个简约的 HTML 元素样式表库,虽然它不属于 css 组织与管理,但当开发 ToC 项目时候,我们也可以把这种做法看作一种简约的模式。</p><p>这个微型库结合 css 变量进行项目开发。不过也许有人会认为他是一种前端反模式,因为他的样式挂在在 元素之上。不过如果面对项目较小,且需要整齐的风格体验,也可能是一个不错的方案。</p><pre><code class="css">:root {
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color: #118bee;
--color-accent: #118bee0b;
--color-bg: #fff;
/* 其他变量 */
}
/* Layout */
article aside {
background: var(--color-secondary-accent);
border-left: 4px solid var(--color-secondary);
padding: 0.01rem 0.8rem;
}
body {
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
line-height: var(--line-height);
margin: 0;
overflow-x: hidden;
padding: 1rem 0;
}</code></pre><p>关于 css 变量的一系列知识点,可以查看我之前的博客 <a href="https://link.segmentfault.com/?enc=M83rJsaHzwhCVmJDqhv92g%3D%3D.JwcdMN71GyZFzjLp2vEhpm16A1X16hjflpRwvygr8hMnicrqEr3eFdijl7NioMedrdi5v3YJ9ze6fg1KHz3xAQ%3D%3D" rel="nofollow">玩转 CSS 变量</a>。该文章详解了 css 变量的利弊以及新奇玩法。</p><h2>工程实践</h2><p>实际开发往往不止需要考虑某一方面,只考虑自己手上要做的东西,需要更高维度查看项目乃至整个开发体系。</p><blockquote>团队合作永远是统一高于一切</blockquote><p>针对于项目团队,任何一样事务能生存下来,都有其自己的优势,当然万物有得就必有失。这是相互的,至于我们前端人员,或者一个团队如何取舍,还是需要从自已或团队力量出发,有利用之,无利就不用了。我认为我最近看的一篇博客 <a href="https://link.segmentfault.com/?enc=2jZKSZwd1jh2Xo%2FFJFL3ng%3D%3D.%2B7zUhsQRI2WToChbeNGanfbaHNKJiVy7klriwDuMb5lQ12bNG%2F9F9p%2Fc%2F%2FYWtApJ8GxhU2maUwFTce3I%2BZzbgA%3D%3D" rel="nofollow">《程序员修炼之道》中的一段废稿</a> 可以表述正交性问题,事实上,无论式团队还是一段代码,正交性越差就越难以治理。</p><p>最后,在这里介绍一些本文没有介绍的工具。</p><ul><li>Design token 辅助库 <a href="https://link.segmentfault.com/?enc=yABoqj15vMg2edDGa4YnkQ%3D%3D.f%2BQ%2FJKJFe465KUevGYYcK58gG8BSZq333e%2BXbZmFHyI07qZ8OG5NW2ibCwsjfYR4" rel="nofollow">theo</a> 来编写多端变量。</li><li>去除未使用的 Css 样式 <a href="https://link.segmentfault.com/?enc=8NamRtCO8Tx7n5FDZrmZJg%3D%3D.wDSq8oPHjYP%2BacDRMhb0lco43Bi6ciotZBgvQUZ0%2BCk%3D" rel="nofollow">uncss</a></li><li>通过 url 提取关键 CSS <a href="https://link.segmentfault.com/?enc=rUR9fAlV769CTpVNniJTbw%3D%3D.t%2FtD8zW3ICkNztsbO2Y5dlcOhBcZE%2Fh3iecl1VxSjjKFM7Z5yXC28QDL2NUskgzy" rel="nofollow">minimalcss</a> ,提升初始渲染速度</li><li>高性能的 CSS-in-JS 库 <a href="https://link.segmentfault.com/?enc=CkfdC4EMIL2lAh5dysJFZg%3D%3D.F54o9UdlnD71%2B9izyua8Dz0kVs7%2BkMFL0LqIWWb%2FU2alo550sFZaDe0SzLzYPIUT" rel="nofollow">Emotion</a></li></ul><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=YUViwQnMpaFsBD%2BRKNTbZQ%3D%3D.RerDYduIGSyMQhTq79uFGO80IrQC5U%2FPzxphV7fjN3OTgoKEcwHMCAlp767PglZtaIds4PeN76%2BeNxv2mpUrZw%3D%3D" rel="nofollow">值得参考的css理论:OOCSS、SMACSS与BEM</a></p><p><a href="https://link.segmentfault.com/?enc=qEc5zDghexa0EaS4UZfmEw%3D%3D.eaH3L8m%2FDKkfGwaGHTnJKNi1nanpQsEtka8aZcuLkAo%3D" rel="nofollow">ACSS</a></p><p><a href="https://link.segmentfault.com/?enc=l94vL43s6DL%2Fl2vU7yLd0A%3D%3D.27TMUapdu4t0wLrXrghhemNKESdt2qGseWaKSMBsog8%3D" rel="nofollow">tailwind</a> </p><p><a href="https://link.segmentfault.com/?enc=%2F%2BzIhX1LIc2m3L2LH5Y7Ug%3D%3D.5bjQ7GTQ3%2FyE%2BWm92cVKlLwIwe5FaMjMpBXdDFBXQx18mex8b0a%2FWhcf7EjtzzqB" rel="nofollow">Mvp.css</a> </p><p><a href="https://link.segmentfault.com/?enc=C82zTwXgcMrtQZDeFHmtdQ%3D%3D.AQXBI4skO71vuOI0Oh2T3%2FMpZtHIzyow0xPxsvCivEIxIwxB%2FCJzyoG2DNqXNjTgRSOKYHDZDyD4fozm%2FA%2BJ0g%3D%3D" rel="nofollow">玩转 CSS 变量</a></p><p><a href="https://link.segmentfault.com/?enc=YHm%2BcqUWCQAjJ7awFz27TQ%3D%3D.7cbxzwdqdnGt7YYFA37htTABbZnDXXhZLSjlkP1hRKR0L6gK9gXEAbLCyXIKM75f" rel="nofollow">theo</a> </p><p><a href="https://link.segmentfault.com/?enc=v7IBNjezvx0RJUkHa2GhjQ%3D%3D.0ecRY%2BTppu2AXaJrGHR%2FnKPspcWY%2B6Hkoo7wMvm4m3Q%3D" rel="nofollow">uncss</a></p><p><a href="https://link.segmentfault.com/?enc=%2FyTgUbCVkS6%2BsiokXVpD%2Fw%3D%3D.pGfbmqa1AAbTa%2FG8gNr9%2BFHwNOtC%2Bz0ICUGB0GNYP1Xe8SeCRDH1DrGR%2Fkj1cXPu" rel="nofollow">minimalcss</a> </p><p><a href="https://link.segmentfault.com/?enc=lAnJQZAJzQ13uX1cunpLbg%3D%3D.4YalwcXH7O0%2BLMF9KzYC9zh7QFzQKQDTVAm%2B9KYjbfRb5OMzzpAXWKZ5RR49wgPu" rel="nofollow">Emotion</a></p><p><a href="https://link.segmentfault.com/?enc=%2FGmjxUf%2BpTtKuN52%2BNkApA%3D%3D.6bxVNUKt1awGJBwDSIQeAPFW4NeaaObo2fDhaewb7pUdVvKBIongBVuozHHBU%2FjosHdSPZyNDD6E7LtPA2dSRA%3D%3D" rel="nofollow">《程序员修炼之道》中的一段废稿</a></p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=TCt1J52xd4MlDoXJgc0UFQ%3D%3D.6MNTDlG%2Fh7Tz7JR350W9DSm%2FSe1kC7gbyTr2MxqfSCApJKRIyoG9RNtBTW9BPX%2F3" rel="nofollow">博客地址</a></p>
总结对象安全访问处理方案
https://segmentfault.com/a/1190000038175306
2020-11-15T17:26:40+08:00
2020-11-15T17:26:40+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<blockquote>前端数据不可信,后端数据不可知</blockquote><p>在前端项目开发与生产的过程中,“cannot read property of undefined”是一个常见的错误。从不可知得到一个空数据问题在所难免。面对这种问题我们该怎么办呢?</p><p>针对于特定的业务和数据,前端即使预知数据不存在,也无法处理,任其崩溃即可。但大部分情况下,如果因为列表某条数据出现错误而导致整个页面无法访问,用户将对我们系统健壮性产生信任危机。</p><p>为了能够在对象( JavaScript 中数组也是对象)中安全的取值,需要验证对象中数据项是否存在,if 语句判断当然是没有问题的,但是如果数据路径较深,代码就过于冗余了,而常见的处理方案有如下几种。</p><h2>短路运算符号嗅探</h2><p>JavaScript 中我们可以通过 && 来实现获取员工的地址邮编代码</p><pre><code class="js">const result = (staff.address && staff.address[0] && staff.address[0].zip) || ''</code></pre><h3>原理解析</h3><p>这种解析方式和 JavaScript 异于其他语言的判断机制有关。大部分语言都仅有 true 和 false, JavaScript 有 <code>truthy</code> 概念,即在某些场景下会被推断为 <code>true</code>。</p><p>当然以下数据会被解析为 false:</p><ul><li>null</li><li>undefined</li><li>NaN</li><li>0</li><li>空字符串</li></ul><p>除此之外,都会被解析为 true,即使空数组, 空对象(注: Python 空字典,空列表,空元组均在判断中会被解析为 false)也不例外。</p><p>同时 && || 不仅仅返回 true 和 false,而是数据项。</p><table><thead><tr><th align="center">运算符</th><th align="center">说明</th></tr></thead><tbody><tr><td align="center">逻辑与,AND(<code>&&</code>)</td><td align="center">若第一项可转换为 <code>true</code>,则返回第二项;否则,返回第一项目。</td></tr><tr><td align="center">逻辑或,OR</td><td align="center">若第一项可转换为 <code>true</code>,则返回第一项;否则,返回第二项目。</td></tr><tr><td align="center">逻辑非,NOT(<code>!</code>)</td><td align="center">若当前项可转换为 <code>true</code>,则返回 <code>false</code>;否则,返回 <code>true</code></td></tr></tbody></table><h2>|| 单元保底值</h2><p>既然可以通过 && 来对数据进行嗅探,那么我们可以退一步,如果当前没有项目数据,利用 || 返回空对象或者空数组。</p><pre><code class="js">const EMPTY_OBJ = {}
const result = (((staff || EMPTY_OBJ).address || EMPTY_OBJ)[0] || EMPTY_OBJ).zip || ''</code></pre><p>对比上一个方案,虽然相比上述代码更为复杂。 但是如果针对拥有完整数据的数据项目而言,对数据的访问次数较少(. 的使用率),而上一个方案针对完善数据的访问会多不少。而大部分数据无疑是正确与完整的。</p><h2>try catch</h2><p>该方法无需验证对象中数据项是否存在,而是通过错误处理直接处理。</p><pre><code class="js">let result = ''
try {
result = staff.address[0].zip
} catch {
// 错误上报
}</code></pre><p>try catch 方案更适合必要性数据缺失作为上报的情况。但如果发生了必要性内容数据缺失,前端界面崩溃反而是一件好事。所以 try catch 不太适合处理对象安全访问这种问题,仅仅作为可选方案。</p><h2>链判断运算符</h2><p>上述处理方式都很痛苦,因此 <a href="https://link.segmentfault.com/?enc=%2Fdwoqlrb7C6d%2Fnr8XqJG0A%3D%3D.ZcirJEjWizJFmqh98bUTC2QGU4ejj60yow2Z5kMTLURJcyUjTCkzyhvjf93r11B2juvsMl5O4CmgF43v9pxJ5w%3D%3D" rel="nofollow">ES2020</a> 引入了“链判断运算符”(optional chaining operator)<code>?.</code>,简化上面的写法。</p><pre><code class="js">const reuslt = staff?.address?.[0]?.zip || ''</code></pre><p>简单来解释:</p><pre><code class="js">a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()</code></pre><p>如果你想要详细了解,可以参考阮一峰 <a href="https://link.segmentfault.com/?enc=NqNCrtBH9%2BJzfzCYuWfPDA%3D%3D.JfFKOpwP9rpIjM3i1o9qKL1Rwr23%2BQ7BGZhr1QufAlnh3mjuqxy7zjxf65NEjcQl%2FGRkRy2RcQAzvgORr5aVhVBRi%2BgeQ7DlU2u6VOhGdop7P%2BIQqHmNONaeqrZDA7lh" rel="nofollow">ECMAScript 6 入门 链判断运算符 </a> 一篇。</p><h2>手写路径获取</h2><p>某些情况下,我们需要传递路径来动态获取数据,如 'staff.address[0].zip', 这里手写了一个处理代码。传入对象和路径,得到对象,对象 key 以及 value。</p><pre><code class="ts">/**
* 根据路径来获取 对象内部属性
* @param obj 对象
* @param path 路径 a.b[1].c
*/
function getObjPropByPath(obj: Record<string, any>, path: string) {
let tempObj = obj
const keyArr = path.split('.').map(x => x.trim())
let i: number = 0
for (let len = keyArr.length; i <len - 1; ++i) {
let key = keyArr[i]
// 简单判断是否是数组数据,如果 以 ] 结尾的话
const isFormArray = key.endsWith(']')
let index: number = 0
if (isFormArray) {
const data = key.split('[') ?? []
key = data[0] ?? ''
// 对于 parseInt('12]') => 12
index = parseInt(data[1], 10)
}
if (key in tempObj) {
tempObj = tempObj[key]
if (isFormArray && Array.isArray(tempObj)) {
tempObj = tempObj[index]
if (!tempObj) {
return {}
}
}
} else {
return {}
}
}
if (!tempObj) {
return {}
}
return {
o: tempObj,
k: keyArr[i],
v: tempObj[keyArr[i]]
}
}</code></pre><p>不过笔者写的方案较为粗糙,但 lodash 对象模块中也有该功能,感兴趣的可以参考其实现方式。<a href="https://link.segmentfault.com/?enc=lsa5aHWgKT0O8rPYCCoNPw%3D%3D.zdjOukX0crXMA5LhTfLSJqNewLfcrFcDk2QwJ%2FjFYWQFJrDQzxEi1ZU3EjJAcR78" rel="nofollow">lodash get</a></p><pre><code class="js">
// 根据 object对象的path路径获取值。 如果解析 value 是 undefined 会以 defaultValue 取代。
// _.get(object, path, [defaultValue])
var object = { 'a': [{ 'b': { 'c': 3 } }] };
_.get(object, 'a[0].b.c');
// => 3
_.get(object, ['a', '0', 'b', 'c']);
// => 3
_.get(object, 'a.b.c', 'default');
// => 'default'</code></pre><h2>其他</h2><h3>Null 判断运算符</h3><p>当然,我们大部分情况下使用 || 都没有问题,但是由于 <code>falsy</code> 的存在. || 对于 false 和 undefined 是一样的。但是某些情况下,false 是有意义的,true, false, undefined 均代表一种含义,这时候,我们还需要对数据进行其他处理,使用 in 或者 hasOwnProperty 进行存在性判断。</p><p>针对于这种情况,<a href="https://link.segmentfault.com/?enc=MX8uG%2BlwxEcaBVYeivKb2Q%3D%3D.fazIvC5gzb%2BFIDdPLS9pxVRyit4O2%2FXcJbsWWtQ%2FPHsMxvp%2Fjttx1iMcVjEToRlkMWHAPUGNTZFIFxQwJv7OIA%3D%3D" rel="nofollow">ES2020</a> 引入了一个新的 Null 判断运算符 <code>??</code>。它的行为类似<code>||</code>,但是只有运算符左侧的值为 <code>null</code> 或 <code>undefined</code> 时,才会返回右侧的值。如</p><pre><code class="js">const result = staff.address && staff.address[0] && staff.address[0].zip ?? ''</code></pre><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=Bah9LNeQ83GMpdHe0RDBqA%3D%3D.wB6OBVY07k5ODV%2BcyBRKdG%2BpOIczHqxuK4T%2BFIStqd5%2BHH%2B4A4kgXZZdIezNk3iR" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=D8ui3LcaTteiPS51MrQWxg%3D%3D.dFZkn0J4TW5b1C9U3SJjlpX0s8dpcjsFUvdAt25AiNA%3D" rel="nofollow">ES6 入门教程</a></p>
命令行错误提示—谈谈模糊集
https://segmentfault.com/a/1190000038140345
2020-11-12T02:36:33+08:00
2020-11-12T02:36:33+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>在开发的过程中,我们会使用各种指令。有时候,我们由于这样或者那样的原因,写错了某些指令。此时,应用程序往往会爆出错误。</p><blockquote>Unrecognized option 'xxx' (did you mean 'xxy'?)</blockquote><p>可以看到,当前代码不仅仅提示了当前你输入的配置错误。同时还提供了类似当前输入的近似匹配指令。非常的智能。此时,我们需要使用算法来计算,即模糊集。</p><p>事实上,模糊集其实可以解决一些现实的问题。例如我们有一个“高个子”集合 A,定义 1.75m 为高个子。那么在通用逻辑中我们会认为某一个元素隶属或者不隶属该集合。也就是 1.78 就是高个子,而 1.749 就不是高个子,即使它距离 1.75 米只差里一毫米。该集合被称为(two-valued 二元集),与此相对的,模糊集合则没有这种问题。</p><p>在模糊集合中,所有人都是集合 A 的成员,所不同的仅仅是匹配度而已。我们可以通过计算匹配度来决定差异性。</p><h2>如何运行</h2><p>言归正转,我们回到当前实现。对于模糊集的实现,我们可以参考 <a href="https://link.segmentfault.com/?enc=3U6hliylYNOZ6mmvAsDZGg%3D%3D.YxhtVBb%2FBiPxhpvBOwTvzd0R4t3OGySaaFMeNzPKcZv21qGsrja6SFvRAGQCsxJF" rel="nofollow">fuzzyset.js</a> (注: 该库需要商业许可) 和 <a href="https://link.segmentfault.com/?enc=Z4ikERs5EN72L4Bgft%2FOyw%3D%3D.2h2Q5pwchOKdwDA4FAncFfUvU4XrIMRJxEwukxn%2FIlyOxNaYY7ADEHVkJGtNUE4C" rel="nofollow">fuzzyset.js 交互式文档</a> 进行学习。</p><p>在这里,我仅仅只介绍基本算法,至于数据存储和优化在完整实现中。</p><p>通过查看交互式文档,我们可以算法是通过余弦相似度公式去计算。</p><p>在直角坐标系中,相似度公式如此计算。</p><blockquote>cos = (a <em> b) / (|a| </em> |b| ). => 等同于<p>( (x1, y1) <em> (x2,y2)) / (Math.sqrt(x1 <strong> 2 + y1 </strong> 2) </em> Math.sqrt(x2 <strong> 2 + y2 </strong> 2))</p></blockquote><p>而相似度公式是通过将字符串转化为数字矢量来计算。如果当前的字符串分别为 “smaller” 和 “smeller”。我们需要分解字符串子串来计算。</p><p>当前可以分解的字符串子串可以根据项目来自行调整,简单起见,我们这里使用 2 为单位。</p><p>两个字符串可以被分解为:</p><pre><code class="ts">const smallSplit: string[] = [
'-s',
'sm',
'ma',
'al',
'll',
'l-'
]
const smelllSplit: string[] = [
'-s',
'sm',
'me',
'el',
'll',
'll',
'l-'
]</code></pre><p>我们可以根据当前把代码变为如下向量:</p><pre><code class="ts">const smallGramCount = {
'-s': 1,
'sm': 1,
'ma': 1,
'al': 1,
'll': 1,
'l-': 1
}
const smallGramCount = {
'-s': 1,
'sm': 1,
'me': 1,
'el': 1,
'll': 2,
'l-': 1
}</code></pre><pre><code class="ts">const _nonWordRe = /[^a-zA-Z0-9\u00C0-\u00FF, ]+/g;
/**
* 可以直接把 'bal' 变为 ['-b', 'ba', 'al', 'l-']
*/
function iterateGrams (value: string, gramSize: number = 2) {
// 当前 数值添加前后缀 '-'
const simplified = '-' + value.toLowerCase().replace(_nonWordRe, '') + '-'
// 通过计算当前子字符串长度和当前输入数据长度的差值
const lenDiff = gramSize - simplified.length
// 结果数组
const results = []
// 如果当前输入的数据长度小于当前长度
// 直接添加 “-” 补差计算
if (lenDiff > 0) {
for (var i = 0; i < lenDiff; ++i) {
value += '-';
}
}
// 循环截取数值并且塞入结果数组中
for (var i = 0; i < simplified.length - gramSize + 1; ++i) {
results.push(simplified.slice(i, i + gramSize));
}
return results;
}
/**
* 可以直接把 ['-b', 'ba', 'al', 'l-'] 变为 {-b: 1, 'ba': 1, 'al': 1, 'l-': 1}
*/
function gramCounter(value: string, gramSize: number = 2) {
const result = {}
// 根据当前的
const grams = _iterateGrams(value, gramSize)
for (let i = 0; i < grams.length; ++i) {
// 根据当前是否有数据来进行数据增加和初始化 1
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
}</code></pre><p>然后我们可以计算 small \* smell 为:</p><table><thead><tr><th align="center">small gram</th><th align="center">small count</th><th align="center"> </th><th align="center">smell gram</th><th align="center">smell gram</th></tr></thead><tbody><tr><td align="center">-s</td><td align="center">1</td><td align="center">\*</td><td align="center">-s</td><td align="center">1</td></tr><tr><td align="center">sm</td><td align="center">1</td><td align="center">\*</td><td align="center">sm</td><td align="center">1</td></tr><tr><td align="center">ma</td><td align="center">1</td><td align="center">\*</td><td align="center">ma</td><td align="center">0</td></tr><tr><td align="center">me</td><td align="center">0</td><td align="center">\*</td><td align="center">me</td><td align="center">1</td></tr><tr><td align="center">al</td><td align="center">1</td><td align="center">\*</td><td align="center">al</td><td align="center">0</td></tr><tr><td align="center">el</td><td align="center">0</td><td align="center">\*</td><td align="center">el</td><td align="center">1</td></tr><tr><td align="center">ll</td><td align="center">1</td><td align="center">\*</td><td align="center">ll</td><td align="center">1</td></tr><tr><td align="center">l-</td><td align="center">1</td><td align="center">\*</td><td align="center">l-</td><td align="center">1</td></tr><tr><td align="center"> </td><td align="center"> </td><td align="center"> </td><td align="center">sum</td><td align="center">4</td></tr></tbody></table><pre><code class="ts">function calcVectorNormal() {
// 获取向量对象
const small_counts = gramCounter('small', 2)
const smell_counts = gramCOunter('smell', 2)
// 使用 set 进行字符串过滤
const keySet = new Set()
// 把两单词组共有的字符串塞入 keySet
for (let key in small_counts) {
keySet.add(key)
}
for (let key in smell_counts) {
keySet.add(key)
}
let sum: number = 0
// 计算 small * smell
for(let key in keySet.keys()) {
sum += (small_count[key] ?? 0) * (smell_count[key] ?? 0)
}
return sum
}
</code></pre><p>同时我们可以计算 |small|\*|smell| 为:</p><table><thead><tr><th align="center">small Gram</th><th align="center">SmAll Count</th><th align="center">Count \<em>\</em> 2</th></tr></thead><tbody><tr><td align="center">-s</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center">sm</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center">ma</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center">al</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center">ll</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center">l-</td><td align="center">1</td><td align="center">1</td></tr><tr><td align="center"> </td><td align="center">sum</td><td align="center">6</td></tr><tr><td align="center"> </td><td align="center">sqrt</td><td align="center">2.449</td></tr></tbody></table><p>同理可得当前 smell sqrt 也是 2.449。</p><p>最终的计算为: 4 / (2.449 \* 2.449) = 0.66 。</p><p>计算方式为</p><pre><code class="ts">// ... 上述代码
function calcVectorNormal() {
// 获取向量对象
const gram_counts = gramCounter(normalized_value, 2);
// 计算
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
for (gram in gram_counts) {
gram_count = gram_counts[gram];
// 乘方相加
sum_of_square_gram_counts += Math.pow(gram_count, 2);
}
return Math.sqrt(sum_of_square_gram_counts);
}
</code></pre><p>则 small 与 smell 在子字符串为 2 情况下匹配度为 0.66。</p><p>当然,我们看到开头和结束添加了 - 也作为标识符号,该标识是为了识别出 sell 与 llse 之间的不同,如果使用</p><pre><code class="ts">const sellSplit = [
'-s',
'se',
'el',
'll',
'l-'
]
const llseSplit = [
'-l',
'll',
'ls',
'se',
'e-'
]</code></pre><p>我们可以看到当前的相似的只有 'll' 和 'se' 两个子字符串。</p><h2>完整代码</h2><p>编译型框架 <a href="https://link.segmentfault.com/?enc=BTIxegzJBN%2FEorTl9jXlKQ%3D%3D.5vSKPinJo5NUchuE3BhteAtn8jL8dfYgSeOTyRt%2FSIA%3D" rel="nofollow">svelte</a> 项目代码中用到此功能,使用代码解析如下:</p><pre><code class="ts">const valid_options = [
'format',
'name',
'filename',
'generate',
'outputFilename',
'cssOutputFilename',
'sveltePath',
'dev',
'accessors',
'immutable',
'hydratable',
'legacy',
'customElement',
'tag',
'css',
'loopGuardTimeout',
'preserveComments',
'preserveWhitespace'
];
// 如果当前操作不在验证项中,才会进行模糊匹配
if (!valid_options.includes(key)) {
// 匹配后返回 match 或者 null
const match = fuzzymatch(key, valid_options);
let message = `Unrecognized option '${key}'`;
if (match) message += ` (did you mean '${match}'?)`;
throw new Error(message);
}</code></pre><p>实现代码如下所示:</p><pre><code class="ts">export default function fuzzymatch(name: string, names: string[]) {
// 根据当前已有数据建立模糊集,如果有字符需要进行匹配、则可以对对象进行缓存
const set = new FuzzySet(names);
// 获取当前的匹配
const matches = set.get(name);
// 如果有匹配项,且匹配度大于 0.7,返回匹配单词,否则返回 null
return matches && matches[0] && matches[0][0] > 0.7 ? matches[0][1] : null;
}
// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js
// BSD Licensed
// 最小子字符串 2
const GRAM_SIZE_LOWER = 2;
// 最大子字符串 3
const GRAM_SIZE_UPPER = 3;
// 进行 Levenshtein 计算,更适合输入完整单词的匹配
function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null)
throw 'Trying to compare two null values';
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);
const distance = levenshtein(str1, str2);
if (str1.length > str2.length) {
return 1 - distance / str1.length;
} else {
return 1 - distance / str2.length;
}
}
// Levenshtein距离,是指两个字串之间,由一个转成另一个所需的最少的编辑操作次数。
function levenshtein(str1: string, str2: string) {
const current: number[] = [];
let prev;
let value;
for (let i = 0; i <= str2.length; i++) {
for (let j = 0; j <= str1.length; j++) {
if (i && j) {
if (str1.charAt(j - 1) === str2.charAt(i - 1)) {
value = prev;
} else {
value = Math.min(current[j], current[j - 1], prev) + 1;
}
} else {
value = i + j;
}
prev = current[j];
current[j] = value;
}
}
return current.pop();
}
// 正则匹配除单词 字母 数字以及逗号和空格外的数据
const non_word_regex = /[^\w, ]+/;
// 上述代码已经介绍
function iterate_grams(value: string, gram_size = 2) {
const simplified = '-' + value.toLowerCase().replace(non_word_regex, '') + '-';
const len_diff = gram_size - simplified.length;
const results = [];
if (len_diff > 0) {
for (let i = 0; i < len_diff; ++i) {
value += '-';
}
}
for (let i = 0; i < simplified.length - gram_size + 1; ++i) {
results.push(simplified.slice(i, i + gram_size));
}
return results;
}
// 计算向量,上述代码已经介绍
function gram_counter(value: string, gram_size = 2) {
const result = {};
const grams = iterate_grams(value, gram_size);
let i = 0;
for (i; i < grams.length; ++i) {
if (grams[i] in result) {
result[grams[i]] += 1;
} else {
result[grams[i]] = 1;
}
}
return result;
}
// 排序函数
function sort_descending(a, b) {
return b[0] - a[0];
}
class FuzzySet {
// 数据集合,记录所有的可选项目
// 1.优化初始化时候,相同的可选项数据,同时避免多次计算相同向量
// 2.当前输入的值与可选项相等,直接返回,无需计算
exact_set = {};
// 匹配对象存入,存储所有单词的向量
// 如 match_dist['ba'] = [
// 第2个单词,有 3 个
// {3, 1}
// 第5个单词,有 2 个
// {2, 4}
// ]
// 后面单词匹配时候,可以根据单词索引进行匹配然后计算最终分数
match_dict = {};
// 根据不同子字符串获取不同的单词向量,最终有不同的匹配度
// item[2] = [[2.6457513110645907, "aaab"]]
items = {};
constructor(arr: string[]) {
// 当前选择 2 和 3 为子字符串匹配
// item = {2: [], 3: []}
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this.items[i] = [];
}
// 添加数组
for (let i = 0; i < arr.length; ++i) {
this.add(arr[i]);
}
}
add(value: string) {
const normalized_value = value.toLowerCase();
// 如果当前单词已经计算,直接返回
if (normalized_value in this.exact_set) {
return false;
}
// 分别计算 2 和 3 的向量
for (let i = GRAM_SIZE_LOWER; i < GRAM_SIZE_UPPER + 1; ++i) {
this._add(value, i);
}
}
_add(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
// 获取 items[2]
const items = this.items[gram_size] || [];
// 获取数组的长度作为索引
const index = items.length;
// 没有看出有实际的用处?实验也没有什么作用?不会影响
items.push(0);
// 获取 向量数据
const gram_counts = gram_counter(normalized_value, gram_size);
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
// 同上述代码,只不过把所有的匹配项目和当前索引都加入 match_dict 中去
// 如 this.match_dict['aq'] = [[1, 2], [3,3]]
for (gram in gram_counts) {
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
if (gram in this.match_dict) {
this.match_dict[gram].push([index, gram_count]);
} else {
this.match_dict[gram] = [[index, gram_count]];
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
// 添加向量 如: this.items[2][3] = [4.323, 'sqaaaa']
items[index] = [vector_normal, normalized_value];
this.items[gram_size] = items;
// 设置当前小写字母,优化代码
this.exact_set[normalized_value] = value;
}
// 输入当前值,获取选择项
get(value: string) {
const normalized_value = value.toLowerCase();
const result = this.exact_set[normalized_value];
// 如果当前值完全匹配,直接返回 1,不必计算
if (result) {
return [[1, result]];
}
let results = [];
// 从多到少,如果多子字符串没有结果,转到较小的大小
for (
let gram_size = GRAM_SIZE_UPPER;
gram_size >= GRAM_SIZE_LOWER;
--gram_size
) {
results = this.__get(value, gram_size);
if (results) {
return results;
}
}
return null;
}
__get(value: string, gram_size: number) {
const normalized_value = value.toLowerCase();
const matches = {};
// 获得当前值的向量值
const gram_counts = gram_counter(normalized_value, gram_size);
const items = this.items[gram_size];
let sum_of_square_gram_counts = 0;
let gram;
let gram_count;
let i;
let index;
let other_gram_count;
// 计算得到较为匹配的数据
for (gram in gram_counts) {
// 获取 向量单词用于计算
gram_count = gram_counts[gram];
sum_of_square_gram_counts += Math.pow(gram_count, 2);
// 取得当前匹配的 [index, gram_count]
if (gram in this.match_dict) {
// 获取所有匹配当前向量单词的项目,并且根据 索引加入 matches
for (i = 0; i < this.match_dict[gram].length; ++i) {
// 获得当前匹配的索引 === 输入单词[index]
index = this.match_dict[gram][i][0];
// 获得匹配子字符串的值
other_gram_count = this.match_dict[gram][i][1];
// 单词索引添加,注:只要和当前子字符串匹配的 索引都会加入 matches
if (index in matches) {
matches[index] += gram_count * other_gram_count;
} else {
matches[index] = gram_count * other_gram_count;
}
}
}
}
const vector_normal = Math.sqrt(sum_of_square_gram_counts);
let results = [];
let match_score;
// 构建最终结果 [分数, 单词]
for (const match_index in matches) {
match_score = matches[match_index];
results.push([
// 分数
match_score / (vector_normal * items[match_index][0]),
// 单词
items[match_index][1]
]);
}
// 虽然所有的与之匹配子字符串都会进入,但我们只需要最高的分数
results.sort(sort_descending);
let new_results = [];
// 如果匹配数目很大,只取的前 50 个数据进行计算
const end_index = Math.min(50, results.length);
// 由于是字符类型数据,根据当前数据在此计算 levenshtein 距离
for (let i = 0; i < end_index; ++i) {
new_results.push([
_distance(results[i][1], normalized_value), results[i][1]
]);
}
results = new_results;
// 在此排序
results.sort(sort_descending);
new_results = [];
for (let i = 0; i < results.length; ++i) {
// 因为 第一的分数是最高的,所有后面的数据如果等于第一个
// 也可以进入最终选择
if (results[i][0] == results[0][0]) {
new_results.push([results[i][0], this.exact_set[results[i][1]]]);
}
}
// 返回最终结果
return new_results;
}
}
</code></pre><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p><p><a href="https://link.segmentfault.com/?enc=UPVO4DgmSH72SEWhTZ8jAQ%3D%3D.lE83UIG114WksBurMr3UTfKhFbaNKxR2GSurfsKyr%2FkmQ82HrBPC2tOUfTy9E4zG" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=AFRFVY029l0OWPOs0UO9yw%3D%3D.a89hahfYA5nwcXWcJW577Stc594EjsND%2F%2BtLY4K65Wsoa7oYdGOT7mE95TSWl7ZT" rel="nofollow">fuzzyset.js 交互式文档</a></p><p><a href="https://link.segmentfault.com/?enc=TndJ48hXWbTtT8V8uRutxA%3D%3D.%2FGTf1bSGHtjJXfMEtlXne28YmaMo1kYup6eOMndRCgehlemG9lSAJb40NXF3KpIogslcLc9Lwc9c%2BGCWdmomIOyZ7yDnSxp8nPM9rQrVt9o%3D" rel="nofollow">svelte fuzzymatch</a></p>
利用 XState(有限状态机) 编写易于变更的代码
https://segmentfault.com/a/1190000037532831
2020-10-20T00:33:57+08:00
2020-10-20T00:33:57+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
5
<p>目前来说,无论是 to c 业务,还是 to b 业务,对于前端开发者的要求越来越高,各种绚丽的视觉效果,复杂的业务逻辑层出不穷。针对于业务逻辑而言,贯穿后端业务和前端交互都有一个关键点 —— 状态转换。</p><p>当然了,这种代码实现本身并不复杂,真正的难点在于如何快速的进行代码的修改。</p><p>在实际开发项目的过程中,ETC 原则,即 Easier To Change,易于变更是非常重要的。为什么解耦很好? 为什么单一职责很有用? 为什么好的命名很重要?因为这些设计原则让你的代码更容易发生变更。ETC 甚至可以说是其他原则的基石,可以说,我们现在所作的一切都是为了更容易变更!!特别是针对于初创公司,更是如此。</p><p>例如:项目初期,当前的网页有一个模态框,可以进行编辑,模态框上有两个按钮,保存与取消。这里就涉及到模态框的显隐状态以及权限管理。随着时间的推移,需求和业务发生了改变。当前列表无法展示该项目的所有内容,在模态框中我们不但需要编辑数据,同时需要展示数据。这时候我们还需要管理按钮之间的联动。仅仅这些就较为复杂,更不用说涉及多个业务实体以及多角色之间的细微控制。</p><p>重新审视自身代码,虽然之前我们做了大量努力利用各种设计原则,但是想要快速而安全的修改散落到各个函数中的状态修改,还是非常浪费心神的,而且还很容易出现“漏网之鱼”。</p><p>这时候,我们不仅仅需要依靠自身经验写好代码,同时也需要一些工具的辅助。</p><h2>有限状态机</h2><p>有限状态机是一个非常有用的数学计算模型,它描述了在任何给定时间只能处于一种状态的系统的行为。当然,该系统中只能够建立出一些有限的、定性的“模式”或“状态” ,并不描述与该系统相关的所有(可能是无限的)数据。例如,水可以是四种状态中的一种: 固体(冰)、液体、气体或等离子体。然而,水的温度可以变化,它的测量是定量的和无限的。</p><p>总结来说,有限状态机的三个特征为:</p><ul><li>状态总数(state)是有限的。</li><li>任一时刻,只处在一种状态之中。</li><li>某种条件下,会从一种状态转变(transition)到另一种状态。</li></ul><p>在实际开发中,它还需要:</p><ul><li>初始状态</li><li>触发状态变化的事件和转换函数</li><li>最终状态的集合(有可能是没有最终状态)</li></ul><p>先看一个简单的红绿灯状态转换:</p><pre><code class="ts">const light = {
currentState: 'green',
transition: function () {
switch (this.currentState) {
case "green":
this.currentState = 'yellow'
break;
case "yellow":
this.currentState = 'red'
break;
case "red":
this.currentState = 'green'
break;
default:
break;
}
}
}</code></pre><p>有限状态机在游戏开发中大放异彩,已经成为了一种常用的设计模式。用这种方式可以使每一个状态都是独立的代码块,与其他不同的状态分开独立运行,这样很容易检测遗漏条件和移除非法状态,减少了耦合,提升了代码的健壮性,这么做可以使得游戏的调试变得更加方便,同时也更易于增加新的功能。</p><p>对于前端开发来说,我们可以从其他工程领域中多年使用的经验学习与再创造。</p><h2>XState 体验</h2><p>实际上开发一个 简单的状态机并不是特别复杂的事情,但是想要一个完善,实用性强,还具有可视化工具的状态机可不是一个简单的事。</p><p>这里我要推荐 <a href="https://link.segmentfault.com/?enc=npp5aY0jF2%2FgrPwcKucvfw%3D%3D.6xppVaOBRnzT0h8uBO%2FwMUAqf6gl8i5ZMZ1ryyqDl%2Bjx1khisoMyz4LGKbBRESns" rel="nofollow">XState</a>,该库用于创建、解释和执行有限状态机和状态图。</p><p>简单来说:上述的代码可以这样写。</p><pre><code class="ts">import { Machine } from 'xstate'
const lightMachine = Machine({
// 识别 id, SCXML id 必须唯一
id: 'light',
// 初始化状态,绿灯
initial: 'green',
// 状态定义
states: {
green: {
on: {
// 事件名称,如果触发 TIMRE 事件,直接转入 yellow 状态
TIMRE: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
}
}
}
})
// 设置当前状态
const currentState = 'green'
// 转换的结果
const nextState = lightMachine.transition(currentState, 'TIMER').value
// => 'yellow'
// 如果传入的事件没有定义,则不会发生转换,如果是严格模式,将会抛出错误
lightMachine.transition(currentState, 'UNKNOWN').value </code></pre><p>其中 <a href="https://link.segmentfault.com/?enc=s8S%2BNNQx8JLjbisyPpIzKw%3D%3D.zuFvHLbnHM%2F%2BtWuu44hpcBUmxB1UCn8bYEO9JiRyiUY%3D" rel="nofollow">SCXML</a> 是状态图可扩展标记语言, XState 遵循该标准,所以需要提供 id。当前状态机也可以转换为 JSON 或 SCXML。</p><p>虽然 transition 是一个纯函数,非常好用,但是在真实环境使用状态机,我们还是需要更强大的功能。如:</p><ul><li>跟踪当前状态</li><li>执行副作用</li><li>处理延迟过度以及时间</li><li>与外部服务沟通</li></ul><p>XState 提供了 interpret 函数,</p><pre><code class="ts">import { Machine,interpret } from 'xstate'
// 。。。 lightMachine 代码
// 状态机的实例成为 serivce
const lightService = interpret(lightMachine)
// 当转换时候,触发的事件(包括初始状态)
.onTransition(state => {
// 返回是否改变,如果状态发生变化(或者 context 以及 action 后文提到),返回 true
console.log(state.changed)
console.log(state.value)
})
// 完成时候触发
.onDone(() => {
console.log('done')
})
// 开启
lightService.start()
// 将触发事件改为 发送消息,更适合状态机风格
// 初始化状态为 green 绿色
lightService.send('TIMER') // yellow
lightService.send('TIMER') // red
// 批量活动
lightService.send([
'TIMER',
'TIMER'
])
// 停止
lightService.stop()
// 从特定状态启动当前服务,这对于状态的保存以及使用更有作用
lightService.start(previousState)</code></pre><p>我们也可以结合其他库在 Vue React 框架中使用,仅仅只用几行代码就实现了我们想要的功能。</p><pre><code class="tsx">import lightMachine from '..'
// react hook 风格
import { useMachine } from '@xstate/react'
function Light() {
const [light, send] = useMachine(lightMachine)
return <>
// 当前状态 state 是否是绿色
<span>{light.matches('green') && '绿色'}</span>
// 当前状态的值
<span>{light.value}</span>
// 发送消息
<button onClick={() => send('TIMER')}>切换</button>
</>
}</code></pre><p>当前的状态机也是还可以进行嵌套处理,在红灯状态下添加人的行动状态。</p><pre><code class="ts">import { Machine } from 'xstate';
const pedestrianStates = {
// 初识状态 行走
initial: 'walk',
states: {
walk: {
on: {
PED_TIMER: 'wait'
}
},
wait: {
on: {
PED_TIMER: 'stop'
}
},
stop: {}
}
};
const lightMachine = Machine({
id: 'light',
initial: 'green',
states: {
green: {
on: {
TIMER: 'yellow'
}
},
yellow: {
on: {
TIMER: 'red'
}
},
red: {
on: {
TIMER: 'green'
},
...pedestrianStates
}
}
});
const currentState = 'yellow';
const nextState = lightMachine.transition(currentState, 'TIMER').value;
// 返回级联对象
// => {
// red: 'walk'
// }
// 也可以写为 red.walk
lightMachine.transition('red.walk', 'PED_TIMER').value;
// 转化后返回
// => {
// red: 'wait'
// }
// TIMER 还可以返回下一个状态
lightMachine.transition({ red: 'stop' }, 'TIMER').value;
// => 'green'</code></pre><p>当然了,既然有嵌套状态,我们还可以利用 type: 'parallel' ,进行串行和并行处理。</p><p>除此之外,XState 还有扩展状态 context 和过度防护 guards。这样的话,更能够模拟现实生活</p><pre><code class="ts">// 是否可以编辑
functions canEdit(context: any, event: any, { cond }: any) {
console.log(cond)
// => delay: 1000
// 是否有某种权限 ???
return hasXXXAuthority(context.user)
}
const buttonMachine = Machine({
id: 'buttons',
initial: 'green',
// 扩展状态,例如 用户等其他全局数据
context: {
// 用户数据
user: {}
},
states: {
view: {
on: {
// 对应之前 TIMRE: 'yellow'
// 实际上 字符串无法表达太多信息,需要对象表示
EDIT: {
target: 'edit',
// 如果没有该权限,不进行转换,处于原状态
// 如果没有附加条件,直接 cond: searchValid
cond: {
type: 'searchValid',
delay: 3
}
},
}
}
}
}, {
// 守卫
guards: {
canEdit,
}
})
// XState 给予了更加合适的 API 接口,开发时候 Context 可能不存在
// 或者我们需要在不同的上下文 context 中复用状态机,这样代码扩展性更强
const buttonMachineWithDelay = buttonMachine.withContext({
user: {},
delay: 1000
})
// withContext 是直接替换,不进行浅层合并,但是我们可以手动合并
const buttonMachineWithDelay = buttonMachine.withContext({
...buttonMachine.context,
delay: 1000
})</code></pre><p>我们还可以通过瞬时状态来过度,瞬态状态节点可以根据条件来确定机器应从先前的状态真正进入哪个状态。瞬态状态表现为空字符串,即 '',如</p><pre><code class="ts">const timeOfDayMachine = Machine({
id: 'timeOfDay',
// 当前不知道是什么状态
initial: 'unknown',
context: {
time: undefined
},
states: {
// Transient state
unknown: {
on: {
'': [
{ target: 'morning', cond: 'isBeforeNoon' },
{ target: 'afternoon', cond: 'isBeforeSix' },
{ target: 'evening' }
]
}
},
morning: {},
afternoon: {},
evening: {}
}
}, {
guards: {
isBeforeNoon: //... 确认当前时间是否小于 中午
isBeforeSix: // ... 确认当前时间是否小于 下午 6 点
}
});
const timeOfDayService = interpret(timeOfDayMachine
.withContext({ time: Date.now() }))
.onTransition(state => console.log(state.value))
.start();
timeOfDayService.state.value
// 根据当前时间,可以是 morning afternoon 和 evening,而不是 unknown 转态</code></pre><p>到这里,我觉得已经介绍 XState 很多功能了,篇幅所限,不能完全介绍所有功能,不过当前的功能已经足够大部分业务需求使用了。如果有其他更复杂的需求,可以参考 <a href="https://link.segmentfault.com/?enc=Um28B6aITbt9j5R8KfodDw%3D%3D.5qFrjLnAPBF2rkMoRfyIwxvEVi4%2FlbJwhNQxN3iXujU%3D" rel="nofollow">XState 文档</a>。</p><p>这里列举一些没有介绍到的功能点:</p><ul><li>进入和离开某状态触发动作(action 一次性)和活动(activity 持续性触发,直到离开某状态)</li><li>延迟事件与过度 after</li><li>服务调用 invoke,包括 promise 以及 两个状态机之间相互交互</li><li>历史状态节点,可以通过配置保存状态并且回退状态</li></ul><p>当然了,对比于 x-state 这种,还有其他的状态机工具,如 <a href="https://link.segmentfault.com/?enc=5qRWJOKsjL0cbjJUBo1eTQ%3D%3D.kRvHj3fw2nwN52kVHZYI6bEZ9%2FnhjME22%2BB%2FcXgPh1cx9%2FNhReuUYVUZF5G1Lk2iyHNdrg9rYJudVA%2BAkk94ug%3D%3D" rel="nofollow">javascript-state-machine</a> , <a href="https://link.segmentfault.com/?enc=U%2BypFtXnrqzOcCfHwf79QA%3D%3D.Q3hdpUVLQJCWQ8%2BAqeD4sxAwKUJh8Pb4XT6SB0glPTKbmBv%2FsSdSE8uxXFUsdJP7" rel="nofollow">Ego</a> 等。大家可以酌情考虑使用。</p><h2>总结</h2><p>对于现代框架而言,无论是如火如荼的 React Hook 还是渐入佳境的 Vue Compoistion Api,其本质都想提升状态逻辑的复用能力。但是考虑大部分场景下,状态本身的切换都是有特定约束的,如果仅仅靠良好的编程习惯,恐怕还是难以写出抑郁修改的代码。而 FSM 以及 XState 无疑是一把利器。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=3zeuVmgWreaW%2BiM8yTM77A%3D%3D.qPtvrxpZH%2FYyyAI%2FHJvg8fU5i%2FyoeSLSQAii286k%2FEma0g0I4AemommCRTrWhM9y" rel="nofollow">博客地址</a></p><h2>参考</h2><p><a href="https://link.segmentfault.com/?enc=Ml0BBV1vVVeopVodN1v2jQ%3D%3D.jL355DQd40ZEtticKKO%2F%2FkCbkW%2FjySjC8Ao7i3W6wNQ%3D" rel="nofollow">XState 文档</a></p><p><a href="https://link.segmentfault.com/?enc=Gmm%2BoyqBnbS4i8qj6G%2B9EA%3D%3D.4%2BJp3ZdxRsTz2cBligty1vMWX9rifte5Eb4C0QbedhyFGHf7cKeWXkPOPWzARQrLSHjqjdYsPWsJk9m8NF27XnMox7vCEu3NlrQzgABcb1E%3D" rel="nofollow">JavaScript与有限状态机</a></p>
根据背景色自适应文本颜色
https://segmentfault.com/a/1190000027086079
2020-10-07T21:08:05+08:00
2020-10-07T21:08:05+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
6
<p>针对企业服务来说,最终用户往往需要更加细化的信息分类方式,而打标签无疑是非常好的解决方案。</p><p>如果标签仅仅只提供几种颜色可能无法满足各个用户的实际需求。那么系统就需要为用户提供颜色选择。事实上我们完全无法预知用户选择了何种颜色,那么如果当前用户选择了黑色作为背景色,同时当前的字体颜色也是黑色,该标签就无法使用。如果配置背景色的同时还要求用户配置文字颜色,那么这个标签功能未免有些鸡肋。让用户觉得我们的开发水平有问题。</p><p>所以需要寻找一种解决方案来搞定这个问题。</p><h2>问题解析</h2><p>对于彩色转灰度,有一个著名的公式。我们可以把十六进制的代码分成3个部分,以获得单独的红色,绿色和蓝色的强度。用此算法逐个修改像素点的颜色可以将当前的彩色图片变为灰色图像。</p><pre><code class="js">gray = r * 0.299 + g * 0.587 + b * 0.114</code></pre><p>但是针对明亮和阴暗的颜色,经过公式的计算后一定会获得不同的数值,而针对当前不同值,我们取反就可以得到当前的文本颜色。即:</p><pre><code class="typescript">const textColor = (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000' : '#FFF' </code></pre><p>当然了,186 并不是一个确定的数值,你可以根据自己的需求调整一个新的数值。通过该算法,传入不同的背景色,就可以得到白色和黑色,或者自定义出比较合适的文本颜色。</p><h2>完善代码</h2><p>当然,虽然解决的方法非常简单,但是中间还是涉及了一些进制转换问题,这里简单传递数值如下所示。</p><pre><code class="typescript">/**
* @param backgroundColor 字符串 传入 #FFFBBC | FBC | FFBBCC 均可
*/
export function contrastTextColor(backgroundHexColor: string) {
let hex = backgroundHexColor
// 如果当前传入的参数以 # 开头,去除当前的
if (hex.startsWith('#')) {
hex = hex.substring(1);
}
// 如果当前传入的是 3 位小数值,直接转换为 6 位进行处理
if (hex.length === 3) {
hex = [hex[0], hex[0], hex[1], hex[1], hex[2], hex[2]].join('')
}
if (hex.length !== 6) {
throw new Error('Invalid background color.' + backgroundHexColor);
}
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
if ([r,g,b].some(x => Number.isNaN(x))) {
throw new Error('Invalid background color.' + backgroundHexColor);
}
const textColor = (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000' : '#FFF'
return textColor
}
</code></pre><p>我们还可以在其中添加 rgb 颜色,以及转换逻辑。</p><pre><code class="ts">/**
* @param backgroundColor 字符串
*/
export function contrastTextColor(backgroundHexColor: string) {
// 均转换为 hex 格式, 可以传入 rgb(222,33,44)。
// 如果当前字符串参数长度大于 7 rgb(,,) 最少为 8 个字符,则认为当前传入的数值为 rgb,进行转换
const backgroundHexColor = backgroundColor.length > 7 ? convertRGBToHex(backgroundColor) : backgroundColor
// ... 后面代码
}
/** 获取背景色中的多个值,即 rgb(2,2,2) => [2,2,2] */
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/
/** 转换 10 进制为 16 进制,
* 计算完成后时字符串前面加 0,同时取后两位数值。使得返回的数值一定是 两位数
* 如 E => 0E | FF => 0FF => FF
*/
const hex = (x: string) => ("0" + parseInt(x).toString(16)).slice(-2);
function convertRGBToHex(rgb: string): string {
const bg = rgb.match(rgbRegex);
if (!bg) {
// 返回空字符串,在后面判断长度为 6 时候会报错。不在此处进行操作
return ''
}
return ("#" + hex(bg[1]) + hex(bg[2]) + hex(bg[3])).toUpperCase();
}</code></pre><p>当然了,我们也可以在其中添加缓存代码,以便于减少计算量。</p><pre><code class="ts">// 使用 map 来缓存
const colorByBgColor = new Map()
// 缓存错误字符串
const CACHE_ERROR = 'error'
export function contrastTextColor(backgroundColor: string) {
// 获取缓存
const cacheColor = colorByBgColor.get(backgroundColor)
if (cacheColor) {
// 当前缓存错误,直接报错
if (cacheColor === CACHE_ERROR) {
throw new Error('Invalid background color.' + backgroundColor);
}
return colorByBgColor.get(backgroundColor)
}
// ...
if (hex.length !== 6) {
// 直接缓存错误
colorByBgColor.set(backgroundColor, CACHE_ERROR)
throw new Error('Invalid background color.' + backgroundColor);
}
// ...
if ([r,g,b].some(x => Number.isNaN(x))) {
// 直接缓存错误
colorByBgColor.set(backgroundColor, CACHE_ERROR)
throw new Error('Invalid background color.' + backgroundColor);
}
const textColor = (r * 0.299 + g * 0.587 + b * 0.114) > 186 ? '#000' : '#FFF'
// 缓存数据
colorByBgColor.set(backgroundColor, textColor)
return textColor
}</code></pre><p>完整代码可以在代码库中 <a href="https://link.segmentfault.com/?enc=MO8d9MCP3tv9Yf8tuGSQrA%3D%3D.YGqfeeZZI8pb2Ll0oIMKDyHAN1ztAw9DjbKCXZeax%2BcU5zE9xGBC7rPFd%2FhtelaOhacjkfhXFDiR4mqQ86J7hEi6A1E4fF2oiR%2BYXrXZX1Hi8yiJm5JzfwPUmyq%2FOtqE" rel="nofollow">转换问题颜色</a> 中看到。</p><p>当然了,如果你不需要严格遵循 W3C 准则,当前代码已经足够使用。但是如果你需要严格遵循你可以参考 <a href="https://link.segmentfault.com/?enc=FBfGidnSSAwEeA%2BrmxZx0w%3D%3D.s35xV3OM3ZUW0wRXEubadLL7bCQGwVSpXaoo2wKstZ%2BlLFOp0zM%2FrYKmPcr5cbAJ" rel="nofollow">http://stackoverflow.com/a/39...</a> 以及 <a href="https://link.segmentfault.com/?enc=g1Vwk5IjrB9mkBTuj81wpQ%3D%3D.etBkvxuiabnB0VD%2BmPjvfEinY1sakifSIBtPZRkuzJk%3D" rel="nofollow">https://www.w3.org/TR/WCAG20/</a>。</p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=lQeJ0glOkgeI%2Fi1vw0nYww%3D%3D.UhpyLoboEXnP39Me6RjKdiEsv1XOn54q5um7poSr3lXtAdVUR5EeG%2BYPNwC7CtTy" rel="nofollow">博客地址</a></p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=xHytD%2FOSnNlB2sWBp%2BFMPg%3D%3D.oPhTwbCrSRA83K2tN5tb%2FKUY0Imov7m6BdvKqQ6zCjPAcllddutD8TpwtOtBMzFR" rel="nofollow">stackoverflow 问题</a></p>
重修算法(1)—以 O(n) 复杂度构建树结构
https://segmentfault.com/a/1190000023732417
2020-08-23T22:26:08+08:00
2020-08-23T22:26:08+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
5
<p>曾经看过一部网络小说,主角在轮回中的第九世是个大反派。而全书都是主角在努力修炼改变第九世,算是圆满自己的修行。因为一些原因没看完,只是记得书名好像叫做《重修第九世》,但是利用收索引擎却没有找到这本书,应该是我记错了名字。不过就像这本书一样,我相信每个人都有自己没有圆满的事情,有些可以弥补,而有些却无法弥补。</p><p>我在大学时期并没有把数据结构与算法学好,在步入工作的这一段时间中,屡次想要去拾起算法。书倒是买了不少,视频也看过一些,但都半途而废了。于是决定通过写文章的形式来学习算法。一边通过讲解的方式加深自己的理解,同时帮助别人,另一方面也是希望通过 flag 的形式来保质保量的学习算法嘛,先定它一个小目标,一周至少两篇关于算法的博客。</p><p>基本上,在开发任意一款 to B 应用,我们都不可避免的涉及到树形结构的增删改查。就个人而言,我接触过所有的产品中,都不可避免的树结构。个人也参考并且手写过树组件以及树操作。对树结构的方案也有一定思考。于是,第一篇我决定就从实际业务出发,从树的构建开始:</p><p>这里为了简化,就简单设定。如果当前书节点不具有父节点,则 parentId 为0。对于其他需求,请自行设定配置项。</p><pre><code class="typescript">interface TreeItem {
id: number
// 父节点的 id
parentId: number
// 当前树的名称
name: string
}</code></pre><h2>for 循环使用</h2><p>事实上,在业务层面构建一棵树不算难。但是可能还是有一些算法基础不太好的小伙伴不能很快的写出来,此时我们可以用最简单的方式。直接多层 for 循环。</p><pre><code class="ts">function buildTree(treeItems) {
/** 构建第一层 */
const treeRoots = treeItems.filter(x => x.parentId === 0)
for (let first of treeRoots) {
/** 第一层子节点 */
first.children = treeItems.filter(x => x.partner === first.id)
/** 构建第二层 */
for(let second of first.children) {
// ...
}
}
}</code></pre><p>该方案在实际业务基本不可以用,除非在实际业务中限制树的层级并且只有前几层。而且层级越大,代码量也就越大,性能也就越差。</p><p>但是基本上所有的树操作在所有的节点中寻并插入父节点,所以该方案作为树结构的基本思路,我在此时列出以便大家可以循序渐进的思考和改进。</p><h2>递归构建</h2><p>通过上述代码,很简单就可以发现我们可以把当前问题分解为多个子问题。而每个子问题都是在寻找该节点的子节点,并且插入父节点的 children 中。根据这一点,我们不难写出如下递归代码。</p><pre><code class="javascript">/** 构建树 */
const buildTree = (treeItems, id = 0) =>
treeItems
// 找到当前节点所有的孩子
.filter(item => item.parentId === id)
// 继续递归找
.map(item => ({ ...item, children: buildTree(treeItems, item.id) }));
</code></pre><p>根据当前递归,我们减少了代码的冗余,并且可以“无限”的构建下去。不计算递归本身的时间复杂度(后面有机会再说递归本身耗费的时间复杂度)的情况下,每一次都要遍历一次数组。而数组每一个数据都要便利一次,可以得出时间复杂度是 O(n<sup>2</sup>)。</p><p>对于大部分业务需求来说,现在可以结束了,因为在大部分业务场景中树结构本身不太会有很多的数据量。就算数据量很大的情况下,我们也可以通过组件延迟加载的方式解决。</p><h2>利用对象引用构建树</h2><p>上述方案是常规方案,但是问题在于,性能还是低下。</p><p>性能低下的原因之一在于递归更加耗费性能而且可能会导致栈溢出错误(js 到目前没有实现尾递归优化),这一点我们可以利用递归转循环来做(后面再说,现在没必要)。</p><p>同时在每次构建一个节点的孩子时,都需要遍历整个数组一次,这个也是很大的损耗。事实上,优秀的算法应该是可以<strong>复用</strong>前面已经计算过的属性。</p><p>那么我们是否能够通过一次循环解决子节点问题呢?答案也是肯定的。先上代码:</p><pre><code class="javascript">function buildTreeOptimize (items) {
// 由业务决定是否需要对 items 深拷贝一次。这里暂时不做
// 把每个子节点保存起来,以便后面插入父节点
const treeDataByParentId = new Map()
// 对每节点循环,找其父节点,并且放到数组中
items.forEach(item => {
// map 中有父数据,插入,没有,构建并插入
treeDataByParentId.has(item.parentId) ? treeDataByParentId.get(item.parentId).push(item) : treeDataByParentId.set(item.parentId, [item])
})
// 树第一层
const treeRoots = []
// 对每一个节点循环,找其子节点
items.forEach(item => {
// 子节点插入当前节点
item.children = (treeDataByParentId.get(item.id) || [])
// 当前节点不具备父节点,插入第一层数组中
if (!item.parentId) {
treeRoots.push({item})
}
})
// 返回树结构
return treeData
}
</code></pre><p>两次 for 循环完成了树的构建?该算法时间复杂度是O(n)!! 可以说相当快,毕竟对于之前的代码,每个节点查询一次都要 O(n) 一次。</p><p>在第一次循环中,我们帮助所有的节点寻找到了父节点。即都存储到了 map 中去。在这一步中,所有的子节点按照服务端给予的数据顺序依次插入。第二次循环中,我们直接在原 items 循环并插入第一布找到的子节点。插入节点.</p><p>其实这个算法的精妙之处在于第一步塞入 map 中的树对象和第二步塞入父节点中的树对象是是同一个对象!!!</p><p>表面上,第二步只是寻找每一个节点的子节点,但实际上在把当前节点修改的“同时”,map 中的对象节点也被改掉了,因为他们都是同一个对象(每一层的父子关系都搞定了)。所以最终仅仅只通过两次遍历便拿到关于树的数据。</p><p>大部分情况下上在业务层面做到这里就没什么太大问题了。例如 Element Tree 树形组件。以及 Ant Design 的 TreeSelect 组件。当然,同样的代码依然适合服务端开发。</p><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p><p><a href="https://link.segmentfault.com/?enc=0JYWuaQDhvwWwdQuuCTMPA%3D%3D.NXxzRN6h3WxLh6jX59MajBHRs0W2sDjDc%2BWMaGU8vv%2BMsMPxRMiuE%2BW38kAZeuOJ" rel="nofollow">博客地址</a></p>
玩转 CSS 变量
https://segmentfault.com/a/1190000023704816
2020-08-20T19:46:04+08:00
2020-08-20T19:46:04+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
24
<p>如果当年的 CSS 预处理器变量对于初入前端的我来说是开启了新世界的大门,那么 CSS 变量对于我来说无疑就是晴天霹雳。其功能不但可以优雅的处理之前 js 不好处理或不适合的业务需求。更在创造力无穷的前端开发者手中大放异彩。</p><h2>基础用法</h2><p>在前端的领域中,标准的实现总是比社区的约定要慢的多,前端框架最喜欢的 $ 被 Sass 变量用掉了。而最常用的 @ 也被 Less 用掉了。官方为了让 CSS 变量也能够在 Sass 及 Less 中使用,无奈只能妥协的使用 --。</p><pre><code class="html"><style>
/* 在 body 选择器中声明了两个变量 */
body {
--primary-color: red;
/* 变量名大小写敏感,--primary-color 和 --PRIMARY-COLOR 是两个不同变量 */
--PRIMARY-COLOR: initial;
}
/** 同一个 CSS 变量,可以在多个选择器内声明。优先级高的会替换优先级低的 */
main {
--primary-color: blue;
}
/** 使用 CSS 变量 */
.text-primary {
/* var() 函数用于读取变量。 */
color: var(--primary-color)
}
<style>
<!-- 呈现红色字体,body 选择器的颜色 -->
<div class="text-primary">red</div>
<!-- 呈现蓝色字体,main 选择器定义的颜色 -->
<main class="text-primary">blue</main>
<!-- 呈现紫色字体,当前内联样式表的定义 -->
<div style='--primary-color: purple" class="text-primary">purple</main> </code></pre><p>这里我们可以看到针对同一个 CSS 变量,可以在多个选择器内声明。读取的时候,优先级最高的声明生效。这与 CSS 的"层叠"(cascade)规则是一致的。</p><p>由于这个原因,全局的变量通常放在根元素<code>:root</code>里面,确保任何选择器都可以读取它们。</p><pre><code class="css">:root {
--primary-color: #06c;
}</code></pre><p>同时, CSS 变量提供了 JavaScript 与 CSS 通信的方法。就是利用 js 操作 css 变量。我们可以使用:</p><pre><code class="html"><style>
/* ...和上面 CSS 一致 */
</style>
<!-- 呈现黄色字体 -->
<div class="text-primary">red</div>
<!-- 呈现蓝色字体,main 选择器定义的颜色 -->
<main id='primary' class="text-primary">blue</main>
<!-- 呈现紫色字体,当前内联样式表的定义 -->
<div id="secondary" style='--primary-color: purple" class="text-primary">purple</main>
<script>
// 设置变量
document.body.style.setProperty('--primary-color', 'yellow');
// 设置变量,js DOM 元素 ID 就是全局变量,所以直接设置 main 即可
// 变为 红色
primary.style.setProperty('--primary-color', 'red');
// 变为 黄色,因为当前样式被移除了,使用 body 上面样式
secondary.style.removeProperty('--primary-color');
// 通过动态计算获取变量值
getComputedStyle(document.body).getPropertyValue('--primary-color')
</script>
</code></pre><p>我们可以在业务项目中定义以及替换 CSS 变量,大家可以参考 <a href="https://link.segmentfault.com/?enc=ovDlvC4WFcuP0OqrfmUINw%3D%3D.3Hf9oWkOdy%2B6quWfhP7tF6APKPCqlY6AezZlJZlutSs3msMJl1kAsA%2Bd68ebUlIG" rel="nofollow">mvp.css</a>。该库大量使用了 CSS 变量并且让你去根据自己需求修改它。</p><pre><code class="css">:root {
--border-radius: 5px;
--box-shadow: 2px 2px 10px;
--color: #118bee;
--color-accent: #118bee0b;
--color-bg: #fff;
--color-bg-secondary: #e9e9e9;
--color-secondary: #920de9;
--color-secondary-accent: #920de90b;
--color-shadow: #f4f4f4;
--color-text: #000;
--color-text-secondary: #999;
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
--hover-brightness: 1.2;
--justify-important: center;
--justify-normal: left;
--line-height: 150%;
--width-card: 285px;
--width-card-medium: 460px;
--width-card-wide: 800px;
--width-content: 1080px;
}</code></pre><p>我们可以看到基于 CSS 变量,可以更友好的和设计师的设计意图结合在一起。也易于修改,在业务项目中合理使用无疑可以事半功倍。</p><h2>实现默认配置</h2><p>如果让我来思考,我肯定无法想象出结合 CSS 预处理器 + CSS 变量便可以实现组件样式的默认配置。这里我先介绍两个关于该功能的前置知识点:</p><p>事实上,CSS 变量的 var() 函数还可以使用第二个参数,表示变量的默认值。如果该变量此前没有定义或者是无效值,就会使用这个默认值。</p><pre><code class="css">/* 没有设置过 --primary-color,颜色默认使用 #7F583F */
color: var(--primary-color, #7F583F);</code></pre><p>虽然目前 CSS 变量不是新的属性,但终究不是所有的浏览器都支持 CSS 变量的,这里我们还是要考虑一下优雅降级。</p><pre><code class="css">/* 对于不支持 CSS 变量的浏览器,可以采用下面的写法。*/
a {
/* 颜色默认值 */
color: #7F583F;
/* 不支持则不识别该条规则 */
color: var(--primary);
}</code></pre><p>结合 CSS 处理器 + CSS 变量便可以实现组件样式的默认配置。这里参考了有赞的 <a href="https://link.segmentfault.com/?enc=LGZkyYPZdg4mu9LHRUY4Tw%3D%3D.j7k1jiaLiC1UBrkIYx00K0fvP3KM63O9%2FijRhaSEHDB2JmCROZ3MgAHir25V5u8H" rel="nofollow">Vant Weapp</a> 的做法。有赞代码 <a href="https://link.segmentfault.com/?enc=bWJs%2F93wE0sgsvFDyPHJBA%3D%3D.vBkJB3kWS61I6XghZ6NAjKQLVPJge5dc4Pd6yNY5v7%2FddaaaVF0zgysbxiThBUNS3Y3LVeEpOupzuc62S%2FC2xUqK%2Fb6j7cJc%2BabbboB3O9PIjecnJXIFOAKKUoCat%2F0%2F" rel="nofollow"><strong>theme.less</strong></a> 如下所示:</p><pre><code class="less">// 先导入所有 Less 变量
@import (reference) './var.less';
// 利用正则去替换 Less 变量 为 CSS 变量
.theme(@property, @imp) {
@{property}: e(replace(@imp, '@([^() ]+)', '@{$1}', 'ig'));
@{property}: e(replace(@imp, '@([^() ]+)', 'var(--$1, @{$1})', 'ig'));
}</code></pre><p>函数效果如下所示:</p><pre><code class="less">@import '../common/style/theme.less';
.van-button {
// ... 其他省略
.theme(height, '@button-default-height');
.theme(line-height, '@button-line-height');
.theme(font-size, '@button-default-font-size');
}
// => less 编译之后生成
.van-button{
// ... 其他省略
height:44px;
height:var(--button-default-height,44px);
line-height:20px;
line-height:var(--button-line-height,20px);
font-size:16px;
font-size:var(--button-default-font-size,16px);
}</code></pre><p>我们可以看到每调用一次 Less 函数将会被编译成两个属性。第一个属性的设定对于不支持 CSS 变量的设备可以直接使用,如果当前设备支持 CSS 变量,则会使用 CSS 变量,但是由于当前 CSS 变量未定义,就会使用变量的默认值。虽然 '@button-default-height 虽然也是一个变量,但是该变量仅仅只是 less 变量,最终生成的代码中并没有 --button-default-height 这样的变量。此时我们就可以在使用样式的位置或者 :root 中添加变量 --button-default-height。</p><p>这种方式更适合组件开发,因为该方案不声明任何 css 变量,只是预留的 css 变量名称和默认属性。这样的话,无论开发者的选择器优先度有多低,代码都可以很容易的覆盖默认属性。因为我们仅仅使用 css 的默认值。</p><p>大家可能有时候会想,这样的话,我们不是有更多的代码了吗?其实未必,事实上我们可以直接直接在页面内部定义变量样式。其他组件直接通过 style 去使用页面内的变量。当然了,事实上书写的代码多少,重点在于想要控制默认样式的粒度大小。粒度越小,则需要在各个组件内部书写的变量越多,粒度大,我们也就不必考虑太多。</p><h2>Space Toggle 逻辑切换</h2><p>CSS 没有逻辑切换似乎是一种共识,但是我们可以利用选框(单选与多选)和 CSS 变量来实现判断逻辑。我们先来看看如何使用 CSS 变量。</p><pre><code class="html"><style>
.red-box {
--toggler: ;
--red-if-toggler: var(--toggler) red;
background: var(--red-if-toggler, green); /* will be red! */
}
.green-box {
--toggler: initial;
--red-if-toggler: var(--toggler) red;
background: var(--red-if-toggler, green); /* will be green! */
}
</style>
<!-- 宽度高度为 100px 的 红色盒子 -->
<div
style="height: 100px; width: 100px"
class="red-box"
></div>
<!-- 宽度高度为 100px 的 绿色盒子 -->
<div
style="height: 100px; width: 100px"
class="green-box"
></div> </code></pre><p>这里因为一个变量 --toggler 使用空格 或者 initial 而产生了不同的结果,基于这样的结果我们不难想象我们可以触发变量的修改而产生不同的结果。</p><p>他不是一个 bug,也不是一个 hack。他的原理完完全全的在 <a href="https://link.segmentfault.com/?enc=0QQ6%2BR%2F75COm5U8cakwlow%3D%3D.5S%2FkJ93vXXHTJl83RVFGQ68ENZ4dw06nQ1SXETzw4r82rjTw2ypexUQ6HhjFOuoGjRXYsQbgJ6XyKEPw6QMZjQ%3D%3D" rel="nofollow">CSS Custom Properties 规范</a> 中。</p><blockquote>This value serializes as the empty string, but actually writing an empty value into a custom property, like --foo: ;, is a valid (empty) value, not the guaranteed-invalid value. If, for whatever reason, one wants to manually reset a variable to the guaranteed-invalid value, using the keyword initial will do this.</blockquote><p>解释如下,事实上 -foo: ; 这个变量并不是一个无效值,它是一个空值。initial 才是 CSS 变量的无效值。其实这也可以理解,css 没有所谓的空字符串,空白也不代表着无效,只能使用特定值来表示该变量无效。这个时候,我们再回头来看原来的 CSS 代码。</p><pre><code class="css">.red-box {
/* 当前为空值 */
--toggler: ;
/* 因为 var(--toggler) 得到了空,所以得到结果 为 --red-if-toggler: red */
--red-if-toggler: var(--toggler) red;
/** 变量是 red, 不会使用 green */
background: var(--red-if-toggler, green); /* will be red! */
}
.green-box {
/** 当前为无效值 */
--toggler: initial;
/** 仍旧无效数据,因为 var 只会在参数不是 initial 时候进行替换 */
--red-if-toggler: var(--toggler) red;
/** 最终无效值没用,得到绿色 */
background: var(--red-if-toggler, green); /* will be green! */
/* 根据当前的功能,我们甚至可以做到 and 和 or 的逻辑
* --tog1 --tog2 --tog3 同时为 空值时是 红色
*/
--red-if-togglersalltrue: var(--tog1) var(--tog2) var(--tog3) red;
/*
* --tog1 --tog2 --tog3 任意为 空值时是 红色
*/
--red-if-anytogglertrue: var(--tog1, var(--tog2, var(--tog3))) red;
}</code></pre><h2>新式媒体查询</h2><p>当我们需要开发响应式网站的时候,我们必须要使用媒体查询 @media。先看一下用传统的方式编写这个基本的响应式 CSS:</p><pre><code class="css">.breakpoints-demo > * {
width: 100%;
background: red;
}
@media (min-width: 37.5em) and (max-width: 56.249em) {
.breakpoints-demo > * {
width: 49%;
}
}
@media (min-width: 56.25em) and (max-width: 74.99em) {
.breakpoints-demo > * {
width: 32%;
}
}
@media (min-width: 56.25em) {
.breakpoints-demo > * {
background: green;
}
}
@media (min-width: 75em) {
.breakpoints-demo > * {
width: 24%;
}
}</code></pre><p>同样,我们可以利用 css 变量来优化代码结构,我们可以写出这样的代码:</p><pre><code class="css">/** 移动优先的样式规则 */
.breakpoints-demo > * {
/** 小于 37.5em, 宽度 100% */
--xs-width: var(--media-xs) 100%;
/** 小于 56.249em, 宽度 49% */
--sm-width: var(--media-sm) 49%;
--md-width: var(--media-md) 32%;
--lg-width: var(--media-gte-lg) 24%;
width: var(--xs-width, var(--sm-width, var(--md-width, var(--lg-width))));
--sm-and-down-bg: var(--media-lte-sm) red;
--md-and-up-bg: var(--media-gte-md) green;
background: var(--sm-and-down-bg, var(--md-and-up-bg));
}</code></pre><p>可以看出,第二种 CSS 代码非常清晰,数据和逻辑保持在一个 CSS 规则中,而不是被 @media 切割到多个区块中。这样,不但更容易编写,也更加容易开发者读。详情可以参考 <a href="https://link.segmentfault.com/?enc=lxj8c2abVHIuNlrMWG%2BBrA%3D%3D.Q6YZSxQ2kr1moOCV4AdBxbK%2B7rYvZg5NRswYHmNeS7Pr0iYE7PTP1USW37qzWPpb" rel="nofollow">css-media-vars</a>。该代码库仅仅只有 3kb 大小,但是却是把整个编写代码的风格修改的完全不同。原理如下所示:</p><pre><code class="css">
/**
* css-media-vars
* BSD 2-Clause License
* Copyright (c) James0x57, PropJockey, 2020
*/
html {
--media-print: initial;
--media-screen: initial;
--media-speech: initial;
--media-xs: initial;
--media-sm: initial;
--media-md: initial;
--media-lg: initial;
--media-xl: initial;
/* ... */
--media-pointer-fine: initial;
--media-pointer-none: initial;
}
/* 把当前变量变为空值 */
@media print {
html { --media-print: ; }
}
@media screen {
html { --media-screen: ; }
}
@media speech {
html { --media-speech: ; }
}
/* 把当前变量变为空值 */
@media (max-width: 37.499em) {
html {
--media-xs: ;
--media-lte-sm: ;
--media-lte-md: ;
--media-lte-lg: ;
}
}
</code></pre><h2>其他</h2><p>继 <a href="https://link.segmentfault.com/?enc=cdP6B2LIz9lb%2FFtOrzvwvA%3D%3D.NR%2FFQeWbCYh%2F1U%2F38u8YL0snOkzOT1NRwFMNBPBCacgc3bGxfE2GsiqmZlNEuxAQ" rel="nofollow">CSS 键盘记录器</a> 暴露了 CSS 安全性问题之后,CSS 变量又一次让我看到了玩技术是怎么样的。CSS Space Toggle 技术不但可以应用于上面的功能,甚至还可以编写 UI 库 <a href="https://link.segmentfault.com/?enc=PsOPUoR1T4AfaNQ3Itx4zQ%3D%3D.U%2BMRKNrbZlHdtdZqOZ0Y5BosCPpI9%2B3N5oTLrJo60aA%3D" rel="nofollow">augmented-ui</a> 以及 <a href="https://link.segmentfault.com/?enc=Q6lb73J2JWKB%2BSENFXrQBw%3D%3D.zBvZJ5MlHYa4Sm7FpkJ0V0k6uG91VuJ3hCrWe642zdWu0ObEytYNPtM1zOO%2BrjLU" rel="nofollow">扫雷</a> 游戏。这简直让我眼界大开。在我有限的开发生涯中,难找到第二种类似于 css 这种设计意图和使用方式差异如此之大的技术。</p><p>CSS 是很有趣的,而 CSS 的有趣之处就在于最终呈现出来的技能强弱与你自身的思维方式,创造力是密切相关的。上文只是介绍了 CSS 变量的一些玩法,也许有更多有意思的玩法,不过这就需要大家的创造力了。</p><h2>参考资料</h2><p><a href="https://link.segmentfault.com/?enc=%2ByvAA2DUE05XLVb1pNaRcA%3D%3D.c5cDtcx%2Bp%2Bnxyllm2lZuLRuz7PUk2l%2FU3KW1fvhDdgGBS3gmNR%2BvL3J9dOGy4emU" rel="nofollow">augmented-ui</a></p><p><a href="https://link.segmentfault.com/?enc=cvdziQxrEaO%2FJW7NVYf47Q%3D%3D.7Kl7UHBUC0qCuwvGoEtSFv4hk50ssazT7anrN8BSPgJK6T2R3MhFHMWtek9OWFNb" rel="nofollow">css-media-vars</a></p><p><a href="https://link.segmentfault.com/?enc=yg51Qnse2AUFekQoeN%2FUQA%3D%3D.CmLv5ds9t23PxBeZyUJjeuSmhNr8h4DoLSAO%2BVMDBaXXz1Heog4c%2FD9FMy34Kium" rel="nofollow">css-sweeper</a></p><h2>鼓励一下</h2><p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=g2bw8iA98Zpgtt8I5575AQ%3D%3D.ii2VRS9Q3j9slTDSdtbspaJ4HvZqVBDLH2Tz8MEO4FBaaAYnHTQxIRwtWflKpZ%2Fl" rel="nofollow">博客地址</a></p>
聊聊游戏开发与动画利器 raf
https://segmentfault.com/a/1190000022808168
2020-06-01T22:58:01+08:00
2020-06-01T22:58:01+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
11
<p>今年下半年打算正式撸一撸小游戏,正好这些天整理一下有关游戏的一些知识,当然了,目前还是打算使用浏览器网页进行游戏开发。</p>
<h2>网页游戏开发的优势与未来</h2>
<p>如果使用其他语言开发游戏,无论游戏本身大小与否,我们都需要游戏引擎来帮助我们构建开发,而对于浏览器来说,我们在开发小游戏时候,利用浏览器本身提供的组件和 api 就可以直接进行业务处理,我们也可以更加高效的学习与实践游戏逻辑。同时网页游戏的构建与发布也非常简单。</p>
<p>例如像 <a href="https://link.segmentfault.com/?enc=pIf%2FvPSUZgzO%2B4kdq7o9yw%3D%3D.CRYWGkAZwcAr9JGiHTRkrL3LBvlW6YFnBzpFYpVRVi8%3D" rel="nofollow"><strong>Js13kGames</strong> </a> (Js13kGames是一个针对 HTML5游戏开发者的 JavaScript 编码竞赛,该竞赛的有趣之处在于将文件大小限制设置为13 kb ) 这样的限制代码量的游戏开发竞赛。对于非网页游戏开发来说,这基本上是不可能完成的,因为它们不具备有像浏览器这种量级的通用型的工具。</p>
<p>随着时间的发展,浏览器的功能,性能也在不断提升。通过 WebGL, WebAssembly 各种层出不穷的技术。让很多之前想都不敢想的功能在浏览器上实现。同时,伴随着 5G 到来,网速的提升,在浏览器上开发游戏充满了无限的可能。</p>
<p>当然了,事实上,不同的游戏需要不同的组件,其中包括数学库,随机算法,碰撞及物理引擎,音频,资源管理,AI机制等等等等,不过在浏览器环境下,这些组件都可以做到按需引用。</p>
<h2>游戏循环架构与风格</h2>
<p>游戏本身是基于动画的。不知道大家在小学的时候有没有买果或者玩过翻纸动画?如果你没有了解过,也可以看一看bilibili 中的视频 <a href="https://www.bilibili.com/video/av24463374/?p=2">高中生自制的翻纸动画短片</a>。视频中通过快速翻动纸张来实现两个火彩人打架的精彩动画。事实上,我们的电脑,手机设备能够展示动画都是基于此原理。</p>
<p>所谓动画,就是不间断,基于时间和逻辑不断更新数据以及重绘界面。其核心一定会有至少一个循环。这里我介绍几种常见架构。</p>
<h3>视窗消息泵</h3>
<p>在 Windows 平台中,游戏除了需要对自身进行服务外,还需要处理来自于 Windows 系统本身的消息,因此,Windows中有游戏都会有一段被称为<strong>消息泵</strong>的代码。其原理是先处理 Windows 的消息,之后再处理游戏循环逻辑。</p>
<pre><code class="c">// 不断循环处理
while (true) {
MSG msg;
// 如果当前消息队列中有消息,取出消息
while(PeekMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg)
}
// 执行游戏循环,类似于更新与重绘
RunGame()
}</code></pre>
<p>以上代码的副作用在于默认设置了游戏处理消息的的优先级,循环中先处理了 Windows 内部消息。如果游戏在调整界面或者移动视窗时候,游戏就会卡住不动。</p>
<h3>回调与事件驱动</h3>
<p>很多游戏框架(包括浏览器)已经在内部实现了主游戏循环,我们无法直接介入内部循环机制,我们只能够填充框架中空缺的自定义部分。通过编写回调函数或者覆盖框架预设行为来实行。</p>
<p>例如一些游戏渲染引擎是这样实现的:</p>
<pre><code class="c++">while(true) {
// 渲染前执行(游戏子系统逻辑)
for(each frameListener) {
// 回调函数
frameListener.frameStarted()
}
// 渲染
renderCurrentScene()
// 场景渲染后执行
for(each frameListener) {
// 回调函数
frameListener.frameEnded()
}
// 结束场景与交换缓冲区
finalizeSceneAndSwapBuffers()
}</code></pre>
<p>而另一种回调是基于事件驱动,实现方式为:事件系统会将事件置于不同的队列之中,然后在合适的时机从队列中取出事件进行处理。这种方式也就是浏览器的处理方式,利用消息队列和事件循环来让网页动起来。</p>
<pre><code class="c++">while(true) {
// 从任务队列中取出任务
Task task = task_queue.takeTask();
// 执行任务
ProcessTask(task);
// 执行各种其他任务 Process...
}</code></pre>
<p>而浏览器提供的回调就包括 <strong>setTimeout</strong>(延迟执行) 与 <strong>setInterval </strong>(间隔执行) <strong>requestAnimationFrame</strong>(动画渲染) <strong>requestIdleCallback </strong>(低优先级任务)。前两者执行时机由用户指定时机执行,后两者是由浏览器控制执行。</p>
<p>setTimeout 是一个定时器,用来指定某个函数在多少毫秒之后执行。他会返回一个编号,表示当前定时器的编号,同时你也可通过 clearTimeout 加入编号来取消定时器的执行。</p>
<pre><code class="js">// 注册 10 ms 后打印 hello world
const id = setTimeout(() => {
console.log('hello world')
}, 10)
clearTimeout(id)</code></pre>
<p>结合上面的事件驱动模型,可以看出该回调函数就是在<strong>循环</strong>中不断执行任务,当发现延迟任务队列中的某个任务超过了当前的时间节点(通过发起时间和设定的延迟时间来计算),就直接取出任务执行调用。等到期的任务都执行完成后,在进行下一个循环过程,通过这样的方式,一个完整的定时器就实现了。浏览器取消定时器则是通过 id 查找到对应的任务,直接将任务从队列中删除。</p>
<p>我们也可以通过 setTimout 回调函数内再次执行 setTimout 来实现 setInterval 函数的功能,看起来也类似于间隔执行,其实还是会有一定的区别。</p>
<pre><code class="js">// 在回调函数完成后才去设置定时器,时间会超过 16 ms
setTimeout(function render(){
// 执行需要 6 ms
// 定时 16ms 后
console.log(+ new Date())
setTimeout(render, 500);
}, 500)
// 尝试每 16 ms 执行一次,不管内部回调函数耗时
setInterval(function render(){
console.log(+ new Date())
}, 16)</code></pre>
<p>重点在于,JavaScript 本身是单线程的,同时基于上面的事件驱动代码,我们只能依赖任务加入顺序依次处理任务,无法切断当前任务的执行,我们只能够控制定时器任务何时能够加入队列,却无法控制何时执行,如果其他任务执行的时间过久的话,定时器任务就必须延后执行,开发者没有任何办法。 当然了,社区的力量也是无穷的,facebook 的 React Fiber 就是实现了在渲染更新过程中断当前任务,执行优先级更高的任务的功能。</p>
<p>不过像使用浏览器的系统(包括游戏等)都是软实时系统。所谓软实时系统,就是即使错过限定期限也不会造成灾难性后果——错过了当前帧数,现实世界不会因此造成灾难性后果,与此相比,航空电子,核能发电等系统都属于是硬实时系统,错过期限会有严重的后果。不过就算如此,谁会喜欢一个经常卡顿的系统呢?所以实际业务开发中的性能优化还是重中之重。</p>
<p>当然了, requestAnimationFrame 本身也是回调函数,那么这个函数究竟有什么过人之处可以提升动画性能呢?在此之前,我们先介绍一下屏幕刷新率与 Fps 的区别。</p>
<h2>屏幕刷新率与 FPS</h2>
<p>屏幕刷新率即图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,它的单位是赫兹(Hz)。 对于一般笔记本电脑,这个频率大概是 60Hz,在 Window 10 上 可以通过桌面上<strong>右键->显示设置->高级显示设置</strong> 中查看和设置。屏幕刷新率表示显示器的物理刷新速度。</p>
<p><img src="/img/bVbHRBP" alt="image-20200512223617840.png" title="image-20200512223617840.png"></p>
<p>对于我的电脑来说,无论我目前是在浏览网页还是在挂机状态,当前显示器都以 1 秒刷新 165 次当前界面,该数值取决于显示器。我们也可以通过修改<strong>适配器属性->监视器</strong>来调整屏幕刷新率,一般来说,我们只要调整到眼睛舒适即可。</p>
<p>事实上,仅仅靠显示屏的刷新率是没有用的,就像上面的循环机制,如果 GPU 处理当前任务的耗时大于当前屏幕刷新的间隔时段。就无法按时提供图像。该特性也就是我们所说的 FPS 每秒传输帧数(Frames Per Second)。而对帧数起到决定性的是电脑中的显卡,显卡性能越强,帧数也就越高。</p>
<p>即 <strong>FPS 帧数是由显卡决定,刷新率是由显示器决定</strong>。如果显卡输出 FPS 低于显示屏的刷新率,则在显示屏刷新中将会复用同一张画面。反过来,显示器也会丢弃提供过多的图像。</p>
<h2>requestAnimationFrame (Raf) 使用与机制</h2>
<p>下面我们就谈一谈 raf 函数对比其他定时器回调的优点。</p>
<p>首先,raf 函数本身并不是一个新特性,就连 IE 10 都提供了支持,所以这里不再介绍兼容。设置这个 API 的目的是为了让各种网页动画效果(DOM动画、Canvas动画、SVG动画、WebGL动画)能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。使用方式如下:</p>
<pre><code class="js">let start = null;
let element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
element.style.left = Math.min(progress / 10, 200) + 'px';
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);</code></pre>
<p>可以看到,其实函数使用方式和 setTimeout 基本一致,只不过不需要提供第二个参数。</p>
<p>那么在没有第二个参数的情况下,函数究竟多久执行一次呢?raf 充分利用显示器的刷新机制,执行频率和显示屏的刷新频率保持同步,利用这个刷新频率进行页面重绘。也就是说在我的电脑上,raf 函数每秒执行 165 次。也就是 6 ms 执行一次,如果其他任务执行事件过长的话,该函数顺延到合适的时机。也就是其他任务执行时间大于 6 ms,函数就会在 12 ms 时候第二次执行,如果大于 12 ms,则会在 18 ms 时候第二次执行。</p>
<p>当看到 raf 函数和屏幕刷新率一致时候,大家也能大致的猜测出,浏览器为什么要提供 raf 函数了。因为显示器和 GPU 属于两个不同的系统,两者很难协调的运行,即使两者周期一致,也是很难同步起来。</p>
<p>所以当显示器将一帧画面绘制完成后,并在准备读取下一帧之前,显示器会发出一个垂直同步信号(vertical synchronization)给 GPU,简称 VSync。这时候浏览器就会利用 VSync 信号来对 raf 进行调用。</p>
<p>CSS 动画是由浏览器渲染进程自动处理的,所以浏览器直接让 css 动画于 VSync 的时钟保持一致。但是 js 中 setTimout 和 setInterval 由开发者控制,调用时机基本不可能和 VSync 保持一致。所以浏览器为 js 提供了raf 函数,用来和 VSync 的时钟周期同步执行。</p>
<p>针对于 VSync,大家可以参考 <a href="https://link.segmentfault.com/?enc=%2BTpqrt3pKUiibNPmFSWFWA%3D%3D.HlDWaFHzrh9g0iRBbOUMt0M2YfizoewVk6y%2FkW1PSQJ8uTqRgbBP6kbnQdLGpjpzEZhJoO01u%2BUtmmYPckLh%2FQ%3D%3D" rel="nofollow">理解 VSync</a> 这篇文章。</p>
<h2>扩展</h2>
<p>注意: 浏览器为了优化后台页面的加载损耗以及降低耗电量,会让没有激活的浏览器标签 setTimeout 执行间隔大于 1s。requestAnimationFrame 执行速率会不断下降,同理 requestIdleCallback 也是如此。</p>
<p>同时,相信浏览器后面也会函数调度提供更加方便的支持,大家感兴趣也可以了解一下 <a href="https://link.segmentfault.com/?enc=QEoy9soYqcbBtJwQ5R89CA%3D%3D.obGQwiYiQLjkQxdgG0JQh7hBzJZBkW980sfJcIQKXGhHkXWBLJ3GdTw1K2I6j12sGaEOHaPMzUTvgHiJPiG9ew%3D%3D" rel="nofollow">isInputPending</a>,不过该提案还处于起草阶段,工作组尚未批准,更不用说投入生产中了。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=TbvNmYEbpbtuRswl58hBDg%3D%3D.Ve%2BKHLgpvq00a%2FEaViPbbjigXn5xaRS%2BAF%2FjYWUHD7yfudn9bxgbp5lYIla7QvTR" rel="nofollow">博客地址</a></p>
<h3>参考资料</h3>
<p><a href="https://link.segmentfault.com/?enc=BiXi27hl6hjrN0Eryb7U0Q%3D%3D.8AIBcpHz0BcDipnKkct%2FR4%2FQPE9eYhtle1mq4y9yGwaBUWiinTH7J83IjNnpR4jF" rel="nofollow">《游戏引擎架构》</a></p>
<p><a href="https://link.segmentfault.com/?enc=5e1daQl4ClFODn9af%2Bgf2g%3D%3D.CFb9U6oYqpCkx%2BKx59mWZx2hCFZTjsqqBb8tjtZ%2BlhbTuYEjCDCw%2FrXH9GilSSrfq%2BTcdX5fFWPuUuMKEoWlAA%3D%3D" rel="nofollow">深入理解 requestAnimationFrame</a></p>
<p><a href="https://link.segmentfault.com/?enc=mdyDB7x0OVtqglR2%2FIBOyw%3D%3D.SXvdiCaZYaSrYd9W5WzHIyw04ygqU72xnJ0lW1uLQrADn6pSpeBePaOfcDDWnaDizsvr5CxQSNwAtrKjcwgYfw%3D%3D" rel="nofollow">理解 VSync</a></p>
移动端列表查询最佳实践
https://segmentfault.com/a/1190000022549636
2020-05-05T20:17:18+08:00
2020-05-05T20:17:18+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
9
<p>无论是 pc 端还是移动端,无可避免都会涉及到列表查询有关的操作,但对于这两种不同的设备,其列表查询的最佳处理方式也是完全不同。</p>
<p>对于 pc 端列表查询来说,前端通常是给与服务端当前需要获取的数据量(如 pageCount,limit 等参数)以及所需要获取数据的位置(如 pageSize,offset 等参数)作为查询条件。然后服务端然后返回数据总数,以及当前数据,前端再结合这些数据显示页面总数等信息。这里我称为相对位置取数。</p>
<p>对于移动端而言,没有pc 端那么大的空间展示以及操作,所以基本上都会采用下拉取数这种方案。</p>
<p>那么我们在处理移动端列表查询时候使用这种相对位置取数会有什么问题呢?</p>
<h2>相对位置取数存在的问题</h2>
<h3>性能劣势</h3>
<p>通过相对位置取数会具有性能问题,因为一旦使用 offset 信息来获取数据,随着页数的增加,响应速度也会变的越来越慢。因为在数据库层面,我们每次所获取的数据都是“从头开始第几条”,每次我们都需要从第一条开始计算,计算后舍弃前面的数据,只取最后多条数据返回前端。</p>
<p>当然了,对于相对位置取数来说,数据库优化是必然的,这里我就不多做赘述了。对于前端开发来说,优秀的的查询条件设计可以在一定方面解决此问题。</p>
<h3>数据显示重复</h3>
<p>事实上,对于一个实际运行的项目而言,数据更新才是常态,如果数据更新的频率很高或者你在当前页停留的时间过久的话,会导致当前获取的数据出现一定的偏差。</p>
<p>例如:当你在获取最开始的 20 条数据后,正准备获取紧接着的后 20 条数据时,在这段时间内 ,发生了数据增加,此时移动端列表就可能会出现重复数据。虽然这个问题在 pc 端也存在,但是 pc 端只会展示当前页的信息,这样就避免了该问题所带来的负面影响。</p>
<h2>结合列表 key 维持渲染正确</h2>
<p>我们在上面的问题中说明了,移动端下拉加载中使用相对位置查询取数是有问题的。</p>
<p>那么,如果当前不能迅速结合前后端进行修改 api 的情况下,当服务端传递过来的数据与用户想要得的数据不一致,我们必须在前端进行处理,至少处理数据重复问题所带来的负面影响。</p>
<p>因为当前分页请求时无状态的。在分页取到数据之后前端可以对取得的数据进行过滤,过滤掉当前页面已经存在的 key(例如 id 等能够确定的唯一键)。</p>
<p>通过这种处理方式,我们至少可以保证当前用户看到的数据不会出现重复。同时当列表数据可以编辑修改的时候,也不会出现因为 key 值相同而导致数据错乱。</p>
<h2>通过绝对位置获取数据</h2>
<p>如果不使用相对位置获取数据,前端可以利用当前列表中的最后一条数据作为请求源参数。前端事先记录最后一条数据的信息。例如当前的排序条件为创建时间,那么记录最后一条数据的创建时间为主查询条件(如果列表对应的数据不属于个人,可能创建时间不能唯一决定当前数据位置,同时还需要添加 ID 等信息作为次要查询条件)。</p>
<p>当我们使用绝对位置获取数据时候,虽然我们无法提供类似于从第 1 页直接跳转 100 页的查询请求,但对于下拉加载这种类型的请求,我们不必担心性能以及数据重复显示的问题。</p>
<p>对于相对位置取数来说,前端可以根据返回数据的总数来判断。但当使用绝对位置取数时,即使获取数据总数,也无法判断当前查询是否存在后续数据。</p>
<p>从服务器端实现的角度来说,当用户想要得到 20 条数据时候,服务端如果仅仅只向数据库请求 20 条数据,是无法得知是否有后续数据的。服务端可以尝试获取当前请求的数据条数 + 1, 如向数据库请求 21 条数据,如果成功获得 21 条数据,则说明至少存在着 1 条后续数据,这时候,我们就可以返回 20 条数据以及具有后续数据的信息。但如果我们请求 21 条数据却仅仅只能获取 20 条数据(及以下),则说明没有后续数据。</p>
<p>如可以通过 “hasMore” 字段来表示是否能够继续下拉加载的信息。</p>
<pre><code class="js">{
data: [],
hasMore: true
}</code></pre>
<h2>结合 HATEOAS 设计优化</h2>
<p>事实上,前面我们已经解决了移动端处理列表查询的问题。但是我们做的还不够好,前端还需要结合排序条件来处理并提供请求参数,这个操作对于前端来说也是一种负担。那么我们就聊一下 HATEOAS 。</p>
<p>HATEOAS (Hypermedia As The Engine Of Application State, 超媒体即应用状态引起) 这个概念最早出现在 Roy Fielding 的论文中。REST 设计级别如下所示:</p>
<ul>
<li>REST LEVEL 0: 使用 HTTP 作为传输方式</li>
<li>REST LEVEL 1: 引入资源的概念(每一个资源都有对应的标识符和表达)</li>
<li>REST LEVEL 2: 引入 HTTP 动词(GET 获取资源/POST 创建资源/PUT 更新或者创建字样/DELETE 删除资源 等)</li>
<li>REST LEVEL 3: 引入 HATEOAS (在资源的表达中包含了链接信息。客户端可以根据链接来发现可以执行的动作)</li>
</ul>
<p>HATEOAS 会在 API 返回的数据中添加下一步要执行的行为,要获取的数据等 URI 的链接信息。客户端只要获取这些信息以及行为链接,就可以根据这些信息进行接下来的操作。</p>
<p>对于当前的请求来说,服务端可以直接返回下一页的信息,如</p>
<pre><code class="js">{
data: [],
hasMore: true,
nextPageParams: {}
}</code></pre>
<p>服务端如此传递数据,前端就不需要对其进行多余的请求处理,如果当前没有修改之前的查询以及排序条件,则只需要直接返回 “nextPageParams” 作为下一页的查询条件即可。</p>
<p>这样做的好处不但符合 REST LEVEL 3,同时也减轻了前端的心智模型。前端无需配置下一页请求参数。只需要在最开始查询的时候提供查询条件即可。</p>
<p>当然,如果前端已经实现了所有排序添加以及查询条件由服务端提供,前端仅仅提供组件,那么该方案更能体现优势。 前端是不需要知道当前业务究竟需要什么查询条件,自然也不需要根据查询条件来组织下一页的条件。同时,该方案的输入和输出都由后端提供,当涉及到业务替换( 查询条件,排序条件修改)时候,前端无需任何修改便可以直接替换和使用。</p>
<h2>其他注意事项</h2>
<p>一旦涉及到移动端请求,不可避免的会有网络问题,当用户在火车或者偏远地区时候,一旦下拉就会涉及取数,但是当前数据没有返回之前,用户多次下拉可能会有多次取数请求,虽然前端可以结合 key 使得渲染不出错,但是还是会在缓慢的网络下请求多次,无疑雪上加霜。这时候我们需要增加条件变量 loading。</p>
<p>伪代码如下所示:</p>
<pre><code class="js">// 查询
function search(cond) {
loading = true
api.then(res => {
loading = false
}).catch(err => {
loading = false
})
}
// 获取下一页数据
function queryNextPage() {
if (!nextPageParams) return
if (!loading) return
search(nextPageParams)
}</code></pre>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p>
<p><a href="https://link.segmentfault.com/?enc=Vxg1BISoTLXe%2FKRiYw5TWA%3D%3D.psrhnw2yX%2BSNMK%2FkN9KvuYLUIH0EtyN2pF3e3BcwM69A2vl3piNi1305%2B9DLWt%2Ba" rel="nofollow">博客地址</a></p>
探讨不需要打包的构建工具 Snowpack
https://segmentfault.com/a/1190000022513055
2020-04-30T01:10:03+08:00
2020-04-30T01:10:03+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
12
<p>当谈到前端构建工具,就不得不提的功能强大的 <a href="https://link.segmentfault.com/?enc=OSCS6Eq1eWY9YE3UBOw8zA%3D%3D.kyL%2F6aCXiF0fg9YhLrWjz%2Fj0CcXxq86PCLld9wZrBzA%3D" rel="nofollow">Webpack</a>, 尤其是在最新版本中提出了 <a href="https://link.segmentfault.com/?enc=veg3c5rVR63lX9EU1TY7Kg%3D%3D.o%2F0XrZ2aP5lpLDnvBtLrWu7aJ3uscsySCmR8tsQFyryJ70iBlov2ApaOuMZiMAbSAxEEbbpx61%2BRNWHKJFLnmMJzz5Mt3yfycgeO2%2FWnxjYT5dY8YkfLS7SubvZ3wcbVztehb1QkFIotVoBga4IEm2aNE4KC%2Bi7fWBuEUZxeNwi3FNW9MUZ6bYuqNgxV0QgE%2FpVfF9LCEmbCHL7D5ly2wm0dctIDUHiCbkeFGMdlZW4VT5jXSwbpfm4tFl2veGMikd1FzAU%2F0wfJOv6%2B%2Bi8kiRSLzrQpmfaj%2B94xCiS2InVZl%2B1ESp5BJdqYTgj%2BTySB5Qcz5AgM9UMKRhQIRXZxNphWltsQneEcdWNVUpFVY%2FPC3NFcVSBGsvt8BRzW8h13" rel="nofollow">Module Federation</a> 功能,该功能进一步增强了 Webpack 的实际作用。我相信凭借 Webpack 这几年的积累,恐怕未来几年内没有哪一个打包工具能够与之媲美,但是这并不妨碍我们探讨一下新的工具 <a href="https://link.segmentfault.com/?enc=OX0ctyVJCmAd33oBDNJSUw%3D%3D.W1b9aen%2FFsQdhNEJpPLpaXMQp5DQCWcdMP0d1oulj%2FE%3D" rel="nofollow">Snowpack</a>。</p>
<h2>现代打包工具的问题</h2>
<p>使用 Snowpack,当您构建现代的 Web 应用程序(使用React,Vue等),无需使用类似于 Webpack,Parcel 和 Rollup 等打包工具。每次您点击保存时,都无需等待打包程序重新构建。相反,所有的文件更改都会立即反映在浏览器中。</p>
<p>请关注立即这个词语, bundleless ? 这个究竟有什么意义?为什么我会这么重点关注这一个工具。</p>
<p>对于一些项目与开发者而言,可能对此没有太大的感触,但是对于我来说,开发中打包曾经是一个噩梦。几年前,我从头开发一个前端 Sass 项目,该项目开始时候就使用 Vue 以 Webpack 根据业务线构建多页面。几个月的时间过去,模块有了十几个。当我修改一个很小的地方时候,需要近一分钟的时候才能热更新完成。当时,我们只能在项目开启的时候设置那些页面需要编译。在忍受了一周后,我们不得不开始拆分项目,因为开发人员和项目开发效率的原因,我们不可能根据业务拆分项目。我们只能把项目中改动较少的的基础服务以及封装的业务组件给提取出去。利用 Webpack 的 externals 来辅助开发。这个方案很好的解决了项目当时的痛点。同时,这件事也让我对于模块划分和性能优化有了一定的认识和见解。</p>
<p>所以,在我看来,bundleless 解决了大部分前端开发者 的一大痛点,就是<a href="#">心流</a>)。当一个程序员被打搅后,他需要 10 - 15 分钟的时间才能重新恢复到之前的编程状态。当你在完成一小块功能时候,想要去查看结果时候,发现当前的项目还在构建状态,这个时候很有可能就会中止心流。</p>
<p>当然,还有一些高明的程序员是完成整块功能再去查看结果。同时开发的 bug 也非常少。该工具对于此类程序员的帮助也不大。谈到这里,我很想要多说两句,虽然程序员在开发中需要学习和使用大量的工具辅助开发,事实上很多工具也确实能够提升效率,但是有时候需要逼自己一把,把自己置身于资源不那么足够的情况下,因为只有这样才能真正的提升能力。这里我推荐一篇博客 <a href="https://link.segmentfault.com/?enc=9BXGjfUAfMOAYgg7BVwawQ%3D%3D.G2kX86sq%2BN339MM1veKvBTMnVw82UK5mtOGveWQ1nArzZE1vG%2BxF0PcRnwznyuMgE581S%2FkEilt2TdxnJeCjrQ%3D%3D" rel="nofollow">断点单步跟踪是一种低效的调试方法</a>,不管大家是认可还是否认,我认为都可以从这篇博客中得到思考。</p>
<h2>前端项目的演进</h2>
<p>就目前成熟的前端框架来说,都会提供一套脚手架工具以便用户使用。之前我们也是把项目直接从 Webpack 转到了 Vue Cli 3。因为 Vue Cli 3 内部封装了 Webpack, 基本上在开发中已经不需要进行 Webpack 基础性配置,同时我们所需要的依赖项目也大大减少了,底层的依赖升级也转移到了 Vue Cli 上,这也变相的减少了很多的开发量。</p>
<p>在探讨 Snowpack 这个新的构建工具之前,我要抛出一个问题,我们究竟为什么要使用 Webpack? </p>
<p>一个当然是因为 Webpack 确实提供了很多高效有用的工程实践为前端开发者赋能。如果让我们自行研究与使用,需要投入的时间以及掌握的知识远远高于配置学习成本。第二个就是因为它本身是一个静态的 JavaScript 应用程序的静态模块打包工具,首要的重点就在于工具为前端提供了模块与打包。</p>
<p>当年的 JavaScript 设计之初为了保持简单,缺少其他通用语言都具有的模块特性。而该特性正是组织大型,复杂的项目所必须的。但当年的 JavaScript 的工作就是实现简单的提交表单,寥寥几行代码即可实现功能。即使 Ajax 标志 Web 2.0 时代的到来阶段,前端开发的重点还是放在减少全局变量上,于是我们开始使用 IIFE(立即调用函数表达式)来减少全局变量的污染, 而 IIFE 方法存在的问题是没有明确的依赖关系树。开发人员必须以准确的顺序来组织文件列表,以保证模块之间的依赖关系。由于当时网页短暂的生命周期以及后端渲染机制保证了网页的依赖是可控的。</p>
<p>在之后,Node.js 出世,让 JavaScript 可以开发后端,在 Node.js 的许多创新中,值得一提的是 CommonJS 模块系统,或者简称为CJS。 利用 Node.js 程序可以访问文件系统的事实,CommonJS 标准更具有传统的模块加载机制。 在 CommonJS 中,每个文件都是一个模块,具有自己的作用域和上下文。当然,虽然这种机制也底层来自也来自于 IIFE(立即调用函数表达式)。这种创新的爆发不仅来自创造力,还来自于自于必要性:当应用程序变得越来越复杂。 控制代码间的依赖关系越来遇难,可维护性也越来越差。我们需要模块系统以适应那些扩展以及变更的需求,然后围绕它们的生态系统将开发出更好的工具,更好的库,更好的编码实践,体系结构,标准以及模式。终于,伴随模块系统的实现,JavaScript 终于具有了开发大型复杂项目的可能性。</p>
<p>虽然在此之后开发了各式各样的模块加载机制,诸如 AMD UMD 等,但是等到 ES6 的到来。JavaScript 才正式拥有了自己的模块,通常称为ECMAScript模块(ESM)。也是 SnowPack 依赖的基础,这一点我们后面展开来说。</p>
<p>相比于CommonJS , ES 模块是官方标准,也是 JavaScript 语言明确的发展方向,而 CommonJS 模块是一种特殊的传统格式,在 ESM 被提出之前做为暂时的解决方案。 ESM 允许进行静态分析,从而实现像 tree-shaking 的优化,并提供诸如循环引用和动态绑定等高级功能。同时在 Node v8.5.0 之后也可以在实现标准的情况下使用 ESM,随着Node 13.9.0 版本的发布,ESM 已经可以在没有实验性标志的情况下使用。</p>
<p>Webpack 一方面解决了前端构建大型项目的问题,另一方面提供了插件优化 Web 前端性能。例如从资源合并以及压缩上解决了 HTTP 请求过程的问题,把小图片打包为 base64 以减少请求量,提供 Tree-Shaking 以及动态导入。伴随着 HTTP 协议的升级以及网速的不断加快,我也希望日后这些工作慢慢变成负担。</p>
<h2>ESM (ecmascript module) 已经到来</h2>
<p>SnowPack 是基于 ESM 的工具。想要通过一次安装来替换每次更改时候重建的构建步骤。ESM 现在可以在浏览器中使用了! 我们可以使用 [Can i use]()了解一下 ESM 的可以使用性。</p>
<p><img src="/img/bVbGCPV" alt="ESM use.png" title="ESM use.png"></p>
<p>可以看到,基本上现代浏览器都已经提供了支持,事实上我们也不需要等到所有的浏览器提供支持。</p>
<p>我们不需要为所有浏览器提供一致性的体验,我们也做不到这一点。事实上,我们在这件事情上是可以做到渐进增强的。对于不支持的浏览器,完全无法理解该语句,也不会去 js 加载。例如可以提供页面预加载的前端库 <a href="https://link.segmentfault.com/?enc=CES7f756xkUpXL%2FtkjGndw%3D%3D.4bXe7MRvE9RErMpNoBIsroyfOpeoUExbyJPBhcSpAPM%3D" rel="nofollow">instant.page</a>。该库在不支持 ESM 的浏览器上压根不会执行。</p>
<p>同时对于暂时不支持的浏览器来说,依然可以加载 js ,唯一需要做的就是为不支持 <script type="module"> 的浏览器提供一个降级方案。。正如下面的代码,在现代浏览器中,会加载 module.mjs,同时现代浏览器会忽略携带 nomodule 的js。而在之前的浏览器会加载后面的 js 文件。如果你想进一步阅读的话,可以参考 <a href="https://link.segmentfault.com/?enc=8aJ8qmlb1822Ut3gpRZveA%3D%3D.3vHKIZhArpQI4YyYBVvUDAG205l02EA4TgPBE9GZtkuffA3Ks9dfeU6Q9BldbiuJdaReSknBw60TvQM9bvkZ3g%3D%3D" rel="nofollow">ECMAScript modules in browsers</a> 。</p>
<pre><code class="html"><script type="module" src="module.mjs"></script>
<script nomodule src="fallback.js"></script></code></pre>
<p>同时,我们可以通过 type="module" 来判断出现代的浏览器。而支持type="module" 的浏览器都支持你所熟知的大部分 ES6 语法,通过这个特性,我们可以打包出两种代码,为现代浏览器提供新的代码,而为不支持 ESM 的浏览器提供另一套代码, 具体可以参考 <a href="https://link.segmentfault.com/?enc=1l4UVOpQ7fK5t0ix%2BRBK%2Fw%3D%3D.54Jc%2FuFHkLdZQcGVUgVotrOPf%2FeWrO1liWxi9hNFc3KsQf1KYylleiV7Pj1VIIjbV1XASf0Ozaf5dt1QzRBjTzPewUEkyrPBtT15E%2BorO%2Bc%3D" rel="nofollow">Phillip Walton 精彩的博文</a>,这里也有翻译版本 <a href="https://link.segmentfault.com/?enc=FFD2cTVeny4N63m9HgSoSQ%3D%3D.KXbF6w8wOXNjS3s1vDEXlL4Q8ADLfJklTZxzF1AFghWAW1ztmissepNEjIHP2IdZ" rel="nofollow">如何在生产环境中部署ES2015+</a>。如果当前项目已经开始从 webpack 阵营转到 Vue CLI 阵营的话,那么恭喜你,上述解决方案已经被内置到 Vue CLI 当中去了。只需要使用如下指令,项目便会产生两个版本的包。具体可以参考 <a href="https://link.segmentfault.com/?enc=CUjUSLNDs4x5eyyl60e91A%3D%3D.Wij12qlDTObXup4GmS3V4UuZfXKgZugQttpmxAd0NRnTopy0aciyCY2cLoqCcnFZljNSIBIKW068Gx8EKjO6zPf73XRlyE9E6qHwThHSC4Ef61DwdDlrA57OqzPjDOJJ" rel="nofollow">Vue CLI 现代模式</a>。</p>
<h2>Snowpack 解决了什么?</h2>
<p>出于对主流浏览器的判断,SnowPack 大胆采用 ESM,其原理也很简单,内部帮助我们将 node_modules 的代码整理并且安装到 一个叫做 web_modules 的文件夹中,需要的时候直接到该文件夹中引入即可。其目标也是为了解决第三方代码的引入问题。(注明: 安装 Snowpack 需要 node v10.17.0 以上版本)</p>
<p>当然,如果仅仅只为了解决第三方引入的问题,事实上我们自己也可以手动解决,但是面对错综复杂的第三方库,我们自己通过 node 来构建未免有些过于复杂。同时,该工具依然会提供使用 TypeScript 以及 Babel 的方案 ,同时也为我们提供了少量的配置选项来帮助我们管理第三方依赖。同时,Snowpack 还可以通过 --nomodule 支持旧版浏览器。同时它也可以根据当前引入的模块来自动构建依赖。</p>
<p>当浏览器本身已经开始支持模块,如果网络速度已经不再是限制,那么我们是否应该离开复杂的构建环境转向简单的代码? 答案是肯定的,简单的方案一定会赢得开发者的青睐。</p>
<p>说到这里,我不禁想到当年刚刚学习软件时侯总是遇到 BS (浏览器与服务器)架构和 CS (客户端与服务器) 架构的问题与选择。历史的选择告诉我们,能够使用 BS 架构的软件,一定会用 BS 架构。如果桌面端的 CS 转向 BS 是一条漫长的探索之路,那么移动端的 CS 转向 BS 则是必经之路。</p>
<p>对于 Vue 这种渐进式的框架来说,即使没有打包工具,我们依然可以在 html 中直接引入开发,而对于 React, Svelte 这些需要编译才能够使用的库来说,SnowPack 也提供了方案来帮助我们协作解决。对于 css 图片这些,浏览器不支持用 js 导入,我们还需要为其改造,具体的实现可以直接学习 <a href="https://link.segmentfault.com/?enc=kG3tan0E1iKdiaFomg5hsw%3D%3D.CLBqmJaYLoQFxlATUbVd7oDtat480rjx4kPIhnZmQhM%3D" rel="nofollow">Snowpack 官网</a> 以及 <a href="https://link.segmentfault.com/?enc=54qlsWmh1dbuQK7ZEzqJ7g%3D%3D.HDAswdBOf0FMcqTq2oyJJK6IuMhfrbAeMcCY11%2BuNEYuCGFN6hMYooja7EQTggj0" rel="nofollow">Snowpack 例子</a>。</p>
<p>说了 Snowpack 的优势,我们也必须聊一聊 Snowpack 的劣势。过于激进一定是劣势,毕竟不支持 ESM 的库还是很多,面对企业应用开发,我们还是需要稳定的工具。同时面对强大脚手架工具,过于捉襟见肘,需要付出更大的精力来维持系统的一致性。同时,在浏览器使用模块化之后,前端代码更容易被分析,这个是否会影响项目本身,事实上也有待商榷。没有模块热更新功能,这点令开发很痛苦。</p>
<h2>题外话 vite</h2>
<p>前两天,Vue3 beta 版本出世,在直播中,Vue 开发者尤雨溪也是顺带说了一下为 Vue 3 提供的“小”工具 <a href="https://link.segmentfault.com/?enc=PeuCWd2tNL5aEXAkSxJomg%3D%3D.BIMbZVpMoJSiTLv8JXHq0DZD8yWZrtKsnXyJCqVHEJ0%3D" rel="nofollow">vite</a> ,我在闲暇之时也是去把玩了一下。该工具也是根据浏览器 ESM 结合 node 来针对每个更改模块进行即时编译后直接提供给浏览器。 也就是说,当你在开发中修改了某个 vue 文件之后,node 会编译该文件并且通过 HMR 提供给浏览器当前编译后的 vue 文件。</p>
<p>相对比 Snowpack 来说,该方案则更加优美 (个人感觉)。同时兼顾了开发和生产环境。虽然在当前阶段还不能投入生产,但是我相信 vite 在未来会大放光芒。</p>
<h2>展望</h2>
<p>前几年,我们需要 Webpack 配置工程师,随着 <a href="https://link.segmentfault.com/?enc=VLSZdwzBJZE5StO3kg5cbw%3D%3D.qg0aXJNXw8kbaZAcIPboMVTdp6G6WzffyM3IAzVhXo8%3D" rel="nofollow">Parcel</a> 的发布, Webpack 随后也提供了零配置,伴随着依赖升级的复杂度一步步变高,各个框架也是将 Webpack 作为自己的依赖提供更优的工具,伴随着 bundleless。我们可以看到,前端开发难度降低的同时体验也在提升,这是一件好事。</p>
<p>同时,云端 Serverless 架构也降低了创业公司对基础建设的需求。也许将来真的会有业务人员投入到开发中来。而我们也有更多的时间投入到业务场景与业务需求中去。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。</p>
<p><a href="https://link.segmentfault.com/?enc=h%2BpV8BamEmfBQAbbCVrRUA%3D%3D.bT%2Fxm8QmIqG4FxE9jlDRjzIySBNMYFL8Kork5jt0Ha2mVUHSNmd%2BUS%2FVdhhUFLnH" rel="nofollow">博客地址</a></p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=WRZ2WV0WPcy2cBYSdQm44Q%3D%3D.ibPcGGUmchbzyJhZwFsd5mu5f1neKwIR6GRg%2F0blA8M%3D" rel="nofollow">Snowpack 官网</a> </p>
<p><a href="https://link.segmentfault.com/?enc=cJR9%2FMh9gu%2FgySIf2TR31Q%3D%3D.nkxbV8wA6ATsn%2BvLQMED%2FX9aZqfFS2Z39OieyC0PEIo9DBXcAWVHEtpChP6OKUZiKzB6RMXqTpa2IDpHdIYB9x%2BP%2BIasxKeXpf5BB3UzdmDI0aYg7sVNdBcwAH2tT4sk" rel="nofollow">Vue CLI 现代模式</a></p>
<p><a href="https://link.segmentfault.com/?enc=PywCZs2R8lwnWDwUb4pGYQ%3D%3D.2FUkttGFYQouWwA8F%2F%2BhNAUU1bV9wvkbCZeoT7rcF3kjC5s9jufMvlHLuKccL%2Fe6" rel="nofollow">如何在生产环境中部署ES2015+</a></p>
<p><a href="https://link.segmentfault.com/?enc=DtEckuJui4xHIA0gedH%2F7Q%3D%3D.7yIATwa3vuuQxX0ccA22%2B8PejA69ScU5uOK1tquu%2BzVF0zxJLu3%2BUA9meQe1KHtv" rel="nofollow">snowpack,提高10倍打包速度</a></p>
<p><a href="https://link.segmentfault.com/?enc=eDuU%2BaDzGvREsj583kEqMw%3D%3D.4bKDmF11Cz2EGOAL0D0oKFrdKerqkg8y1N316WxPMSCFi5d35d5v4ZC2QfWHFOK8dgsFs%2FId0lKQH77ol3Xfbg%3D%3D" rel="nofollow">断点单步跟踪是一种低效的调试方法</a></p>
<p><a href="https://link.segmentfault.com/?enc=OaD3sFMEokiU5QrpV17a2g%3D%3D.DZ0%2BnBkhXKXMEYTrKREs12xaVAJSfF9lJKVP%2Bc9g05E%3D" rel="nofollow">vite</a></p>
读 《HTML5 揭秘》有感
https://segmentfault.com/a/1190000022262738
2020-04-04T14:10:02+08:00
2020-04-04T14:10:02+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
3
<p>最近在补一些 HTML 的书籍,偶尔读到这本书,虽然这本书已经是10年以前的书籍了,不过其中有些有趣的知识点与观点被我提取了出来。</p>
<h2>标准创建与技术实现冲突</h2>
<p>作者在开始就提出了 Mozilla 开发人员关于标准与实现之间的冲突的一个观点:</p>
<blockquote>一份技术规范和他的具体实现必须要做到步调一致。实现先于规范完成不是什么好事情,因为人们会开始依赖这些已实现的细节,这样会对规范造成制约。然而,你也不希望在规范已经完成时还没有任何相关的具体实现与实践经验,因为这样规范就得不到任何反馈。这里面如果存在着无法避免的冲突,而我们也需要硬着头皮去克服了。</blockquote>
<p>事实上,对于前端开发而言,具体实现早于技术规范的制定已经是一种常态了。但与其他领域不同的是:前端的新标准非必要条件下是不可以破坏之前的实现。</p>
<p>在这里,我可以举几个兼容性的例子:</p>
<p>大家可能都使用过 String.prototype.includes 来判断一个字符串是否包含在另一个字符串中。但是实际上,在 Firefox 18 - 39中,这个方法的名称叫 <code>contains()</code>。由于在Firefox 17上,一些使用 <a href="https://link.segmentfault.com/?enc=qagICkWGd2M3eWesTOVgkw%3D%3D.qcc%2B%2BFJf1qw1k4%2FvWcqfwP%2FY7nETADWnvg9zuWUbHVg%3D" rel="nofollow">MooTools</a> 1.2的网站会崩溃掉。当年由于各个框架为了能够更简单的使用函数,在各自的代码库中修改内置对象的 prototype,同时框架也考虑到了未来标准可能会实现,为了兼容以后的标准,他们对 prototype 进行判断,然后如果对象当前 prototype 上没有函数实现,就使用当前自己定制的函数,如果有函数实现的话,就使用浏览器所提供的函数。虽然他们考虑到了兼容标准,但是他们却没能考虑到标准是会发生改变的。可能在若干年后,标准实现后,函数已经与当前大相径庭。所使用的代码就会发生错误。所以,无奈之下,该函数被重命名为 <code>includes()</code> 。事实上我们可以看出,contains 命名要比 includes 好得多。 </p>
<p>在最新的提案之一就是在类中添加私有变量方法,标准将使用 <code>#</code> 符号来表示类的私有变量。</p>
<pre><code class="js">class Count {
#a = 1;
getCount() {
return this.#a
}
}
const count1 = new Count()
count1.getCount()
// 1
console.log(count1)
// Uncaught SyntaxError: Private field '#a' must be declared in an enclosing class</code></pre>
<p>emmm...,美丑大家自行鉴别。也正是因为前端之前没有所谓的私有变量,所以大家都会“约定” <code> _</code> 就是私有变量,但是事实上,任何约定都只会防君子不防小人。一定会有大量的代码直接进行调用。一旦浏览器支持后,必然会影响大量网页。 所以我们也只能硬着头皮去克服了。</p>
<p>受到影响的不仅仅是 JavaScript,同时也有 Css。 Css 变量为了能够在 Sass(变量用了 $ ) 和 Less(变量用了 @) 中使用,也是不得不去使用 --。可以看到这样进行 Css 变量定义也是不那么美观的。</p>
<pre><code class="css">:root {
--main-bg-color: brown;
}</code></pre>
<h2>交付出东西的人才是赢家</h2>
<p>该书也讨论了为什么会存在 <img> 这个元素标签。为什么它是 img,而不是 include,image(事实上,貌似 image 元素标签也是存在在浏览器中的,可以看这一篇 blog <a href="https://link.segmentfault.com/?enc=80EomzvXQXF390pi7FO0Uw%3D%3D.yLbBxfSs2n0b7OTMmE%2BVkdB0ySrscWexRXV0Ujd6sgyNGs9wl2vgKZrfL0PE2A87LMUFRaLnOSfr1Fwxea50sA%3D%3D" rel="nofollow">Having fun with <image></a>)?</p>
<p>答案以 93 年一群大佬的精彩的对话为主线,其中可以看到一些真知灼见,也有一些前瞻性很强的言论。众口难调是必然的。很多时候,协议的制定本身就不是一个技术问题,很难有对错而言。但是为什么一定是 <img>,答案很简单,因为提议者马克·安德里森在对话后直接发布了代码来处理这个元素。</p>
<p>这并不是说给出代码实现的就一定是赢家,但是这是赢家的必要条件,不是吗?讨论当然重要,是一种思想的交流,但是当我们不能从道理上说服别人的时候,就只能用其他的方案来表明自己的态度。</p>
<h2>过于超前就会死亡</h2>
<p>该书也讨论了当年 WHAT 小组和 W3C HTML 小组之间对于 HTML 发展的不同思考和见解,开始时候,两个小组之间各自为政,无视对方存在。WHAT 小组针对 Web 表单和新的特性进行工作,而另一组忙于制定 XHTML 2.0 版本,但可惜的事情是: 没有浏览器为之提供实现。</p>
<p>XHTML 是和 HTML 不兼容的,这意味者不但浏览器开发者要做大量的工作,还需要让前端开发者一下子切换到 XML,完全书写良好的规范 — 这是行不通的。而 WHAT 小组采用宽容的错误处理,把重点放在新特性上。</p>
<p>无论从观念还是互联网产品,过于超前的结局其实都不是太好。当然,这从侧面也印证了一个道理,选择比努力更重要的不是一句空话。</p>
<h2>难以消失的浏览器兼容技术</h2>
<p>最后,我们来聊一下技术吧。</p>
<p>新的功能已经到来,但是我们要等到什么时候才可以采用它?这个问题不但出现在 10 年前,同时也会在现在。优秀的开发者总是希望使用最新的特性来提升用户体验。当然了,我们现在可以依赖 <a href="https://link.segmentfault.com/?enc=sB9UI5XCzcFCR7Wgeg62EA%3D%3D.%2FUvYLrSBqofTuvgjzdMA6A3I3prBoo6eLjthVBhs1RQ%3D" rel="nofollow">Can I use</a> 来判断浏览器支持情况。</p>
<p>很早之前,我们用 <noscript> 来处理不使用脚本的网页。</p>
<p>再然后,我们通过浏览器特性检测来进行兼容处理。例如 <a href="https://link.segmentfault.com/?enc=GoppXa%2B9XHudZbmLdU%2BnHg%3D%3D.csJOx22FZm2FBjEXcVgQJYpj%2BR1oHyI8i207bS8Qkcq6O8f1GflHyadmSMscTLqd" rel="nofollow">Modernizr</a> 库来检查浏览器是否支持 HTML5 以及 css3。利用 polyfill 来升级不支持特性的浏览器。</p>
<p>即使在今天,这些问题依然没有被解决。当然我们利用更加先进的技术来支持罢了。</p>
<h3>Polyfill.io 根据不同的浏览器确立不同的 polyfill</h3>
<p>如果进行过前端开发,就不可能没有使用过 polyfill。polyfill你可以理解为“腻子”,就是装修的时候,可以把缺损的地方填充抹平。针对于各个浏览器的把差异化抹平。由于各个浏览器版本不同,所需要的 polyfill 也不同,</p>
<p><a href="https://link.segmentfault.com/?enc=RFL2xGUadGB9Z9oVITcc3w%3D%3D.wNP6kI5T89F2Ft72madJAjLBUus%2BeRPvQs88P0HtRCdOpfD1owcVUuXNa2yPkGo3" rel="nofollow">Polyfill.io</a>是一项服务,可通过选择性地填充浏览器所需的内容来减少 Web 开发的烦恼。Polyfill.io读取每个请求的User-Agent 标头,并返回适合于请求浏览器的polyfill。</p>
<p>如果是最新的浏览器且具有 Array.prototype.filter</p>
<pre><code>https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.filter
/* Disable minification (remove `.min` from URL path) for more info */</code></pre>
<p>但是如果当前浏览器没有此函数,就会在 正文下面添加有关的 polyfill。</p>
<p>国内的阿里巴巴也搭建了一个服务,可以考虑使用,网址为 <a href="https://link.segmentfault.com/?enc=sQ1rKivUeI46CF55jbnpEA%3D%3D.KK%2FhOsJDmPFRB55z8xD025Gy0t1QlnC2sTZQ%2FJkUDctvkIZ8aqfozZ1KVP%2BRROWh" rel="nofollow">polyfill.alicdn.com/polyfill.mi…</a></p>
<h3>type='module' 辅助打包与部署 es2015+ 代码</h3>
<p>使用新的 DOM API,可以有条件地加载 polyfill,因为可以在运行时检测。但是,使用新的 JavaScript 语法,这会非常棘手,因为任何未知的语法都会导致解析错误,然后所有代码都不会运行。</p>
<p>该问题的解决方法是</p>
<pre><code><script type="module">。</code></pre>
<p>早在 2017 年,我便知道 type=module 可以直接在浏览器原生支持模块的功能。具体可以参考 <a href="https://link.segmentfault.com/?enc=1Ghz5bWc8ahCj69VNKAWxQ%3D%3D.d2PwT1b7QlblKMHwKds01R54%2B0dvr%2Brd36ss4GfPZYTyZFZ9hJxv7i94edlKxaX2Ubs26TEJMJW%2FHtMvWFaaBhDcEqwVoHApgZDhvopRsSc%3D" rel="nofollow">JavaScript modules 模块</a> 以及 <a href="https://link.segmentfault.com/?enc=Yen4FHnfsD6bwUj8ryPF8w%3D%3D.WrscKpbSZnxWvGqm%2Bmzb2MMm8lcbzfP%2Fb2esAfxyVLt%2FXgMWsblGP4fBLKSqpQRx4sgSeoMtpVmK%2BPxj28V0Gw%3D%3D" rel="nofollow">ECMAScript modules in browsers</a>。但是当时感觉只是这个功能很强大,并没有对这个功能产生什么解读。但是却没有想到可以利用该功能识别你的浏览器是否支持 ES2015。</p>
<p>每个支持 type="module" 的浏览器都支持你所熟知的大部分 ES2015+ 语法!!!!!</p>
<p>例如</p>
<ul>
<li>async await 函数原生支持</li>
<li>箭头函数 原生支持</li>
<li>Promises Map Set 等语法原生支持</li>
</ul>
<p>因此,利用该特性,完全可以去做优雅降级。在支持 type=module 提供所属的 js,而在 不支持的情况下 提供另一个js。具体可以参考 <a href="https://link.segmentfault.com/?enc=G%2F6abiKux4vdXfTzSZVrsQ%3D%3D.Z92iesbb4mPbanZN7HQcunV2Trhy3EuxAUxGm3CkXDPZBh1YOCNs4DZiaJum1ZNCLkso%2Bxwr0fdSLbGsku9Uj2RW6fhzJQeTfnIa%2FHDHEdA%3D" rel="nofollow">Phillip Walton 精彩的博文</a>,这里也有翻译版本 <a href="https://link.segmentfault.com/?enc=uAEgPCB8%2FzS%2F%2FAugcC6BxA%3D%3D.X5FGJxPOttlKUS6W3G%2BclXg%2BoA%2FrrQw3jXu2qUbbXmqJid2a%2FgC27nFCBkI%2Bt%2BiI" rel="nofollow">【译】如何在生产环境中部署ES2015+</a>。</p>
<p>如果当前项目已经开始从 webpack 阵营转到 Vue CLI 阵营的话,那么恭喜你,上述解决方案已经被内置到 Vue CLI 当中去了。只需要使用如下指令,项目便会产生两个版本的包。</p>
<pre><code>vue-cli-service build --modern</code></pre>
<p>具体可以参考 <a href="https://link.segmentfault.com/?enc=DyPODqPANncZn1KDcPNCvQ%3D%3D.UHeFUFJVDwvi%2FTrSfxSlWKKkqPKQlWmTThQr%2B67UohnFWPQa1Lm8Bzdfnmtf0u1prZJ8gnkKNr%2FsAAYu2KCkUmqKaiURV6WtZX5Kxzasu0Uc%2BCwB9jcRMq%2FObQovvUDv" rel="nofollow">Vue CLI 现代模式</a>。</p>
<p>不得不说的是: 目前也有开发者把 ESM module 已经作为主流浏览器的功能来思考。还提供了 10 倍打包速度的 <a href="https://link.segmentfault.com/?enc=g2GJqmlAXbUBDhCRHpi86w%3D%3D.oKs4o8WMD%2FTPvAqb42LcPN5WZh%2Bop1xZWtEBoxK9DKE%3D" rel="nofollow">Snowpack</a>。虽然目前距离生产环境还有一定差距,不过如果下次需要开发小型个人项目,我会尝试使用该工具。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 </p>
<p><a href="https://link.segmentfault.com/?enc=zG3p76MWjrnQ%2BvrYFUXMSA%3D%3D.kXc07dIQ1lV27XzpL0SH1kyve6HWsmvdbvt6con4xlt7Nr9kCe7sUMGl8nWYcWdH" rel="nofollow">博客地址</a></p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=AVF5UFsEhysxPtxRl7jubA%3D%3D.RrR4K41qESyM1VPUj7sgUt%2FN6cR0yraI2UmP%2FGScJADTv1f%2BwQ9Hag7n8V69yNh8qfxTKesh2Q2KrnneX3hC9w%3D%3D" rel="nofollow">ECMAScript modules in browsers</a></p>
<p><a href="https://link.segmentfault.com/?enc=G294q1CO%2FQV1i8z2vgCHMw%3D%3D.ssu2iwYJ9OusnzUKwIS9oOUa%2FIMHCy4wm9RV%2Bp11fmdzOxZQiArP%2BANPAga4tcqb9PR%2F3NrJds9XUngrJzl6FIau9WQh5GULsCTB4mX%2BgTcP4wpQt3L9f%2BGp%2F44VVDFl" rel="nofollow">Vue CLI 现代模式</a></p>
<p><a href="https://link.segmentfault.com/?enc=gXP4NigvMETPhlMvYzrnnw%3D%3D.LJH%2BRkbVzEL2Bq0j5EBEw2es6cGrYOQ%2BvBzD1AoLumDHibCUVgHm%2BsXDaBAz8%2BnN" rel="nofollow">【译】如何在生产环境中部署ES2015+</a></p>
记一次小程序样式优化重构
https://segmentfault.com/a/1190000022121437
2020-03-24T00:03:09+08:00
2020-03-24T00:03:09+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<p>上周花了 3 天的时间和老大一起重构了一下小程序的样式开发,虽然说在开发的过程中遇到了一些问题,但是最终减少了不少样式代码,同时功能上也更加强大。进一步来说,如果在后面我们的小程序用户想要自己定制化主题,也可以很快的实现。</p>
<h2>全局样式开发</h2>
<p>之前的小程序开发中,我们全方面使用了 <a href="https://link.segmentfault.com/?enc=I5dSNbgpTsj6FQhMPN84dw%3D%3D.dtKNMnP2l0aIpcB7wVagtzgUN7iF%2FcRVsvCuoimonuhW%2BWGzzJARusqwyjNaMcpBL%2Fzr0dJEG0%2FeCFYRKwxH83izaKrdBbnGskLMY82ELf506M8jEWABIlfIsWReI2d5" rel="nofollow">Component</a> 构造小程序组件以及页面(页面也可以使用 Component 构造器来编写)。当然一方面是因为小程序 Component 的开发体验非常好,拥有类似于 Vue mixin, watch 的 <a href="https://link.segmentfault.com/?enc=LS%2BIjkTDDpQSs%2BAEQhOlfg%3D%3D.fM58KxbkSQTJuFdbjsHkL2INn9lptACTe1qijNhVDXBOzlJnkfGCJNKbQUZ2QDSzp8LtRy2frUWBVC4KfSfFN2NZ7CDSyB5q%2BpeAfOQ7%2FkvgjOaLxEksfZT40xkhmJEl" rel="nofollow">behaviors</a> 和 <a href="https://link.segmentfault.com/?enc=SCSozBf%2F668yvxOEb2nirw%3D%3D.Y4bj%2FwTS2ZEyKOhgxDOTbdKkMItxtwzvqs70kW5ay2o6OGo4CryuyCGZ%2FHzv63dgt3oV35y%2Fu9nOZDJBNamXhkhx6jmPnQfP3Y2Jq6jhUs5ms1%2B49cztv%2BrHikYkuZWF" rel="nofollow">observers</a>,比 Page 构造器强大了很多。另一方面,对于业务较重的小程序来说, Component 也有性能优势。可以参照 <a href="https://link.segmentfault.com/?enc=l%2FAn4XltYi7QUBRFlHyU%2FA%3D%3D.rjxYCb7s%2BP%2Fn5feErO1Vg%2F7OBy3bHxsKsRdU%2FPCNSgb%2Fatz%2F%2FDIft%2BizdUcUkKpS" rel="nofollow">滴滴开源小程序框架Mpx</a> 中的 <em>Page与Component setData性能对照</em>。</p>
<p>在开发过程中,有很多样式是可以复用的。如果在之前开发中经常使用 <a href="https://link.segmentfault.com/?enc=nLjtoPPw846AZ6%2Bm%2B34azg%3D%3D.Davp0jcRC0u16slDSPwTZIfNmSNt9tUmECrNb4Y5%2Fss%3D" rel="nofollow">Bootstrap</a> 之类的 ui 库,那么你就会习惯使用这种库的 <a href="https://link.segmentfault.com/?enc=YuRPjbWqmxs0kzMmLZUh3Q%3D%3D.5HPPm3XNu%2B6B3Uwdmz%2FYDYMGCo6xbDulCSGbOrDmcrPxsxdyh0v2yZHDwpxEyyznwTJQc547LOj9JzrgIrLL4A%3D%3D" rel="nofollow">utilities</a> 类。但是默认情况下,自定义组件的样式只受到自定义组件 wxss 的影响。不会受到全局样式 app.wxss 的影响。所以我们只能通过增加 <a href="https://link.segmentfault.com/?enc=CRFZZc0izg2q2D%2B%2B6SJWSQ%3D%3D.rPpfQ%2Ba6BHUo93FJVeLpUWjb%2FlLxjFgD1zV%2BpxDnAn0HUUO029SYCapB7UH2rMRr" rel="nofollow">@import</a> 语法来辅助各个组件进行开发。</p>
<pre><code class="css">@import "xxx.css";</code></pre>
<p>如果你使用 CSS 预处理器来辅助小程序开发的话,可能就需要通过 <a href="https://link.segmentfault.com/?enc=MKb%2F2YFWgCpUkTqGQF%2BQow%3D%3D.AjWRruFzI9jJI5qEp92aaIZgLDbj7yfQOMP5EoiKOd1XZj%2FMq92g9SJeOAErbLRY" rel="nofollow">gulp-insert</a> 为编译出来的 wxss 文件前置添加该语句。请注意: 之所以 <a href="https://link.segmentfault.com/?enc=pMyqqi3MrboZ5PazaFj0mw%3D%3D.Ek4TmD09dLryqMNsS93VqOXv1jmmIUyC%2F6dn1KFQSJn%2FJikOnJGj7H2LRbywE2Qv" rel="nofollow">@import</a> 需要前置,是因为 <a href="https://link.segmentfault.com/?enc=g%2B2KXRY8nnXg8jsri%2FT8Iw%3D%3D.ah%2FCYbVfiBO0nvfEdTeAXqQlELCb5wLEi0kj38Z%2FbuunqThxyhrPeN4T4XuLDytg" rel="nofollow">@import</a> 语法会把引入的样式按照导入的位置来生效,也就是说,按照 CSS 同等权重看先后的规则来说,如果把 <a href="https://link.segmentfault.com/?enc=TRn6oTx%2BkgRWK3VzXxdV9Q%3D%3D.%2FyznMk%2B7e12M%2BtT9TbEkOlepu3JBtyU4StsQrGQM7llAScBe9CxsdXh%2FffBF1sj3" rel="nofollow">@import</a> 放在中间位置,前面位置定义的样式可能会被 <a href="https://link.segmentfault.com/?enc=g%2FYbt7K4i%2F7Iy8wIiRUbFw%3D%3D.7Bb3tdVaTdVQvKgf0l%2BsBPs330ig2U6jvB%2BPYL811JU1kWB%2FsZDWS0WSLUFe33yF" rel="nofollow">@import</a> 给覆盖掉。</p>
<h3>小程序全局样式</h3>
<p>当然,小程序基础库版本在 2.2.3 以上就支持了addGlobalClass 配置项,即在 Component 的 options 中设置 <code>addGlobalClass: true</code> 。</p>
<pre><code class="js">Component({
options: {
addGlobalClass: true
}
})</code></pre>
<p>该配置项目表示页面级别的 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面。也就是说我们可以用该配置替代之前的每个组件的 @import。只要在 app.wxss 上导入 CSS 样式即可,同时我们可以在页面上对组件内部的样式进行修改。不过需要说明的是: 该配置并不影响父子组件间的样式。各个子组件只受到 app.wxss 和页面的样式的侵入。小程序开发基本上以页面为单位,所以这个配置是非常适合开发的。不过在之前的开发中并没有在意过这个配置。</p>
<h3>组件样式隔离</h3>
<p>当然了,在后面的版本 2.6.5 中,微信小程序也提供了更为详细的隔离选项 <code>styleIsolation</code> 。</p>
<pre><code class="js">Component({
options: {
styleIsolation: 'isolated'
}
})</code></pre>
<ul>
<li>
<code>isolated</code> 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值)。</li>
<li>
<code>apply-shared</code> 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面。</li>
<li>
<code>shared</code> 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 <code>apply-shared</code> 或 <code>shared</code> 的自定义组件。(这个选项在插件中不可用)。</li>
</ul>
<h4>styleIsolation 浅析</h4>
<p>如果大家不想了解太多,只想使用的话, 简短来说: </p>
<p>大家在组件中直接使用 <code>apply-shared</code>,如果当前的 Component 构造器应用于页面,那么不要配置隔离选项即可。其余的隔离选项都是基本没什么用的。</p>
<h4>styleIsolation 详解</h4>
<p><code>isolated</code> 等同于什么都不干,设置不设置一般没有区别,所以可以当该配置项目不存在。</p>
<p><code>apply-shared</code> 等同于<code>addGlobalClass: true</code>,也是最有用的配置项 。</p>
<p><code>shared</code> 最复杂,在子组件设置了样式,不但会影响自身和页面(同时包括了其他设置了<code>apply-shared</code> 或 <code>shared</code> 的自定义组件),同时呢,又会被页面样式和其他设置了 <code>shared</code> 的组件样式影响。在我使用该功能的过程中,我认为,这个配置项千万不要在组件中去使用,除非你“疯了”。</p>
<p>但是不介绍这个配置项目又不行,因为当你使用 Component 去构建页面时候,该页面的配置项目默认就是 <code>shared</code>。这是因为页面又需要全局样式,又需要影响其他设置了<code>apply-shared</code> 或 <code>shared</code> 的自定义组件。</p>
<p>不过可以放心的是: 小程序样式隔离是以页面为单位,不会影响全局样式,即使当前页面你有组件使用了以 <code>shared</code> 影响了当前页面。跳转到下一个页面中,不会出现问题。所以我们基本上按照上面的设置即可。</p>
<p>针对于页面级别的 Component 还有几个额外的样式隔离选项可用:</p>
<ul>
<li>
<code>page-isolated</code> 表示在这个页面禁用 app.wxss ,同时,页面的 wxss 不会影响到其他自定义组件;</li>
<li>
<code>page-apply-shared</code> 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式不会影响到其他自定义组件,但设为 <code>shared</code> 的自定义组件会影响到页面;</li>
<li>
<code>page-shared</code> 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式会影响到其他设为 <code>apply-shared</code> 或 <code>shared</code> 的自定义组件,也会受到设为 <code>shared</code> 的自定义组件的影响。</li>
</ul>
<p>基本上这些配置都会让页面上禁用 app.wxss,所以在开发中并不使用。大家如果有需求,可以自行研究。</p>
<p>从小程序基础库版本 <a href="https://link.segmentfault.com/?enc=b7p64FF1WZu0aytnMkxDoQ%3D%3D.ljFW6i1zYurP3okJBdq6PLWJetuiklMS%2FrJOhLy5Fc1euahBleu%2F25edXDm24GdZx%2BVidAS8hXsEzIzWavX7Tt%2FkcyvR7FVlDxPN2rof3yU%3D" rel="nofollow">2.10.1</a> 开始,也可以在页面或自定义组件的 json 文件中配置 <code>styleIsolation</code> (这样就不需在 js 文件的 <code>options</code> 中再配置)。例如:</p>
<pre><code class="json">{
"styleIsolation": "isolated"
}</code></pre>
<h3>其他样式配置功能</h3>
<p>诸如 <a href="#">外部样式类</a>) 和 <a href="#">引用页面或父组件的样式</a>) 这些功能,大家也可酌情学习使用。不过有了组件样式隔离之后,这些功能可能就有些鸡肋,我可以直接通过页面的样式控制组件内部的样式。而且外部样式类功能需要父组件直接提供样式,不会被 app.wxss 所影响。</p>
<p>在样式隔离功能使用的情况下,我们可以大幅度减少各个组件的代码。并且让整个小程序内部更加干净整洁,可重用性更高。同时我们的主题色等全局配置都可以通过修改 app.wxss 来修改。</p>
<h2>CSS var 定制主题</h2>
<h3>var 功能简单描述</h3>
<p>如果当年 CSS 预处理器变量对于我来说是开启了新世界的大门,那么 CSS 变量这个功能对于无疑就是晴天霹雳。</p>
<pre><code class="css">// 在 body 选择器中声明了两个变量
body {
--primary-color: #7F583F;
--secondary-color: #F7EFD2;
}
/** 同一个 CSS 变量,可以在多个选择器内声明。优先级高的会替换优先级低的 */
.a {
--primary-color: #FFF;
--secondary-color: #F4F4F4;
}
/** 使用 CSS 变量 */
.btn-primary {
color: var(--primary-color)
}</code></pre>
<p>在前端的领域中,标准的实现总是比社区的约定要慢的多,前端框架最喜欢的 $ 被 Sass 变量用掉了。而最常用的 @ 也被 Less 用掉了。官方为了让 CSS 变量也能够在 Sass 及 Less 中使用,无奈只能妥协的使用 --。</p>
<p>当然,我们也可以通过 JS 来操作 CSS 变量。如此,CSS 变量可以动态的修改。</p>
<pre><code class="js">
// 设置变量
document.body.style.setProperty('--primary', '#7F583F');
// 读取变量
document.body.style.getPropertyValue('--primary').trim();
// '#7F583F'
// 删除变量
document.body.style.removeProperty('--primary');</code></pre>
<h3>var 默认配置</h3>
<p>事实上,var() 函数还可以使用第二个参数,表示变量的默认值。如果该变量此前没有定义,就会使用这个默认值。如果让我来思考,我肯定无法想象出结合 Less 和 CSS 变量便可以实现小程序样式的默认配置。这里我们参考了有赞的 <a href="https://link.segmentfault.com/?enc=uT%2BpbA%2BoQY5UfUFGKedN0A%3D%3D.IQkxhXpkCR49kvol5IUt5mfNSCxW0wKJ2seMcirD0jDTDdQHL2FTb337FBzg3haq" rel="nofollow">Vant Weapp</a> 的做法。有赞代码 <a href="https://link.segmentfault.com/?enc=oXAAS3Cj48JuNYwFx7es7g%3D%3D.O7AtnjbEk5vcrQJs3nz2WhToAfpXBsoFs6%2FxwvXapVHfuPKzkygTpfen%2BU1fQnWmBLxeSLO7U98hbHsqYGIfLMwE2PHI4dgQ5BupZ38hphGuM75cgLUZ1DYFoVjlzPDE" rel="nofollow"><strong>theme.less</strong></a> 如下所示:</p>
<pre><code class="less">// 先导入所有 less 变量
@import (reference) './var.less';
// 利用正则去替换变量
.theme(@property, @imp) {
@{property}: e(replace(@imp, '@([^() ]+)', '@{$1}', 'ig'));
@{property}: e(replace(@imp, '@([^() ]+)', 'var(--$1, @{$1})', 'ig'));
}</code></pre>
<p>函数效果如下所示:</p>
<pre><code class="less">@import '../common/style/theme.less';
.van-button {
// ... 其他省略
.theme(height, '@button-default-height');
.theme(line-height, '@button-line-height');
.theme(font-size, '@button-default-font-size');
}
// => 编译之后
.van-button{
// ... 其他省略
height:44px;
height:var(--button-default-height,44px);
line-height:20px;
line-height:var(--button-line-height,20px);
font-size:16px;
font-size:var(--button-default-font-size,16px);
}</code></pre>
<p>我们可以看到每调用一次 Less 函数将会被编译成两个属性。第一个属性的设定对于不支持 CSS 变量的设备可以直接使用,如果当前设备支持 CSS 变量,则会使用 CSS 变量,但是由于当前 css 变量未定义,就会使用变量的默认值。</p>
<p>经过这种函数的修改,我们就可以完成定制主题。详细请参考 <a href="https://link.segmentfault.com/?enc=hXWk6S6yD3cwzZxmj0by8w%3D%3D.FZSeiR2kbeQpkFvYBYg2WjJ1V%2F4Xybcp3unmgHYytwUJp%2Bg5nuHU4trNMolFWoCp" rel="nofollow">Vant Weapp 定制主题</a>。</p>
<pre><code class="wxml">// component.wxml
<van-button class="my-button">
默认按钮
</van-button>
// component.wxss
.my-button {
--button-border-radius: 10px;
--button-default-color: #f2f3f5;
}</code></pre>
<p>大家可能有时候会想,这样的话,不是有更多的代码了吗?其实未必,事实上我们可以直接直接在页面内部定义变量样式。其他组件直接通过样式隔离去使用页面内的变量。当然了,事实上书写的代码多少,重点在于想要控制默认样式的粒度大小。粒度越小,则需要在各个组件内部书写的变量越多,粒度大,我们也就不必考虑太多。</p>
<p>当然了,我们可以基于用户机型提供默认和适合当前机型修改的的样式配置,这样的话。即使用户想要自己定义,也不会出现样式特别怪异的状况。</p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=K1%2BuoI%2F77jFytEF4fISZ0Q%3D%3D.rmForNQxHqpmHquvvdZo7F6WVlZS599S7C%2BeSXe1DGai%2FQ889WfOVagtL%2FWRU6%2BCAIwFuJH5OALwUmD1bVMVWM%2Bwd88rni0WGQ4zRzXp%2FsQEYqVVAMtoiSbbKQUAcg8d" rel="nofollow">小程序 组件模板和样式</a></p>
<p><a href="https://link.segmentfault.com/?enc=%2BTiJg%2BSzSV2gxNucHzR%2FIg%3D%3D.19a9CRKEB0ZlPhu5pimK8mLVd9dodrcoZcu%2BnHGvVRfFXUdrA91AEm2x89YPXDAJGHIi%2BfAIbLeOX6A5WGgROw%3D%3D" rel="nofollow">CSS 变量教程</a></p>
<p><a href="https://link.segmentfault.com/?enc=eeh%2FUkZw92U2roOqhtCEvA%3D%3D.ymUlnTmV8D1Ufg5nczKR79RumruDpFOGFgtMPxg7MAFeVdjsGqEr0yVSZ8dEly%2BujZb08wzVgiWawhwlJ%2F3W9w%3D%3D" rel="nofollow">Vant Weapp 样式覆盖</a></p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <br><a href="https://link.segmentfault.com/?enc=ggfT05tgdIGVbw4%2B3pZZ%2Bw%3D%3D.1XN4%2BWPfD8h4GwHfHuxCRlFUWBzJFoIiIm9CpZqtmUxHr7KjJdc38Rzj0Iyrh%2FaT" rel="nofollow">博客地址</a></p>
从微前端聊聊架构演进
https://segmentfault.com/a/1190000021678990
2020-02-03T19:15:14+08:00
2020-02-03T19:15:14+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<p>就目前来看,微前端已经不是一个新话题了。随着越来越多的公司的深入研究,当前也提出了很多的解决方案。不过本文不是想要来介绍微前端,更想介绍项目如何一步步到达微前端架构的实际需求。</p>
<p>当然,也不排除有些项目在初期就需要微前端这样的架构,不过我一直相信,任何架构模式都是根据实际需求来构建的。为什么很多大公司投入那么多的精力去做这样一件事,更多的也是因为他们真正需要这样一种架构,甚至达到了不用会影响业务开发的可能。不过对于大部分企业,不太需要关注这一点。</p>
<p>事实上,无论是什么架构形式,都是为了项目能够更快的进行开发。 所以不难得出,ETC 原则 (Easier To Change ,易于修改) 贯穿始终。</p>
<p>对于 ToC 端应用而言,可能生存期只有 2,3 年就会结束或者重写。但是对于 ToB 端应用基本上是公司不关门之前都会持续开发和使用下去。当然很多 ToC 端应用提供的更多是服务而不是业务,他们更多的关注重点放在服务上而并非业务范畴。</p>
<h2>单项目应用</h2>
<p>对于后端开发而言,都是由单体应用开始的,但是对于前端开发,所谓单体应用的说法并不合适,所以我在这里把它叫做单项目应用。</p>
<p>对于一个刚刚开始的创业公司,是没有足够的人力储备以及代码实践。此时我们要做的就是利用脚手架开启项目进行开发。我们需要做的是做好代码规范,把代码写好。更多的考虑前端组件化与服务分离。</p>
<h3>依赖注入</h3>
<p>如果不考虑项目中基于业务的各种依赖库以及其中的设计模式,那么依赖注入必然是打造一个易于修改与扩展的项目不可或缺的设计模式。控制反转和依赖注入可以参考 <a href="https://link.segmentfault.com/?enc=8T65dwDHqL5d61GZVdEuWw%3D%3D.WP3LOJpcHaN69XreeqIPVMv5WJzibeb%2BcYsTWKZ6gcj46xoazm7JNjP2YwoZrEdwYgUoxzP%2FMvpMPVAZO27IaA%3D%3D" rel="nofollow">控制反转和依赖注入的理解(通俗易懂)</a>。</p>
<p>但很可惜,在众多的前端框架中仅仅只有 Angular 内置该功能,且仅有 Angular 有服务类(独立文件)的概念(不同框架关注点不同)。可以通过 <a href="https://link.segmentfault.com/?enc=pGM%2FwfdJ2a6Yh1gmLqkQ1w%3D%3D.dqklfc96FrvUG2OWefmV7ftHs%2Br0JSTTvtzMOF0hh%2Be5VvMxr5aF4qHk9L0N%2BFEE" rel="nofollow">Angular 中的依赖注入</a> 进行学习。事实上,在开发较为复杂的业务时,对拉取以及处理数据的代码作为独立文件是不错的处理方式。这样其实也符合 react 和 vue 只做界面渲染层的需求,把功能服务提取出来,这样界面端框架切换就不太会影响服务的提供,就像公司在今年年中计划升级 vue3 这类的大的框架接口改动,出错的可能也会减少。</p>
<p>以小程序请求服务为依赖诸如的例子,开始时候我们注入了自己开发的请求类服务。后面发现了可以自带登陆管理的网络请求组件 <a href="https://link.segmentfault.com/?enc=LCE1Wwhr4AXYqKoBHhOtJQ%3D%3D.nTD5BJ70nexRVtVt4ta5ALm%2FfrDk0IXJaREZw93e2YGmPuETXwlyZ%2FYfwjobaxqa" rel="nofollow">weRequest</a> (如果你有此需求,也可以看看我之前写的 <a href="https://link.segmentfault.com/?enc=OFgFmhKWQiJQ1sFtD2jP2g%3D%3D.8zT3ihNwkKpqdyCUJvi%2FUL9259ziSubQQMhYgQWPbXnuB5jUsXjGWLVJskqfaSZeHYbSHWQYhpvo1Zay3aVnCw%3D%3D" rel="nofollow">从 weRequest登陆态管理来聊聊业务代码</a> 这篇博客)。此时,我们要替换之前的请求服务,只需要对 weRequest 再包装一层,提供与之前服务相等的接口就不需要大幅度修改与业务相关的代码,这样修改基本上也不会出现 bug。诸如此类还有缓存服务等,只要接口不变,究竟存储在 sessionStorage 还是内存,又或者 LRU (Least recently used) 还是 LFU (Least frequently used),这只取决于服务是如何提供的。</p>
<p>个人认为如果想要开发一个持续迭代的应用,一定要在 ETC 上下足了功夫。创业公司更是如此。否则,如果在开发中需要增加一个新的需求或者修改当前需求,却发现当前的服务难以使用乃至于需要重写整个服务,这个在业务开发中是无法接受的。同时,初创公司的需求修改是最为常见的。不过,过度设计也是程序员的通病。</p>
<h2>多项目分离</h2>
<p>随着项目的慢慢变大,虽然我们可以依靠 Webpack 按需加载,但项目的编译与构建时间却不断增长。同时遇到某一个局部 bug 也需要全量编译与发布。项目在开发阶段遇到了巨大的阻力。</p>
<h3>组件与服务提取</h3>
<p>分离出不易变化的共通代码,独立作为项目发布与部署是可行的方法。就像我们会使用 <a href="https://link.segmentfault.com/?enc=Vc7QYCDxF9mjdf8fXiDOSw%3D%3D.vUztgDNF82dtjIvrRTZh2CGyeQpV%2Bs8p3UUOBbTTI4LQmNnx9JkizVDVd4ScP%2F70IoTDfmZ0lxffucyCjCfOSA%3D%3D" rel="nofollow">Webpack externals</a> 来导入外部依赖一样,我们把项目中不易变化的代码分离出去。就像我们会使用开源库来协助开发一样,不可避免的会因为自身需求与开源库不相符合,同时又因为业务需求没那么通用所以不适合提炼出来,只能自己修改。包括很多的服务以及组件。我这里叫做业务型组件。因为这一类基于业务从基础组件中开发出来,但在稳定后改动性也不大。</p>
<p>当然,这里的话,可以采用的是单项目多程序包,类似于 <a href="https://link.segmentfault.com/?enc=It3r3crx3511iXKl1fOVmg%3D%3D.CkqbEGcKNPSS3JnpAdnhvRTl%2BNC26hAxtbiPjoGJkIE%3D" rel="nofollow">lerna</a> 这种模式,也可以采用多项目模式。当然,对于目前初创公司而言,不太可能有几个产品同时在开发,所以可能类似 <a href="https://link.segmentfault.com/?enc=bSwNIRcgI9i9DBTo8ECxlg%3D%3D.8qlSVPs0irAj%2FORT%2Bu2uaTw7uSZTh4wP30IOsqr1gt8%3D" rel="nofollow">lerna</a> 解决方案会更好。但是如果可以提取几个产品的共通服务,倒是可以作为公司的基础库来维护。因为不太容易改变,所以让两个开发人员在有需求的时候去维护也绰绰有余,毕竟不像开源项目那样,需要服务的是各种各样的公司,千奇百怪的需求。当然,提炼出来的业务一定要非常基础。因为比不提取共同代码更难接受的就是一有需求就要改动依赖库的代码。与业务紧密相关的组件或者服务还是放在主项目中,以便于修改。</p>
<h3>widget 开发</h3>
<p>微件(Web Widget),中文可译作:小部件、小工具、微件、挂件等,是一小块可以在任意一个基于HTML的网页上执行代码构成的小部件,它的表现形式可能是视频、地图、新闻或小游戏等等。我们在 ToC 应用经常会使用,例如在网页中插入百度地图,或者百度商桥(在线客服网页插件)等网页 wediget。</p>
<p>在持续工作与开发的过程中,我们有一些业务是与当前业务关系不是那么大,例如登陆业务,很多时候,登陆是独立于我们当前业务之外的业务。例如像阿里很多登陆都会有钉钉登陆,支付宝登陆等模式,把整个登陆界面作为单独的项目然后用 iframe 进行登陆业务开发。如果做的好的话,我们多个产品就可以使用同一个登陆项目。这个对于小公司可以节省不少开发时间。</p>
<p>我们在开发中遇到的报表等无强业务相关的代码都可以提出来作为单独项目进行开发。如果有需求的情况下,也可以开发浏览器插件来辅助业务开发。</p>
<h3>多页面业务拆分</h3>
<p>伴随着代码的进一步扩展,我们开始基于不同的业务来拆分我们的项目。当然,事实上基于业务的多页面拆分并不意味真正的需要多个项目。在当前人手不够的情况下,不拆分为多个项目更好。当然此时各个单页面需要都需要加载共同的控制台头部(参考阿里云控制台)。当然,事实上,在单项目应用时期,我们就可以按照多页面拆分。但是当前如果没有做过上述几个操作,项目的编译和构建在开发中将会是一个灾难。同时,我们如果可以按照单页面拆分整个项目,就可以减少全局状态,减少各个业务间的全局状态也在一定程度上规避 bug。</p>
<p>可以看到,到达这一步,初创公司从零开发的产品可能已经开发了 2-3 年。如果当前的公司没有特别大的资金介入,可能在未来的几年内还是以这种形态进行开发。</p>
<h2>微前端</h2>
<blockquote>Techniques, strategies and recipes for building a <strong>modern web app</strong> with <strong>multiple teams</strong> that can <strong>ship features independently</strong>. -- <a href="https://link.segmentfault.com/?enc=B0r762ecdD3kj2QKE%2F4x5Q%3D%3D.iNqvQwqVyVL7JQWO1ph485A9F6wOOdwF%2FLOt1m0bt9o%3D" rel="nofollow">Micro Frontends</a>
</blockquote>
<p>上面是微前端的定义,首先第一个关键词就是多团队,多团队代表涉及的业务和人员有一定的基数,如果人员团队不够多,实在没必要上微前端。同时,微前端不会限制技术栈。某些特定场景下可能特定的技术栈有更好的生态。</p>
<p>当然,我认为微前端的最大的作用就是在遗留系统中做增量升级。面对已经上线几年的老项目,我们不可能一步到位就升级现有系统的技术栈。微前端是我已知实现渐进式重构的最好方案。</p>
<p>上述基于业务拆分虽然也可以解决状态隔离和独立开发部署等功能,但是由于需要加载同一个控制台头部,所以不能简单的做到各业务技术栈无关(可以做,但是目前需要双方协同)。不过个人目前也遇不到此类需求。</p>
<p>简而言之,微前端就是将大而恐怖的东西切成更小,更易于管理的部分,然后明确地表明它们之间的依赖性。各自团队的技术选择,代码库,发布流程都应该能够彼此独立地运行和扩展。无需过多的协调,让各个团队之间高内聚而低耦合。更自由灵活的使用 ETC 原则。</p>
<p>因为公司目前在创业阶段,没有微前端的需求。所以对微前端更多的是概念解读,而并非落地实践。当然,对于需求微服务的团队来说,实际上想要落地,还是需要做非常多的工作的,个人也没有什么技术实践和经验,就不再继续再写下去了。关于业界实践,大家可以看一看D2 中关于微前端专场的资料 <a href="https://link.segmentfault.com/?enc=hj7sOdcx1NdY3q9exO7W5g%3D%3D.UDesAWumyXV9HYIobx4AwjG2QeEhmRxR1DK433b2lng%3D" rel="nofollow">d2forum 14th</a> 与 <a href="https://link.segmentfault.com/?enc=kSLsmJfYwmN9Oz7RJfZ%2BFw%3D%3D.RXby%2BTVC2T3CgKP28BkhLSD4uAs5XrKqAtDpRblOSfwjXCCCh%2B9nCd8qydsChG1IqVobnfXPLJB5RTAlqMsTaQSYWro3iHmlaaYz0lr3gKFWtNu1iLKx0Z%2Fu59y9s0VjWOj%2BbB4wGUAEwliUdurkRa4GCc3WmWq5RCXCPPeHzYw%3D" rel="nofollow">视频</a>,也可以关注 <a href="https://link.segmentfault.com/?enc=agro5epSi4asxz%2BMGl5kZw%3D%3D.7DfrCDeHcEpIRYAYIsvD%2BvDcTN%2B0ggCwWUe3WufOnuM%3D" rel="nofollow">qiankun</a> 和 <a href="https://link.segmentfault.com/?enc=fCeosjP%2FBmf1T%2BrGTKrjyg%3D%3D.6LevWToS8IvxYZcVqHzCuz1OvgEV5bZh84Rd8BCciZ7bsn48jUYoL%2BCsfi%2FSdXQ5uQ%2B8VRoxVzDSh%2F95Jg0Yaw%3D%3D" rel="nofollow">alibabacloud-console-os</a>。后面我也会多研究这两个开源库来提升自己。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=oZRUHZvonq7icTQYo36lkg%3D%3D.nTUcTaNwieIApwUEBkCIYjOHyfDXTbcZC%2FlmIjsAl47Di%2F6HGTNHWHfreHIo31tx" rel="nofollow">博客地址</a></p>
<h2>参考资料</h2>
<p><a href="https://link.segmentfault.com/?enc=i3fuqp10kPFUmTczrOeUhA%3D%3D.brJ7HVqMwWd3gVOkjaM9tJO08e1z2qAzzYFcR88cMcwc%2FRzAGa16RoppbuMRxyhy" rel="nofollow">易于修改原则</a></p>
<p><a href="https://link.segmentfault.com/?enc=zA8nDNo4anl482cyeZiYbg%3D%3D.D4QypwR9U4EL6FZQkvEqWbXv9Ycr5OLHGcyDYrtFPQY%3D" rel="nofollow">micro-frontends</a></p>
<p><a href="https://link.segmentfault.com/?enc=uV1s56Z%2FW%2B6U0DgzSz9n0Q%3D%3D.as7uZnxwEKKBFf7DsrlinDJrvbiE%2FaOWZ2h2LkmSkOs%3D" rel="nofollow">d2forum 14th</a></p>
<p><a href="https://link.segmentfault.com/?enc=oYTmODTDYudniLNxE2CJbg%3D%3D.8nKXfkDOBSAe4vKfOcQ20YSbecBzJYHXkMW9MUX1Egg%3D" rel="nofollow">qiankun</a> </p>
<p><a href="https://link.segmentfault.com/?enc=0Dq3pYyEBOqjd77YJZCH0g%3D%3D.Yts7TfSPwQTLIKp2qrUzAVie48Ej0sgFtrH4PorbJFSMMaX67cpInHoS5wJtvl5HwKYELztnGiGvSFtXuKOcKA%3D%3D" rel="nofollow">alibabacloud-console-os</a></p>
记一次愚蠢的 issue (css env)
https://segmentfault.com/a/1190000021634735
2020-01-21T11:54:47+08:00
2020-01-21T11:54:47+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
9
<p>在去年 11 月份中旬,随着有赞 Vant Weapp 库 1.0 版本的正式发布,我们的小程序也是立即跟随官方的脚步,着手把依赖库升级了一下。但是却发现 Vant Weapp 对于苹果的底边适配却在微信开发者工具中消失了。在不了解 css env 实际作用和开发工具适配的的情况下,就鲁莽的发了一个 <a href="https://link.segmentfault.com/?enc=vRdW3aE3mIL420Jxxi3OvA%3D%3D.6oMssMi%2Bx0XncA2x6KE9uO8Kj7CvwX1XLvGZWbrTMi4FTaEnuQFYKC76mlpROdcrT1qGrSjbZSjtROtaTde8Fg%3D%3D" rel="nofollow">issue</a>,在此也记录一下,以便于今后查阅与学习。</p>
<h2>safe-area</h2>
<p>事实上,因为个人之前工作的重心在了 js 与 pc 上,对 css 以及移动端有所忽略,才会对这个安全区域没有更多的学习以及理解。</p>
<p><img src="/img/bVbCWlg" alt="safe-area.png" title="safe-area.png"></p>
<p>面对新式手机的刘海以及胡子,在开发移动端的小伙伴们不得不对手机型号做适配,如果当前使用的界面是整个屏幕,就会发生当前的显示被遮挡的问题。当然,其实对于小程序来说,绝大情况下完全不用考虑上面的刘海,一方面是因为当前的小程序的 navigationBar 做到了适配的功能,不需要考虑头部的问题。从另一方面来说,小程序没有特别的需求下也不需要横屏展示。但是对于底部的胡子,我们需要留给其 34px 的高度。在 1.0 版本之前,Vant Weapp 的适配是这样的。</p>
<p>下面代码相对于源代码有所修改,但是基本逻辑是在组件中获取到当前的手机信息。</p>
<pre><code class="ts">// 缓存数据
let cache = null;
// 获取有关安全区域的数据(有缓存得缓存)
function getSafeArea() {
return new Promise((resolve, reject) => {
if (cache != null) {
resolve(cache);
} else {
wx.getSystemInfo({
success: ({ model, statusBarHeight }) => {
const deviceType = model.replace(/\s/g, '-');
const iphoneNew = /iphone-x|iPhone11|iPhone12/i.test(deviceType);
cache = {
isIPhoneX: iphoneNew,
statusBarHeight
};
resolve(cache);
},
fail: reject
});
}
});
}
// 提供对外的 函数调用
export const safeArea = ({
safeAreaInsetBottom = true,
safeAreaInsetTop = false
} = {}) =>
Behavior({
properties: {
safeAreaInsetTop: {
type: Boolean,
value: safeAreaInsetTop
},
safeAreaInsetBottom: {
type: Boolean,
value: safeAreaInsetBottom
}
},
lifetimes: {
attached(): void {
getSafeArea().then(({ isIPhoneX, statusBarHeight }) => {
// 当前 data 中就有了判断数据
this.setData({ isIPhoneX, statusBarHeight });
});
}
}
});</code></pre>
<p>Behavior 等同于 Vue 中的 mixin, 提供了 isIPhoneX 这个数据,我们也把该代码拷贝了出来以便于在业务中使用。具体可以参考 <a href="https://link.segmentfault.com/?enc=Ms0s8ezejemEOzf%2BdzzN3w%3D%3D.cq9XUw1tKqULx6C9ISa6xUXkOyo2VdRDREt3OXa2lgXAXbJ8tzMgrcVDcC3SDhfAwt8bSV15VDodFfpy0KM%2BQdVHfTZZKDjdWnPKRDLUL6vct9EIGfbTdQPjiVj%2FvXkE" rel="nofollow">Component 构造器</a> 来方便小程序本身的开发。</p>
<h2>env</h2>
<p>我们为了得到可用空间 safe-area 不得不写了大量的 js 代码来判断当前的手机型号,然后再在 wxml 中通过数据驱动来添加 css。单从代码维护性上来看,为了处理 safe-area, 我们做了一些非必要的耦合,也增加了代码的复杂度。当前代码也不能处理将来可能会有的更多手机型号。</p>
<p>所以,浏览器提供了另外的函数 env 来帮助我们处理这一切,不过在此之前,我们要通过 meta 来让网页使用整个屏幕。</p>
<pre><code class="html"><meta name='viewport' content='viewport-fit=cover'></code></pre>
<p>刚开始,我拿到这个也是一脸懵逼,不是说要使用安全区域吗,为什么在此之前却要平铺整个页面呢?后来查阅资料才知道,如果没有该属性,当前的屏幕就会上面和底部的安全区域呈现出白条,非常的木有用户体验。</p>
<p><img src="/img/bVbCWlm" alt="default.png" title="default.png"></p>
<p>事实上,保留安全区,浏览器已经帮我们做好了,但是我们肯定不能满足于如上所示的样式。如果让我来出设计的话,我可能就直接把网页分成多分,然后各自写css 样式。但是那群大佬不是这样考虑的,他们直接让你把页面填满,然后再提供变量让你对网页的区域做功课。</p>
<p>viewport-fit 出现的本意是对智能手表进行网页显示的适配,但是却被苹果公司先用在了 iPhone X 上(纵观 css 的历史,就会发现目前我们在业务中使用的很多功能原来的意图根本不是为了解决当前问题)。</p>
<p>在告知了浏览器使用整个屏幕后,我们就可以结合 env 来做适配了。在不兼容 env() 的浏览器中,会自动忽略规则。</p>
<pre><code class="css">/* 使用 safe-area-inset 来对应 竖屏的 top和 bottom,以及横屏的 left 和 right*/
env(safe-area-inset-top);
env(safe-area-inset-right);
env(safe-area-inset-bottom);
env(safe-area-inset-left);
/* 如果当前浏览器没有提供 safe-area-inset-top,那就回退使用 20px */
env(safe-area-inset-top, 20px);
env(safe-area-inset-right, 1em);
env(safe-area-inset-bottom, 0.5vh);
env(safe-area-inset-left, 1.4rem);
.console {
padding: 12px;
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}</code></pre>
<p>任何一个提案都不是一蹴而就的,在 ios 11 时候,我们是使用 constant 来控制的,如果为了兼容,我们就必须使用如下代码:</p>
<pre><code class="css">.console {
/* iOS 11.0 */
padding-bottom: constant(safe-area-inset-bottom);
/* iOS 11.2 */
padding-bottom: env(safe-area-inset-bottom);
}</code></pre>
<p>当然,constant 这个单词我认为是命名上的一种“失误”,它在命名上对应的应该是css var,但是在功能上却并无此意义。而 env 是更符合当前语义的。我也有理由相信,今后的 env 可以提供更多的变量来辅助开发。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=FROkCYEV07UxzGM5f22XOA%3D%3D.t%2B8XrYY90IhIj09Jtp9SIehrrcsByYQWAZ%2Fkod58Ge%2BecqgMSeqZqPMd604GG8fg" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=z5MDgO%2FuzpK23gLlSlZlgw%3D%3D.ZRvTudL9bbFnNFIO9v1Hu%2BCejuzdRE2iYRySd4ToglrXo7tfwZpMAUnHOPZxNBgeCmSbEa7oerEiHRPxIrcI6g%3D%3D" rel="nofollow">Designing Websites for iPhone X</a></p>
<p><a href="https://link.segmentfault.com/?enc=BzfWruzkequC0k16a3b4qQ%3D%3D.49Lvc0fqTIYuyFCp%2F5ECGVRrJrC6BPt0dwYU0nqfCInBvbKYAn3009f8mh1mmO%2Be9NeF7VfJ6eVZGr%2Bp27MqAxFcSUoP%2B%2BLlh8ujkBi4CVY%3D" rel="nofollow">MDN viewport-fit</a></p>
<p><a href="https://link.segmentfault.com/?enc=J7nVJNxtUCsWBaWZTiyrsw%3D%3D.W4WSIYdaOrxS1jCJBgrlmbA7iPEGRr5LMlSdsKVd3GmsPEnsvGB6%2Fm7uoTIb9VvD" rel="nofollow">https://drafts.csswg.org/css-...</a></p>
<p><a href="https://link.segmentfault.com/?enc=7bJt59bkYdVrpOSp7DBPBw%3D%3D.I4QcufdLP3uUJ%2FlDcchbbc1gMYGIqWt5sZiXrz6oUkFVl8tFSmCA9oS1PSHjOzJI" rel="nofollow">css env</a></p>
<p><a href="https://link.segmentfault.com/?enc=P0qtDKqXrbVfIms27%2F09Yg%3D%3D.MszkaREP0rZVJeYVNLH7KGpX4Lm%2FRLqhNuikhPDATf%2F1kCVyr1eb3sdi2tldBwcv" rel="nofollow">兼容iphone x刘海的正确姿势</a></p>
小程序跨页面交互的作用与方法
https://segmentfault.com/a/1190000021507339
2020-01-07T00:59:03+08:00
2020-01-07T00:59:03+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
4
<p>去年年末,微信小程序的分包大小已经到达了 12M 大小,一方面说明小程序的确逐步为开发者放开更大的权限,另一方面也说明了对于某些小程序 8M 的大小已经不够用了。我个人今年也是在开发一个 to B 小程序应用。这里列举一些跨页面交互的场景。</p>
<p>对于 B 端应用的业务需求来说,小程序开发的复杂度相对比网页开发要复杂一些。一个是双线程的处理机制问题,另一个是页面栈之间交互问题。</p>
<p>注: 笔者目前只需要开发微信小程序,为了在小程序页面中可以使用 properties behaviors observers 等新功能,已经使用 Component 构造器来构造页面。具体可以参考<a href="https://link.segmentfault.com/?enc=Qj8y6i%2Fldns6vVqsQqtGjQ%3D%3D.exzMS3zBhZHk1wC5S1hYY9nEKoUwCVfLhdCXi596tfaN8a17egd2f1VBlU%2BvvWlF%2F9QCHtllQ19Kyn7lzAznxkOqnKLC54JIyU6KFiVfE1iudMmIfiRJuXe30EJgk3S5" rel="nofollow">微信小程序 Component 构造器</a>。如果你也没有多端开发的需求,建议尝试使用,可以得到不错的体验。</p>
<h2>性能优化类</h2>
<p>对于小程序在页面中点击触发 wx.navigateTo 跳转其他页面,中间会有一段时间的空白加载期(对于分包出去的页面,空白期则会更长),但是这是小程序内部机制,没有办法进行优化。我们只能眼睁睁的等待这段没有意思的空白期过去。</p>
<p>当考虑到跳转页面后的第一件事情便是取数逻辑,那么我们是否能够进行优化呢?答案是肯定的。我们没有办法直接在当前页面取得数据之后再进行跳转操作(这样操作更不好),但是我们却可以利用缓存当前的请求,详情可以参考我之前的博客文章 ——<a href="https://link.segmentfault.com/?enc=FDclFbcJFl0F14164gAxCw%3D%3D.vwPaZz2DrfUCMSScN%2F2%2FM9vwzd5XKQGOwufRtNzDnjsyJXACxU46dvFbu2Rzcg1yFn1fEjztKFwUHtzFzA7Hrg%3D%3D" rel="nofollow">Promise对象 3 种妙用</a>。</p>
<p>代码如下:</p>
<pre><code class="js">const promiseCache = new Map()
export function setCachePromise(key, promise) {
promiseCache.set(key, promise)
}
export function getCachePromise(key) {
// 根据key获取当前的数据
const promise = promiseCache.get(key)
// 用完删除,目前只做中转用途,也可以添加其他用途
promiseCache.delete(key)
return promise
}</code></pre>
<p>做一份全局的 Map,然后利用 Map 缓存 promise 对象,在跳转之前代码为:</p>
<pre><code class="js">// 导入 setCachePromise 函数
Component({
methods: {
getBookData(id) {
const promise = // promise 请求
setCachePromise(`xxx:${id}`, promise)
},
handleBookDetailShow(e) {
const id = e.detail
this.getBookData(id)
wx.navigateTo({url: `xx/xx/x?id=${id}`})
}
}
})</code></pre>
<p>而跳转之后的代码也如下所示:</p>
<pre><code class="js">// 导入 getCachePromise 函数
Component({
properties: {
id: Number
},
lifetimes: {
attached () {
const id = this.data.id
// 取得全局缓存的promise
const bookPromise = getCachePromise(`xxx:${id}`)
bookPromise.then((res) => {
// 业务处理
}).catch(error => {
// 错误处理
})
}
},
methods: {
getBook() {
// 获取数据,以便于 错误处理 上拉刷新 等操作
}
}
})</code></pre>
<p>如此便可以同时处理取数和页面加载的逻辑,当然,这个对于页面有耦合性,不利于后续的删除与修改。但考虑如果仅仅加在分包跳转之间可能会有不错的效果。</p>
<p>想要无侵入式,可以进一步学习研究 <a href="https://link.segmentfault.com/?enc=rjqmjNdDLIO3kHv96fAglQ%3D%3D.shnjqWCKQWMYyrno03E7in5jxFNrFlybKdJV7%2F%2F1xaVpNNcCH6uTTpjkE%2FsWlQ5fV8kBNmhvgokCp3g3gdLqkw%3D%3D" rel="nofollow">微信小程序之提高应用速度小技巧</a> 以及 <a href="https://link.segmentfault.com/?enc=2xFJ%2Bkp9Uj0f2v0DcuFe7g%3D%3D.2CkK%2BRiWDLcldFrZYgyVzNCOCl0IGCgO9ijCEleZecM%3D" rel="nofollow">wxpage</a> 框架,同时考虑到无论是 ToC 还是 ToC 用户,都有可能存在硬件以及网络环境等问题,该优化还是非常值得的。</p>
<p>当然微信小程序为了减少冷启动时间,提供了<a href="https://link.segmentfault.com/?enc=ygTP1Ylt%2B429XceFmzX9pg%3D%3D.alNGZYDowhXgVDfP8wiL4V1rckdOKfRMiNT%2FmKqJVB75kUz15nOEYJ87V3ljLoZcl49KtLfVccQRWJQ4KGj%2BUlOVaWdurdFsnAJgpSCbxQrGmOC4yoTdURd2Gw%2F3Fuj8" rel="nofollow">周期性更新</a> <a href="https://link.segmentfault.com/?enc=GHE2TDbQXljdhF0aZsq27w%3D%3D.PyY6sCS5AZLIKryrBzcuDexOew9SnV7p2iLIu8vp%2F16sDh8sdSkXvBq0cR89QM%2BkDFoUR0p7qIW644tTRlWqBAdJ6mLh21QuVk%2Fiiq0%2FiNpAjX5tzTbqSK8MBPMtj5yZ" rel="nofollow">数据预拉取</a> 功能。</p>
<p>注: 上面的 promiseCache 只作为中转的用途,不作为缓存的用途,如果你考虑添加缓存,可以参考我之前的博客文章—— <a href="https://link.segmentfault.com/?enc=QVZd9BTw%2FE4n81hGyxVdXg%3D%3D.PyCNiY85a6UxJvmvvLYAzBPzRRP6vMV%2Bg7BrEgivqioGgIXJXW53iHgJIrX2ZEvt" rel="nofollow">前端 api 请求缓存方案</a>。</p>
<h2>通知类</h2>
<p>如果是 pc 端中进行交互,对于数据的 CRUD。例如在详情页面进行了数据的修改和删除,返回列表时候就直接调取之前存储的列表查询条件再次查询即可,而对于移动端这种下拉滚动的设计,就没有办法直接调用之前的查询条件来进行搜索。</p>
<p>如果从列表页面进入详情页面后,在详情页面只会进行添加或者修改操作。然后返回列表页面。此时可以提示用户数据已经进行了修改,请用户自行决定是否进行刷新操作。</p>
<p>如在编辑页面修改了数据:</p>
<pre><code class="js">const app = getApp()
component({
methods: {
async handleSave() {
//...
app.globalData.xxxChanged = true
//...
}
}
})</code></pre>
<p>列表界面:</p>
<pre><code class="js">const app = getApp()
component({
pageLifetimes: {
show() {
this.confirmThenRefresh()
}
},
methods: {
confirmThenRefresh() {
// 检查 globalData,如果当前没有进行修改,直接返回
if(!app.globalData.xxxChanged) return
wx.showModal({
// ...
complete: () => {
// 无论确认刷新与否,都把数据置为 false
app.globalData.xxxChanged = false
}
})
}
}
})</code></pre>
<p>当然了,我们也可以利用 wx.setStorage 或者 getCurrentPage 获取前面的页面 setData 来进行数据通知,以便用户进行页面刷新。</p>
<h2>订阅发布类</h2>
<p>如果仅仅只涉及到修改数据的前提下,我们可以选择让用户进行刷新操作,但是如果针对于删除操作而言,如果用户选择不进行刷新,然后用户又不小心点击到了已经被删除的数据,就会发生错误。所以如果有删除的需求,我们最好在返回列表页面前就进行列表的修改,以免造成错误。</p>
<h3>mitt</h3>
<p>github 上有很多的 pub/sub 开源库,如果没有什么特定的需求,找到代码量最少的就是 <a href="https://link.segmentfault.com/?enc=P5DHiYgeGlElJwpc717x5w%3D%3D.wd8SI2fI6Q29OrmtG7%2FSJi5GwlK2XkBxtMBeX85AcWa5PnZ5CYQU%2BGE4VOV4Ap9e" rel="nofollow">mitt</a> 这个库了,作者是喜欢开发微型库的 <a href="https://link.segmentfault.com/?enc=OMrw3AojT0HpFTuo11%2Ba2Q%3D%3D.XHrx0pfxsAZwQpChVqCepdXU8EpYwMDCCNpQA6CQLqY%3D" rel="nofollow">developit</a> 大佬,著名的 <a href="https://link.segmentfault.com/?enc=HUofRfns3QPo851eyOIs2w%3D%3D.0QlW%2F6WNlBTsC3CLq4WybLGvB0xhcWUX%2Bl5wxkiIVqU%3D" rel="nofollow">preact</a> 也是出于这位大佬之手。 这里就不做过多的介绍,非常简单。大家可能都能看明白,代码如下(除去 flow 工具的检查):</p>
<pre><code>export default function mitt(all) {
all = all || Object.create(null);
return {
on(type, handler) {
(all[type] || (all[type] = [])).push(handler);
},
off(type, handler) {
if (all[type]) {
all[type].splice(all[type].indexOf(handler) >>> 0, 1);
}
},
emit(type, evt) {
(all[type] || []).slice().map((handler) => { handler(evt); });
(all['*'] || []).slice().map((handler) => { handler(type, evt); });
}
};
}</code></pre>
<p>仅仅只有3个方法,on emit以及 off。</p>
<p>只要在多个页面导入 生成的 mitt() 函数生成的对象即可(或者直接放入 app.globalData 中也可)。</p>
<pre><code>Component({
lifetimes: {
attached: function() {
// 页面创建时执行
const changeData = (type, data) => {
// 处理传递过来的类型与数据
}
this._changed = changeData
bus.on('xxxchanged', this._changed)
},
detached: function() {
// 页面销毁时执行
bus.off('xxxchanged', this._changed)
}
}
})</code></pre>
<p>这里mitt可以有多个页面进行绑定事件,如果需求仅仅只涉及到两个页面之间,我们就可以使用 wx.navigateTo 中的 EventChannel (页面间事件信息通道)。可以参考<a href="https://link.segmentfault.com/?enc=FZ6aViIVRh%2FJGsmMSmN3oA%3D%3D.uUoPCp%2BJA%2FpI881JmThOJdwxUw0pteL94ewplNlQJc6HIQVKl3BCmDsvoSH%2F4XZKvyKeskITIzdGttrdis4x9A%3D%3D" rel="nofollow">微信小程序wx.navigateTo方法里的events参数使用详情及场景</a>,该方案的利好在于,传递到下一个页面的参数也可以通过 EventChannel 来通知,以便于解决 properties 传递数据不宜过大的问题。</p>
<p>注: 一个页面展示很多信息的时候,会造成小程序页面的卡顿以及白屏。小程序官方也有长列表组件 <a href="https://link.segmentfault.com/?enc=ayZKZyZrxGGa5EECSiAMcQ%3D%3D.mksLLRJRGTeDLn2%2B2phNy9UAUyC4NrXViXoCI79jKAtlVBf2ZsES6%2B8sA59aN61J%2FzhveyVBOR2y2IaftUUsN6Ad3tXDbQ1CzcfXkAeRfm2qLcbJVXToaWv1fwDS%2FpvI" rel="nofollow">recycle-view</a>。有需求的情况下可以自行研究,这个不在这里详述。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=F7aXzgUrXWVDF7Yzop3S7Q%3D%3D.VAsWXh9UBdlpF%2F2hvepA92XMvwZM%2FEXKDKzj2%2BFBGRY2ebS8w%2BMjtHaI2Q31srG0" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=4YVSlrNRtx82VhBTeRRrIg%3D%3D.FhW2qpNY84HM0NBrusGNUknes4oIqxGzQ31%2BUrWz6MUze2FoYXssOZQQ9yNXrEPwkTJslXQe%2Bd2obk79ocKqPwVZjJuicigO8sDFmi%2FyW1pK6bV%2B2UXhJMrRCGvFB5jD" rel="nofollow">微信小程序 Component 构造器</a></p>
<p><a href="https://link.segmentfault.com/?enc=2Z40ulLIUDhC%2FP%2Fb792phA%3D%3D.so2aPkd9ZcMciPfzGYCXV1Lq5IJJb%2FFYcjb%2FkOfPCBXr4JuIgxUzn8WmgYYICXC1Kcyt4BwaSUoLi4l96pY1Cw%3D%3D" rel="nofollow">微信小程序之提高应用速度小技巧</a></p>
<p><a href="https://link.segmentfault.com/?enc=DW4g13p3dUqz1GMWcEezFg%3D%3D.KqBFvN%2BgtpmCmMcF580BV%2FsCSmAC8eZcM8YDZ1FsV04%3D" rel="nofollow">wxpage</a> </p>
<p><a href="https://link.segmentfault.com/?enc=0i%2BUro%2FSglk2gYspLv%2BkbQ%3D%3D.kVdxZ1R6FLo184tdeVMgRncOg%2BP7zGCR6hCNDE%2Bh4oeMQcpmsVZP4KO3k%2Fw7IIGL" rel="nofollow">mitt</a> </p>
<p><a href="https://link.segmentfault.com/?enc=g3BbRsbMZKn1wfLKT2%2Bvcg%3D%3D.ActGvIy00q0QwjQH2ww2oS9npXdLMwgrWVQ65c3WS3TP%2F9K9bAsC9uGHvUTUNdUozwPF%2BDN2CJG7uuNRsAxlEg%3D%3D" rel="nofollow">Promise对象 3 种妙用</a></p>
<p><a href="https://link.segmentfault.com/?enc=BD47o6iaQtQQaXP9Xg99rQ%3D%3D.OCAQIlpyCpjXdEmE5AMw20VcSXDAK%2FC0x7he9DRLoOst%2Bdv6TVP232wrlFXtgBPZ" rel="nofollow">前端 api 请求缓存方案</a></p>
谈谈魔法消失UI框架 Svelte
https://segmentfault.com/a/1190000021489687
2020-01-04T20:44:18+08:00
2020-01-04T20:44:18+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
8
<p>最近基于公司业务需求,可能会要开发一款浏览器插件,调查后发现插件UI开发本质上就是开发页面。于是我便开始寻找一个非常小又非常快的新玩具(工具)。毕竟前端 3 大框架无论哪一个去开发浏览器插件都无异于大炮打蚊子。至于开发效率极低的 Dom 操作我也不想去碰了。于是我就找到了这个已经在国外非常火热的魔法消失 UI 框架 —— <a href="https://link.segmentfault.com/?enc=B%2FfB%2Bn4T33AZ9R%2F52nnKpQ%3D%3D.OHfDwDRT55y0JHsZlhyIlLv44tTyN4UALKyrZ%2FwE3Sw%3D" rel="nofollow">Svelte</a>。</p>
<h3>Svelte是什么</h3>
<p><a href="https://link.segmentfault.com/?enc=YJMoviM34x6rYT73oG6mGQ%3D%3D.NF6%2BLdSgIEjdGwH0hEMmsU1XfsmG5xPFOz72ALkFrCM%3D" rel="nofollow">Svelte</a> 是一个编译型的前端组件框架。该框架没有使用虚拟 dom,而是通过编译在应用状态发生改变时提供异步响应。</p>
<h2>编译型框架</h2>
<p>任何前端框架都是有运行时的,(以 Vue 为例) 该框架至少需要在浏览器携带虚拟dom 以及 diff 算法。如果在页面中直接引入 Vue 脚本,还需要追加 Vue 前端编译器代码。可以参考<a href="https://link.segmentfault.com/?enc=9AvK7Wu1mxCvs5BTO9D2wQ%3D%3D.nssQCk2Umn8SbUJqXxW9fDt9vFo83PW7xZWFp%2F5y3paPerfBa3lMPPErpJPio2pY8AhlKHYCBzIYNkCvYuKX5NBmC6aPeDlRMaGO5YMtDY33VO6wQZRzts4ME0bC3POQKNThkA4NNA0b0dE5J%2FmlzldngXQbDbeFSyxWqiCmeDUvc4%2Bvz7A9hb5rfBu7Sx7r" rel="nofollow">Vue 对不同构建版本的解释</a>。</p>
<p>Svelte 则不同,它从开始就决定把其他框架在浏览器所完成的大部分工作转换到构建中的编译步骤,以便于减少应用代码量。它通过静态分析来做到按需提供功能(完全不需要引入),同时它也可以分析得出根据你当前的修改精准更新 dom的代码来提升性能。</p>
<p>我们以最简单的代码为例子。</p>
<pre><code class="svelte">// App.svelte
<h1>Hello world!</h1>
// main.js
import App from './App.svelte';
const app = new App({
target: document.body
});
export default app;</code></pre>
<p>实际上开发版会被编译为(为了简化,只分析部分,不分析全部代码)</p>
<pre><code class="js">// IIFE 立即执行函数表达式
var app = (function () {
'use strict';
// 空函数,用于某些需要提供函数的代码
function noop() { }
// 当前 元素所在的行 列 前面有多少字符等信息,开发版存在
function add_location(element,
file,
line,
column,
char) {
element.__svelte_meta = {
loc: { file, line, column, char }
};
}
//
// 操作dom辅助函数,减少代码量...(相当于运行时)
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function element(name) {
return document.createElement(name);
}
function children(element) {
return Array.from(element.childNodes);
}
// 异步处理修改过的组件
// (实际上这些代码在当前场景下不需要,可以去除,但没必要)
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
// ...
}
function add_render_callback(fn) {
// ...
}
function flush() {
// ...
}
function update($$) {
}
// 当前文件,开发版
const file = "src\\App.svelte";
function create_fragment(ctx) {
let h1;
const block = {
// 创建
c: function create() {
h1 = element("h1");
h1.textContent = "Hello world!";
add_location(h1, file, 1, 9, 10);
},
// 挂载刚才创建的元素
m: function mount(target, anchor) {
// 上面的 target 是 document.body
insert_dev(target, h1, anchor);
},
// 修改,脏组件在上述 update 中被调用
p: noop,
// 删除
d: function destroy(detaching) {
if (detaching) detach_dev(h1);
}
};
dispatch_dev("SvelteRegisterBlock", {
block,
id: create_fragment.name,
type: "component",
source: "",
ctx
});
return block;
}
}());</code></pre>
<p>可以看到,在开发版本编译完成后,你所写的所有代码都变成了原生的 js 操作Dom。同时具有很高的可读性(这点非常重要)。我在我的另一篇 blog <a href="https://segmentfault.com/a/1190000020672868">优化 web 应用程序性能方案总结</a> 也表明了,提升代码的覆盖率是所有优化机制中收益是最高的。这意味着可以加载更少的代码,执行更少的代码,消耗更少的资源,缓存更少的资源。同时在 Vue 3 中也会又静态分析而进行的按需提供优化。</p>
<p>值得一提的是,因为 Svelte 是编译型框架,无论是开发还是生产环境,都会在相同文件夹诞生同样的文件(至少在笔者开始写时候是这样的结果,于2020-1-4)。如果前端没有使用构建部署工具又或者像我这样仅仅想要开发一个浏览器插件的情况下,可能会造成因为文件夹已经存在而忘记进行构建命令,从而错误的使用开发版所产生的代码。</p>
<p>同时,Svelte 直接支持各种编译配置项目。只要在组件中添加 options 即可:</p>
<pre><code class="html"><svelte:options option={value}/></code></pre>
<ul>
<li>
<p>immmutable 当你确认了当前已经使用不可变数据结构,编译器会执行简单的引用相等(而不是对象属性)来确定值是否更改,以便获得更高的性能优化,默认为 false。当此选项设置为true时,如果父组件修改了子组件的对象属性,则子组件将不会检测到更改并且也不会重新渲染。</p>
<pre><code class="html"><svelte:options immmutable={true}/></code></pre>
</li>
<li>
<p>tag 可以将手写的组件编译成 Web Components 而让其他框架使用。根据如下就可以使用。至于 Web Components 则可以参考阮一峰的 <a href="https://link.segmentfault.com/?enc=wcDh4CZ9GVlk%2FF8BHmJ6eg%3D%3D.eh4%2B1uk5jOC9qV1gy2gYo5MLKh2ALrahnXZf5AwZfyvwImKXEP1pw7WqKJGVu1ohAMNgurPyAfAv3cKgVboRfA%3D%3D" rel="nofollow">Web Components 入门实例教程</a>。这也是新框架不可或缺的功能点。</p>
<pre><code class="html"><svelte:options tag="my-custom-element"/></code></pre>
</li>
<li>accessors 可以为组件的 props 添加 getter 和 setter,默认为false。</li>
<li>namespace 是将要使用该组件的命名空间,一种用途是为 SVG 指定命名空间。</li>
</ul>
<blockquote>当一门语言的能力不足,而用户的运行环境又不支持其它选择的时候,这门语言就会沦为 “编译目标” 语言</blockquote>
<p>几年前的 js 便是这样的语言,当时有表现力更强的 coffeeScript,也有限制性更强的 Typescript。随着时间的推移,前端的想要编译的不再局限于编程语言层面上,而在框架层面也想做的更多。</p>
<p>自从 2018 年末,前端框架更多的往编译型发展 。一种是为了更强大的表现能力和性能增益,如同 Svelte 一般, 另一种则是为了抹平多个平台的差距, 例如 国内的各个小程序框架 <a href="https://link.segmentfault.com/?enc=IRSQvdHAEW%2BHM%2F%2BmPd6uHw%3D%3D.Gvrhl22vMIPMIKKHI09kUwXStUIGeEEQF2ciV5F1rSs%3D" rel="nofollow">Taro</a>(React 系,适合新项目), <a href="https://link.segmentfault.com/?enc=Td%2BX5AeHaK9aPukwp8Zliw%3D%3D.fSbqSZLulmRbda8htOZ0V%2Fded5wTJ4R9p3XWZ8F8dwo%3D" rel="nofollow">Mpx</a>(微信小程序,适合老项目)等。</p>
<h2>更高的表现力</h2>
<p>对比同类型的 <a href="https://link.segmentfault.com/?enc=dpPGyx7Kr%2FzJfDV%2F4O%2BPsg%3D%3D.mIl3%2BRqPWTibBdIszzUOjp%2BcWQxztdoxnm2znLQUcm0%3D" rel="nofollow">Elm</a> <a href="https://link.segmentfault.com/?enc=eSlhOyi5sCTUbwkTEgCvrQ%3D%3D.4CbNOVhW42zfftxWuqyrhtarURLw7BBQ2zaIWRhwDRg%3D" rel="nofollow">Imba</a> 以及 <a href="https://link.segmentfault.com/?enc=dRVSP7L6VQ6%2F4fq8uw2g1Q%3D%3D.5%2FNT7OoCfrL1VTWuGHj8Zy0AhoHYVkseS1sc9zK284%2BOXuO%2BbjKzzJgmrTxUkABQ" rel="nofollow">ClojureScript</a> 这些编译型框架,无论是工具链还是语法表现力, Svelte 的对于前端小伙伴们友好度是最高的。如果想要学习 Svelte,可以去看官网提供的 <a href="https://link.segmentfault.com/?enc=%2FsujLeGGAzcqYHMaovv%2Bxg%3D%3D.gPTyCFwmL0lSwCiunnhNrv7HbVn5iuvYbz5wHs21ZWMQ8hJDIA6unmjAZY%2FmnrXQ" rel="nofollow">tutorial 模块</a>,Svelte 官网对于新手的友好度也是非常棒的。下面则介绍一些特定的语法。</p>
<h3>状态与双向数据绑定</h3>
<p>利用 let 可以设置状态,利用bind:value可以进行数据绑定。</p>
<pre><code class="svelte"><script>
// 直接使用 数据
let name = 'world';
// 配置项,可以直接传入 input
const inputAttrs = {
// input 类型为 text
type: 'text',
// 最大长度
maxlength: 10
};
</script>
<!-- 名字, 同时传递属性可以利用 ...语法来进行优化 -->
<input {...inputAttrs} bind:value={name} />
<hr/>
Hello {name} </code></pre>
<p>可以看到, 上述代码很容易进行了双向数据绑定,以及非常强大的代码表现能力。</p>
<h3>属性的使用</h3>
<p>父组件:</p>
<pre><code class="svelte">
<script>
import Hello from './Hello.svelte';
</script>
<Hello name="Mark" /></code></pre>
<p>Hello 组件:</p>
<pre><code>
<script>
// 这样便是可以被外部接受,但是name也可以被内部修改
export let name = 'World';
// 计算属性,name 修改了,doubleName 发生改变
$: doubleName = name + name
</script>
<div>
Hello, {name}!
</div>
<hr/>
{doubleName}</code></pre>
<h3>同组件直接的交互</h3>
<p>这个功能就是最吸引我的地方,我用过很多组件框架。但对于同组件之间的交互都是要写到父组件中作为业务类型组件。例如地址之间的交互(默认地址),复杂表单之间的交互, 代码如下所示:</p>
<p>Input 组件</p>
<pre><code class="svelte"><!-- 模块 -->
<script context="module">
// 组件全局 Map
const map = new Map();
// 清楚所有的输入数据,导出函数
export function clearAll() {
map.forEach(clearfun => {
clearfun()
});
}
</script>
<script>
import { onMount } from 'svelte';
// 记录的标签
export let index;
let value = ''
// 挂载时候把当前组件的标签和函数放入map
onMount(() => {
map.set(index, clear);
});
// 把当前 input 元素的数值清空
function clear () {
value = ''
}
// 输入时候把其他的数据清除
function clearOthers() {
map.forEach((clearfun, key) => {
if (key !== index) clearfun();
});
}
</script>
<div>
<button on:click={clear}>清楚当前输入数据</button>
<input on:input={clearOthers} type="text" bind:value={value}>
{value}
</div></code></pre>
<p>App 组件:</p>
<pre><code class="svelte"><script>
import Input, { clearAll } from './Input.svelte'
</script>
<div>
// 可以清楚子组件输入的全部数据
<button on:click={clearAll}>清楚全部数据</button>
<Input index='1'/>
<Input index='2'/>
<Input index='3'/>
<Input index='4'/>
<Input index='5'/>
</div></code></pre>
<p>如此,同组件内之间的交互便完成了,非常的简单,但是又是因为 module 全局性的,无论是否在同一父组件内,所有的子组件都会有全局的功能。如果有两个以上的模块在同一页面中,又需要添加多余的属性来为辅助开发。所以这个功能是一把可能会伤到自己的利器。</p>
<h3>其他</h3>
<p>Svelte 有许多简单好用的语法以及动画,这里就不一一介绍了。因为实在是太简单了,如果你使用过其他类型的框架,可能不到几小时就可以上手写业务代码了。当然,如果在开发插件过程中遇到一些不可避免的问题,我也会记录下来再写一篇 blog 。</p>
<h2>无可避免的缺点</h2>
<p>Svelte 已经开发到 3.0 版本后了,大致上来看,一些开发上的问题可能没有,但是毕竟没有大公司支持,所以可能还是会有一些不可避免的缺陷。</p>
<ul>
<li>不支持 TypeScript</li>
<li>生态环境不够好</li>
<li>单组件文件后缀名为 svelte,过于冗长</li>
<li>暂时没有构建工具</li>
<li>
<p>if 判断 for 循环 不好用(为了编译)</p>
<pre><code class="svelte">
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
{#each cats as { id, name }, i}
<li>
<a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a>
</li>
{/each}</code></pre>
</li>
</ul>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=vQ%2BvBum4eSz0OGOaZO9X7w%3D%3D.JsWXlQB3cQLzIcRG2i7CLmzuQXBU9yAI%2FvGd9d72RNpMaOZSqYhrd2kI%2BfRBsv3P" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=%2BIqZEUk4PbxczPGx4sOJnA%3D%3D.I8q0bMPCUx88X0IHFeaDo0tUPJqBIn6Nw13DhK7oO4k%3D" rel="nofollow">Svelte 官网</a></p>
2020年 我要这样写代码
https://segmentfault.com/a/1190000021331264
2019-12-19T00:40:18+08:00
2019-12-19T00:40:18+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
12
<p>在 9102 年年初,一位室友问我一个问题,如何才能够提升写代码的能力?</p>
<p>可惜的是: 当时仅仅回复了一些自己的想法,如多看开源代码,多读书,多学习,多关注业界的动向与实践,同时也列了一些原则。但是这些并没有所总结,又或者说没有例子的语言始终是空泛的。所以在今年年底之际,对应着今年中遇到的形形色色的代码问题来一一讲解一下。</p>
<h2>好代码的用处</h2>
<blockquote>实际上本书建立在一个相当不可靠的前提之上:好的代码是有意义的。我见过太多丑陋的代码给他们的主人赚着大把钞票,所以在我看来,软件要取得商业成功或者广泛使用,“好的代码质量”既不必要也不充分。即使如此,我仍然相信,尽管代码质量不能保证美好的未来,他仍然有其意义:有了质量良好的代码以后,业务需求能够被充满信心的开发和交付,软件用户能够及时调整方向以便应对机遇和竞争,开发团队能够再挑战和挫折面前保持高昂的斗志。总而言之,比起质量低劣,错误重重的代码,好的代码更有可能帮助用户取得业务上的成功。</blockquote>
<p>以上文字摘抄于《实现模式》的前言,距离本书翻译已经时隔 10 年了,但是这本书仍旧有着很大的价值。同时对于上述言论,我并不持否认意见。但是我认为,坏代码比好代码更加的费财(嗯,没打错,我确定)。对于相同的业务需求,坏代码需要投入的精力,时间更多,产出反而会更少。同时根据破窗理论( 此理论认为环境中的不良现象如果被放任存在,会诱使人们仿效,甚至变本加厉 ),坏代码会产生更坏的代码。这是一个恶性循环,如果不加以控制,完成需求的时间会慢慢失去控制。需要完成需求的人也会失落离开。</p>
<p>也就是说,好代码可以实现多赢,能够让用户爽,能够让老板爽,能够让开发者爽。总之,大家爽才是真的爽。</p>
<h2>怎么写出好代码</h2>
<h3>少即使多</h3>
<p>利用开源出来的设计与代码来减轻来自于业务线的时间压力。</p>
<blockquote>The best way to write secure and reliable applications. Write nothing; deploy nowhere.</blockquote>
<p>以上取自 github 上最火的项目之一 <a>nocode</a>。懒惰是程序员的美德之一。所以学习业务,理解业务,拒绝不必要的需求也是一个程序员的必修功课。详情可以参考<a>如何杜绝一句话需求?</a> 这一篇 blog,当然,在大部分场景下,我们是不具备对需求说不的能力与权力的,但是无论如何,深度的理解业务,对客户有同理心是对程序员的更高要求。解决问题才是一个程序员需要做的事情。能够理解好题意才能解决问题。</p>
<p>对于软件开发而言,时间一定是最宝贵,最有价值的资源。相应的,尽量把时间耗费在解决新的问题,而不是对已经存在确切解决方案的问题老调重弹。所以,尽量不要自己写代码,而是借用别人的设计与实现。而在事实上,你也很难在极短的时间压力下设计并完成比开源更加合适的代码。</p>
<p>当然,开源作者一定是想让他的产品有更多的受众,所以从设计上而言,会采用较为通用的设计,如果你的需求较为特殊并且你觉得不能说服作者帮你“免费打工”(或者作者拒绝了),那么你也只需要在特定之处进行包装与改写,但是要比完全重写要简单太多了。</p>
<p>当然,调研新的技术方案并且使用到项目中是一种能力,但是千万不要因为一个小功能添加一个非常大的项目。</p>
<p>笔者在之前就遇到过其他小伙伴因为无法使用数字四舍五入。说 fixed 方法有问题而使用 math.js 的小伙伴。</p>
<pre><code>(11.545).toFixed(2)
// "11.54"</code></pre>
<p>如果想要了解 fixed 方法为何有问题的,可以参考 <a href="https://link.segmentfault.com/?enc=t7SHvHpLOkNYBtHAx5Fdtw%3D%3D.u%2B0QD5mxtNn%2FfD4rzDXz2NtP3p47l0JERBtRKSvXT8Gv4FaRxEuYPTLTNg0XPoi4" rel="nofollow">为什么(2.55).toFixed(1)等于2.5?</a> 作者以 v8 源码来解释为何会有这样的问题,以及提供了部分修正 fixed 的方案。</p>
<p>事实上如果没有很大的精度需求,前端完完全全利用一个函数便可以解决的问题,完全不需要复杂的math 这种高精度库。</p>
<pre><code>function round(number, precision) {
return Math.round(+number + 'e' + precision) / Math.pow(10, precision);
}</code></pre>
<p>当然,也有小伙伴来找我询问大量数据的表格优化,我第一反应就是 <a href="https://link.segmentfault.com/?enc=YhSKcOi9YLG568JPmun8ng%3D%3D.v0aSFfg03p2lYhmze7lcQwZt2zhVU8u80JgrBfxbfIZMe7632Nn%2F%2B2LDc74u4gKe" rel="nofollow">React Infinite</a> 或者 <a href="https://link.segmentfault.com/?enc=kSxgeq3P1H5J4U%2BW%2BMKlyQ%3D%3D.0aylY%2F%2B%2FCgqsJAGhtFzn9YMh%2ByUgTSAMDDfuWyCcJEB5vjyxvGRdsDH4%2BcoyRFmL" rel="nofollow">vue-infinite-scroll</a> 此类解决方案。但是对方能够多提供一些信息包括上下文,采用的技术栈,当前数据量大小,未来可能需要达到的大小,当前表格是否需要修改等。得到了这些信息,结合业务来看,相比于增加一个库,是否如下方式更为便捷与快速。</p>
<pre><code>// 因为 vue 模型的原因,使用 Object.freeze 性能可以有很大增益
this.xxx = Object.freeze(xxx);</code></pre>
<p>随着堆积业务,代码的增长。管理复杂度的成本与日俱增,把依赖降低。 利用开源代码使得任务更容易实现。时间就是成本。关键是让收益可以最大化。</p>
<p>学习更多是为了做的更少。</p>
<h3>统一</h3>
<p>不同的人由于编码经验和编码偏好不同,项目中同一个功能的实现代码可能千差万别。但是如果不加以约束,让每一个人都按照自己的偏好写自己的模块,恐怕就会变成灾难。</p>
<p>所以每次在学习一些新技术的时候,我总是想多看看作者的实例代码,作者是如何理解的,社区又是如何理解的。以求实现起来代码风格不至于偏离社区太多,这样的话可以提高沟通与协作的效率。类似于 《阿里巴巴Java开发手册》 或者 <a href="https://link.segmentfault.com/?enc=hy8AJENKnSPcDbFdqCdRyg%3D%3D.sVRpoaLlkx8SYOQgML%2FFxhMZTDGWClj%2FfKjWfdjcgFlMPF9g72WMHO5%2F1s%2B4Dpg1" rel="nofollow">vue 风格指南</a> 这种取自大公司或社区的经验之谈,要多读几遍。因为他们所遇到的问题和业务更加复杂。</p>
<p>对于公司内部开发来说,写一个组件时候,生命周期的代码放在文件上面还是放在最下面,如何把代码的一个功能点集中放置。通用型代码的修改。代码行数的限制。能够列出统一的方案,多利而少害。</p>
<h3>化繁为简(抽象)</h3>
<p>抽象是指从具体事物抽出、概括出它们共同的方面、本质属性与关系等,而将个别的、非本质的方面、属性与关系舍弃的思维过程。</p>
<p>如果你面对一个较大的系统,你会发现重构并不能解决根本问题,它仅仅只能减少少许的代码的复杂度以及代码行数,只有抽象才可以解决实质性问题。</p>
<p>无论是数据库设计,架构设计,业务设计,代码设计,但凡设计都离不开抽象。抽象能力强的所面临的困难会比能力弱的少很多。</p>
<p>或者说抽象能力弱一些的小伙伴遇到一些问题甚至需要重新推翻然后再设计,这个是在时间和业务开发中是不能被接受的。</p>
<p>这里就谈谈代码,以下也举个例子,如 <a href="https://link.segmentfault.com/?enc=b6LfrD2UZBGaOT%2B9ysdj4g%3D%3D.XHP8GDYzTP03oBRb%2FwrV5JNyhhJF%2B4EuihbmDmzQPPA%3D" rel="nofollow">axios</a> 库中有拦截器与本身业务,在没有看到源码之前,我一直认为他是分 3 阶段处理:</p>
<ul>
<li>请求拦截</li>
<li>业务处理</li>
<li>响应拦截</li>
</ul>
<p>但如果你去看源码,你就会发现其实在作者看来,这 3 个阶段其实都是在处理一个 Promise 队列而已。</p>
<pre><code>// 业务处理
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 前置请求拦截
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 后置响应拦截
chain.push(interceptor.fulfilled, interceptor.rejected);
});
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;</code></pre>
<p>这就是一种代码抽象能力。让自己的代码可以适应更多的场景是程序员需要思考的。代码不是给机器看的,是给人看的,更高的要求是: 代码不仅仅是给人看的,更是给人用的。需要考虑到协作的人与事,灵活的配置也是必须要考虑到的。就拿前端的 虚拟 dom 来说。能够适配更多的平台。</p>
<p>当然了,抽象能力需要时间,需要经验,需要学习大量的设计。</p>
<p>注意!:不要过早的抽象业务代码,甚至不要抽象业务代码。多写一点代码无所谓,千万别给自己找事做。 在业务上尽量保持简单和愚蠢。除非你是业务专家,确认当前业务不太会产生变化。</p>
<h3>权责对等(拆分与合并)</h3>
<p>责任与义务本质上就是对等的,且越对等的就越稳定。这几年,微服务架构,中台,微前端理论层出不穷,本质上就是为了权责对等,对于更加基础的服务,更有产出的业务投入更高的人力与物力以保证更稳定的运行是很正常的一件事。而不是之前的大锅饭(单体应用)。</p>
<p>从代码上来看,某个模块是否承担了它不应该做的事情,或者某个模块过于简单,徒增复杂度。</p>
<p>当然,事实上有些东西目前是做不到的让所有人都觉得满意,增一分则肥,减一分则瘦,刚刚好很难界定。就像 Dan Abramov 说的那样:</p>
<blockquote>Flux libraries are like glasses: you’ll know when you need them.</blockquote>
<h3>只做一件事</h3>
<p>Unix 哲学,这个很好理解,就像我今年想做的事情太多,反而什么都没有做(或者说都做了,但都不好)。</p>
<p>代码上来看,不要因为一点点性能的原因,把几件事合在一起去做。例如在一次 for 循环中解决所有问题,或者将所有代码写在一个函数中,例如:</p>
<pre><code>created() {
const {a,b,c,d} = this.data
// ... 三件事情彼此有交互同时需要 a,b,c,d
// 完成之后的逻辑
}</code></pre>
<p>改造后:</p>
<pre><code>created() {
const axx = doA()
doB()
const cxx = doC()
// 完成之后的逻辑
}
// 分离出3个函数
doA() {
const {a,b,c} = this.data
// ... 三件事情彼此有交互同时需要 a,b,c,d
// 完成之后的逻辑
}
// 其他代码</code></pre>
<p>相比于第一个只需要一次取数,一次setData,第二个性能无疑更低,但是可维护性变高了,3 件事情都被拆分出来,后面修改代码时候,我可以追加一个 doD 而不是再次把第一份代码中逻辑整理清楚再小心翼翼的修改代码。</p>
<h3>命名与注释</h3>
<blockquote>There are only two hard things in Computer Science: cache invalidation and naming things.</blockquote>
<p>命名与缓存失效是两大难题,今年讲了不少缓存问题,同时,命名的确是很困难的一件事情。通过一句话来解释你们在做什么事情,通过一句话来解释一件事的意图。</p>
<p>不说在程序世界中,在现实世界中也是如此。例如: 《震惊!xxx居然xxx》等新闻,虽然说看完后都会想要骂一句,但是,正如这样的名字才能吸引人家点击进入,让人情不自禁的被骗一次又一次。所以在项目没有发布前,要取一个简单而又好记的名字。</p>
<p>但在程序内部,我们不需要“骗取”人家的点击量,反而是要务实点,不要欺骗另外的同伴,比如说写了一个简单的名字,结果内部却封装了很多的业务代码。同时我认为这也是函数越写越短的理由,因为大家难以通过命名来解释那一大坨代码的意图。所以,需要编写可以自我解释的代码,而这种代码最佳实践就是好的命名。</p>
<p>对于开源代码,你往往会发现,这些文件开头都会有一系列注释,这个注释告诉我们了这个模块的意图与目的。让你无需看代码就可以进行开发。</p>
<p>对于业务开发而言,仅在你不能通过代码清晰解释其含义的地方,才写注释。在多个条件下都无法解释你的代码。</p>
<ul>
<li>项目名</li>
<li>模块名</li>
<li>文件名(类名)</li>
<li>函数名(方法名)</li>
</ul>
<p>这并不是让你不写注释。但是我觉得更多的注释应该放在数据结构而不是代码逻辑上。聪明的数据结构和笨拙的代码要比相反的搭配工作的更好。更多的时候,看数据结构我能了解业务是如何运行的,但是仅仅看到代码并不能实际想象出来。</p>
<p>实际上,随着时间的推移,代码做出了许多改动,但注释并没有随之修改,这是一个很大的问题。注释反而变得更有欺骗性。</p>
<p>这里也提供一篇 <a href="https://link.segmentfault.com/?enc=vu0B7G3YsTT9n%2FUh8M7UTw%3D%3D.mxJV2HE1CuH5Co2iSP70uSTZ5d6Oz%2FomED7mgVots7ebvI0MMVHjXlCkpHDCrhFCaqvZBSd1MjrlyEf1JJOlKYSRdt%2B3t2dyuqttp2HQ%2FTU%3D" rel="nofollow">export default 有害</a> 的文章。我觉得 export default 导出一个可以随意命名的模块就是一种欺骗性代码(随着时间的推移,该模块的意图会发生变化)。</p>
<h3>考虑场景</h3>
<p>没有放眼四海皆准的方案,所以我们必须要考虑到场景的问题,我们总是说可修改性,可读性是第一位的(往往可读,可修改的代码性能都不差)。但是如果是急切需求性能的场景下,有些事情是需要再考虑的。</p>
<p>if 是业务处理中最常用的,在每次使用前要考虑以下,哪个更适合作为主体,哪个更适合放在前面进行判断。如果有两个维度上的参数,一个是角色,一个是事件。一定是会先判断角色参数,然后再去判断事件参数,反之则一定不好。因为前者更符合人的思维模式。在同一维度下,至于哪个放前面,一定是更多被使用的参数放在前面更好,因为更符合机器的执行过程。</p>
<p>就像在 if 中你究竟是使用 else 还是 return。大部分情况下处理业务逻辑互斥使用 else,处理错误使用return。因为这样的代码最符合人的思维逻辑。</p>
<p>但是在这里我也要举出来自《代码之美》的例子,在第五章中,作者 Elliotte Rusty Harold 设计了一个 xml 验证器,其中有一段在验证数字字符:</p>
<pre><code>public static boolean isXMLDigit(char c) {
if (c >= 0x0030 && c <= 0x0039) return true;
if (c >= 0x0660 && c <= 0x0669) return true;
if (c >= 0x06F0 && c <= 0x06F9) return true;
// ...
return false
}</code></pre>
<p>这个优化之后如下:</p>
<pre><code>public static boolean isXMLDigit(char c) {
if (c < 0x0030) return false; if (c <= 0x0039) return true;
if (c < 0x0660) return false; if (c <= 0x0669) return true;
if (c < 0x06F0) return false; if (c <= 0x06F0) return true;
// ...
return false
}</code></pre>
<h3>全局思考,善于交流</h3>
<p>软件开发已经不是一个人打天下的时代了,你要不停的触达边界。在前后端分离的时代,前端可以不知道数据库如何优化,后端也可以不清楚浏览器的渲染机制,但是却不能不明白对方在做什么。否则等于鸡同鸭讲,也会浪费时间。在开发时候,把一段逻辑放在那一端取决安全的思考以及简化逻辑。</p>
<p>善于交流是一种能力,在与别人交流时给与足够的上下文,让你的 leader 沟通,让她知道你的难处。和小伙伴沟通,说服他人按照你的想法推进,同时,善于聆听才能不断进步。</p>
<h3>算法</h3>
<p>我不是一个算法达人( leetcode 中等题目都费劲 ),但这个没什么可说的,你拿你的 O(n**3) 算法去对战人家 O(n * logn) 算法就是费财。所以,知道自己某方面不够好去努力就行了。</p>
<h2>辅助工具</h2>
<h3>TypeScript</h3>
<p>虽然早就接触和实践过,但是以往都是 AnyScript。今年也算重度使用了。才体会到该工具的利好。一个好的开发工具并不是让你少写那一点点代码,而是让你在交付代码时候能够更加自信。</p>
<p>TypeScript 最大的好处就是让你在写代码前先思考,先做设计。就像之前说的。聪明的数据结构和笨拙的代码要比相反的搭配工作的更好。</p>
<p>TypeScript 同时也可以让大部分运行时错误变为编译时,并且可以减少使用中的防御性编程(信任但是仍要验证)。你不是一个人在写代码,协作优先。</p>
<p>在开发中,如果你接触过复杂性数据结构,并且还要在模块中不断进行数据转化,你就会不断的遇到:我的数据呢?到底在那一步丢失了?并且即使是代码对的,你仍旧害怕,仍旧怀疑。我已经过了那个“写 bug 是因为想的不够多,不够彻底”的年龄。</p>
<h3>函数式思维</h3>
<p>js 是有函数式的血统的,当年一直听说,函数是一等公民,只是当时完全不能理解。</p>
<p>纯函数,数据不可变以及代码即数据这三点是我认为是函数式思维对代码能力提升最大的三点。</p>
<p>这个我不想展开去聊,因为我没有熟练掌握过任何一门纯函数式语言。但是我的代码一定有函数式的影子,并且它的确让我的代码更优美。</p>
<h3>其他</h3>
<p>单元测试,代码审查,安全等等都没有讲到,这个我也需要足够的学习才能有所输出。不过这里列出一些资料供大家学习与了解:</p>
<p><a href="https://link.segmentfault.com/?enc=m4BKJ16p8D0bH34Q3zsaag%3D%3D.x8syUIr94vkNDMK%2FZ5yAvjlQOUAaZo3ZQKpGdfFKuJH2Ns9CjfdVlhfBosGFeN7Q" rel="nofollow">谷歌代码审查指南</a></p>
<p><a href="https://link.segmentfault.com/?enc=Yw%2BU4F96supN2djaIA4R9g%3D%3D.8S5sqTIIgmK7LVclzmh5iYHm34OY5veoeab%2FEPIEHXsvgMgDrPQ%2Bo8YU2Oj3kvQhX88AnrC1jJlF91aoqR4ZlU%2FyCRi8lrCGhacOHUtXaX0%3D" rel="nofollow">SaaS型初创企业安全101</a></p>
<h2>有理有据就是好代码</h2>
<p>工作在别人遗留的糟糕代码上是常有的事情,同时面对开发需求实际表,为了兼容,我们也不得不写出一些不那么好的代码。但是面对他人的疑问,我们需要给与别人这样做的理由,也就是你的每一行代码写下去一定有充分的理由和依据。</p>
<h2>结语</h2>
<p>明显不等于简单,上述都是很明显的事情,但是要做好都需要很长时间的学习与经验。</p>
<p>所以如何才能写好代码呢?那就是多看开源代码,多读书,多学习,多关注业界的动向与实践。不断学习,不断进化的代码才是好代码。</p>
<p>最近有一些小伙伴(无中生友)问我的名字为什么要叫 jump_jump。我是为了让自己以及看到我的小伙伴们牢记多锻炼,闲下来的时候多跳一跳。对身体有好处。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <a href="https://link.segmentfault.com/?enc=5EX3fkyZCxfcZXlAqGW0IQ%3D%3D.Dw4GoM%2F%2BuzjVc9FSXsryobP0OARsQuln6HZ305sVbZdEGQdXTUva8w2PqQjWHzsm" rel="nofollow">博客地址</a></p>
Promise对象 3 种妙用
https://segmentfault.com/a/1190000021291349
2019-12-16T09:00:00+08:00
2019-12-16T09:00:00+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
20
<p>9012 年末,作为一个前端,说不了解 Promise 对象用法的基本不存在,这里就不对功能用法进行介绍了。但本文将会讲述你可能不知道的 Promise 3 种奇妙用法。当然,每种用法都会有其适用的特殊场景。</p>
<h2>Promise 对象是可以缓存</h2>
<h3>需求</h3>
<p>对于一个对象而言,能够被缓存并不是一件难以理解的事情。缓存使用的意义往往是为了解决性能问题。而对于一个特定请求的 Promise 对象而言,缓存的意义在于同时多个组件的使用该请求,会因为请求未返回而进行多次请求。一图胜千言,图示如下:</p>
<p><img src="/img/bVbBu0Z" alt="别觉得丑" title="别觉得丑"></p>
<p>因为在某些特定需求或者场景下(甚至因为团队的因素),某个组件在可以在页面单独使用,也可以结合其他组件共同使用。若此时多个组件都需要对某个通用数据进行请求,就会发生多次请求,对性能不利。但如果全部移植到父组件去请求,又是需要一顿操作,对开发不爽。</p>
<h3>解决方案</h3>
<p>所以这时候我们基于 api 与 请求参数加缓存。先写一个生成 key 的函数(此函数仅仅只适用简单的请求参数,不适合对象等复杂数据结构,因为是通用型数据,不考虑太复杂的请求参数,如有需求可以自行改造)。</p>
<pre><code class="js">// 生成key值错误
const generateKeyError = new Error("Can't generate key from name and argument")
// 根据当前的请求参数生成 key 值
function generateKey(name, argument) {
// 从arguments 中取得数据然后变为数组
const params = Array.from(argument).join(',')
try{
// 返回 字符串,函数名 + 函数参数
return `${name}:${params}`
}catch(_) {
// 返回生成key错误
return generateKeyError
}
}</code></pre>
<p>下面是数据请求缓存,不过令人觉得可惜的是: 数据请求缓存并不能解决多次请求的问题。</p>
<pre><code class="js">const dataCache = new Map()
async getxxx(params1, params2) {
const key = generateKey('getxxx', [params1, params2])
// 从data 缓存中获取 数据
let data = dataCache.get(key)
if (!data) {
// 没有数据请求服务器
const res = await request.get('/xxx')
// 其他操作
...
data = ...
// 设置数据缓存
dataCache.set(key, data)
}
return data
} </code></pre>
<p>因为虽然 js 是单线程的,所以在第二个以及以上的组件请求时候,会因为请求未返回而进行再次请求 api。流程如下:</p>
<ul>
<li>a 组件请求</li>
<li>dataCache.get == null</li>
<li>建立请求(等待返回)</li>
<li>其他操作</li>
<li>b 组件请求</li>
<li>dataCache.get == null</li>
<li>建立请求(等待返回)</li>
<li>其他操作</li>
<li>.... ...</li>
<li>放入缓存且返回数据</li>
<li>放入缓存且返回数据</li>
<li>.... ...</li>
</ul>
<p>如果缓存的是 Promise 对象,则该方案可以解决问题。</p>
<pre><code class="js">const promiseCache = new Map()
async getxxx(params1, params2) {
const key = generateKey('getxxx', [params1, params2])
// promiseCache 缓存中获取 缓存
let xxxPromise = promiseCache.get(key);
// 当前promise缓存中没有 该promise
if (!xxxPromise) {
xxxPromise = request.get('/getxxx').then(res => {
// 对res 进行操作
...
}).catch(error => {
// 在请求回来后,如果出现问题,把promise从cache中删除 以避免第二次请求继续出错
promiseCache.delete(key)
return Promise.reject(error)
})
promiseCache.set(key, promise)
}
return xxxPromise
} </code></pre>
<p>流程如下:</p>
<ul>
<li>a 组件请求</li>
<li>promiseCache.get == null</li>
<li>建立请求</li>
<li>返回 promise</li>
<li>其他操作</li>
<li>b 组件请求</li>
<li>promiseCache.get != null</li>
<li>返回 promise</li>
<li>其他操作</li>
<li>.... ...</li>
</ul>
<p>同时,因为 promise 是异步操作,所以在发生错误时候 catch 中去除缓存以便于缓存了错误的promise。</p>
<h3>进一步了解与学习</h3>
<p>该方案可以减轻同一时间多次请求同一数据所带来的性能问题。</p>
<p>如果你还想结合过期时间与装饰器来对缓存进行赋能,可以参考我之前的博客文章 <a href="https://segmentfault.com/a/1190000018940422">前端 api 请求缓存方案</a></p>
<h2>Promise 可以封装大量异步操作</h2>
<h3>需求</h3>
<p>在写关于异步请求时候,通常是基于请求直接返回 api 请求响应数据,对其进行正常和错误处理。当时多次异步操作从而返回正确与错误的流程却很少进行梳理。如果在一次请求内有多个异步操作:代码就会变得难以维护。</p>
<h3>解决方案</h3>
<p>学习 Promise 时候,往往会与有限状态机结合在一起说,如果你实现过 Promise,你就清晰的知道: 如果内部没有状态没有发生变化,可以执行大量异步操作。体现为如果没有调用 resolve 或者 reject 函数,则不会对于当前 Promise 的状态和值进行修改,也就不会执行后面的链式调用。</p>
<pre><code class="js">// 异步操作封装
function asyncOpt(opt: any) {
return new Promise((resolve, reject) => {
// 传入的 opt 异步操作
// 如 请求失败,失败的逻辑判断后再次请求
// 又如多个 异步操作, 在最后一个异步操作成功后执行
reslove(result)
// 多个 异步操作中的 catch, 在每个错误中执行
reject(error)
})
}
asyncOpt(data).then(result => {
// 正常流程
}).catch(error => {
// 错误流程
})
</code></pre>
<p>写出如上的代码,就可以在很多业务项内进行操作,诸如某些操作有前置权限请求,或者某些错误代码需要重新请求或者埋点等操作。</p>
<h3>进一步了解与学习</h3>
<p>如果觉得上述的例子不够复杂,不够体现出 Promise 封装的妙用,你可以研究关于微信登陆态的管理。事实上,在没有知道这种用法之前,确实没有很好的办法解决这种问题。</p>
<p><img src="/img/bVbBu00" alt="wx-request" title="wx-request"></p>
<p>当然,github 上已经有了开源实践 <a href="https://link.segmentfault.com/?enc=b7%2BakFqrSYLA7eHurqxzEQ%3D%3D.THCLIYffrz9dMnPL%2BlFwoqRCI%2FegKvGNw378%2F7tNSNAbmLkR%2FlUutucPITIh1%2BT9" rel="nofollow">weRequest</a>,该库实现了无感知登陆,且代码风格与结构非常值得学习,可以参考我之前的博客文章 <a href="https://segmentfault.com/a/1190000020794315">从 WeRequest 登陆态管理来聊聊业务代码</a>。</p>
<p>同时,可以封装异步操作可并不仅仅只是指代异步请求,如果是你使用过<a href="https://link.segmentfault.com/?enc=6sFDq2kLzty1FHiEROO5Yw%3D%3D.PUATLoPSjbsUHD%2Fa0QaddJ40V75FFX2Usgt8zqHi%2BNlvqxUV1g%2BhghdEAKQC45h3dT1ZL1lBMWMCjDOGrXlfT9hcBfiJJ9yy9dWHlzUvyO4%3D" rel="nofollow">Element confirm</a>,一定对如下代码不陌生。</p>
<pre><code class="js">this.$confirm('此操作将永久删除该文件, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$message({
type: 'success',
message: '删除成功!'
});
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});</code></pre>
<p>这样的话,不需要再界面上写 confirm 以及一些控制显隐的代码,基于配置(字符串) 触发 promise 开始显示后销毁。</p>
<blockquote>如果你完整引入了 Element,它会为 Vue.prototype 添加如下全局方法:$msgbox, $alert, $confirm 和 $prompt。因此在 Vue instance 中可以采用本页面中的方式调用 <code>MessageBox</code>。调用参数为:</blockquote>
<ul>
<li><code>$msgbox(options)</code></li>
<li>
<code>$alert(message, title, options)</code> 或 <code>$alert(message, options)</code>
</li>
<li>
<code>$confirm(message, title, options)</code> 或 <code>$confirm(message, options)</code>
</li>
<li>
<code>$prompt(message, title, options)</code> 或 <code>$prompt(message, options)</code>
</li>
</ul>
<p>最后结合全局方法和 <a href="https://link.segmentfault.com/?enc=S05lCGpavc5a%2FDHBGl%2Blag%3D%3D.DW8h61gNJjuZ5pD5TEvt3Oc8w8ouxZEB1GHC1PIHITRz%2FxRJ1AZ2CsmGZrJr%2BvxAiAQNEAA0Ql9YX8iymcet1Q%3D%3D" rel="nofollow">渲染函数</a> 甚至也可以实现 Modal 配置化,传入组件,配置以及数据。可以类似于如下写法(当然,事实上用不用 Promise 都可以实现该方案,只不过 Promise 的状态转化很适合,与其自己实现一个状态机,倒不如使用promise):</p>
<pre><code class="js">this.$modal(xxxComponent, componentConfig, propConfig).then(result => {
// 根据不同返回结果来处理
}).catch(reason => {
// 取消处理方法
})
// 甚至还可以加 finally 方法</code></pre>
<h2>Promise 泄露触发转化方法</h2>
<h3>需求</h3>
<p>最近有小伙伴来找我询问,如何解决后一个请求比前一个请求还要快,因为他写了输入实时查询的功能。我直接让他使用防抖函数,但是他告诉我他已经使用了 500ms 的防抖但是服务端仍旧是会存在问题。</p>
<p>本来考虑再前一个请求成功后再进行下一次,但是考虑到这个方案会慢点很明显,后面考虑请求唯一化,但是因为使用 axios 做请求库,该请求并不特殊,特殊化处理明显是增加了代码复杂度,也是不太好。</p>
<h3>解决方案</h3>
<p>后面他告诉我,他已经解决了此问题,因为 axios 有一个方法可以取消请求。也就是如果他进行下一个请求,便会取消上一个请求。下面代码是官方示例:</p>
<pre><code class="js">const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});
axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})
// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');</code></pre>
<p>其实我是知道 Promise 中是有 <a href="https://link.segmentfault.com/?enc=oyJgJspId19DJTBbvN8gCA%3D%3D.DliTxkdrTRQmJvi09N6tXUCPGdNl%2FWGlzYB6tq%2ByBprZqrLIMrMO6yySsrZXgs8X%2BVSgSGCFBz01H99kggbOcA%3D%3D" rel="nofollow">cancelble 提案</a>,但是该提案在第一阶段就因为被谷歌的强烈反对而取消了,那么我就去看 axios 源码来看一看如何实现取消。下面代码在 <a href="https://link.segmentfault.com/?enc=e3Q%2F748vlcbWT2Fla%2BrPsA%3D%3D.4Xs%2BX6N4NYsN03Je7WMKmlIx%2Fvyedxp9yTxD1uyLg8B4aF4%2BEnoQvpbgONRVzmLHsw5Jxi4YolZQpejOyR7VNA%3D%3D" rel="nofollow">xhr.js</a> 中。</p>
<pre><code class="js">// 如果配置出现 cancelToken
if (config.cancelToken) {
// Handle cancellation
// 设定 处理取消方法
config.cancelToken.promise.then(function onCanceled(cancel) {
// 请求被置空,直接返回,以避免出错
if (!request) {
return;
}
// xhr.abort 取消请求
request.abort();
// 执行 reject
reject(cancel);
// 请求置空
request = null;
});
}</code></pre>
<p>先谈谈 abort 函数,abort 是 xhr 对象中的方法,根据 mdn :</p>
<blockquote>如果该请求已被发出,<strong>XMLHttpRequest.abort()</strong> 方法将终止该请求。当一个请求被终止,它的 readyState 属性将被置为0( <code>UNSENT</code> )。</blockquote>
<p>这个请求指的是 http 请求,这样就会出现一个问题,基于http请求原理,当一个请求从客户端发出去之后,服务器端收到请求后,一个请求过程就结束了,这时就算是客户端abort这个请求,服务器端仍会做出完整的响应,只是这个响应客户端不会接收。所以实质上,后端还是处理了请求,但是前端不对该方法进行处理。</p>
<p>其中 promise 取消 核心代码如下 <a href="https://link.segmentfault.com/?enc=JvfTJ7om%2BzK1HZlazjQwZQ%3D%3D.hsqXQDBGiS3CJ7uxzTNcyQzsxDG2IBncnBGXitoS6EWW0kePdyeax1Mtg7dK%2Bu9hB0zGsosvixpYTZtfBSReLOKwKj0UnbofFr9L%2FJ%2Bx15c%3D" rel="nofollow">CancelToken.js</a>。</p>
<pre><code class="js">function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
// 设定 resolvePromise
var resolvePromise;
// xhr config.cancelToken.promise.then 就是当前的 promise
this.promise = new Promise(function promiseExecutor(resolve) {
// 设定 和导出 resolve
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
// 执行 resolvePromise
resolvePromise(token.reason);
});
}
/**
* 对应使用中 source
* axios.get('/user/12345', {
* cancelToken: source.token
* })
* source.cancel('Operation canceled by the user.');
*/
CancelToken.source = function source() {
var cancel;
// 返回 cancel token 对象
var token = new CancelToken(function executor(c) {
// 利用 excutor 来把取消函数导出来。也就是 CancelToken excutor函数
cancel = c;
});
return {
token: token,
cancel: cancel
};
};</code></pre>
<p>可以看到 axios 的代码关系还是有一定的复杂度。当然也是因为当前 Promise 没有办法像 setTimeout 等一些方法在调用时候直接返回取消函数,所以不得不借助另一个 promise 异步来处理。同时,也是把复杂度留给了自己,所以还是需要多读几遍。 调用关系如下。</p>
<p>CancelToken 设定了取消的 promise 调用关系, xhr 在有请求配置 cancelToken 的情况下,将当前请求注入的 cancelToken 中的 then 结合,使得调用了 cancel 后可以直接改变 xhr 内部状态。</p>
<h3>进一步学习</h3>
<p>当然在频繁的页面跳转,同时还有定时请求时候,跳转中的数据请求实际上意义不大: 可以参考 <a href="https://link.segmentfault.com/?enc=6VNKJzhBw7cHLoxpJejaoQ%3D%3D.bifIJw5w5OosK05tB8o3SQtJtj6DjhK0nKTZjWZUgQjR2wEaGxCqynrs21AQgR2IXk6Nb6s3IqNqkQONnrG9Rg%3D%3D" rel="nofollow">vue axios请求 取消上一个页面所有请求 批量取消请求</a></p>
<p>取消请求问题其实较为小众的,大部分是可以从请求源头来解决的,同时也因为对于服务端的处理并没有减轻,所以事实上不处理其实倒也没什么问题。但是其中也遇到过小程序中有一些全局的服务,在请求完成后由于触发不到页面数据而报错的问题。</p>
<p>虽然是小众问题,但是遇到该特定场景需要提供解决方案。</p>
<p>同时对于上面的异步操作组件来说,泄露出 resolve 和 reject 函数以便于直接执行 then 或者 catch 是否有意义,又或者中途改变异步组件的实现流程是否真的对业务有所帮助也是值得思考的。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=H3xxmq6UEUAtQPVWlogbZw%3D%3D.JFkglHF8NxfmRadcKQFA3fg4WclTElA7aXZ%2FmyoXg%2F83FsulP9R0QcTMV%2FDMu4fa" rel="nofollow">博客地址</a></p>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=GBmBE%2BPkzlmEkwgKwFsbIw%3D%3D.pe3E7gO9KE6C6KLCAfajvpzH0cpsK0LM4H0c1sUaKsYombVnvZQ2FC4%2F6hWoMqh67xHDd89eMG68rUT0Vi9xF8oVOQMohvuLPmaQBfz706I%3D" rel="nofollow">Element confirm</a></p>
<p><a href="https://link.segmentfault.com/?enc=DXisXU5AHELc9IcMwYdFsw%3D%3D.PRS8UbcGAXIjk0QqLRT57ij1W5Z67sH8eteXsjVFSuXL1ZcD1c9Jm3%2FRWuZAta4EE3USJxhifuqk%2B9BXD7YtwQ%3D%3D" rel="nofollow">Vue 渲染函数</a> </p>
<p><a href="https://link.segmentfault.com/?enc=G1%2FE2nGSpqYSwBoi4E6OCA%3D%3D.I0wMM8Q6pZoF3xSY8%2F6xBdfFDKNhoI9qqVcTxWZNIII%3D" rel="nofollow">axios</a></p>
<p><a href="https://link.segmentfault.com/?enc=Sez4MwkL6QT83wKbk2DVcw%3D%3D.nNjCqwvbt4THyZB77GN2RrrGEpdCSvWJSXaeKy92CtI%2BKeGyZR6hL9XWaTuwuVj2d%2B4wMvYzmsF3G%2BT24C3ZRA%3D%3D" rel="nofollow">vue axios请求 取消上一个页面所有请求 批量取消请求</a></p>
从 UX 与 DX 来谈一谈 React SWR
https://segmentfault.com/a/1190000021097491
2019-11-23T23:54:12+08:00
2019-11-23T23:54:12+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
8
<p>自从 9102 年初 react 推出了 Hook 之后,我就开始在私人项目中先行了。不得不说的是,react Hook 的确足够“跨时代”。大量的文章研读以及伴随着项目中组件的改造,对Hook 的优点,缺点,以及本身的机制也有一定的了解。</p>
<p>如果你是 Hook 初学者,建议先阅读 <a href="https://link.segmentfault.com/?enc=2KSKv%2BU6E3xRqYZpgFzBgQ%3D%3D.Fdh3SWn%2Bv2WePi65%2FvfIK7DiUVAfS%2B6cPwmq5TUqJ0c%3D" rel="nofollow"> https://usehooks.com/</a> 以及 <a href="https://link.segmentfault.com/?enc=yhGKLSSkcpjb%2FLhjxtmgfg%3D%3D.MdbE8FIfIEqtzdRok%2Bs2j04zlxrtWJICyvENadWp3UU%3D" rel="nofollow">Dan Abramov 的个人博客</a>。</p>
<p>伴随着 Hook 时代的带来,react 社区也是到来了无 Hook 不欢的时代。 如火如荼的封装。包括 axios 以及 immer 等库都未能“幸免”,被 Hook 包裹了一层而变成了 <a href="https://link.segmentfault.com/?enc=Lgycp7%2FyO8sfzy1CRS%2FXVw%3D%3D.XF5jIrujiy5dctI%2FH7%2BbcPE4NOlpla2eyz7ubfQ7U%2BWFQxD%2FqR7N092VeePKL4%2Bz" rel="nofollow">axios-hooks</a> 以及 <a href="https://link.segmentfault.com/?enc=I80kQVL9VFFtgEnCrp3Nuw%3D%3D.YS5AgOdN6ajIJJcLNffJ8jdWZj9pmQScEUAAL0S%2BwtVsGpAjmACquUzQtWJfg5BU" rel="nofollow">use-immer</a>。但是却始终没有一个杀手级应用。</p>
<p>在我今天阅读 <a href="https://link.segmentfault.com/?enc=At06LUd4ljIe%2B4JBOykKqw%3D%3D.%2BagPigQcNcR2ZE%2BNl0P0%2FPPceFQZf%2FH1Na4MU15Kk2JisNksh5uy%2FmWYdjPodFsI" rel="nofollow">精读《Hooks 取数 - swr 源码》</a> 时候,了解到这个 12天就拿到4000+ star 的杀手级应用 <a href="https://link.segmentfault.com/?enc=%2Ft5VKoZwSgcaOhtUdO8r0g%3D%3D.E5AcmMPY45H1LrEDGqTiIv3aotFIWTrkzJbHq%2BxxUtc%3D" rel="nofollow">swr</a>。既然大牛已经从 swr 源码来展开。那我就从 UX(用户体验) 以及 DX(开发者体验) 来聊一聊。</p>
<h2>基础数据加载</h2>
<p>作为一个开发者而言,始终面临着一个问题,究竟当前数据是否应该放入 状态管理库或者仅仅只在组件中使用?就个人开发而言,始终秉承着一个思想,如果一个数据,不会被两个及以上的不能直接通信的祖先组件使用。那么它就不配使用状态管理,用完便直接抛弃。不要因为多写一些代码而放弃简单性。</p>
<p>用 SWR 最基础的功能如下所示:</p>
<pre><code class="jsx">import useSWR from 'swr'
function Profile () {
const { data, error } = useSWR('/api/user', fetch)
// 保镖模式(the Bouncer Pattern), 后面处理正确业务逻辑
if (error) return <div>failed to load</div>
// 没有错误,且没有数据 只有可能是正常业务流程中的等待取数据
if (!data) return <div>loading...</div>
// 没有错误有数据,进行渲染
return <div>hello {data.name}!</div>
}</code></pre>
<p>所以我觉得如上代码十分切合 DX 的设计与思想。在取数据之前,取数据是一个 UI 展示,发生错误也是一个 UI 展示。仅仅 4 行代码,就囊括了 error, loading 以及正常业务的所有 UI 切换。在 数据没有获取之前,data 与 error 都是 undefined。进行 loading。在数据获取之后, data 与 error 两者必居其一。</p>
<p>如果你写过 Go 语言,一定对这种代码不陌生。</p>
<pre><code class="go">val, err := myFunction( args... );
if err != nil {
// handle error
return
}
// success
</code></pre>
<p>由于 Go 中有大量此类代码的处理,所以在 Go2 中有新的草案提出,这里就不进行深入讨论。不过对于任何语言和业务而言,错误处理设计都是非常重要的。这种代码在需要大量书写的 Go 语言中,是一种负担。但是,对于当前 SWR 而言,反而并不繁琐,具有更加清晰的状态切换。</p>
<h2>自定义数据提取</h2>
<p>对于用户而言,并不关心我们如何取得数据,但是对于开发者来说,形形色色的需求使得自定义配置不会是可选的,而是必需的。大到 vue, react 的平台(Weex, react native ) 适配。小到我们提供给他人的基础功能模块,都是需要对他人负责的。</p>
<p>例如,如果需要提供功能代码给别人用,通常就会这样写。</p>
<pre><code class="js">const DEFAULT_CONFIG = {
// 基础配置
// ...
}
// 利用 Object.assign 后面配置来覆盖
const config = Object.assign({}, DEFAULT_CONFIG, config)
</code></pre>
<p>而在 SWR 中,在其中追加了全局配置:</p>
<pre><code class="ts"> config = Object.assign(
{},
// 默认配置
defaultConfig,
// 全局配置
useContext(SWRConfigContext),
// 当前组件配置
config
)</code></pre>
<p>这里我们来介绍一下 fetcher 函数,接受传入的 key 值,返回一个 promise 或者数据。中间也可以结合各种库来进行数据处理。</p>
<pre><code class="ts">import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
const { data } = useSWR('/api/data', fetcher)
// ...
}</code></pre>
<pre><code class="ts">import { request } from 'graphql-request'
const API = 'https://api.graph.cool/simple/v1/movies'
const fetcher = query => request(API, query)
function App () {
const { data, error } = useSWR(
`{
Movie(title: "Inception") {
releaseDate
actors {
name
}
}
}`,
fetcher
)
// ...
}</code></pre>
<p>当时看到这里之前,我一度不能理解 useSWR 函数第一个参数叫 key 的原因。当使用 GraphQL时候,我终于知道,我还是 So young So simple。毕竟对 GraphQL 缺乏实战经验,所以往往会对不熟悉的技术产生遗漏。当然了,如果你参考过其他关于 api 的缓存的开源代码一定可以立即想到,缓存工作一定围绕着当前的 key 值。</p>
<p>如果你并不需要特殊处理,直接略过 fetcher 这个参数即可,就像基础功能版。当看到这里时,基本上我们可以判断在实际使用过程中,即使遇到了无法预料的业务情景,我们也可以通过我们的代码来解决掉问题。</p>
<h2>多窗口同步功能</h2>
<p>在使用 SWR 之后,如果我们在当前应用打开多个窗口或者选项卡。重新聚焦当前页面时候,无需手动或者在代码中重新刷新。SWR 会自动取得数据然后基于 React diff 进行渲染。</p>
<p>基于 DX 而言,这帮我们解决了一个痛点。在很多情况下,用户或基于两个数据页面的比对。或者 To C 应用,我们需要打开多个窗口或选项卡。而窗口或者 tab 切换时候,是否能够基于业务进行处理是值得思考的。</p>
<p>以下代码是判断当前文档是否可见,代码风格依然是保镖模式(the Bouncer Pattern)。</p>
<pre><code class="ts">export default function isDocumentVisible(): boolean {
if (
typeof document !== 'undefined' &&
typeof document.visibilityState !== 'undefined'
) {
return document.visibilityState !== 'hidden'
}
// always assume it's visible
return true
}
</code></pre>
<p>当然了,我们可以通过配置来决定是否使用该功能。</p>
<pre><code class="tsx">// revalidateOnFocus = true:窗口聚焦时自动重新验证
const { data } = useSWR('dynamic-6', () => value++, {
revalidateOnFocus: false
})
// 或者全局配置
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (...args) => fetch(...args).then(res => res.json()),
revalidateOnFocus: false
}}
>
<Dashboard />
</SWRConfig>
)
}</code></pre>
<p>同时,值得一提的是,在多个窗口或者选项卡中,我们也可以配置间隔刷新来进行多窗口同步,不过这需要更多的网络资源。</p>
<h2>快速导航(cache-then-network )</h2>
<p>在开发 web 应用程序时,性能都是必不可少的话题。 而事实上,缓存一定是提升web应用程序最有效有效方法之一,尤其是用户受限于网速的情况下。提升系统的响应能力,降低网络的消耗。当然,内容越接近于用户,则缓存的速度就会越快,缓存的有效性则会越高。 之前,我曾经写过 <a href="https://segmentfault.com/a/1190000018940422">前端 api 请求缓存方案</a>。</p>
<p>但是如果使用 SWR,我们如果在系统内部进行导航或者按下后退按钮,我们直接会取得缓存版本数据。然后系统为了一致性,呈现了数据之后,会继续请求服务端,重新拉去数据。看到这里,我不禁要说一句,这很 ServiceWorker。类似于 cache-then-network 机制。</p>
<p>如果想要仔细研究 ServiceWorker 来帮助开发离线应用程序,可以学习 <a href="https://link.segmentfault.com/?enc=CW4jjwSprkoythJlyQNtJQ%3D%3D.7ZoPkSd4v1fgi6Kl%2FO3LGewyh8t%2FCQ84BucNm%2B%2FZncMuCbye3tekqwS22YqSRmOWxPuGeJ5rjzZKsNWtPS6Oia%2FBbLwPolXCcBUlaDOT5VE%3D" rel="nofollow">The offline cookbook</a> 以及 <a href="https://link.segmentfault.com/?enc=eMbcP1gX%2Ft3zJvV6cTJtKw%3D%3D.EsUngW8fmri9h8RieelMkYi2jDUfgv7FcfD3M0SLfklfUmFGF5czRxJ0fVigm%2BvJ" rel="nofollow">workbox 文档</a>。</p>
<h2>条件与依赖获取</h2>
<p>如果一个语言(库)不能给你带来思想上的扩展,那么就不要学习它。SWR 在获取数据方面的确有他特殊之处。一方面是条件获取。</p>
<pre><code class="js">// 条件获取
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// 条件获取获得 fetcher
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
</code></pre>
<p>如果当前 shouldFetch 是 falsy,那么如果 useSWR 则不会进行请求。那么依赖获取则更加有趣。SWR 为了性能而确保了最大的并行性。按照代码解析如下</p>
<pre><code class="js">import useSWR from 'swr'
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(
() => '/api/projects?uid=' + user.id
)
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}</code></pre>
<p>如果按照平时书写代码的逻辑,如果后一个请求依赖前一个请求的响应,是需要promise 或者 async 与 await。但是在当前 SWR 框架中,却仅仅只需要把顺序写好。</p>
<p>由于 SWR 不是一个与编译结合的依赖库,所以不要想像的太过复杂,仅仅只是因为错误重试。当执行到 user.id 时候,因为 user 并不是一个对象,所以在当前请求之前会发生错误。然后再继续重试请求。等到第一次请求 user 取到之后,项目才会真正的向后端进行请求。</p>
<p>请求时机以 2 的指数性增长,代码如下:</p>
<pre><code class="js"> const count = Math.min(opts.retryCount || 0, 8)
const timeout =
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval
setTimeout(revalidate, timeout, opts)</code></pre>
<p>上述也是带有随机性质的 <a href="#">截断指数退避算法</a> ),当使用这种策略时候,客户端不断增加重试的延迟时间,而不是固定的延时。这样的话会更加符合现实世界的逻辑。当然我们也是可以控制重试的。</p>
<pre><code class="js">useSWR(key, fetcher, {
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
if (retryCount >= 10) return
if (error.status === 404) return
// retry after 5 seconds
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
}
})</code></pre>
<p>这种决策非常有趣,类似于全部的请求都是 promise.all 。我个人虽然认可这种模式,但是在极端情况下,会出现前置依赖仅仅延迟一点,后置请求延迟一轮的情况。即使在不那么极端的情况中,也有一定的时间损耗。</p>
<p>如果在可以商议的情况下,将多个取数 api 结合为一个多参数的 api 也不失为一种可行的解决方案。是否采用 SWR 依赖取数,这取决于项目是否能够接受这种时间损耗。</p>
<h2>局部突变</h2>
<p>使用 mutate,您可以通过编程方式更新本地数据,同时重新验证并最终用最新数据替换它。</p>
<pre><code class="js">import useSWR, { mutate } from 'swr'
function Profile () {
const { data } = useSWR('/api/user', fetcher)
return (
<div>
<h1>My name is {data.name}.</h1>
<button onClick={async () => {
const newName = data.name.toUpperCase()
// 请求更新名称
await requestUpdateUsername(newName)
// 先更新名称,后重新拉去数据验证
mutate('/api/user', { ...data, name: newName })
}}>Uppercase my name!</button>
</div>
)
}
// requestUpdateUsername 返回 200 无需验证。填写 new User
// 不过该方案仅仅只能修改无乐观锁的数据
mutate('/api/user', newUser, false)
// promise 返回更新的 user。直接更新
mutate('/api/user', requestUpdateUsername(newUser))
// 也可以返回 id 与乐观锁
const modifiedUser = requestUpdateUsername(newUser).then(res => {
return Object.assign({}, newUser, res)
})
// promise 返回更新的 user。直接更新
mutate('/api/user', modifiedUser) </code></pre>
<p>而是为当前的取数服务提供了修改的功能,使得 SWR 不单单是一个单纯的取数框架。如此以来,修改列表,编辑页面便都实现。( 在没有仔细看该功能的情况下,我一度以为该功能类似 Meteor (Meteor 是一个实时框架, 在客户端也自带数据库,查询与更新都是先针对客户端数据库,后面再交由服务端来允许与拒绝,也就是失败补偿)但是后面却发现,该功能并不是我预想的)。</p>
<h2>结语</h2>
<p>对比自身书写的 Hook 方法,不得不说的是,SWR 的确够硬核,作者虽然只解决了取数这一方面,但是无不彰显出作者的代码和业务的设计功底。在这个仅仅只有 4kb 的小库中,真正深度运用了 Hook,同时也给与了用户很大的便利。同时,我也相信该库一定对任何想要深入学习 Hook 的人有所帮助。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。 <a href="https://link.segmentfault.com/?enc=RU7d8z06ic37OO8AoGe7UQ%3D%3D.e%2FdSETKwYxXeGxuP0ZbfOVaM6YKn64OlfoFXqTHnqXU%2Bpyc16akDACLQLsyGANeJ" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=a0EOV5jiy04QZiQgV8FDvw%3D%3D.vO6MxoHKCsFgvKG1xuvwrDdyz92xV8AeN3G0SWnl%2BsE%3D" rel="nofollow">SWR</a></p>
<p><a href="https://link.segmentfault.com/?enc=ZX3QisT8KmU8IYF7kDKjXA%3D%3D.%2BrLQqcYfaIB3%2FHPTrDhCfUbc8diu%2Bst5TAsA0hZjduM0GNYuKnZmxH2UP0HHvNds" rel="nofollow">精读《Hooks 取数 - swr 源码》</a></p>
从 WeRequest 登陆态管理来聊聊业务代码
https://segmentfault.com/a/1190000020794315
2019-10-24T23:28:27+08:00
2019-10-24T23:28:27+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
8
<p>在开发微信小程序之前,个人从来没有接触过开发中涉及到第三方服务器交互的流程。在开发的过程本身倒是没有什么太大的意外,只是在维护服务器登陆状态这一点很讨厌。因为涉及到自身服务器的登录状态以及微信官方服务器登陆状态三方的关系。 </p>
<p>下图是微信登陆机制:</p>
<p><img src="/img/bV7S7Y?w=710&h=720" alt="api-login.jpg" title="api-login.jpg"></p>
<p>在这种场景下,个人非常关注的点在于: 如何能够无感知的进行登陆(并且无多余请求)。微信的登陆状态倒是还好解决,可以利用 wx.checkSession 来进行判定,但是在与后台服务器交互时候,如果后台交互中返回 HTTP 状态码 401 (未授权)或者其他未登陆指示时候。则需要对其进行额外处理。 </p>
<p>当时记得为了优雅的解决这个问题,想了很多方案,也与一些伙伴讨论过这个问题。虽然当时的确实现了无感知的登陆,但是要么需要多请求服务器,要么就是代码上实现逻辑过于复杂,代码维护。虽然不满意,但是在当时也没想到什么非常好的解决方法。</p>
<h2>weRequest 自带状态管理的请求组件</h2>
<p>后面经过老大的介绍,看到这个组件时,我顿时眼前一亮,这正是我所需要的解决方案,该方案的图示如下:</p>
<p><img src="/img/bVbzpIa?w=1315&h=959" alt="flow_login.png" title="flow_login.png"></p>
<p>只需要配置一些初始化项目,便可以直接拿去使用了。</p>
<pre><code>// 导入
import weRequest from 'we-request';
weRequest.init({
// [可选] 存在localStorage的session名称,且CGI请求的data中会自动带上以此为名称的session值;可不配置,默认为session
sessionName: "session",
// [可选] 请求URL的固定前缀;可不配置,默认为空
urlPerfix: "https://www.example.com/",
// [必填] 触发重新登录的条件,res为CGI返回的数据
loginTrigger: function (res) {
// 此处例子:当返回数据中的字段errcode等于-1,会自动触发重新登录
return res.errcode == -1;
},
// [必填] 用code换取session的CGI配置
codeToSession: {
// [必填] CGI的URL
url: 'user/login',
// [可选] 调用改CGI的方法;可不配置,默认为GET
method: 'GET',
// [可选] CGI中传参时,存放code的名称,此处例子名称就是code;可不配置,默认值为code
codeName: 'code',
// [可选] 登录接口需要的其他参数;可不配置,默认为{}
data: {},
// [必填] CGI中返回的session值
success: function (res) {
// 此处例子:CGI返回数据中的字段session即为session值
return res.session;
}
},
// [可选] 登录重试次数,当连续请求登录接口返回失败次数超过这个次数,将不再重试登录;可不配置,默认为重试3次
reLoginLimit: 2,
// [必填] 触发请求成功的条件
successTrigger: function (res) {
// 此处例子:当返回数据中的字段errcode等于0时,代表请求成功,其他情况都认为业务逻辑失败
return res.errcode == 0;
},
// [可选] 成功之后返回数据;可不配置
successData: function (res) {
// 此处例子:返回数据中的字段data为业务接受到的数据
return res.data;
},
// [可选] 当CGI返回错误时,弹框提示的标题文字
errorTitle: function(res) {
// 此处例子:当返回数据中的字段errcode等于0x10040730时,错误弹框的标题是“温馨提示”,其他情况下则是“操作失败”
return res.errcode == 0x10040730 ? '温馨提示' : '操作失败'
},
// [可选] 当CGI返回错误时,弹框提示的内容文字
errorContent: function(res) {
// 此处例子:返回数据中的字段msg为错误弹框的提示内容文字
return res.msg ? res.msg : '服务可能存在异常,请稍后重试'
},
// [可选] 当出现CGI错误时,统一的回调函数,这里可以做统一的错误上报等处理
errorCallback: function(obj, res) {
// do some report
},
// [可选] 是否需要调用checkSession,验证小程序的登录态过期,可不配置,默认为false
doNotCheckSession: true,
// [可选] 上报耗时的函数,name为上报名称,startTime为接口调用开始时的时间戳,endTime为接口返回时的时间戳
reportCGI: function(name, startTime, endTime, request) {
//wx.reportAnalytics(name, {
// time: endTime - startTime
//});
//request({
// url: 'reportCGI',
// data: {
// name: name,
// cost: endTime - startTime
// },
// fail: function() {
//
// }
//})
console.log(name + ":" + (endTime - startTime));
},
// [可选] 提供接口的mock,若不需使用,请设置为false。url为调用weRequest.request()时的url。mock数据的格式与正式接口提供的数据格式一致。
mockJson: {
url1: require("../../mock1.json"),
url2: require("../../mock2.json"),
url3: require("../../mock3.json")
}
// [可选] 所有请求都会自动带上globalData里的参数
globalData: function() {
return {
version: getApp().version
}
},
// [可选] session本地缓存时间(单位为ms),可不配置,默认不设置本地缓存时间
sessionExpireTime: 24 * 60 * 60 * 1000,
// [可选] session本地缓存时间存在Storage中的名字,可不配置,默认为 sessionExpireKey
sessionExpireKey: "sessionExpireKey"
})
export default weRequest;</code></pre>
<p>使用时候直接拿到 weRequest 既可使用</p>
<pre><code>weRequest.request({
url: 'order/detail',
data: {
id: '107B7615E04AE64CFC10'
},
method: 'GET'
}).then((data)=>{
// 省略...
})
</code></pre>
<h3>代码浅析</h3>
<p>简单的介绍一下 weRequest 库的实现机制, 在这里代码简化一下,只会说明最主要调用的三个函数。</p>
<ul>
<li>requestHandler.request 管理请求,即每一次请求都要执行该函数</li>
<li>sessionManager.main 管理 session 状态。session 的设置与删除,同时也在第一次确认拥有 session 时设置标识符,即只会在第一次缺失登陆态或者错误时候才会执行。</li>
<li>responseHandler.response 管理返回数据,对返回数据进行解析,如果没有登陆态,删除 session,重新请求,结合第二个 sessionManager.main 来做。</li>
</ul>
<pre><code>// requestHandler.request 方法
function request(obj: IRequestOption): any {
return new Promise((resolve, reject) => {
// 传入 api 请求对象进行处理
obj = preDo(obj);
// 读取 session, 如果 session 没有问题。成功的话,进行业务请求
sessionManager.main().then(() => {
// 进行业务请求
return doRequest(obj)
}).then((res) => {
// 对 返回的数据进行解析 responseHandler.response 方法
let response = responseHandler(res as wx.RequestSuccessCallbackResult, obj, 'request');
if (response != null) {
// 返回请求结果
return resolve(response);
}
}).catch((e) => {
// 异常处理机制
catchHandler(e, obj, reject)
})
})
}
// sessionManager.main 方法
function main() {
return new Promise((resolve, reject) => {
// 检查登陆态并返回, 如果登陆态过期,直接登陆,登陆成功后返回成功
return checkLogin().then(() => {
// 如果登陆态 ok, 把 config.doNotCheckSession 设置为 true。避免下次再次执行检查
return config.doNotCheckSession ? Promise.resolve() : checkSession()
}, ({title, content}) => {
errorHandler.doError(title, content);
return reject({title, content});
}).then(() => {
// 对checkSession 进行检查操作
return resolve();
}, ({title, content})=> {
errorHandler.doError(title, content);
return reject({title, content});
})
})
}
// responseHandler.response 方法
function response ( res: wx.RequestSuccessCallbackResult,
obj: IRequestOption,
method: "request") {
if (res.statusCode === 200) {
// ... 省略代码
// 登录态失效,且重试次数不超过配置
if (config.loginTrigger!(res.data) &&
obj.reLoginCount !== undefined &&
obj.reLoginCount < config.reLoginLimit!) {
// 删除session
sessionManager.delSession();
if (method === "request") {
// 重新请求
return requestHandler.request(obj as IRequestOption);
}
}
}
}
</code></pre>
<p>我们可以利用结合官方网站的图示进行代码分析 </p>
<p>如果用户从来没有登陆过时,或者 checkSession 过期:</p>
<ul>
<li>request 直接请求需要的 api</li>
<li>sessionManager.main 检查登陆态,即 checkSession 是否过期</li>
<li>isSessionExpireOrEmpty 如果 session 过期或者为空(当前为空)</li>
<li>wx.login -> code2Session 登陆两个服务器</li>
<li>成功后,设置标识符 doNotCheckSession 继续 request 请求</li>
</ul>
<p><img src="/img/bVbzpIg?w=867&h=589" alt="flow2.png" title="flow2.png"></p>
<p>用户登陆态未过期,再次打开小程序:</p>
<ul>
<li>request 直接请求需要的 api</li>
<li>sessionManager.main 检查登陆态,看到 doNotCheckSession</li>
<li>继续第一步 request 请求</li>
</ul>
<p><img src="/img/bVbzpIi?w=873&h=590" alt="flow1.png" title="flow1.png"><br>如果是请求成功后的第二次请求,直接会取得内存中的 session,而并非 getStorage,所以不必担心</p>
<p>用户某次登陆后端,后端登陆态过期:</p>
<ul>
<li>request 直接请求需要的 api</li>
<li>sessionManager.main 检查登陆态,即 checkSession 是否过期</li>
<li>isSessionExpireOrEmpty 如果 session 过期或者为空(当前为不为空)</li>
<li>成功后,设置标识符 doNotCheckSession 继续 第一步 request 请求</li>
<li>后台返回错误码,通过 responseHandler.response 解析。发现错误,删除session,重复请求。</li>
</ul>
<p><img src="/img/bVbzpIj?w=877&h=589" alt="flow3.png" title="flow3.png"><br>提个点,一定要设定 reLoginCount 至少1次,否则该业务无法完成。</p>
<h2>new Promise 内部封装异步操作</h2>
<p>之前在写关于异步代码操作时候,通常是基于 axios 直接返回 api 请求响应数据,对其进行正常和错误处理。当时多次异步操作从而返回正确与错误的流程却很少进行梳理。如果在一次请求内有多个异步操作:代码就会变得难以维护。事实上我们可以把 Promise 看成状态机。只有在某些情况下才会返回正确。</p>
<pre><code>// 异步操作封装
function asyncCompnent(opt: any) {
return new Promise((resolve, reject) => {
// 传入的 opt 异步操作
// 多个 异步操作, 在最后一个异步操作成功后执行
reslove(result)
// 多个 异步操作中的 catch, 在每个错误中执行
reject(error)
})
}
asyncComponent(data).then(result => {
// 正常流程
}).catch(error => {
// 错误流程
})</code></pre>
<p>写出如上的代码,就可以在很多业务项内进行操作,诸如某些操作有前置权限请求,或者某些错误代码需要重新请求或者埋点等操作。 </p>
<p>可能会有人认为,在http 请求框架中都会有 interceptor 拦截器, 完全用不到 new Promise 来判断与操作。但是往往来说,拦截器对于代码是全局的,如果是单单对于某些模块,在拦截器中写大量 if 判断以及业务处理,这绝不是一件好事。因为场景上,业务的易变性使得全局代码被大量修改不利于项目的维护,但是如果该方案使用不当,则又会造成业务代码的可控性降低。</p>
<p>当然以上代码也可以使用 async 与 await 来处理,建议多研究一下 async 错误处理,这里推荐两篇关于 async 错误处理的博客(因为个人一直不喜欢 async 函数需要配合回调函数或者 Promise.reject 来处理错误,所以一般来说,我更多用 async 来处理非 api 请求的异步操作,这样的话基本上不太需要处理错误)。 <br><a href="https://link.segmentfault.com/?enc=8Er1eruH90Y4AszPLCcYNQ%3D%3D.3z50swhVg%2BS%2BVV33YI1o8kVLJTWNduhnKeA1SMvB94wepF%2FaWDBzuuN%2B%2FI8iJAOJdrXy0Nqwur8AobbvUNzlmQ%3D%3D" rel="nofollow">如何在Javascript中优雅的使用Async和Await进行错误的处理?</a> <br><a href="https://segmentfault.com/a/1190000011802045">从不用 try-catch 实现的 async/await 语法说错误处理</a></p>
<p>之前在阅读 《MobX Quick Start Guide》 时候,我看到一个公式</p>
<pre><code> VirtualDOM = fn (props, state) </code></pre>
<p>只要输入等同的 属性和状态,得到的一定是 相同的 VirtualDOM 数据。</p>
<p>但是我想说的是对于一个业务而言,如果不考虑界面美观性,以及必要的中间状态,我认为符合以下公式:</p>
<pre><code> 前端业务封装 = 管理 (交互状态, 数据状态, 配置项)</code></pre>
<p>其中,结合交互状态和数据状态面向的是最终用户,用户看到怎样的界面取决于前两个。而后一个配置项是面向于开发者,你的代码能究竟支持多少种场景。能够通过配置来减少多少的代码量。</p>
<p>难道只要输入等同的交互以及数据就能的到同样的业务吗?当然并非如此,因为对于前端而言,始终有不知道的数据状态。我们只能通过防御式编程与错误处理来搞定不清楚的数据状态(通过增加各种交互状态来解决数据数据状态未知情况)。</p>
<p>对于 weRequest 这个库而言,整个 微信的登陆态是保存在 storage 中,整个库都在维护微信的登陆状态(和后台的交互状态并没有保存,只要出现没有权限状态时,就会删除微信登陆状态,重新login)。那么除去代码,整个的交互状态就是被存到内存以及 Storage 中的 session,doNotCheckSession 。数据状态是我们需要请求的api配置以及我们未知的后端状态。</p>
<pre><code>session, doNotCheckSession // 可变的交互状态
weRequest.init({
// 固定的交互状态(配置项)
// ...
})
{
url: '../'
} // 数据状态</code></pre>
<p>这里也推荐了一篇关于前端 axios 重新请求的方案参考,相比于 weRequest 更加清晰:</p>
<p><a href="https://link.segmentfault.com/?enc=6dXsLK2XQxPMZDNq9lDzyA%3D%3D.q0ZiYZVI%2BmtXtEi00K884nnsA%2B6uPOqbA2434BJjh%2BhDqi6Qs35HoinQYOKZikCX" rel="nofollow">axios请求超时,设置重新请求的完美解决方法</a></p>
<p>同样对于我们的业务代码而言(组件内部实现),往往有些数据也是配置项目。如果你对于这三者清楚的了解并且管理的很好,那么写出来的业务代码一定不会差。</p>
<h2>"无状态"的优势</h2>
<p>在 request 代码中很容易发现,代码能够维护和后端的状态并不是因为持有了后端的 session,而是一种试错机制,只要上一次请求和下一次请求之间的数据没有变过,那么在错误处理中重复请求就没有问题。同时呢,虽然是有 session,doNotCheckSession 这个数据在,但是被移除到非业务中,只作为交互使用。所以我在这里想说的是,思考如何减少中间状态,也就是销毁,新建的模型更简单。</p>
<p>在刚开始处理业务代码时候,我总是较多处理 state,总是使用组件的显隐来控制 Dialog,有时候填写 form 表单很麻烦,因为当时组件的一些机制不够完善,在处理完一个form后,reset并不能清除 上一次数据的验证错误,需要多写一部分代码来搞定开发,后来开始转变了,对于大部分场景而言,不如直接销毁,新建,无需管理中间态。 </p>
<p>其实前端业务中,其实很多这样的例子,处理子组件与父组件的关系,甚至来说架构端,把单页面应用改成基于业务的多页面应用,也是一种销毁,新建的模式。利用浏览器本身的机制去除大部分的中间态。</p>
<p>当我们费劲心思去维护一个中间状态的时候,利用各种工具提升性能,不妨多思考以下,去除是否是一种更简单的方案。</p>
<p>之所以会有这样的感慨: 是因为在我刚毕业时曾经做过一个需求,里面有 5,6 个复杂的功能点,现在还要增加一个功能点。但是当时完全无法通过增加代码来解决问题,必须把代码拆分重组才可以搞定。遇到这种事情,有小伙伴可能会想,是不是当时的代码写的不好。其实并不是该代码写的不好,而是之前的代码写的相当好,契合的非常好,完全不知道怎么搞定,初出茅庐的我完全无法控制(需要对所有功能点通盘考虑,复杂度很高),因此,我在这个需求上完全失控了。所以,能契合复杂的代码,考虑到各种可能是能力。能解析复杂的的代码,做一定的牺牲决策,化繁为简,也是一种能力。前者的能力是个人能力的强大,是不可复制和替代的。后者则是让团队实现更加简单,快速的实现各种功能。对于一个成熟的程序员而言,两者的提升都是很重要的。</p>
<p>如果对于前端来说,无状态的优势是简单的话,web 后端的无状态的利好就更多了,可以通过外部扩展实现水平扩容,其实质也是把交互状态(用户数据)移除到其他介质上来实现请求可以打到不同的服务器上,而不是单服务器。同时实现了每次请求都是独一无二的,完全不需要考虑中间状态的迁移,有利于开发速度与正确性。</p>
<h2>weRequest 源码结构解析</h2>
<p>weRequest 是一个非常小而美的库,代码非常简单干练,我个人非常喜欢他的源码结构,所以列出来:</p>
<ul>
<li>
<p>api</p>
<ul>
<li>getConfig.ts 获取配置信息,把代码中配置的数据导出</li>
<li>getSession.ts 获取 session</li>
<li>init.ts 初始化设置配置 同时读取 storage 中的 session 与 session 过期时间 放入呢次</li>
<li>login.ts 直接调用 sessionManager.main()</li>
<li>request.ts 直接调用 requestHandler.request(obj)</li>
<li>setSession.ts 直接 setSession(内部接管了,不建议调用,可能是一个用户两个小程序之间的特殊需求)</li>
<li>uploadFile.ts 上传文件</li>
</ul>
</li>
<li>
<p>module</p>
<ul>
<li>cacheManager.ts api 基于 api 与参数 进行缓存管理,目前没有过期时间,只适合不变化的数据</li>
<li>catchHandler.ts 异常处理</li>
<li>durationReporter.ts 耗时上报</li>
<li>errorHandler.ts 错误处理</li>
<li>mockManager.ts mock 假数据接口</li>
<li>requestHandler.ts 请求处理,格式化,上传文件等</li>
<li>responseHandle.ts 响应处理,从请求等</li>
<li>sessinManager.ts session 管理,对于登陆态进行控制,设置与删除</li>
</ul>
</li>
<li>
<p>store</p>
<ul>
<li>config.ts 默认配置,在 init 时候会使用 Object.assign 来进行默认配置覆盖</li>
<li>status.ts session, 在 init 时候会从 storage 中设置</li>
</ul>
</li>
<li>typings 小程序的接口 .d.ts</li>
<li>
<p>util</p>
<ul>
<li>loading.ts 请求中显示 loading 配置</li>
<li>url.ts 根据传入的对象来构造 get 请求url</li>
</ul>
</li>
<li>index.ts 所有 api 的导出</li>
<li>interface.ts weRequest 接口</li>
<li>version.ts 版本信息</li>
</ul>
<h2>题外话</h2>
<blockquote>很难有人能一次搞定业务需求,只有在它出现后,才知道什么他是它最需要的。业务代码也一样。</blockquote>
<p>同时 weRequest 不是万能的,它符合大众的需求,但不一定符合每个业务的需求。你也可以根据代码改造甚至改进。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=ajPIPjAci8TwZC%2FqaIH6lw%3D%3D.sNF0z2Zd6VBeLRxpakzP8gJ1x9cYfWkWnlZJ0q5nI5KdsRBD5jptAbWjbi4GWNUi" rel="nofollow">博客地址</a></p>
<h2>参考</h2>
<p><a href="https://link.segmentfault.com/?enc=FvoorCM7kCfFnSenYMU7TA%3D%3D.ibyGpbmbakOt0DNeMSyz%2FnzkxzJEJXwxlMBULHJ%2BnwVuymVYVfXyw5Rw7zTB%2FP3s" rel="nofollow">weRequest</a> <br><a href="https://link.segmentfault.com/?enc=igREtcxGOuLtzR7TJ5r%2B0A%3D%3D.2BWFbuEu3XI%2F7bc5nWcp3YH8n40wu2dBBGJzlGgRo7BjLLFR4kAi9d1TUOXyBdv0OYriXVaNW5ajUwT8QSn5sQ%3D%3D" rel="nofollow">如何在Javascript中优雅的使用Async和Await进行错误的处理?</a> <br><a href="https://segmentfault.com/a/1190000011802045">从不用 try-catch 实现的 async/await 语法说错误处理</a> <br><a href="https://link.segmentfault.com/?enc=KS1eo7GA5ZPXBkZUymXSzw%3D%3D.tvgQcqnE4WJMSgcDsFJOKglEOJtXecjvVa1DwmB9KiSu%2BGi4MpDd3lerpCujDSQr" rel="nofollow">axios请求超时,设置重新请求的完美解决方法</a></p>
优化 web 应用程序性能方案总结
https://segmentfault.com/a/1190000020672868
2019-10-13T21:42:57+08:00
2019-10-13T21:42:57+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
23
<p>在开发 web 应用程序时候,性能都是必不可少的话题。而大部分的前端优化机制都已经被集成到前端打包工具 webpack 中去了,当然,事实上仍旧会有一些有趣的机制可以帮助 web 应用进行性能提升,在这里我们来聊一聊能够优化 web 应用程序的一些机制,同时也谈一谈这些机制背后的原理。</p>
<h2>Chrome Corverage 分析代码覆盖率</h2>
<p>在讲解这些机制前,先来谈一个 Chrome 工具 Corverage。该工具可以帮助查找在当前页面使用或者未使用的 JavaScript 和 CSS 代码。</p>
<p>工具的打开流程为:</p>
<ul>
<li>打开浏览器控制台 console</li>
<li>ctrl+shift+p 打开命令窗口</li>
<li>在命令窗口输入 show Coverage 显示选项卡</li>
</ul>
<p><img src="/img/bVbyT7n?w=1400&h=1006" alt="coveragetab.png" title="coveragetab.png"></p>
<p>webpackjs</p>
<ul>
<li>其中如果想要查询页面加载时候使用的代码,请点击 reload button</li>
<li>如果您想查看与页面交互后使用的代码,请点击record buton</li>
</ul>
<p>这里以淘宝网为例子,介绍一下如何使用</p>
<p><img src="/img/bVbyT7o?w=1370&h=640" alt="taobao-coverage.png" title="taobao-coverage.png"></p>
<p><img src="/img/bVbyT7p?w=1376&h=769" alt="taobao-coverage2.png" title="taobao-coverage2.png"></p>
<p>上面两张分别为 reload 与 record 点击后的分析。</p>
<p>其中从左到右分别为</p>
<ul>
<li>所需要的资源 URL</li>
<li>资源中包含的 js 与 css</li>
<li>总资源大小</li>
<li>当前未使用的资源大小</li>
</ul>
<p>左下角有一份总述。说明在当前页面加载的资源大小以及没有使用的百分比。可以看到淘宝网对于首页代码的未使用率仅仅只有 36%。</p>
<p>介绍该功能的目的并不是要求各位重构代码库以便于每个页面仅仅只包含所需的 js 与 css。这个是难以做到的甚至是不可能的。但是这种指标可以提升我们对当前项目的认知以便于性能提升。</p>
<p>提升代码覆盖率的收益是所有性能优化机制中最高的,这意味着可以加载更少的代码,执行更少的代码,消耗更少的资源,缓存更少的资源。</p>
<h2>webpack externals 获取外部 CDN 资源</h2>
<p>一般来说,我们基本上都会使用 Vue,React 以及相对应的组件库来搭建 SPA 单页面项目。但是在构建时候,把这些框架代码直接打包到项目中,并非是一个十分明智的选择。</p>
<p>我们可以直接在项目的 index.html 中添加如下代码</p>
<pre><code> <script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script>
<script src="//https://cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js" crossorigin="anonymous"></script></code></pre>
<p>然后可以在 webpack.config.js 中这样配置</p>
<pre><code>
module.exports = {
//...
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
}
};
</code></pre>
<p><a href="https://link.segmentfault.com/?enc=awoeIK7kP%2F%2FlbjWy4bb8EQ%3D%3D.KPrscEMZy9PLEeqYpJ%2BLC2wRBwNf7va%2FAg8QpC1EtXl77bAJbQE6rOVWrlvASL8r4X5bsMo9mk2ERfoRPjQcJQ%3D%3D" rel="nofollow">webpack externals</a> 的作用是 不会在构建时将 Vue 打包到最终项目中去,而是在运行时获取这些外部依赖项。这对于项目初期没有实力搭建自身而又需要使用 CDN 服务的团队有着不错的效果。</p>
<h3>原理</h3>
<p>这些项目被打包成为第三方库的时候,同时还会以全局变量的形式导出。从而可以直接在浏览器的 window 对象上得到与使用。即是</p>
<pre><code>window.Vue
// ƒ bn(t){this._init(t)}</code></pre>
<p>这也就是为什么我们直接可以在 html 页面中直接使用</p>
<pre><code><div id="app">
{{ message }}
</div>
// Vue 就是 挂载到 window 上的,所以可以直接在页面使用
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})</code></pre>
<p>此时我们可以通过 <a href="https://link.segmentfault.com/?enc=c7kRpNxsvibPgzcspWYIJw%3D%3D.iGE3lnaaMrGqKVGj0wyNMDVvXi0cV8gwWhNZ2RRDVl9hB4ISxHJxRbsQUkplQ8bkluHt4AxdCFOt6RyyGcGXsQ%3D%3D" rel="nofollow">webpack Authoring Libraries</a> 来了解如何利用 webpack 开发第三方包。</p>
<h3>优势与缺陷</h3>
<h4>优势</h4>
<p>对于这种既无法进行代码分割又无法进行 Tree Shaking 的依赖库而言,把这些需求的依赖库放置到公用 cdn 中,收益是非常大的。</p>
<h4>缺陷</h4>
<p>对于类似 Vue React 此类库而言,CDN 服务出现问题意味着完全无法使用项目。需要经常浏览所使用 CDN 服务商的公告(不再提供服务等公告),以及在代码中添加类似的出错弥补方案。</p>
<pre><code><script src="//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.runtime.min.js" crossorigin="anonymous"></script>
<script>window.Vue || ...其他处理 </script></code></pre>
<h2>webpack dynamic import 提升代码覆盖率</h2>
<p>我们可以利用 webpack 动态导入,可以在需要利用代码时候调用 getComponent。在此之前,需要对 webpack 进行配置。具体参考 <a href="https://link.segmentfault.com/?enc=dTbf1lycUd1o9yEV%2BoRklg%3D%3D.XW5EgCFvRtA3XiQDHkKkDr3zFJjM6llQXL7j9u%2Fo8QyDK32lhJKgmZrHQtzosAFcZJkhqYNFvqBfOD9oIKaqoQ%3D%3D" rel="nofollow">webpack dynamic-imports</a>。</p>
<p>在配置完成之后,我们就可以写如下代码。</p>
<pre><code>
async function getComponent() {
const element = document.createElement('div');
/** webpackChunkName,相同的名称会打包到一个 chunk 中 */
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
element.innerHTML = _.join(['Hello', 'webpack'], ' ');
return element;
}
getComponent().then(component => {
document.body.appendChild(component);
});
</code></pre>
<h3>优势与缺陷</h3>
<h4>优势</h4>
<p>通过动态导入配置,可以搞定多个 chunk,在需要时候才会加载而后执行。对于该用户不会使用的资源(路由控制,权限控制)不会进行加载,从而直接提升了代码的覆盖率。</p>
<h4>缺陷</h4>
<p>Tree Shaking,可以理解为死代码消除,即不需要的代码不进行构建与打包。但当我们使用动态导入时候,无法使用 Tree Shaking 优化,因为两者直接按存在着兼容性问题。因为 webpack 无法假设用户如何使用动态导入的情况。</p>
<pre><code> 基础代码X
模块A 模块B
-----------------------------------
业务代码A 业务代码B 业务代码...
</code></pre>
<p>当在业务中使用多个异步块时后,业务代码A 需求 模块A,业务代码 B 需求 模块B,但是 webpack 无法去假设用户在代码中 A 与 B 这两个模块在同一时间是互斥还是互补。所以必然会假设同时可以加载模块 A 与 B,此时基础代码 X 出现两个导出状态,这个是做不到的!从这方面来说,动态导入和 Tree Shaking 很难兼容。具体可以参考 <a href="https://link.segmentfault.com/?enc=3opE2dJIvKqG2AsScUlDfQ%3D%3D.kRGChaBbyt87NyWxhCkqAK27Q4V28aQEwIJfJjBPIezxGtnSXaP%2B1N4Aze8Uf5ca1dQ%2FwBtaXxN6x2ToWMDBxw%3D%3D" rel="nofollow">Document why tree shaking is not performed on async chunks </a>。</p>
<p>当然,利用动态导入,也会有一定的性能降低,毕竟一个是本地函数调用,另一个涉及网络请求与编译。但是与其说这是一种缺陷,倒不如说是一种决策。究竟是哪一种对自身的项目帮助更大?</p>
<h3>使用 loadjs 来辅助加载第三方 cdn 资源</h3>
<p>在普通的业务代码我们可以使用动态导入,在当今的前端项目中,总有一些库是我们必需而又使用率很低的库,比如在只会在统计模块出现的 ECharts 数据图表库,或者只会在文档或者网页编辑时候出现的富文本编辑器库。</p>
<p>对于这些苦库其实我们可以使用页面或组件挂载时候 loadjs 加载。因为使用动态导入这些第三方库没有 Tree shaking 增强,所以其实效果差不多,但是 loadjs 可以去取公用 CDN 资源。具体可以参考 <a href="https://link.segmentfault.com/?enc=xVgsKI71x66CXhn8B4kMjQ%3D%3D.tMVCQAqEVscEAtp0Q4hPphfVzH32HrBUO7VyqsZb%2FQ2jXdj3dQHXooxosSLdu%2Fsk" rel="nofollow">github loadjs</a> 来进行使用。因为该库较为简单,这里暂时就不进行深入探讨。</p>
<h2>使用 output.publicPath 托管代码</h2>
<p>因为无论是使用 webpack externals 或者 loadjs 来使用公用 cdn 都是一种折衷方案。如果公司可以花钱购买 oss + cdn 服务的话,就可以直接将打包的资源托管上去。</p>
<pre><code>module.exports = {
//...
output: {
// 每个块的前缀
publicPath: 'https://xx/',
chunkFilename: '[id].chunk.js'
}
};
// 此时打包出来的数据前缀会变为
<script src=https://xx/js/app.a74ade86.js></script> </code></pre>
<p>此时业务服务器仅仅只需要加载 index.html。</p>
<h2>利用 prefetch 在空缺时间加载资源</h2>
<p>如果不需要在浏览器的首屏中使用脚本。可以利用浏览器新增的 prefetch 延时获取脚本。 </p>
<p>下面这段代码告诉浏览器,echarts 将会在未来某个导航或者功能中要使用到,但是资源的下载顺序权重比较低。也就是说prefetch通常用于加速下一次导航。被标记为 prefetch 的资源,将会被浏览器在空闲时间加载。</p>
<pre><code> <link rel="prefetch" href="https://cdn.jsdelivr.net/npm/echarts@4.3.0/dist/echarts.min.js"></link></code></pre>
<p>该功能也适用于 html 以及 css 资源的预请求。</p>
<h3>利用 instant.page 来提前加载资源</h3>
<p><a href="https://link.segmentfault.com/?enc=t1BJNuYG3Ise%2Bz%2B0vbOtLg%3D%3D.Djbi3t%2BZ1n7uW52jVSQ9sHreBQfBMaxjzXbtK55vOUQ%3D" rel="nofollow">instant.page</a> 是一个较新的功能库,该库小而美。并且无侵入式。<br>只要在项目的 </body> 之前加入以下代码,便会得到收益。</p>
<pre><code><script src="//instant.page/2.0.1" type="module" defer integrity="sha384-4Duao6N1ACKAViTLji8I/8e8H5Po/i/04h4rS5f9fQD6bXBBZhqv5am3/Bf/xalr"></script></code></pre>
<p>该方案不适合单页面应用,但是该库很棒的运用了 prefetch,是在你悬停于链接超过65ms 时候,把已经放入的 head 最后的 link 改为悬停链接的 href。</p>
<p>下面代码是主要代码</p>
<pre><code>// 加载 prefetcher
const prefetcher = document.createElement('link')
// 查看是否支持 prefetcher
const isSupported = prefetcher.relList && prefetcher.relList.supports && prefetcher.relList.supports('prefetch')
// 悬停时间 65 ms
let delayOnHover = 65
// 读取设定在 脚本上的 instantIntensity, 如果有 修改悬停时间
const milliseconds = parseInt(document.body.dataset.instantIntensity)
if (!isNaN(milliseconds)) {
delayOnHover = milliseconds
}
// 支持 prefetch 且 没有开启数据保护模式
if (isSupported && !isDataSaverEnabled) {
prefetcher.rel = 'prefetch'
document.head.appendChild(prefetcher)
...
// 鼠标悬停超过 instantIntensit ms || 65ms 改变 href 以便预先获取 html
mouseoverTimer = setTimeout(() => {
preload(linkElement.href)
mouseoverTimer = undefined
}, delayOnHover)
...
function preload(url) {
prefetcher.href = url
}</code></pre>
<p>延时 prefetch ? 还是在鼠标停留的时候去加载。不得不说,该库利用了很多浏览器新的的机制。包括使用 type=module 来拒绝旧的浏览器执行,利用 dataset 读取 instantIntensity 来控制延迟时间。</p>
<h2>optimize-js 跳过 v8 pre-Parse 优化代码性能</h2>
<p>认识到这个库是在 v8 关于新版本的文章中,在 github 中被标记为 UNMAINTAINED 不再维护,但是了解与学习该库仍旧有其的价值与意义。该库的用法十分简单粗暴。居然只是把函数改为 IIFE(立即执行函数表达式)。<br>用法如下:</p>
<pre><code>optimize-js input.js > output.js</code></pre>
<p>Example input:</p>
<pre><code>!function (){}()
function runIt(fun){ fun() }
runIt(function (){})</code></pre>
<p>Example output:</p>
<pre><code>!(function (){})()
function runIt(fun){ fun() }
runIt((function (){}))</code></pre>
<h3>原理</h3>
<p>在 v8 引擎内部(不仅仅是 V8,在这里以 v8 为例子),位于各个编译器的前置Parse 被分为 Pre-Parse 与 Full-Parse,Pre-Parse 会对整个 Js 代码进行检查,通过检查可以直接判定存在语法错误,直接中断后续的解析,在此阶段,Parse 不会生成源代码的AST结构。</p>
<pre><code>// This is the top-level scope.
function outer() {
// preparsed 这里会预分析
function inner() {
// preparsed 这里会预分析 但是不会 全分析和编译
}
}
outer(); // Fully parses and compiles `outer`, but not `inner`.</code></pre>
<p>但是如果使用 IIFE,v8 引擎直接不会进行 Pre-Parsing 操作,而是立即完全解析并编译函数。可以参考<a href="https://link.segmentfault.com/?enc=VinR3n%2BwB2gIPZl4eGUuMA%3D%3D.7aBjxKAA8kcUzHiJNGI2AMzjjL2j8BOx%2BVcKXCkEl%2FY%3D" rel="nofollow">Blazingly fast parsing, part 2: lazy parsing</a></p>
<h3>优势与缺陷</h3>
<h4>优势</h4>
<p><img src="/img/bVbyT7q?w=1448&h=982" alt="optimize-js.png" title="optimize-js.png"></p>
<p>快!即使在较新的 v8 引擎上,我们可以看到 optimize-js 的速度依然是最快的。更不用说在国内浏览器的版本远远小于 v8 当前版本。与后端 node 不同,前端的页面生命周期很短,越快执行越好。</p>
<h4>缺陷</h4>
<p>但是同样的,任何技术都不是银弹,直接完全解析和编译也会造成内存压力,并且该库也不是 js 引擎推荐的用法。相信在不远的未来,该库的收益也会逐渐变小,但是对于某些特殊需求,该库的确会又一定的助力。</p>
<h4>再聊代码覆盖率</h4>
<p>此时我们在谈一次代码覆盖率。如果我们可以在首屏记载的时候可以达到很高的代码覆盖率。直接执行便是更好的方式。在项目中代码覆盖率越高,越过 Pre-Parsing 让代码尽快执行的收益也就越大。</p>
<h2>Polyfill.io 根据不同的浏览器确立不同的 polyfill</h2>
<p>如果写过前端,就不可能不知道 polyfill。各个浏览器版本不同,所需要的 polyfill 也不同,</p>
<p><a href="https://link.segmentfault.com/?enc=gbr4U2Oro26AqLbnd92yug%3D%3D.vHhZtSntiiUE9ZE8RLMTEWHvCXCfKhc1R1KIZuDyRPC854DDEJmmKn9rX8Zj7XYN" rel="nofollow">Polyfill.io</a>是一项服务,可通过选择性地填充浏览器所需的内容来减少 Web 开发的烦恼。Polyfill.io读取每个请求的User-Agent 标头,并返回适合于请求浏览器的polyfill。</p>
<p>如果是最新的浏览器且具有 Array.prototype.filter</p>
<pre><code>https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.filter
/* Disable minification (remove `.min` from URL path) for more info */</code></pre>
<p>如果没有 就会在 正文下面添加有关的 polyfill。</p>
<p>国内的阿里巴巴也搭建了一个服务,可以考虑使用,网址为 <a href="https://link.segmentfault.com/?enc=mE9NgVxYrbs%2B9KA8l6FwtQ%3D%3D.bhR4VNOkpaGeqvZmWpWotf7kyVZCwWuUbiwlGlvm2pluZuO%2BotJMLN3s2RLoe2YS" rel="nofollow">https://polyfill.alicdn.com/p...</a></p>
<h2>type='module' 辅助打包与部署 es2015+ 代码</h2>
<p>使用新的 DOM API,可以有条件地加载polyfill,因为可以在运行时检测。但是,使用新的 JavaScript 语法,这会非常棘手,因为任何未知的语法都会导致解析错误,然后所有代码都不会运行。</p>
<p>该问题的解决方法是</p>
<pre><code><script type="module">。</code></pre>
<p>早在 2017 年,我便知道 type=module 可以直接在浏览器原生支持模块的功能。具体可以参考 <a href="https://link.segmentfault.com/?enc=NMuDetISPcg0ir%2FGDm7W5w%3D%3D.QgyL34C2iQCmVkfSt6U%2BijchoEDVpWOZrJECTuv3TLWDaTBxLKdDhzkWia%2F2RjGahgZght02UI9enuGGQi7VdvubfJ8qeYZkkcLmuYSGZm4%3D" rel="nofollow">JavaScript modules 模块</a>。但是当时感觉只是这个功能很强大,并没有对这个功能产生什么解读。但是却没有想到可以利用该功能识别你的浏览器是否支持 ES2015。 </p>
<p>每个支持 type="module" 的浏览器都支持你所熟知的大部分 ES2015+ 语法!!!!!</p>
<p>例如</p>
<ul>
<li>async await 函数原生支持</li>
<li>箭头函数 原生支持</li>
<li>Promises Map Set 等语法原生支持</li>
</ul>
<p>因此,利用该特性,完全可以去做优雅降级。在支持 type=module 提供所属的 js,而在 不支持的情况下 提供另一个js。具体可以参考 <a href="https://link.segmentfault.com/?enc=jjEocJ%2FF27mFnGP9syKNrA%3D%3D.X3dItgf3GOq2R8XeDpSS5Kl6%2BwODEHnZyR9UObn4sjnrUPSuQkY772sr6u04xgmhJg60TShC3J3aDgBc%2BXqeeeHRq%2F91xL1K5PxFqakpVzA%3D" rel="nofollow">Phillip Walton 精彩的博文</a>,这里也有翻译版本 <a href="https://link.segmentfault.com/?enc=pxifdkijdHnqb0o1b5eHNQ%3D%3D.8eOEPFMDEGznSPjzYPepZrtpt6ogRjoLWOi1bEUVvbrZ9sH%2FIFXXKOfOKg7Klzxc" rel="nofollow">https://jdc.jd.com/archives/4911</a>.</p>
<h3>Vue CLI 现代模式</h3>
<p>如果当前项目已经开始从 webpack 阵营转到 Vue CLI 阵营的话,那么恭喜你,上述解决方案已经被内置到 Vue CLI 当中去了。只需要使用如下指令,项目便会产生两个版本的包。</p>
<pre><code>vue-cli-service build --modern</code></pre>
<p>具体可以参考 <a href="https://link.segmentfault.com/?enc=AfiYY0ZCTnleSbDj21L1cQ%3D%3D.rWawk%2BfQvtmNzOZMx4ECegVVkDBZ2zfImdfDRIBR%2FFsg2ddl%2FAA2%2FaCGhPmU6U1IG35%2FGUQMcY5LmZc%2BxyeWBbiVL8hw5ymBLsFunAxC56lHSP%2FKuJuKY553B66UDuA%2F" rel="nofollow">Vue CLI 现代模式</a></p>
<h3>优势与缺陷</h3>
<h4>优势</h4>
<p>提升代码覆盖率,直接使用原生的 await 等语法,直接减少大量代码。 </p>
<p>提升代码性能。之前 v8 用的时 Crankshaft 编译器,随着时间的推移,该编译器因为无法优化现代语言特性而被抛弃,之后 v8 引入了新的 Turbofan 编译器来对新语言特性进行支持与优化,之前在社区中谈论的 try catch, await,JSON 正则等性能都有了很大的提升。具体可以时常浏览 <a href="https://link.segmentfault.com/?enc=Uiat9fu0PktqTBKan5nc4A%3D%3D.mQSyI4h9YsI%2BXIdRZSdVKQ%3D%3D" rel="nofollow">v8 blog</a> 来查看功能优化。</p>
<blockquote>Writing ES2015 code is a win for developers, and deploying ES2015 code is a win for users.</blockquote>
<h4>缺陷</h4>
<p>无,实在考虑不出有什么不好。</p>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=Bgc%2FaU9V1d6xJarPtbG4Xw%3D%3D.OXBRmVgvhWRAweIau9AFoRPToMnXHYLA7Zklrm8WNCR3LyppTlS9TOxlILP0iCZX" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=hrpkyuF7c8me8StUS5taWg%3D%3D.uQX3hO97iC31cp9OhvRLFGXp3d%2FD9SVaa2i86fFAAKI%3D" rel="nofollow">webpackjs 中文文档</a> <br><a href="https://link.segmentfault.com/?enc=VH38mDx9wY7h9ga1fQgX1Q%3D%3D.8ACgAyw5x7nkfHS3klWXMc98l8zRxoqiVFEaTkGgNKo%3D" rel="nofollow">Blazingly fast parsing, part 2: lazy parsing</a> <br><a href="https://link.segmentfault.com/?enc=xi3jRRAY8Bl10K3%2F0V%2Bgzg%3D%3D.ro6SnfjORlKA1uOX2v%2Fkf3YWup3SnblmKsNHTGKSesB6F6QwEeSzyYuvHbrSgjlN" rel="nofollow">Polyfill.io</a> <br><a href="https://link.segmentfault.com/?enc=MzA%2BMCB4mndnOXLdipf8nA%3D%3D.jQLUFCiNJm6WQDtnLcFmVnq%2FL86cRCdogPeFL6cQ8E4kljhJAd1ja17oTRfoEfO8Ba3SmIPkeYUfa2y2MuhQNOwaDantaBtNsRqJAwznldI%3D" rel="nofollow">JavaScript modules 模块</a> <br><a href="https://link.segmentfault.com/?enc=%2F77WSqu0p%2Fga2tug%2FJs92A%3D%3D.fJFOdNZgy4mdaH9oC0%2FhY%2BP6Y%2BmbXZbkmqgJo%2BhO0nB6gl%2BIeqG6wGVnPOVyS7mRWHuNJE4pAmPh4uQ6FRY20HrGJZaHF0J3HD4ElICHjug%3D" rel="nofollow">deploying-es2015-code-in-production-today</a> <br><a href="https://link.segmentfault.com/?enc=8AN3QJMV1zgKnLIKc4XmAQ%3D%3D.YQHj2%2FZqmFqbPdGeXGLhv6trnY1oQQwID%2BE3yxMR%2BSJ07l%2F6gmySzduoupeTjnPyumZSONEgmWziI98SUxXO10a6SPfAUThY2l0QNYN0UCW6%2BfagjX3qbe6SlVr6CMkL" rel="nofollow">Vue CLI 现代模式</a> <br><a href="https://link.segmentfault.com/?enc=UnmPezOrxr3NkYJANRk3aA%3D%3D.N4uLtY1uJJ25wCbDRB%2Fkqg%3D%3D" rel="nofollow">v8 blog</a></p>
手把手教你使用issue作为博客评论系统
https://segmentfault.com/a/1190000019517784
2019-06-18T23:45:55+08:00
2019-06-18T23:45:55+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
1
<p>自从上周在阮一峰的 <a href="https://link.segmentfault.com/?enc=td5ShYO38k0JMgO6KS5VNw%3D%3D.etn%2BRdiY96N5y1Zqyqmdj41fbcwsUf1qrcqgCFwCdUMX9EVsJD5Hyadrz8c2xYNqU5KcJAK3h0Orl2egwLczDw%3D%3D" rel="nofollow">每周分享第 60 期</a> 看到了可以将 GitHub 的 issue 当作评论系统,插入第三方网页的 JS 库——<a href="https://link.segmentfault.com/?enc=tJmSWdLcTuJXpc%2FzCOy%2FSw%3D%3D.2C67OJau14pUFmP%2B29wBMRgA7OQv%2Fz9OrFOeJQvMG1g%3D" rel="nofollow">utterances</a>。我就对此“魂牵梦绕”。个人博客使用的是<a href="https://link.segmentfault.com/?enc=%2BGgHVT%2F2mv1hRHOU4WL5jg%3D%3D.3TQEnTphn4Fd5ZGnQlFXYzRn%2BLq7pM2H3ZK3KJ5TuzM%3D" rel="nofollow">VuePress</a>。</p>
<h2>TLDR (不多废话,先看效果)</h2>
<p>之前是使用了 Valine 作为博客的评论系统。</p>
<p><img src="/img/bVbt3CL?w=1370&h=945" alt="valine" title="valine"></p>
<p>下图是改为 utterances 风格。</p>
<p><img src="/img/bVbt3CO?w=1370&h=945" alt="utterances" title="utterances"></p>
<h2>utterances 介绍及使用</h2>
<p>utterances 是基于github issue,拥有多种主题的开源免费小组件。</p>
<p>1.首先我们所需要的 github 存储库必须是公开的,而不是私有的,这样我们的读者才可以查看以及发表评论。</p>
<p>2.我们必须在 github 上进行安装 utterances,首先我们访问 <a href="https://link.segmentfault.com/?enc=uaPC6EjOtudpmPcuNuy2JQ%3D%3D.pqbUeqP9aBYQuLSJM6jgcPLxC7FYVhHFkd%2F9AsL2ODMT4ZKyG9z8kpNSiAaKryh2" rel="nofollow">utterances应用程序</a> 然后点击 Install 按钮进行安装。<br><img src="/img/bVbt3CQ?w=1253&h=711" alt="utterances index" title="utterances index"></p>
<p>3.在这里可以选择可以关联的存储库,可以选择我们所拥有的库(也包括未来建立的库)或者某一个仓库,这里我们只选择某一个需要进行评论的库,这样比较好。</p>
<p><img src="/img/bVbt3CY?w=1340&h=819" alt="utterances select" title="utterances select"></p>
<p>4.安装完成即可,随后我们访问<a href="https://link.segmentfault.com/?enc=7QYoxWxWsk9BzH37IdtkoQ%3D%3D.tn21%2FmESgRO2DGapjYDB56KVQMBF07DarPOZ%2BGJI6fWdcy4SgeOMEbUqkcCfIYsm" rel="nofollow">utterances应用程序</a>就不再是安装而是是执行配置项目。</p>
<p><img src="/img/bVbt3C0?w=1340&h=743" alt="utterances index2" title="utterances index2"></p>
<p><img src="/img/bVbt3C2?w=1323&h=865" alt="utterances select2" title="utterances select2"></p>
<p>5.此时服务端配置已经完成,现在我们可以进行客户端的操作,也就是 blog 端。在blog端我们只需要添加以下这段脚本就可以直接运行。</p>
<pre><code><script
// 加载的客户端脚本
src="https://utteranc.es/client.js"
// repo 就是访问的仓库,格式 用户名/仓库名
// 个人就是 repo="wsafight/personBlog"
repo="[ENTER REPO HERE]"
// 选定的当前blog 与 issue 之间的关系
// 个人使用的是不会自动创建的 issue-number,每个issue都有自己的number。该选项是使用特定的issue
issue-term="pathname"
// 主题为 github-light 还有其他主题可以选择
theme="github-light"
crossorigin="anonymous"
async>
</script></code></pre>
<p>6.因为我的博客是采用 <a href="https://link.segmentfault.com/?enc=YciJ0RPe8SQN7CmOrNxCCQ%3D%3D.gTcFJPrdEpvEjBAvMI4iGKH%2BVT72wlA%2BfJ9HpBTxMrM%3D" rel="nofollow">VuePress</a>,所以在 markdown 中是无法使用 script 脚本的。我们就需要编写写一个 vue 组件。(组件的文件路径为 [blog name]/.vuepress/components/)</p>
<pre><code>// Utterances 组件
<template>
<div id="comment"></div>
</template>
<script>
export default {
name: 'Utterances',
props: {
// 传入的 issue-number
id: Number
},
methods: {
initValine () {
// 建立脚本以及属性
const utterances = document.createElement('script');
utterances.type = 'text/javascript';
utterances.async = true;
utterances.setAttribute('issue-number', this.id)
utterances.setAttribute('theme','github-light')
utterances.setAttribute('repo',`wsafight/personBlog`)
utterances.crossorigin = 'anonymous';
utterances.src = 'https://utteranc.es/client.js';
// comment 是要插入评论的地方
window.document.getElementById('comment').appendChild(utterances);
}
},
mounted: function(){
// 每次挂载时候,进行初始化
this.initValine()
}
}
</script></code></pre>
<p>7.最后。在 md 文档中直接使用上面编写的组件</p>
<pre><code>## 参考资料
[高性能JS-DOM](https://www.cnblogs.com/libin-1/p/6376026.html)
[imba 性能篇](http://imba.io/guides/advanced/performance)
// 可以在 md 文档中直接使用组件
<Utterances :id="1"/></code></pre>
<h2>utterances其他配置项</h2>
<p>主题 Theme 选项如下,我们可以选择各色主题:</p>
<ul>
<li>Github Light</li>
<li>Github Dark</li>
<li>Github Dark Orange</li>
<li>Icy Dark</li>
<li>Dark Blue</li>
<li>Photon Dark</li>
</ul>
<p>评论 issue-term 映射配置选项如下:</p>
<ul>
<li>pathname</li>
<li>url</li>
<li>title</li>
<li>og:title</li>
<li>issue-number</li>
</ul>
<p>issue-term="1" <br>特定number的issue,不会自动创建,个人使用该方案</p>
<ul><li>specific-term</li></ul>
<h2>鼓励一下</h2>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=Yp%2BhehIxXGUuLSL4OCu7ng%3D%3D.AaWG1DvgVYj24Xhcm4Jnycd4mvyy6fj3A7nlPrWhxDrVFZ2I9WNhpERIS8FRM6gQ" rel="nofollow">博客地址</a></p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=Ygn%2BrNbXvhtIIr81G6tbSw%3D%3D.Zkq%2BwzockLGmB92dGvRTg4%2FsLONLdiIuxCJ2qrPvJ%2BQ%3D" rel="nofollow">utteranc 文档</a> <br><a href="https://link.segmentfault.com/?enc=oyetZV8JC5rm6SKdJ3fQyA%3D%3D.lLr56yEoiLwlbk20zOSWSR5mdSNa7yYslThCCOXvnUbdDb%2FGwlw%2FaIyXtIuJdqzL" rel="nofollow">博客使用 utterances 作为评论系统</a></p>
探讨奇技淫巧
https://segmentfault.com/a/1190000019389840
2019-06-04T23:37:01+08:00
2019-06-04T23:37:01+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
16
<h2>探讨奇技淫巧</h2>
<h3>起源</h3>
<p>在工程实践中,我们常常会遇到一些奇技淫巧。所谓奇技淫巧,就是官方在设计或者实践中并未想象出的代码风格或者使用场景。其实也就是类似于 react 的 hoc,本来源自于社区,但是该方案却成为了官方肯定的方案。那么究竟应不应在平时学习呢?究竟应不应该在工程中使用呢,或者使用怎么样的奇技淫巧。</p>
<p>两年前。我还没有毕业,在大学的最后一个学期中选择了进入前端,同时,被吸引到前端阵营中一个不得不说的原因就是 js 的奇技淫巧,同时个人是一个比较猎奇的人,所以就学了很多关于 js 的奇技淫巧。</p>
<p>现在这些奇技淫巧要么变成了这门语言不可或缺的一部分,要么随着时间的推移而消失,还有一些在不知不觉中却忘记了,既然这次的文章是介绍这方面的知识,也就多介绍一下之前学习的一些例子。</p>
<h3>~ 运算符 + indexOf</h3>
<p>在 es6 includes 尚未推行之前,我们判断判断字符串或者数组包含只能使用 indexOf 这个方法,但是 indexOf 返回的确实元素的索引,如果不存在则返回 -1。<br>因为在之前写 c 语言的时候,我们往往使用 0 代表成功,1 2 3代表着不同的错误。因为0是独一无二的。在类c的语言中是具有 truthy falsy 这个概念。并不指代bool的 true 与 false。</p>
<p>下表代表了js 的 truthy 以及 falsy。</p>
<table>
<thead><tr>
<th>变量类型</th>
<th align="right">falsy</th>
<th align="center">truthy</th>
</tr></thead>
<tbody>
<tr>
<td>布尔</td>
<td align="right">false</td>
<td align="center">true</td>
</tr>
<tr>
<td>字符串</td>
<td align="right">" "</td>
<td align="center">非空字符串</td>
</tr>
<tr>
<td>数值</td>
<td align="right">0 NaN</td>
<td align="center">任何不为falsy的数值</td>
</tr>
<tr>
<td>null</td>
<td align="right">是</td>
<td align="center">否</td>
</tr>
<tr>
<td>undefined</td>
<td align="right">是</td>
<td align="center">否</td>
</tr>
<tr>
<td>对象(数组), {} 以及 []</td>
<td align="right">否</td>
<td align="center">是</td>
</tr>
</tbody>
</table>
<p>对于数值而言,我们知道 0 对于数值是唯一的,而 -1不是。那么我们可以通过 ~ 运算符来把-1 变为 0.</p>
<pre><code>~-1
// 0
~1
//-2
</code></pre>
<p>解释下 <br>对每一个比特位执行非(NOT)操作。NOT a 结果为 a 的反转(即反码)。</p>
<pre><code>9 (base 10) = 00000000000000000000000000001001 (base 2)
~9 (base 10) = 11111111111111111111111111110110 (base 2) = -10 (base 10)</code></pre>
<p>因为在计算机中第一位代表着 符号位置。 </p>
<p>同时简单理解。对任一数值 x 进行按位非操作的结果为 -(x + 1)。<br>也就是说通过 ~ 可以把 -1(且仅仅只是 -1) 变为 falsy。</p>
<pre><code>var str = 'study pwa';
var searchFor = 'a';
// 这是 if (str.indexOf('a') > -1) 或者 if ( -1 * str.indexOf('a') <= 0) 条件判断的另一种方法
if (~str.indexOf(searchFor)) {
// searchFor 包含在字符串 str 中
} else {
// searchFor 不包含在字符串 str 中
}</code></pre>
<h3>惰性函数</h3>
<p>没学习惰性函数时候,如果创建 xhr,每次都需要判断。</p>
<pre><code>function createXHR(){
var xmlhttp;
try{
//firfox,opear,safari
xmlHttp=new XMLHttpRequest();
} catch(e) {
try{
xmlHttp=new ActiveXobject('Msxm12.XMLHTTP');
} catch(e) {
try{
xmlHttp=new ActiveXobject("Microsoft.XMLHTTP")
} catch(e) {
alert("您的浏览器不支持AJAX")
return false;
}
}
}
return xmlHttp;
}
</code></pre>
<p>在学习完了惰性函数之后</p>
<pre><code>function createXHR(){
// 定义xhr,
var xhr = null;
if (typeof XMLHttpRequest!='undefined') {
xhr=new XMLHttpRequest();
createXHR=function(){
return new XMLHttpRequest(); //直接返回一个懒函数
}
} else {
try{
xhr=new ActiveXObject("Msxml2.XMLHTTP");
createXHR=function(){
return new ActiveXObject("Msxml2.XMLHTTP");
}
} catch(e) {
try{
xhr =new ActiveXObject("Microsoft.XMLHTTP");
createXHR=function(){
return new ActiveXObject("Microsoft.XMLHTTP");
}
} catch(e) {
createXHR=function(){
return null
}
}
}
}
// 第一次调用也需要 返回 xhr 对象,所以需要返回 xhr
return xhr;
}</code></pre>
<p>如果代码被使用于两次调用以上则会有一定的性能优化。第一次调用时候 把 xhr 赋值并返回,且在进入层层 if 判断中把 createXHR 这个函数赋值为其他函数。</p>
<pre><code> // 如果浏览器中有 XMLHttpRequest 对象在第二次调用时候
createXHR=function(){
return XMLHttpRequest(); //直接返回一个懒函数
}</code></pre>
<p>该方案可以在不需要第二个变量的情况下直接对函数调用进行优化。同时对于调用方也是透明的,不需要修改任何代码。</p>
<h3>扩展运算符号的另类用法</h3>
<p>在最近的学习中,我看到了一篇关于 ... (扩展运算符)的另类用法,<a href="https://link.segmentfault.com/?enc=k8QW3jjTCJDdEN4Kr2HZNQ%3D%3D.sB04p%2FCyli1QPOF3QXhJu35jvru9G2%2FhoQz3T12vUIwHtL8PessVXSKVQGYD%2Bb0CzJnbyU8CcEjo0pgnpz80XZ7ASjveakzENYytSs%2FUEsyddct5m53ZMgfDHQtqV3Jczb3erZmTorMgmyYp69jsAA%3D%3D" rel="nofollow">The shortest way to conditional insert properties into an object literal</a>, 这篇文章介绍了如何最简化的写出条件性插入对象属性。 </p>
<p>在没有看过这篇文章时会写出如下代码:</p>
<pre><code>// 获得手机号
const phone = this.state.phone
const person = {
name: 'gogo',
age: 11
}
// 如果手机号不为空,则添加到person中
if (phone) {
person.phone = phone
}
</code></pre>
<p>但是,看完该文章之后可以写出这样的代码</p>
<pre><code>// 获得手机号
const phone = this.state.phone
const person = {
name: 'gogo',
age: 11,
...phone && {phone}
}
</code></pre>
<p>上面的代码与该代码功能相同,但是代码量却减少很多。</p>
<p>要理解上述代码的运行原理,首先先介绍一下 ... 运算符,<br>对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。</p>
<pre><code>let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
// 如果是 空对象,没有任何效果
{...{}, a: 1}
// { a: 1 }
// 如果扩展运算符后面不是对象,则会自动将其转为对象。但是如果对象没有属性,就会返回空对象
// {...1} 会变为 {...Object(1)} 但是因为没有属性
{...1}
// {}
// 同理得到
{...undefined} {...null} {...true}
// 都会变为 {}</code></pre>
<p>可以参考 阮一峰的 es6入门的<a href="https://link.segmentfault.com/?enc=u%2FtVokXv%2FZoiU1lQeE97xg%3D%3D.3ulv%2BnU8RqeivKyZS2WQUK2%2BArh5e547mpyHnd2K5VFp6wm9N6v7CBkKe2pwL3jU%2BiK96pFYAwQxFpAFwqc6w%2FjjEteT48ZaIR1CaUBlp%2F8dTcJzruUKAI5JM%2B5tI5LssIU6M4Fufh0E995xGJ6cKhtqfam1Dst4o8ts30gJxco%3D" rel="nofollow">对象的扩展运算符</a></p>
<p>原理是因为代码可以如下理解:</p>
<pre><code>const obj = {
...(phone && {phone})
}
// 如果 phone 有数据,&& 执行则会变为
const obj = {
...{phone}
}
// 而对象扩展运算符 执行就会变为
const obj = {
phone
}
但是 如果 phone 为空字符串或者其他 falsy 数据,则代码会直接短路
const obj = {
...false
...null
...0
...undefined
}
则不会添加任何属性进入对象
</code></pre>
<h3>讨论与思考</h3>
<p>关于 ~ 操作符 + indexOf 其实加深了对位运算与比特位的理解。但是在es6之后我们完全可以使用 includes。完全可以不再使用~indexOf。</p>
<p>对于惰性函数,在typescript中,该代码是不可以使用的。当然,我们可以通过函数变量以及增加代码实现上述功能。</p>
<pre><code>function createXHR(){}
// 修改为
let createXHR = function() {
// ...
}</code></pre>
<p>这里也可以看出 ts 不认可函数声明的函数名是一个变量。</p>
<p>对于扩展运算符的特殊用法。关于 typescript 使用,上述代码是可以在ts中使用的,不过不可以使用 &&,要使用 三元运算符</p>
<pre><code>{
...phone ? {phone} : {}
}</code></pre>
<p>但是不建议在ts中使用,因为该代码不会被代码ts检测到。</p>
<pre><code>const phone = '123'
// 定义接口
interface Person {
name: string;
}
// 不会爆出 error
const person: Person = {
name: 'ccc',
...phone ? {phone} : {}
}</code></pre>
<p>该代码是与 ts 严重相悖的,ts首要就是类型定义,而使用该代码逃出了 ts 的类型定义,这个对于语言上以及工程维护上是无法接受的。<br>同样的代码,我认为 js 是可以接受的(但是未必要在工程中使用),但是 ts 确实无法接受的,这也是不同的语言之间的差异性。</p>
<p>在关于这片文章的评论中,最大的论点在于 为什么要使用最简的代码,最好的代码应该是不言自明的。 </p>
<p>而作者也相对而言探讨了自己的一些看法,应该学习一些自己不理解的东西。同时如果一个东西能够解释来龙去脉,完全可以从原理性解释,那么值得学习与使用。同时我个人其实是和作者持着相同意见的。</p>
<h3>总结</h3>
<ul>
<li>js 是一门灵活的语言(手动滑稽)。</li>
<li>应该多学习一些奇技淫巧,因为很多奇技淫巧往往代表一些混合的知识,往往会有一些新奇的思考与体验(怎么我想不出来?)同时,在别人使用了奇技淫巧时候我可以迅速理解。</li>
<li>在项目中是否使用此类代码要取决团队类型,以及项目体系,并非个人喜恶。</li>
</ul>
<h3>鼓励一下</h3>
<p>如果你觉得这篇文章不错,希望可以给与我一些鼓励,在我的 github 博客下帮忙 star 一下。<br><a href="https://link.segmentfault.com/?enc=HzYIa9GKNiAgYdmFjmZ%2FJg%3D%3D.TH%2BZGEh2u0dw1EG2ZKEJ30%2Fv252p8zbaZXGQEOAp%2BcioLWA4MTHrBd2VDBatFdjT" rel="nofollow">博客地址</a></p>
<h3>参考资料</h3>
<p><a href="https://link.segmentfault.com/?enc=pcU5gkqNAE6ztke58InYyw%3D%3D.OKIBq4B5BKuk31bIF1r%2ByR93oREjc3MNBfcdEtFfcHuaHr1XX3yhtiJKbn3Mj0qYF%2FgXm1nWUJUHvkH6GC0R2G5hC7YL8Ge0UXvCq0vpYWcfg63nBdXrGTe0D4kdZXS7EochotPj3qS2F32f%2BTBg8Q%3D%3D" rel="nofollow">The shortest way to conditional insert properties into an object literal</a></p>
<p><a href="https://link.segmentfault.com/?enc=79PMMo9VlCN3wI%2Bitff6og%3D%3D.JUxNEEkShUxadYASBXrkM5XHZKFQv4miIsJLDmlvhCoKvBtW7y%2B8bcW5dObVxi2YkzFnOQxOZEHKGcxVijd6YLyEwhwhvtgFR3CfoP3y%2BlkdOrGOpksvN3AQjQHWR87bKjYgS2CVnLfF3KlqA2sRHa7S7sVt9JaAbbgykQPEzVg%3D" rel="nofollow">对象的扩展运算符</a></p>
小程序绑定用户方案 优化
https://segmentfault.com/a/1190000019182298
2019-05-15T00:30:34+08:00
2019-05-15T00:30:34+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
10
<p>在做过一系列小程序之后,对小程序的登陆鉴权的流程也有一定的理解,类似于 B 端小程序自不必说,要用户信息手机号地址可以一把梭,做一个引导页面进行判断然后要求用户给与绑定,用户自然不会多说什么,毕竟这是企业级别应用。但是当涉及到 C 端小程序时候。想让用户进行绑定,就势必要给与用户便利。这里我列出一些我觉得较为不错的小程序应用方案以供参考。</p>
<h2>预先绑定类</h2>
<p>该类小程序在使用之前就需要绑定用户信息。常见于线下门店类功能性小程序。线下操作时有大量的优惠活动来支持小程序的流量。</p>
<h3>功能介绍</h3>
<p>例如 便利蜂。之前在上海经常使用,价格和优惠都非常不错,这类小程序属于线下功能类小程序,内部有抽奖,付款等一系列功能。该小程序第一次打开就先用户直接要求用户绑定信息和地址,考虑到线下门店都会有一定的店员辅助。所以该小程序的绑定操作实际上用户都是可以接受的。图片如下所示。</p>
<p><img src="/img/bVbsEl2?w=1003&h=2014" alt="便利蜂首次进入" title="便利蜂首次进入"></p>
<h3>技术要点</h3>
<ul><li>技术1: 使用自定义导航栏让头部可以配置</li></ul>
<p>全局配置</p>
<pre><code>"window": {
"navigationStyle": "custom"
}</code></pre>
<p>如果微信 app 的版本在 7.0.0之上,我们就可以使用页面级别的配置了。</p>
<pre><code>{
"usingComponents": {},
"navigationStyle": "custom"
}</code></pre>
<p>该配置默认时default,当使用custom时候可以自定义导航,可以在头部配置 loading。</p>
<p>第二种这个需要 app 版本,所以如果是想简化,反而在全局下定义,再使用微信官方的组件 <a href="https://link.segmentfault.com/?enc=M84Pn6QeNRBW8W1HUA4s0w%3D%3D.Rj4GkjGibxLWYeXwk9KizHiRb3eAKuHJYJ3qA%2B6UlUHQ7Psb9nxKwZav50nMbIlm0JnV%2FyRHxcN3Dqrxoi5nqQ%3D%3D" rel="nofollow">avigation-bar</a> 即可。</p>
<ul><li>技术2:使用小程序骨架屏</li></ul>
<p>骨架屏方案在后端不能很快给与前端数据时候采用这种方案,亦或者前端可以使用 Service Worker 把上次缓存数据返回到前端,等到从后端获取数据之后刷新页面也是一种方案,但是因为这是第一次打开小程序,所以采用骨架屏是一个很好的方法。 </p>
<p>采用 <a href="https://link.segmentfault.com/?enc=6g73sIY7KvTTg%2F7r6Z4zGg%3D%3D.M4biNYLkgWX%2BUsCT8370EOPbkptpbnR%2BP49rbkbIDyoBsLW8wLrKG2vig07fETg1" rel="nofollow">小程序骨架屏</a> 组件,如果不需要骨架屏动画效果,可以试试直接加载图片作为骨架屏。</p>
<h2>惰性绑定类</h2>
<p>该类小程序在展示时无需绑定用户信息,但是当用户进行操作时在询问绑定。常用于线上商城等一系列无需专人引导的用户项目。</p>
<h3>功能介绍</h3>
<p>基本上线上大部分 c 端小程序都采用此做法,功能上倒是没什么可以介绍的,但是实践上却有不同做法。</p>
<h3>实践方式</h3>
<ul><li>方式 1: 页面跳转 (京东购物)</li></ul>
<p>在每个需要绑定的按钮上添加跳转逻辑,如果当前小程序没有绑定,可以跳转到另外一个页面上确认授权。</p>
<ul><li>方式2: 按钮控制 (华为商城+)</li></ul>
<p>在每个需要绑定按钮上添加 open-type='getuserinfo',后续可以根据状态变化,切换掉按钮(也可以不切换,因为第二次绑定数据不会跳出组件)。</p>
<ul><li>方式3: 遮罩层拦截 (抽奖助手)</li></ul>
<p>在需要绑定的页面添加一个 透明模态框,增加以整个页面大小的button。用fixed布局,还可以向下滚动。无论在当前页面点击任何地方都会出现需要绑定选项。 </p>
<p>组件代码:</p>
<pre><code>// wxml
<view style="z-index: {{zIndex}}" class="mask">
<button open-type="{{ openType }}"
bindtap="onClick"
bindgetuserinfo="bindGetUserInfo"
bindgetphonenumber="bindGetPhoneNumber"
bindopensetting="bindOpenSetting"
binderror="bindError"
class="mask"/>
</view>
// wxss
.mask{
position: fixed;
top: 0;
bottom:0;
left:0;
right:0;
background-color: inherit;
opacity: 0;
}</code></pre>
<p>然后在绑定后令 mask 消失。该方案初看起来不是那么的合适,但是仔细想想却也没什么问题,因为用户99%可能点击所需求的按钮,就算点击到按钮之间的空隙之处跳出要求绑定也没有什么问题。</p>
<p>上面方式实际上都没有太大的问题,需要在不同场景下做最合适的选择。</p>
<h2>结语</h2>
<p>人机交互功能是决定计算机系统“友善性”的一个重要因素。读书学习时候要先把书读厚,再把书读薄,做程序也是一样,如何把系统做的复杂而更加复杂,如何让用户的体验简单而更为简单都不是那么容易的一件事。</p>
利用 es6 new.target 来对模拟抽象类
https://segmentfault.com/a/1190000019131068
2019-05-10T00:11:31+08:00
2019-05-10T00:11:31+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
7
<h2>起源</h2>
<p>最近在使用 Symbol 来做为唯一值,发现 Symbol 无法进行 new 操作,只能当作函数使用,只要进行了new 就会发生类型错误</p>
<pre><code>new Symbol()
// error
Uncaught TypeError: Symbol is not a constructor
at new Symbol (<anonymous>)
at <anonymous>:1:1</code></pre>
<p>在不考虑底层实现的情况下,在代码层面是否能够实现一个函数只可以进行调用而不可以进行 new 操作呢?思考之后如下写出:</p>
<pre><code>function disConstructor() {
if (this !== window) {
throw new TypeError(' disConstructor is not a constructor')
}
console.log('gogo go')
}
// 测试结果如下
disConstructor() // gogo go
new disConstructor()
// error
Uncaught TypeError: disConstructor is not a constructor
at new disConstructor (<anonymous>:3:15)
at <anonymous>:1:1</code></pre>
<p>如果使用 nodejs,window 可以切换为 global, 代码运行结果不变,因为对于个人而言没有适用场景。于是就没有继续研究下去,可是最近在从新翻阅 es6 时候发现 new.target这个属性。</p>
<h2>new.target 属性</h2>
<h3>介绍(引用 mdn 文档)</h3>
<p>new.target属性允许你检测函数或构造方法是否是通过new运算符被调用的。 <br>在通过new运算符被初始化的函数或构造方法中,new.target返回一个指向构造方法或函数的引用。在普通的函数调用中,new.target 的值是undefined。</p>
<p>这样的话 我们的代码就可以这样改为</p>
<pre><code>function disConstructor() {
// 普通的函数调用中,new.target 的值是undefined。
if (new.target) {
throw new TypeError(' disConstructor is not a constructor')
}
console.log('gogo go')
}</code></pre>
<p>得到与上述代码一样的答案。</p>
<h3>深入</h3>
<p>难道 es6 特地添加的功能仅仅只能用于检查一下我们的函数调用方式吗? <br>在查阅的过程各种发现了大多数都方案都是用 new.target 写出只能被继承的类。类似于实现java的抽象类。</p>
<pre><code>class Animal {
constructor(name, age) {
if (new.target === Animal) {
throw new Error('Animal class can`t instantiate');
}
this.name = name
this.age = age
}
// 其他代码
...
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
new Animal()
// error
Uncaught Error: Animal class can`t instantiate
at new Animal (<anonymous>:4:13)
at <anonymous>:1:1
new Dog('mimi', 12, '公')
// Dog {name: "mimi", age: 12, sex: "公"}
</code></pre>
<p>但是 java抽象类抽象方法需要重写,这个是没有方案的。于是在测试与使用的过程中,却意外发现了超类可以在构造期间访问派生类的原型,利用起来。</p>
<pre><code>class Animal {
constructor(name, age) {
console.log(new.target.prototype)
}
// 其他代码
...
}</code></pre>
<p>之前运行时调用需要重写的方法报错是这样写的。</p>
<pre><code>class Animal {
constructor(name, age) {
this.name = name
this.age = age
}
getName () {
throw new Error('please overwrite getName method')
}
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
const pite = new Dog('pite', 2, '公')
a.getName()
// error
Uncaught Error: please overwrite getName method
at Dog.getName (<anonymous>:8:11)
at <anonymous>:1:3</code></pre>
<p>然而此时利用 new.target ,我就可以利用 构造期间 对子类进行操作报错。</p>
<pre><code>class Animal {
constructor(name, age) {
// 如果 target 不是 基类 且 没有 getName 报错
if (new.target !== Animal && !new.target.prototype.hasOwnProperty('getName')) {
throw new Error('please overwrite getName method')
}
this.name = name
this.age = age
}
}
class Dog extends Animal{
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
}
const pite = new Dog('pite', 2, '公')
// error
Uncaught Error: please overwrite getName method
at new Animal (<anonymous>:5:13)
at new Dog (<anonymous>:14:5)
at <anonymous>:1:1</code></pre>
<p>此时可以把运行方法时候发生的错误提前到构造时期,虽然都是在运行期,但是该错误触发机制要早危害要大。反而对代码是一种保护。</p>
<p>当然了,利用超类可以在构造期间访问派生类的原型作用远远不是那么简单,必然是很强大的,可以结合业务场景谈一谈理解和作用。</p>
<h2>其他方案</h2>
<p>增加 编辑器插件 <br>proxy <br>修饰器</p>
谈谈前端工程化 js加载
https://segmentfault.com/a/1190000019061704
2019-05-03T19:00:39+08:00
2019-05-03T19:00:39+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
12
<h2>当年的 js 加载</h2>
<p>在没有 前端工程化之前,基本上是我们是代码一把梭,把所需要的库和自己的代码堆砌在一起,然后自上往下的引用就可以了。 <br>那个时代我们没有公用的cdn,也没有什么特别好的方法来优化加载js的速度。最多用以下几个方案。</p>
<h3>可用的性能方案</h3>
<ul>
<li>可以在代码某些需要js的时候去使用 loadjs 来动态加载 js 库。这样就不会出现开始时候加载大量js文件。</li>
<li>再大点的项目可能用一下 Nginx <a href="https://link.segmentfault.com/?enc=L60mjGBIxwocdW%2Bl%2F3I68w%3D%3D.qZEw51dF%2F2cASoXQbO9QN1%2FAziQjSJU9IrpUYzyghZadLFFFl1M5HpolA2o8saAHYzAW%2BXLN%2BS3zRpE4wBa5mQ%3D%3D" rel="nofollow">ngx_http_concat_module</a> 模块来合并多个文件在一个响应报文中。也就是再加载多个小型 js 文件时候合并为一个 js 文件。</li>
<li>BigPipe 技术也是可以对页面分块来进行优化的,但是因为与本文关系不大,方案也没有通用化和规范化,加上本人其实没有深入了解所不进行深入介绍,如果先了解可以参考 <a href="https://link.segmentfault.com/?enc=aqjhgL3HQ9%2FNKY3QSxcJbw%3D%3D.5FZA%2FtilP175RZjWJ8BozXkQSvjRrXYOwaoY92OsdDOSQOBWY5mvw0v9XTmq3Avt0Mn1v%2BGuVwRdGctVcEcQEw%3D%3D" rel="nofollow">新版卖家中心 Bigpipe 实践(一)</a> 以及 <a href="https://link.segmentfault.com/?enc=r8gZ3CMkdAmxJknhxdLIog%3D%3D.CmSJJ%2FHGZoxxW1dtrHuUcYckOeZbb%2FDqN%2BtwI%2Bft6vt5BBE%2BzU0KDpqSyxanCqk7TnaETBS%2BAqOsXAQQ0KOWOw%3D%3D" rel="nofollow">新版卖家中心 Bigpipe 实践(二)</a>。</li>
</ul>
<p>当然那个时期的代码也没有像现在的前端的代码量和复杂度那么高。</p>
<h2>Webpack 之后的js加载</h2>
<p>与其说 Webpack 是一个模块打包器,倒不如说 Webpack 是一份前端规范。</p>
<h3>需要库没有被大量使用情况</h3>
<p>对于我们代码中所需要的代码库没有大量使用,比如说某种组件库我们仅仅只使用了 2、3个组件的情况下。我们更多需要按需加载功能。 <br>比方说在 <a href="https://link.segmentfault.com/?enc=rrEJfuMzVDUnZOxqbkTAsQ%3D%3D.jRhif9h5VprdByE9rLZncZ0isMyQciPU%2BPlreYeWvJg%3D" rel="nofollow">MATERIAL-UI</a> 我们可以用</p>
<pre><code>import TextField from '@material-ui/core/TextField';
import Popper from '@material-ui/core/Popper';
import Paper from '@material-ui/core/Paper';
import MenuItem from '@material-ui/core/MenuItem';
import Chip from '@material-ui/core/Chip';</code></pre>
<p>代替</p>
<pre><code>import {
TextField,
Popper,
Paper,
MenuItem,
Chip
} from '@material-ui'
</code></pre>
<p>这样就实现了按需加载,而不是动辄需要整个组件库。但是这样的代码中这样代码并不好书写。我们就需要一个帮助我们转换代码的库。这可以参考 <a href="https://link.segmentfault.com/?enc=RU27btdDRXm0YFM8nY1kQg%3D%3D.faYSte5d2TItnLHbqMuTHNfOW7swK2v%2BBi%2BNuJ9g2mUhx93S3KnTE9ymtAuKn6oKEu5XJJpNqTEbviKxYzgXsEQBDyub2r4%2FQw8PvNBo8p%2Bf6A21wvVXv%2Fhv72fqEuJZCHriRYm2yjCHh13Zi7VlsQ%3D%3D" rel="nofollow">Babel 插件手册</a> 以及 <a href="https://link.segmentfault.com/?enc=mdvfsRtVfs%2BcIMsFSXo6Wg%3D%3D.HXxkET%2BEPutBuKe4ghputJfD0MX7HoH1vxJPi0Lqdr%2F78m4yl4Bm7GPZHutb0TF2A%2Bsn7Z2eYhUIhsIyzli93Q%3D%3D" rel="nofollow">简单实现项目代码按需加载</a> 来实现我们的需求。</p>
<h3>需要库大量被使用情况</h3>
<p>如果我们的库被当前的项目大量使用了,按需加载其实就未必是最好的方法了,如果我们的服务器不是特别好的情况下我们可以使用 Webpack 的 externals 配置来优化项目的js。就简单的对 externals 配置简单说明一下。externals其实是在全局中的得到库文件。</p>
<pre><code> // 页面中使用 cdn,这样的话,我们就会在 window 中得到 jQuery
// 也就是 global.JQuery 浏览器中 global === window
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
</script>
// 在项目中导入 jquery 使用
import $ from 'jquery';
// 配置中 左边是 配置的 jquery 告诉 Webpack 不需要导入
// 配置中 右边是 配置的 JQuery 告诉 Webpack 记载 jquery 时候使用 global.JQuery
externals: {
jquery: 'jQuery'
}</code></pre>
<p>但是使用 externals 曾遇到这样的情况。我在使用 material-ui 组件库时候发现该库在全局导出的代码是 material-ui。 <br>也就是:</p>
<pre><code> externals: {
'@material-ui/core': 'material-ui'
}</code></pre>
<p>此时会发生导入错误,错误原因为: window.material-ui。 <br>本来我是想要引入material-ui,却 - 符号变为了减号。 <br>本来想要利用用 ['material-ui'] 来替换,却发现行不通: windows.['material-ui'] <br>解决方法:</p>
<pre><code> externals: {
'@material-ui/core': "window['material-ui']"
}</code></pre>
<p>因为 window 对象有自己引用自己,所以 window === window.window.window。所以代码为 window.window['material-ui']。可以参考 <a href="https://link.segmentfault.com/?enc=AUG4%2F81k6j8LlOzSwLfBiQ%3D%3D.oai5qyQfYzZ3sAx40oedDlPm%2F0GnuN7mHGwUQt4jMWBFQlKoOTjpUhECdU4cLiVbJXokOVIm8Oa9Ag89HnXvtQ%3D%3D" rel="nofollow">MDN Window.window</a></p>
<h2>上文中的性能优化方案依然可用</h2>
<h3>loadjs 动态加载</h3>
<p>在当前所需要 js 文件不需要大量使用同时需要的 js 文件是不需要开始时加载(如 React, React-Router 一类)的时候我们依然可以使用loadjs来加载(比如说 图标库一类,只在某一些页面使用)。</p>
<h3>合并多个小型 js</h3>
<p>对于在 HTTP2 中合并多个 小js文件未必好。因为在 HTTP2 中,HTTP 请求是廉价的,合并便不再显得有优势。</p>
<h3>BigPipe 方案</h3>
<p>当然了,BigPipe 方案不是针对单页面应用程序。而且对于前后端的技术要求较高,所以对于项目未必是最有效的方案。</p>
<h2>其他</h2>
<p>现如今也可以使用一些其他的方法。例如 Service Worker,Wasm 等一系列方案。不知道还有什么其他方法,也可以介绍给我。</p>
<h2>参考文档</h2>
<p><a href="https://link.segmentfault.com/?enc=tcGYM7YBGlHrp906nOMp%2Fw%3D%3D.jbtnj8fh82%2FQ0OIiD2PTWoAr6%2FrE55lWJwU7vgS9Ogtbr%2FoQHuCY6qYBV%2FGMpcV1XcUSaJRIvfEg97AQXskYJA%3D%3D" rel="nofollow">新版卖家中心 Bigpipe 实践(一)</a> <br><a href="https://link.segmentfault.com/?enc=UitCOccH3xwDbzluzsmGwA%3D%3D.v2JIbfwkI2K3nCJYZhMTY%2Fa06R7RzjYMnFYBpnPgVBtrRDwOPR5VKk93%2BdGzqURPn43aWVX8BvbNMQHeLIPxYA%3D%3D" rel="nofollow">新版卖家中心 Bigpipe 实践(二)</a> <br><a href="https://link.segmentfault.com/?enc=xwOo0AhxdAnZt4AMMbc69g%3D%3D.Tpa4RN%2BtuPWNiXpTHIZ8c9b89vJDMDTn9U%2FSDpxnxgItAZMjB7CSgOVQ%2FpjmHUKR5hzccZVnN7Ija6l0hpvP5NqKIgHxuOncC1BoEN%2FA3wv0n1vIljH9rBK3h4UODoZZn1o1oxe%2FZGn4ra%2FMx4ddiA%3D%3D" rel="nofollow">Babel 插件手册</a> <br><a href="https://link.segmentfault.com/?enc=rxUVAbd3rXXO30lunuQrUg%3D%3D.15cCIO9ydrAX7eShE739V9uAOeCIfIl8%2B0TpylYwSiDUO6sxO39MaEotb99qpiD%2FikieofxnQDWugFARn1co6Q%3D%3D" rel="nofollow">简单实现项目代码按需加载</a> <br><a href="https://link.segmentfault.com/?enc=I35oMI3AEsgJlFk4iG8ESw%3D%3D.dAOkNXNjBXtXo7zHk4XLJXRProR4mzSN9ms8Qf3LuluMHyVQiZar7z1KJPLbppK2I%2BiIcaBTInJzI4tfsQeZzg%3D%3D" rel="nofollow">MDN Window.window</a></p>
从 VantComponent 谈 小程序维护
https://segmentfault.com/a/1190000019003796
2019-04-27T01:09:45+08:00
2019-04-27T01:09:45+08:00
jump__jump
https://segmentfault.com/u/jump_and_jump
14
<p>在开发小程序的时候,我们总是期望用以往的技术规范和语法特点来书写当前的小程序,所以才会有各色的小程序框架,例如 mpvue、taro 等这些编译型框架。当然这些框架本身对于新开发的项目是有所帮助。而对于老项目,我们又想要利用 vue 的语法特性进行维护,又该如何呢? <br>在此我研究了一下youzan的 vant-weapp。而发现该项目中的组件是如此编写的。</p>
<pre><code>import { VantComponent } from '../common/component';
VantComponent({
mixins: [],
props: {
name: String,
size: String
},
// 可以使用 watch 来监控 props 变化
// 其实就是把properties中的observer提取出来
watch: {
name(newVal) {
...
},
// 可以直接使用字符串 代替函数调用
size: 'changeSize'
},
// 使用计算属性 来 获取数据,可以在 wxml直接使用
computed: {
bigSize() {
return this.data.size + 100
}
},
data: {
size: 0
},
methods: {
onClick() {
this.$emit('click');
},
changeSize(size) {
// 使用set
this.set(size)
}
},
// 对应小程序组件 created 周期
beforeCreate() {},
// 对应小程序组件 attached 周期
created() {},
// 对应小程序组件 ready 周期
mounted() {},
// 对应小程序组件 detached 周期
destroyed: {}
});</code></pre>
<p>居然发现该组件写法整体上类似于 Vue 语法。而本身却没有任何编译。看来问题是出在了导入的 VantComponet 这个方法上。下面我们开始详细介绍一下如何利用 VantComponet 来对老项目进行维护。</p>
<h2>TLDR (不多废话,先说结论)</h2>
<p>小程序组件写法这里就不再介绍。这里我们给出利用 VantComponent 写 Page 的代码风格。</p>
<pre><code>import { VantComponent } from '../common/component';
VantComponent({
mixins: [],
props: {
a: String,
b: Number
},
// 在页面这里 watch 基本上是没有作用了,因为只做了props 变化的watch,page不会出现 props 变化
// 后面会详细说明为何
watch: {},
// 计算属性仍旧可用
computed: {
d() {
return c++
}
},
methods: {
onLoad() {}
},
created() {},
// 其他组件生命周期
})</code></pre>
<p>这里你可能感到疑惑,VantComponet 不是对组件 Component 生效的吗?怎么会对页面 Page 生效呢。事实上,我们是可以使用组件来构造小程序页面的。 <br>在官方文档中,我们可以看到 <a href="https://link.segmentfault.com/?enc=q42K73yXWrj4EVrvcBHAgQ%3D%3D.7A3kCBUkzuPtvrH%2BsILtMj3vfusH4MH%2BPK8PLjGr7pkkn7f%2FxchXUH7HJkhyOIsJ7vqvCQ%2FvZsmxy80lp6sxjUQMqg6qu%2Fufmz1HKl75Foe33arKFDzltwcPyWNPJFyo" rel="nofollow">使用 Component 构造器构造页面</a> <br>事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造,拥有与普通组件一样的定义段与实例方法。代码编写如下:</p>
<pre><code>Component({
// 可以使用组件的 behaviors 机制,虽然 React 觉得 mixins 并不是一个很好的方案
// 但是在某种程度该方案的确可以复用相同的逻辑代码
behaviors: [myBehavior],
// 对应于page的options,与此本身是有类型的,而从options 取得数据均为 string类型
// 访问 页面 /pages/index/index?paramA=123&paramB=xyz
// 如果声明有属性 paramA 或 paramB ,则它们会被赋值为 123 或 xyz,而不是 string类型
properties: {
paramA: Number,
paramB: String,
},
methods: {
// onLoad 不需要 option
// 但是页面级别的生命周期却只能写道 methods中来
onLoad() {
this.data.paramA // 页面参数 paramA 的值 123
this.data.paramB // 页面参数 paramB 的值 ’xyz’
}
}
})</code></pre>
<p>那么组件的生命周期和页面的生命周期又是怎么对应的呢。经过一番测试,得出结果为: (为了简便。只会列出 重要的的生命周期)</p>
<pre><code>// 组件实例被创建 到 组件实例进入页面节点树
component created -> component attched ->
// 页面页面加载 到 组件在视图层布局完成
page onLoad -> component ready ->
// 页面卸载 到 组件实例被从页面节点树移除
page OnUnload -> component detached</code></pre>
<p>当然 我们重点不是在 onload 和 onunload 中间的状态,因为中间状态的时候,我们可以在页面中使用页面生命周期来操作更好。 <br>某些时候我们的一些初始化代码不应该放在 onload 里面,我们可以考虑放在 component create 进行操作,甚至可以利用 behaviors 来复用初始化代码。 <br>某种方面来说,如果不需要 Vue 风格,我们在老项目中直接利用 Component 代替 Page 也不失为一个不错的维护方案。毕竟官方标准,不用担心其他一系列后续问题。</p>
<h2>VantComponent 源码解析</h2>
<h3>VantComponent</h3>
<p>此时,我们对 VantComponent 开始进行解析</p>
<pre><code>// 赋值,根据 map 的 key 和 value 来进行操作
function mapKeys(source: object, target: object, map: object) {
Object.keys(map).forEach(key => {
if (source[key]) {
// 目标对象 的 map[key] 对应 源数据对象的 key
target[map[key]] = source[key];
}
});
}
// ts代码,也就是 泛型
function VantComponent<Data, Props, Watch, Methods, Computed>(
vantOptions: VantComponentOptions<
Data,
Props,
Watch,
Methods,
Computed,
CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
> = {}
): void {
const options: any = {};
// 用function 来拷贝 新的数据,也就是我们可以用的 Vue 风格
mapKeys(vantOptions, options, {
data: 'data',
props: 'properties',
mixins: 'behaviors',
methods: 'methods',
beforeCreate: 'created',
created: 'attached',
mounted: 'ready',
relations: 'relations',
destroyed: 'detached',
classes: 'externalClasses'
});
// 对组件间关系进行编辑,但是page不需要,可以删除
const { relation } = vantOptions;
if (relation) {
options.relations = Object.assign(options.relations || {}, {
[`../${relation.name}/index`]: relation
});
}
// 对组件默认添加 externalClasses,但是page不需要,可以删除
// add default externalClasses
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('custom-class');
// 对组件默认添加 basic,封装了 $emit 和小程序节点查询方法,可以删除
// add default behaviors
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// map field to form-field behavior
// 默认添加 内置 behavior wx://form-field
// 它使得这个自定义组件有类似于表单控件的行为。
// 可以研究下文给出的 内置behaviors
if (vantOptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
// 添加组件默认配置,多slot
options.options = {
multipleSlots: true,// 在组件定义时的选项中启用多slot支持
// 如果这个 Component 构造器用于构造页面 ,则默认值为 shared
// 组件的apply-shared,可以研究下文给出的 组件样式隔离
addGlobalClass: true
};
// 监控 vantOptions
observe(vantOptions, options);
// 把当前重新配置的options 放入Component
Component(options);
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=Y7rEMDffwA1paS8Sitrbxw%3D%3D.H%2FVzXJ6Vyb6I5wxjABCMWSz3HhnZ%2FRELRyveDyt%2FlTRPrywn0tD1xxutGLjV%2FJVbgBaGtudWRMyP3O3p3zJ0peU504phv6HuNyZyfyVkS3dNowkX78IK5uvBtHxzysqne3GWy4MsRd%2B98MtO1hAFKb%2Fm165EH3uh26muDKNVXMk%3D" rel="nofollow">内置behaviors</a> <br><a href="https://link.segmentfault.com/?enc=8BmYbe0%2FQq4BnIgoEBvdrA%3D%3D.x5%2Bv23wSZQVKUsHN6%2Bupj%2FMNBL4I1PWjJB8d2d9dKPNgk4SCu7uM1NWEuiIOxpQBkiYq%2FXWPEC6M%2BCe7gnBic0VDBMIBu7sv6v%2FqIPkafcONNVEek7rAN1V3JA1HfeSzV%2BrkX7idsMiv8roODd76ulDllPgfXOVq5zn%2FgfumdVYpLis5qHk1SPIbM8VM8KuOzsyGOoIUdujbAaq%2FUG63%2Fw%3D%3D" rel="nofollow">组件样式隔离</a></p>
<h3>basic behaviors</h3>
<p>刚刚我们谈到 basic behaviors,代码如下所示</p>
<pre><code>export const basic = Behavior({
methods: {
// 调用 $emit组件 实际上是使用了 triggerEvent
$emit() {
this.triggerEvent.apply(this, arguments);
},
// 封装 程序节点查询
getRect(selector: string, all: boolean) {
return new Promise(resolve => {
wx.createSelectorQuery()
.in(this)[all ? 'selectAll' : 'select'](selector)
.boundingClientRect(rect => {
if (all && Array.isArray(rect) && rect.length) {
resolve(rect);
}
if (!all && rect) {
resolve(rect);
}
})
.exec();
});
}
}
});</code></pre>
<h3>observe</h3>
<p>小程序 watch 和 computed的 代码解析</p>
<pre><code>export function observe(vantOptions, options) {
// 从传入的 option中得到 watch computed
const { watch, computed } = vantOptions;
// 添加 behavior
options.behaviors.push(behavior);
/// 如果有 watch 对象
if (watch) {
const props = options.properties || {};
// 例如:
// props: {
// a: String
// },
// watch: {
// a(val) {
// // 每次val变化时候打印
// consol.log(val)
// }
}
Object.keys(watch).forEach(key => {
// watch只会对prop中的数据进行 监视
if (key in props) {
let prop = props[key];
if (prop === null || !('type' in prop)) {
prop = { type: prop };
}
// prop的observer被watch赋值,也就是小程序组件本身的功能。
prop.observer = watch[key];
// 把当前的key 放入prop
props[key] = prop;
}
});
// 经过此方法
// props: {
// a: {
// type: String,
// observer: (val) {
// console.log(val)
// }
// }
// }
options.properties = props;
}
// 对计算属性进行封装
if (computed) {
options.methods = options.methods || {};
options.methods.$options = () => vantOptions;
if (options.properties) {
// 监视props,如果props发生改变,计算属性本身也要变
observeProps(options.properties);
}
}
}</code></pre>
<h3>observeProps</h3>
<p>现在剩下的也就是 observeProps 以及 behavior 两个文件了,这两个都是为了计算属性而生成的,这里我们先解释 observeProps 代码</p>
<pre><code>export function observeProps(props) {
if (!props) {
return;
}
Object.keys(props).forEach(key => {
let prop = props[key];
if (prop === null || !('type' in prop)) {
prop = { type: prop };
}
// 保存之前的 observer,也就是上一个代码生成的prop
let { observer } = prop;
prop.observer = function() {
if (observer) {
if (typeof observer === 'string') {
observer = this[observer];
}
// 调用之前保存的 observer
observer.apply(this, arguments);
}
// 在发生改变的时候调用一次 set 来重置计算属性
this.set();
};
// 把修改的props 赋值回去
props[key] = prop;
});
}</code></pre>
<h3>behavior</h3>
<p>最终 behavior,也就算 computed 实现机制</p>
<pre><code>
// 异步调用 setData
function setAsync(context: Weapp.Component, data: object) {
return new Promise(resolve => {
context.setData(data, resolve);
});
};
export const behavior = Behavior({
created() {
if (!this.$options) {
return;
}
// 缓存
const cache = {};
const { computed } = this.$options();
const keys = Object.keys(computed);
this.calcComputed = () => {
// 需要更新的数据
const needUpdate = {};
keys.forEach(key => {
const value = computed[key].call(this);
// 缓存数据不等当前计算数值
if (cache[key] !== value) {
cache[key] = needUpdate[key] = value;
}
});
// 返回需要的更新的 computed
return needUpdate;
};
},
attached() {
// 在 attached 周期 调用一次,算出当前的computed数值
this.set();
},
methods: {
// set data and set computed data
// set可以使用callback 和 then
set(data: object, callback: Function) {
const stack = [];
// set时候放入数据
if (data) {
stack.push(setAsync(this, data));
}
if (this.calcComputed) {
// 有计算属性,同样也放入 stack中,但是每次set都会调用一次,props改变也会调用
stack.push(setAsync(this, this.calcComputed()));
}
return Promise.all(stack).then(res => {
// 所有 data以及计算属性都完成后调用callback
if (callback && typeof callback === 'function') {
callback.call(this);
}
return res;
});
}
}
});</code></pre>
<h2>写在后面</h2>
<ul>
<li>js 是一门灵活的语言(手动滑稽)</li>
<li>本身 小程序 Component 在 小程序 Page 之后,就要比Page 更加成熟好用,有时候新的方案往往藏在文档之中,每次多看几遍文档绝不是没有意义的。</li>
<li>小程序版本 版本2.6.1 Component 目前已经实现了 observers,可以监听 props data <a href="https://link.segmentfault.com/?enc=xzbilwkTS5%2FdAK5nQ5I8VA%3D%3D.AbqUMmsCkkPlHt529oRomhgyLCtNIAa0xDrzDRbVQDC0gernUIet%2BRXJHCMpkio5%2BkoJDm%2B2RkgPv4o3SL3CtTiZO7HPNf%2FGX5FqjUSdHUkXJs1UUfazbaFwtOH%2FmU7k" rel="nofollow">数据监听器</a>,目前 VantComponent没有实现,当然本身而言,Page 不需要对 prop 进行监听,因为进入页面压根不会变,而data变化本身就无需监听,直接调用函数即可,所以对page而言,observers 可有可无。</li>
<li>该方案也只是对 js 代码上有vue的风格,并没在 template 以及 style 做其他文章。</li>
<li>该方案性能一定是有所缺失的,因为computed是每次set都会进行计算,而并非根据set 的 data 来进行操作,在删减之后我认为本身是可以接受。如果本身对于vue的语法特性需求不高,可以直接利用 Component 来编写 Page,选择不同的解决方案实质上是需要权衡各种利弊。如果本身是有其他要求或者新的项目,仍旧推荐使用新技术,如果本身是已有项目并且需要维护的,同时又想拥有 Vue 特性。可以使用该方案,因为代码本身较少,而且本身也可以基于自身需求修改。</li>
<li>同时,vant-weapp是一个非常不错的项目,推荐各位可以去查看以及star。</li>
</ul>