跳到主要内容

· 阅读需 4 分钟
超级可爱加菲

JS 异步编程进化史

JS 异步编程事件顺序如下:

  1. Callback 回调函数
  2. Promise 链式调用
  3. Generator / yield
  4. async / await
  5. 由于 JS 事件处理是异步的。在 JS 中, 事件监听会被添加到事件队列中,等待主线程处理。通常我们会将事件监听作为 JS 算入异步编程中的一种。
  6. 作为一种拓展模式,发布/订阅模式,是属于设计模式中的行为模式。也常常被用来做异步编程。

Callback

Callback(回调函数)本质就是被: 作为实参传入另一个函数,并在外部函数内被调用,用以来完成某些任务的函数,成为会调函数。

function greeting(name) {
alert("Hello " + name);
}

function processUserInput(callback) {
setTimeout(() => {
var name = prompt("Please enter your name.");
}, 1000);
callback(name);
}

processUserInput(greeting);

Callback Hell: 最大的问题就是使用复杂嵌套进行回调会导致,每个回调都在接受参数,该参数是上一个回调的返回。这种结构类似于一个金字塔,难以阅读和维护。

// Example of Callback Hell.
const Axios = require("axios").default;

const USERS_URL = "https://jsonplaceholder.typicode.com/users";
const POSTS_URL = "https://jsonplaceholder.typicode.com/posts";
const COMMENTS_URL = "https://jsonplaceholder.typicode.com/comments";

function getFunc(URL, cb) {
Axios.get(`${URL}`).then((response) => {
const { data } = response;
cb(data);
});
}

function getCommentByUser(username) {
getFunc(`${USERS_URL}?username=${username}`, (user) => {
getFunc(`${POSTS_URL}?userId=${user[0].id}`, (posts) => {
getFunc(`${COMMENTS_URL}?postId=${posts[0].id}`, (comments) => {
const firstComment = comments[0];
console.log(firstComment);
});
});
});
}

getCommentByUser("Samantha");

Promise

一个 Promise 是一个代理,它代表一个在创建 promise 时不一定已知的值。它允许你将处理程序与异步操作的最终成功值或失败原因关联起来。这使得异步方法可以像同步方法一样返回值:异步方法不会立即返回最终值,而是返回一个 promise,以便在将来的某个时间点提供该值。

Promise 必然存在的三种状态:

  • pending: 初始状态
  • fulfilled: 操作完成
  • rejected: 操作失败

Alt text

function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
}

myAsyncFunction("/xxx")
.then((res) => successFunc(res))
.catch((err) => errorFunc(err));

Promise 链式调用

myPromise
.then((value) => `${value} and bar`)
.then((value) => `${value} and bar again`)
.then((value) => `${value} and again`)
.then((value) => `${value} and again`)
.then((value) => {
console.log(value);
})
.catch((err) => {
console.error(err);
});

Generator / yield

Generator 函数(生成器函数): 它允许自定义一个非连续执行函数作为迭代算法。

最初调用时,生成器函数不执行任何代码,而是返回一种称为生成器的特殊迭代器。通过调用 next()方法消耗生成器时,生成器函数将执行,直至遇到 yield 关键字。

function* fibonacci() {
let current = 0;
let next = 1;
while (true) {
const reset = yield current;
[current, next] = [next, next + current];
if (reset) {
current = 0;
next = 1;
}
}
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
function* fetchUsers() {
yield fetch("https://jsonplaceholder.typicode.com/users")
.then((resp) => resp.json())
.then((users) => {
return users;
});
}

const usersIt = fetchUsers();
usersIt.next().value.then((resp) => console.log(resp));

async / await

function resolveAfter2Seconds() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}

async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// Expected output: "resolved"
}

asyncCall();

· 阅读需 5 分钟
超级可爱加菲

为什么需要自定义hooks

  1. 在创建工具类的时候,我们可能遇到需要在工具类中创建调用一些其他的hooks。但是由于使用的是常规JavaScript创建的工具类,此时便会出现Invalid hook call的错误。这是因为React hooks只能在React Component或另一个hook中被调用。
  2. 在React Component中,页面有许多逻辑代码包含多个hook。因为在最开始我们知道hooks只能在hooks被调用,所有我们这时最好的办法便是创建一个hook用来抽离这些逻辑代码。这是一段常见代码:
// react component
const TestPage = () => {
const [data, setDate] = useState();
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const fetchData = async (params) => {
setLoading(true);
setError("");
try {
const res = await fetch(params);
setDate(res.data);
} catch (e) {
setError(e.error);
}
setLoading(false);
};
useEffect(() => {
fetchData();

return () => {
setLoading(false);
setError("");
setDate();
};
}, []);

const handler = (params) => {
fetchData(params);
};
return (
<div>
{data}
<button onClick={handler}></button>
</div>
);
};

我们可以对这个React component进行简单的抽离:

// customize hook -- useFetchData
const useFetchData = () => {
const [data, setDate] = useState();
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const fetchData = async (params) => {
setLoading(true);
setError("");
try {
const res = await fetch(params);
setDate(res.data);
} catch (e) {
setError(e.error);
}
setLoading(false);
};
useEffect(() => {
fetchData();

return () => {
setLoading(false);
setError("");
setDate();
};
}, []);

const handler = (params) => {
fetchData(params);
};
return { data, error, loading, handler };
};

在React component中调用这个hook

// react component
const TestPage = () => {
const { data, error, loading, handler } = useFetchData();
return (
<div>
{data}
<button onClick={handler}></button>
</div>
);
};

通过这种方式我们可以将React项目中的逻辑代码提取出来,或者就是当前component/hook逻辑复杂,代码量大,不便于理解,可以复用。我们都可以进行抽取。

创建规则

命名

hook必须以小写字母开头,use后跟大写字母逻辑 例如useOnLineStatus。当此函数不调用任何hooks(此时就是普通函数),避免使用use前缀。

理想情况下自定义hook,名称需要清晰。通过名字能猜出作用,并知道入参和返回值。因为当你无法选择一个清晰的名称意味着你的组建过于耦合,并未进行提取。

共享状态逻辑

自定义hook允许共享状态逻辑,但不能共享状态本身。 对于hook的每次调用都完全独立于对同一hook的所有其他调用。

import { useFormInput } from "./useFormInput.js";

export default function Form() {
const firstNameProps = useFormInput("Mary");
const lastNameProps = useFormInput("Poppins");

return (
<>
<label>
First name:
<input {...firstNameProps} />
</label>
<label>
Last name:
<input {...lastNameProps} />
</label>
<p>
<b>
Good morning, {firstNameProps.value} {lastNameProps.value}.
</b>
</p>
</>
);
}
import { useState } from "react";

export function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);

function handleChange(e) {
setValue(e.target.value);
}

const inputProps = {
value: value,
onChange: handleChange,
};

return inputProps;
}

hook之间传递reactive values

自定义hook的代码将在每次重新渲染组建期间运行。需要像函数一样,自定义hook需要保证纯粹。

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
};
const connection = createConnection(options);
connection.connect();
connection.on("message", (msg) => {
showNotification("New message: " + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
});

return (
<>
<label>
Server URL:
<input
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}

传递handler到自定义hook

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState("https://localhost:1234");

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification("New message: " + msg);
},
});

return (
<>
<label>
Server URL:
<input
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId,
};
const connection = createConnection(options);
connection.connect();
connection.on("message", (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

· 阅读需 5 分钟
超级可爱加菲

编程范型编程范式程序设计法(Programming paradigm),是指软件工程重的一类典型的编程风格。常见的编程范型有:函数式编程1指令编程过程编程面向对象编程等等

纯函数

函数只会有输入输出,不应有任何额外操作。例如:操作全局变量,对全局实例化对象进行操作等等。

var name: string = "John";
function greet() {
console.log("Hi, I'm " + name); // Not Pure
}

function greet(name) {
return "Hi, I'm " + name; // Pure
}

一等公民

函数是'一等公民'(First-class Function)

一等公民”,头等函数(first-class function第一级函数)是指在程序设计语言中,函数被当作头等公民2。这就意味着,函数可以作为别的函数的参数、函数的返回值,赋值给变量或存储数据结构中

由于在 javascript 中函数是一等公民因此具备:

  • 内置的属性和方法
  • 可以添加属性和方法
  • 可以为参数传递从其他函数返回
  • 可以分配给变量,数组元素和其他对象

内置属性

//Assign a function to a variable originalFunc
const originalFunc = (num) => {
return num + 2;
};

//Re-assign the function to a new variable newFunc
const newFunc = originalFunc;

//Access the function's name property
newFunc.name; //'originalFunc'

//Return the function's body as a string
newFunc.toString(); //'(num) => { return num + 2 }'

//Add our own isMathFunction property to the function
newFunc.isMathFunction = true;

高阶函数:函数作为实参传递

const isEven = (n) => n % 2 === 0;

const judgFunc = (num, callback) => {
const flag = callback(num);
return `The number ${num} is an even number: ${isNumEven}.`;
};

高阶函数:返回函数作为结

function makeAdjectifier(adjective) {
return function (string) {
return adjective + " " + string;
};
}

var coolifier = makeAdjectifier("coll");
coolifier("conference"); // "cool conference"

避免迭代

使用 map、 reduce、filter 等,取代 for、 while 进行迭代。

  • reduce 函数求和
  • filter 函数过滤
  • map 函数每个元素执行回调函数并返回由回调函数返回值组成的新数组
  • forEach 函数按序对数组每个元素执行回调函数

避免数据变异

var rooms = ["H1", "H2", "H3"];
var newRooms = rooms.map(function (rm) {
if (rm == "H3") {
return "H4";
} else {
return rm;
}
});

// newRooms => ["H1", "H2", "H4"]
// rooms => ["H1", "H2", "H3"]

当然随着工程变大会引发效率问题,为进一步提升可以使用一些持久数据结构进行优化

优势

函数式编程与其说是一种书写代码规范,不如说是一种思维方式。在面对一个问题,会使用特定思考方式解决问题。当然没有绝对的孰优孰劣,具体需要看应用场景。

  • 代码简洁 - 功能抽离,减少重复代码
  • 易于理解 - 偏向于自然语言
  • 方便维护与拓展 - 函数式编程只要保证输入输出不变,内部实现与外部无关
  • 更易于并发 - 函数式编程不修改变量所以不需要考虑"死锁问题"

Footnotes

  1. 函数式编程,或称函数程序设计泛函编程(英语:Functional programming),是一种编程范型,它将电脑运算视为函数运算,并且避免使用程序状态以及可变物件。

  2. 头等公民:指称支持其他实体通常能获得的所有运算的实体。包括不限于拥有特定的基本权利,都可作为函数的实参,作为函数的结果返回,是赋值语句的主体。

标签:

· 阅读需 4 分钟
超级可爱加菲

引言

此博客作为一份如何在 React 中使用 bing maps api 参考,其他地图 api 原理上也可参考此教程。

前期准备

首先需要在bing mpas dev center中,具体在 My accout 下 My keys 中创建 new key。

bing map create key

Application URL 作为可选项,若填写 url 相当于白名单。非此域名下的网站则无法通过此 key 获取对应的一些 api 操作。

创建后 key details 下面 key 默认隐藏点击 Show key 便可拿到所需要的 bing maps api 中的 key。

bing maps key detail

开发者文档:

加载 bing maps api script

该教程以 bing maps v8 组件为例子

首先需要在 React 中有两种方式引入第三方引入第三方 js 资源:

  • 通常 React 项目中直接在 index.html 中通过 script 标签引入
<script
type="text/javascript"
src="http://www.bing.com/api/maps/mapcontrol?callback=GetMap&branch=experimental&key=[YOUR_BING_MAPS_KEY]"
async
defer></script>
  • 由于某些情况该工程不包含 index.html 或者其他因素则需要 dom 操作。需要注意以下事项。
    • bing map script 资源推荐全局引用,若在对应组件再加载 scipt 资源此时会极大影响体验。操作起来也会更为繁琐(需要留意是在地图资源加载完成后才进行初始化地图;由于是组件挂载 bing maps script 资源,每次组件卸载 scipt 资源也会被卸载,意味着每次组件加载时都需要获取一遍 script 资源)
    • 推荐 dom 操作时,在 body 中插入 scipt 资源

以下是我的片段代码(自定义 hook)

/**
* useLoadMap hook
* @description 加载bing map script资源
* @returns {void}
*/
const useLoadMap = () => {
useEffect(() => {
if (!window.Microsoft) {
const script = document.createElement("script");
script.src = `https://www.bing.com/api/maps/mapcontrol?key=${BING_KEY}`;
script.async = true;
script.defer = true;
script.type = "text/javascript";
document.body.appendChild(script);
}
}, [window.Microsoft]);
};

export default useLoadMap;

引用

// ...app

// ...
useLoadMap();
// ...

bing maps script 参数

初始化 bing map

简易获取当前用户位置并在地图图钉显示

import { BING_KEY, PIN } from "../utils";

const useMap = () => {
const mapRef = useRef(null);

/**
* Init map
*/
const getMap = () => {
// Initialize the map
const map = new window.Microsoft.Maps.Map("#myMap", {
credentials: BING_KEY,
enableCORS: false,
ShowNavigationBar: false,
});
mapRef.current = map;

// Load the spatial math module
window.Microsoft.Maps.loadModule("Microsoft.Maps.SpatialMath", function () {
// Request the user's location
navigator.geolocation.getCurrentPosition(function (position) {
var loc = new window.Microsoft.Maps.Location(
position.coords.latitude,
position.coords.longitude
);

// Create an accuracy circle
var path = window.Microsoft.Maps.SpatialMath.getRegularPolygon(
loc,
position.coords.accuracy,
36,
window.Microsoft.Maps.SpatialMath.Meters
);
var poly = new window.Microsoft.Maps.Polygon(path);
mapRef.current.entities.push(poly);
});
});
};

return [getMap, mapRef];
};

export default useMap;

在对应组件调用

// ...
const [getMap, mapRef] = useMap();

// ...
useLayoutEffect(() => {
// init map
if (window.Microsoft) {
// delay to wait for map loaded
setTimeout(() => getMap(), 100);
}
}, [window.Microsoft]);

// ...

模块

使用 loadModule 可加载不同模块

window.Microsoft.Maps.loadModule("Microsoft.Maps.SpatialMath", function () {
// todo
});

图钉

通过 Maps Pushpin 实例化图钉,在创建的 mapRef entities 中 push() 可对其进行增加 pop()移除

var pin = new window.Microsoft.Maps.Pushpin(
{
latitude: 47.58106995,
longitude: -122.34111023,
},
{
icon: PIN,
draggable: false,
}
);

mapRef.current.entities.push([location, pin]);

事件

在初始化的时候调用 Events addHandler 为其添加不同的事件

const getMap = () => {
window.Microsoft.Maps.Events.addHandler(map, "mouseup", function (e) {
// todo mouse up handler
});
};

· 阅读需 9 分钟
超级可爱加菲

CSS Modules vs CSS-in-JS vs Atomic CSS

在这篇博客中我想介绍在前端中不同的使用 CSS 的方法。绝大多数情况下,都是决定基于开发者的偏好,但是我们也需要考虑项目类型,开发人员,团队经验和工作流程。例如,后端或全栈在 CSS 开发方面没有太多经验。

我将围绕在 React 中分别介绍三种流行方案:模块化 CSS(CSS Module)、CSS-in-JS、原子化 CSS(Atomic CSS)。由于业务场景多种多样,我不可能告诉你什么是最佳的方案,但是我想带你从不同的视角了解他们,希望有助于你选择。

CSS Module

在我最开始使用 React 开发的时候,最流行的解决方案是 Sass/Less 配合 BEM 命名方法。CSS 最初并不是为了 Single Application Page(SPA)单独设计的,不支持变量、嵌套。但是在那个时候 Sass/Less 是当时最好的选择。但是自从 SPA 流行起来,我们对实现 Web 程序样式的要求也发生了变化。

当我的一个 React 项目中需要进行大量布局修改的时候,我的 Less 代码将会很混乱。即使在今天,我在使用 Less 进行样式设置的时候依然有同样的问题。将 Less 和 BEM 或类似方法结合一起使用的时候,开发人员需要有 CSS 经验,并遵守相应规范以保持样式布局实现的可维护性和一致性。

BEM 规范

.block {
}

.block__element {
}

.block--modifier {
}

每一个块(block)名应该有一个命名空间(前缀)

  • block 代表了更高级别的抽象或组件。
  • block__element 代表 .block 的后代,用于形成一个完整的 .block 的整体。
  • block--modifier 代表 .block 的不同状态或不同版本。
  • 使用两个连字符和下划线而不是一个,是为了让你自己的块可以用单个连字符来界定。

React、Vue 项目中 CSS 常见的问题之一是组件导入 css 代码被全局引用,导致响应冲突 CSS Modules。使用命名空间和访问符进行限制。

Less 示例代码:

@width: 10px;

.bordered {
border-top: dotted 1px black;
border-bottom: solid 2px black;
}

#bundle() {
.button {
display: block;
border: 1px solid black;
background-color: grey;
&:hover {
background-color: white;
}
}
.tab {
.bordered();
}
.citation {
width: @width;
}
}

如果我们希望把 .button 类混合到 #header a 中,我们可以这样做:

#header a {
color: orange;
#bundle.button(); // 还可以书写为 #bundle > .button 形式
}

我将 Less 配合 BEM 使用归入 CSS Modules 的原因,他们本质都是在解决全局命名空间的相同问题。

这种方法仍然有效,但是如果开发人员没有相关 CSS 开发经验,很容易出各种问题。在简单项目中维护 CSS 也相对容易,即使大公司:阿里,Facebook...的工程师也在维护他们的 CSS 代码库。

使用 CSS module 作为样式开发方法,建议满足以下条件:

  1. 使用 CSS 编码规范
  2. 了解 BEM 规范
  3. 开发团队具备 CSS 开发经验

CSS in JS

后来,出现一种新的方法 CSS-in-JS,现在依然流行。我第一次使用的时候,在没有任何学习曲线的情况下,便能写出易于维护的代码。即使没有任何前端开发经验的后端开发人员也可以实现可维护的布局。这种方式的好处之一便是通过 props 实现动态样式,并且易于 Typescript 集成。

import styled from "@emotion/styled/macro";

import { theme } from "../../../styles/theme";

const { sizes, colors } = theme;

type Side = "bottom" | "left" | "right" | "top";

interface ArrowProps {
arrowX: number;
arrowY: number;
staticSide: Side;
}

const gapFromAnchorElement = 4;

export const Arrow = styled.div<ArrowProps>`
position: absolute;
pointer-events: none;

left: ${({ arrowX }) => `${arrowX}px`};
top: ${({ arrowY }) => `${arrowY}px`};
${({ staticSide }) => ({
[staticSide]: `-${sizes.gap - gapFromAnchorElement - 2}px`,
...(staticSide === "left" || staticSide === "right"
? {
borderTop: `${sizes.gap - gapFromAnchorElement}px solid transparent`,
borderBottom: `${
sizes.gap - gapFromAnchorElement
}px solid transparent`,
}
: {}),
...(staticSide === "left"
? {
borderRight: `${sizes.gap - gapFromAnchorElement}px solid ${
colors.White
};`,
}
: {}),
...(staticSide === "right"
? {
borderLeft: `${sizes.gap - gapFromAnchorElement}px solid ${
colors.White
};`,
}
: {}),
})};
`;

此方法存在两个问题:

  1. 即使再简单的样式也需要很多重复的模板代码。这种方法会导致生产率下降
  2. SEO 优化不是很好的选择。大多数 CSS-in-JS 库在运行时将生成的样式表注入倒文档头部的末尾。他们无法将样式提取到 css 文件中。无法缓存 CSS。有人试图用Linaria来解决这个问题。但无法进一步优化,如内联关键 CSS 和 lazy loading。由于这个问题, CSS-in-JS 对于需要优化 First Contentful Paint 和 SEO 性能指标的项目来说,并不是明智的选择。例如在 E-Commerce Next.js 项目中使用 CSS-in-JS。

另外一方面,CSS-in-JS 对于需要深度 Typescript 集成且 SEO 性能指标无关紧要的项目来说是不错的选择。企业或者面向数据的项目非常合适,因为它们需要 Typescript 集成,而且不关心缓存 CSS 和 SEO 性能指标,同时也适用于 React Native。

简单的 CSS-in-JS 组件示例(emotion js)

import styled from "@emotion/styled";

const Button = styled("button")`
padding: 20px;
background-color: ${(props) => props.theme.someLibProperty};
border-radius: 3px;
`;

export default Button;
import "@emotion/react";

declare module "@emotion/react" {
export interface Theme extends Record<string, any> {}
}

原子化 CSS

然后,又出现了一个非常流行的方案,称为原子化。 样式开发维护非常容易,且无需任何学习曲线。与 CSS-in-JS 相比,它的优点是显著提升了生产力。缺点也很明显,代码可读性较差,这也是许多开发人员不喜欢这种方法的关键原因。

import { ICard, SearchItemStatus } from "../SearchResults";

export function ResultCard({
title,
snippet,
url,
status,
isFocused,
}: ICard & {
isFocused: boolean;
}) {
const cardColor =
status === SearchItemStatus.ACCEPTED
? "bg-green-100 border-green-300"
: status === SearchItemStatus.REFUSED
? "bg-yellow-100 border-yellow-300"
: status === SearchItemStatus.BANNED
? "bg-red-100 border-red-300"
: "bg-white border-gray-200";

return (
<div
className={`flex justify-start items-center ${cardColor} rounded-md border py-7 px-7 ${
isFocused ? "outline-blue" : ""
}`}>
<div className="flex flex-col items-start">
<div className="flex items-center mb-1 text-sm text-left">
<a href={url} target="_blank" rel="noreferrer">
{url}
</a>
</div>
<div className="mb-1 text-lg text-blue-700">{title}</div>
<p className="text-sm text-left">{snippet}</p>
</div>
</div>
);
}

最流行的原子化 CSS 框架是 Tailwind,它有预配置涉及系统和主题配置,可根据需求进行自定义。

结论

在这篇博客中,我讲了在项目中使用 CSS Modules、CSS-in-JS 和原子化 CSS 的优缺点。如果在 React 中需要配合 Typescript 并在项目中需要着重需要定义组件特定类型。甚至更进一步在 React Native 项目中,CSS-in-JS 就是最佳选择。 但是如果在 CSS Module 和原子化 CSS 之前做选择就需要多方面考虑了。需要从团队 CSS 开发经验,以及可以从原子化 CSS 框架(例如 Tailwind 中)受益等多方面考虑。