Skip to main content

· 4 min read
Jonas Lang

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 min read
Jonas Lang

为什么需要自定义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]);
}

· 4 min read
Jonas Lang

Programming paradigm refers to a typical programming style in software engineering.Common programming paradigms are: functional programming1, instruction programming, procedural programming, object-oriented programming, etc..

pure function

Functions only have input and output, and should not have any additional operations. For example: operate global variables, operate on global instantiated objects, and so on.

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

first-class function means that in a programming language, functions are treated as first-class citizens2.This means that functions can be used as parameters of other functions, return values of functions, assigned to variables or stored in data structures

Since functions are first-class citizens in javascript, they have:

  • Built-in properties and methods
  • properties and methods can be added
  • can be returned from other functions for parameter passing
  • can be assigned to variables, array elements and other objects

properties

//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;

Higher-order functions: functions are passed as arguments

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

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

Higher-order functions: return functions as knots

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

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

avoid iteration

Use map, reduce, filter, etc. instead of for and while for iteration.

  • reduce function summation
  • filter function filtering
  • The map function executes the callback function for each element and returns a new array composed of the return value of the callback function
  • The forEach function executes the callback function on each element of the array in sequence

Avoid data mutation

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"]

Of course, as the project becomes more efficient, it will cause efficiency problems. For further improvement, some persistent data structures can be used for optimization.

Advantage

Functional programming is not so much a way of writing code as it is a way of thinking. When faced with a problem, use a specific way of thinking to solve the problem. Of course, there is no absolute superiority or inferiority, depending on the application scenario.

  • Code is concise - function extraction, reduce duplication of code
  • Easy to understand - favor natural language
  • Convenient maintenance and expansion - Functional programming only needs to ensure that the input and output remain unchanged, and the internal implementation has nothing to do with the external
  • Easier to concurrency - functional programming does not modify variables so there is no need to worry about "deadlock problems"

Footnotes

  1. Functional programming is a programming paradigm that treats computer operations as functions and avoids the use of program state and mutable objects.

  2. First Class Citizen: Refers to an entity that supports all operations normally available to other entities. Including but not limited to having specific basic rights, they can be used as the actual parameters of the function, returned as the result of the function, and are the main body of the assignment statement.

· 4 min read
Jonas Lang

引言

此博客作为一份如何在 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 min read
Jonas Lang

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 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 中)受益等多方面考虑。