手把手带你走进Babel的编译世界

大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列

前言

谈及 Babel,必然离不开 AST。有关 AST 这个知识点其实是很重要的,但由于涉及到代码编译阶段,大多情况都是由各个框架内置相关处理,所以作为开发(使用)者本身,往往会忽视这个过程。希望通过这篇文章,带各位同学走进 AST,借助 AST 发挥更多的想象力。

AST 概述

想必大家总是听到 AST 这个概念,那么到底什么是 AST?

AST 全称是是 Abstract Syntax Tree,中文为抽象语法树,将我们所写的代码转换为机器能识别的一种树形结构。其本身是由一堆节点(Node)组成,每个节点都表示源代码中的一种结构。不同结构用类型(Type)来区分,常见的类型有:Identifier(标识符),Expression(表达式),VariableDeclaration(变量定义),FunctionDeclaration(函数定义)等。

AST 结构

随着 JavaScript 的发展,为了统一ECMAScript标准的语法表达。社区中衍生出了ESTree Spec[1],是目前社区所遵循的一种语法表达标准。

ESTree 提供了例如Identifier、Literal等常见的节点类型。

节点类型

类型说明
File文件 (顶层节点包含 Program)
Program整个程序节点 (包含 body 属性代表程序体)
Directive指令 (例如 "use strict")
Comment代码注释
Statement语句 (可独立执行的语句)
Literal字面量 (基本数据类型、复杂数据类型等值类型)
Identifier标识符 (变量名、属性名、函数名、参数名等)
Declaration声明 (变量声明、函数声明、Import、Export 声明等)
Specifier关键字 (ImportSpecifier、ImportDefaultSpecifier、ImportNamespaceSpecifier、ExportSpecifier)
Expression表达式

公共属性

类型说明
typeAST 节点的类型
start记录该节点代码字符串起始下标
end记录该节点代码字符串结束下标
loc内含 line、column 属性,分别记录开始结束的行列号
leadingComments开始的注释
innerComments中间的注释
trailingComments结尾的注释
extra额外信息

AST 示例

有的同学可能会问了,这么多类型都需要记住么? 其实并不是,我们可以借助以下两个工具来查询 AST 结构。

  • AST Explorer (常用)[2]0119bfb93514188044553a1132b0a827.png

  • AST 可视化[3]1e345ef8882a78885489bae6e627f8f8.png

结合一个示例,带大家快速了解一下 AST 结构。

function test(args) {const a = 1;console.log(args);
}

上述代码,声明了一个函数,名为test,有一个形参args

函数体中:

  • 声明了一个const类型变量a,值为 1

  • 执行了一个 console.log 语句

将上述代码粘贴至AST Explorer[4],结果如图所示:

ebe22e3157cd07534cc7ab6363721a9f.png
图解

接下来我们继续分析内部结构,以const a = 1为例:

0606357174beb3859bece89a2c5d6acc.png
图解

变量声明在 AST 中对应的就是 type 为VariableDeclaration的节点。该节点包含kinddeclarations两个必须属性,分别代表声明的变量类型和变量内容。

细心的同学可能发现了declarations是一个数组。这是为什么呢?因为变量声明本身支持const a=1,b=2的写法,需要支持多个VariableDeclarator,故此处为数组。

而 type 为VariableDeclarator的节点代表的就是a=1这种声明语句,其中包含idinit属性。

id即为Identifier,其中的name值对应的就是变量名称。

init即为初始值,包含type,value属性。分别表示初始值类型和初始值。此处 type 为NumberLiteral,表明初始值类型为number类型

Babel 概述

Babel 是一个 JavaScript 编译器,在实际开发过程中通常借助Babel来完成相关 AST 的操作。

Babel 工作流程

c81d68223831e7484f71ac25b64a387c.png
图解

Babel AST

Babel 解析代码后生成的 AST 是以ESTree[5]作为基础,并略作修改。

官方原文如下:

The Babel parser generates AST according to Babel AST format. It is based on ESTree spec with the following deviations:

  • Literal token is replaced with StringLiteral, NumericLiteral, BigIntLiteral, BooleanLiteral, NullLiteral, RegExpLiteral

  • Property token is replaced with ObjectProperty and ObjectMethod

  • MethodDefinition is replaced with ClassMethod

  • Program and BlockStatement contain additional directives field with Directive and DirectiveLiteral

  • ClassMethod, ObjectProperty, and ObjectMethod value property's properties in FunctionExpression is coerced/brought into the main method node.

  • ChainExpression is replaced with OptionalMemberExpression and OptionalCallExpression

  • ImportExpression is replaced with a CallExpression whose callee is an Import node.

Babel 核心包

工具包说明
@babel/coreBabel 转码的核心包,包括了整个 babel 工作流(已集成@babel/types)
@babel/parser解析器,将代码解析为 AST
@babel/traverse遍历/修改 AST 的工具
@babel/generator生成器,将 AST 还原成代码
@babel/types包含手动构建 AST 和检查 AST 节点类型的方法
@babel/template可将字符串代码片段转换为 AST 节点
npm i @babel/parser @babel/traverse @babel/types @babel/generator @babel/template -D

Babel 插件

Babel 插件大致分为两种:语法插件和转换插件。语法插件作用于 @babel/parser,负责将代码解析为抽象语法树(AST)(官方的语法插件以 babel-plugin-syntax 开头);转换插件作用于 @babel/core,负责转换 AST 的形态。绝大多数情况下我们都是在编写转换插件。

Babel 工作依赖插件。插件相当于是指令,来告知 Babel 需要做什么事情。如果没有插件,Babel 将原封不动的输出代码。

Babel 插件本质上就是编写各种 visitor 去访问 AST 上的节点,并进行 traverse。当遇到对应类型的节点,visitor 就会做出相应的处理,从而将原本的代码 transform 成最终的代码。

export default function (babel) {// 即@babel/types,用于生成AST节点const { types: t } = babel;return {name: "ast-transform", // not requiredvisitor: {Identifier(path) {path.node.name = path.node.name.split("").reverse().join("");},},};
}

这是一段AST Explorer[6]上的 transform 模板代码。上述代码的作用即为将输入代码的所有标识符(Identifier)类型的节点名称颠倒

其实编写一个 Babel 插件很简单。我们要做的事情就是回传一个 visitor 对象,定义以Node Type为名称的函数。该函数接收path,state两个参数。

其中path(路径)提供了访问/操作AST 节点的方法。path 本身表示两个节点之间连接的对象。例如path.node可以访问当前节点,path.parent可以访问父节点等。path.remove()可以移除当前节点。具体 API 见下图。其他可见handlebook[7]

215109c23f807af26d874c67755d7652.png
图解

Babel Types

Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库,它包含了构造、验证以及变换 AST 节点的方法。

类型判断

Babel Types 提供了节点类型判断的方法,每一种类型的节点都有相应的判断方法。更多见babel-types API[8]

import * as types from "@babel/types";// 是否为标识符类型节点
if (types.isIdentifier(node)) {// ...
}// 是否为数字字面量节点
if (types.isNumberLiteral(node)) {// ...
}// 是否为表达式语句节点
if (types.isExpressionStatement(node)) {// ...
}

创建节点

Babel Types 同样提供了各种类型节点的创建方法,详见下属示例。

注: Babel Types 生成的 AST 节点需使用@babel/generator转换后得到相应代码。

import * as types from "@babel/types";
import generator from "@babel/generator";const log = (node: types.Node) => {console.log(generator(node).code);
};log(types.stringLiteral("Hello World")); // output: Hello World

基本数据类型

types.stringLiteral("Hello World"); // string
types.numericLiteral(100); // number
types.booleanLiteral(true); // boolean
types.nullLiteral(); // null
types.identifier(); // undefined
types.regExpLiteral("\\.js?$", "g"); // 正则
"Hello World"
100
true
null
undefined
/\.js?$/g

复杂数据类型

  • 数组

types.arrayExpression([types.stringLiteral("Hello World"),types.numericLiteral(100),types.booleanLiteral(true),types.regExpLiteral("\\.js?$", "g"),
]);
["Hello World", 100, true, /\.js?$/g];
  • 对象

types.objectExpression([types.objectProperty(types.identifier("key"),types.stringLiteral("HelloWorld")),types.objectProperty(// 字符串类型 keytypes.stringLiteral("str"),types.arrayExpression([])),types.objectProperty(types.memberExpression(types.identifier("obj"),types.identifier("propName")),types.booleanLiteral(false),// 计算值 keytrue),
]);
{key: "HelloWorld","str": [],[obj.propName]: false
}

JSX 节点

创建 JSX AST 节点与创建数据类型节点略有不同,此处整理了一份关系图。

77361c9abdef6df37c6fe6f5e8c1badd.png
图解
  • JSXElement

    types.jsxElement(types.jsxOpeningElement(types.jsxIdentifier("Button"), []),types.jsxClosingElement(types.jsxIdentifier("Button")),[types.jsxExpressionContainer(types.identifier("props.name"))]
    );
    <Button>{props.name}</Button>
  • JSXFragment

    types.jsxFragment(types.jsxOpeningFragment(), types.jsxClosingFragment(), [types.jsxElement(types.jsxOpeningElement(types.jsxIdentifier("Button"), []),types.jsxClosingElement(types.jsxIdentifier("Button")),[types.jsxExpressionContainer(types.identifier("props.name"))]),types.jsxElement(types.jsxOpeningElement(types.jsxIdentifier("Button"), []),types.jsxClosingElement(types.jsxIdentifier("Button")),[types.jsxExpressionContainer(types.identifier("props.age"))]),
    ]);
    <><Button>{props.name}</Button><Button>{props.age}</Button>
    </>

声明

  • 变量声明 (variableDeclaration)

    types.variableDeclaration("const", [types.variableDeclarator(types.identifier("a"), types.numericLiteral(1)),
    ]);
    const a = 1;
  • 函数声明 (functionDeclaration)

    types.functionDeclaration(types.identifier("test"),[types.identifier("params")],types.blockStatement([types.variableDeclaration("const", [types.variableDeclarator(types.identifier("a"),types.numericLiteral(1)),]),types.expressionStatement(types.callExpression(types.identifier("console.log"), [types.identifier("params"),])),])
    );
    function test(params) {const a = 1;console.log(params);
    }

React 函数式组件

综合上述内容,小小实战一下~

我们需要通过 Babel Types 生成button.js代码。乍一看不知从何下手?

// button.js
import React from "react";
import { Button } from "antd";export default (props) => {const handleClick = (ev) => {console.log(ev);};return <Button onClick={handleClick}>{props.name}</Button>;
};

小技巧: 先借助AST Explorer[9]网站,观察 AST 树结构。然后通过 Babel Types 逐层编写代码。事半功倍!

types.program([types.importDeclaration([types.importDefaultSpecifier(types.identifier("React"))],types.stringLiteral("react")),types.importDeclaration([types.importSpecifier(types.identifier("Button"),types.identifier("Button")),],types.stringLiteral("antd")),types.exportDefaultDeclaration(types.arrowFunctionExpression([types.identifier("props")],types.blockStatement([types.variableDeclaration("const", [types.variableDeclarator(types.identifier("handleClick"),types.arrowFunctionExpression([types.identifier("ev")],types.blockStatement([types.expressionStatement(types.callExpression(types.identifier("console.log"), [types.identifier("ev"),])),]))),]),types.returnStatement(types.jsxElement(types.jsxOpeningElement(types.jsxIdentifier("Button"), [types.jsxAttribute(types.jsxIdentifier("onClick"),types.jSXExpressionContainer(types.identifier("handleClick"))),]),types.jsxClosingElement(types.jsxIdentifier("Button")),[types.jsxExpressionContainer(types.identifier("props.name"))],false)),]))),
]);

应用场景

AST 本身应用非常广泛,例如:Babel 插件(ES6 转化 ES5)、构建时压缩代码 、css 预处理器编译、 webpack 插件等等,可以说是无处不在。

a0b646feb5ded946c39fb358ab7e25f6.png
图解

如图所示,不难发现,一旦涉及到编译,或者说代码本身的处理,都和 AST 息息相关。下面列举了一些常见应用,让我们看看是如何处理的。

代码转换

// ES6 => ES5 let 转 var
export default function (babel) {const { types: t } = babel;return {name: "let-to-var",visitor: {VariableDeclaration(path) {if (path.node.kind === "let") {path.node.kind = "var";}},},};
}

babel-plugin-import

在 CommonJS 规范下,当我们需要按需引入antd的时候,通常会借助该插件。

该插件的作用如下:

// 通过es规范,具名引入Button组件
import { Button } from "antd";
ReactDOM.render(<Button>xxxx</Button>);// babel编译阶段转化为require实现按需引入
var _button = require("antd/lib/button");
ReactDOM.render(<_button>xxxx</_button>);

简单分析一下,核心处理: 将 import 语句替换为对应的 require 语句。

export default function (babel) {const { types: t } = babel;return {name: "import-to-require",visitor: {ImportDeclaration(path) {if (path.node.source.value === "antd") {// var _button = require("antd/lib/button");const _botton = t.variableDeclaration("var", [t.variableDeclarator(t.identifier("_button"),t.callExpression(t.identifier("require"), [t.stringLiteral("antd/lib/button"),])),]);// 替换当前import语句path.replaceWith(_botton);}},},};
}

TIPS: 目前 antd 包中已包含esm规范文件,可以依赖 webpack 原生 TreeShaking 实现按需引入。

LowCode 可视化编码

当下LowCode,依旧是前端一大热门领域。目前主流的做法大致下述两种。

  • Schema 驱动

    目前主流做法,将表单或者表格的配置,描述为一份 Schema,可视化设计器基于 Schema 驱动,结合拖拽能力,快速搭建。

  • AST 驱动

    通过CloudIDECodeSandbox等浏览器端在线编译,编码。外加可视化设计器,最终实现可视化编码。

c46d68ff804dd70d1cc0f87647e96e7d.png
图解

大致流程如上图所示,既然涉及到代码修改,离不开AST的操作,那么又可以发挥 babel 的能力了。

假设设计器的初始代码如下:

import React from "react";export default () => {return <Container></Container>;
};

此时我们拖拽了一个Button至设计器中,根据上图的流程,核心的 AST 修改过程如下:

  • 新增 import 声明语句 import { Button } from "antd";

  • <Button></Button>插入至<Container></Container>

话不多说,直接上代码:

import traverse from "@babel/traverse";
import generator from "@babel/generator";
import * as parser from "@babel/parser";
import * as t from "@babel/types";// 源代码
const code = `import React from "react";export default () => {return <Container></Container>;};
`;const ast = parser.parse(code, {sourceType: "module",plugins: ["jsx"],
});traverse(ast, {// 1. 程序顶层 新增import语句Program(path) {path.node.body.unshift(t.importDeclaration(// importSpecifier表示具名导入,相应的匿名导入为ImportDefaultSpecifier// 具名导入对应代码为 import { Button as Button } from 'antd'// 如果相同会自动合并为 import { Button } from 'antd'[t.importSpecifier(t.identifier("Button"), t.identifier("Button"))],t.stringLiteral("antd")));},// 访问JSX节点,插入ButtonJSXElement(path) {if (path.node.openingElement.name.name === "Container") {path.node.children.push(t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier("Button"), []),t.jsxClosingElement(t.jsxIdentifier("Button")),[t.jsxText("按钮")],false));}},
});const newCode = generator(ast).code;
console.log(newCode);

结果如下:

import { Button } from "antd";
import React from "react";
export default () => {return (<Container><Button>按钮</Button></Container>);
};

ESLint

自定义 eslint-rule,本质上也是访问 AST 节点,是不是跟 Babel 插件的写法很相似呢?

module.exports.rules = {"var-length": (context) => ({VariableDeclarator: (node) => {if (node.id.name.length <= 2) {context.report(node, "变量名长度需要大于2");}},}),
};

Code2Code

以 Vue To React 为例,大致过程跟ES6 => ES5类似,通过vue-template-compiler编译得到 Vue AST => 转换为 React AST => 输出 React 代码

有兴趣的同学可以参考vue-to-react[10]

其他多端框架:一份代码 => 多端,大体思路一致。

总结

在实际开发中,遇到的情况往往更加复杂,建议大家多番文档,多观察,用心去感受 ~

参考文章

  1. babel-handlebook[11]

  2. @babel/types[12]

  3. 透過製作 Babel-plugin 初訪 AST

  4. @babel/types 深度应用

本文发布自网易云音乐技术团队,文章未经授权禁止任何形式的转载。我们常年招收各类技术岗位,如果你准备换工作,又恰好喜欢云音乐,那就加入我们 grp.music-fe(at)corp.netease.com!

参考资料

[1]

ESTree Spec: https://github.com/estree/estree

[2]

AST Explorer (常用): https://astexplorer.net/

[3]

AST 可视化: https://resources.jointjs.com/demos/rappid/apps/Ast/index.html

[4]

AST Explorer: https://astexplorer.net/

[5]

ESTree: https://github.com/estree/estree

[6]

AST Explorer: https://astexplorer.net/

[7]

handlebook: https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-visitors

[8]

babel-types API: https://babel.docschina.org/docs/en/babel-types/

[9]

AST Explorer: https://astexplorer.net/

[10]

vue-to-react: https://github.com/dwqs/vue-to-react

[11]

babel-handlebook: https://github.com/jamiebuilds/babel-handbook/tree/master/translations/zh-Hans

[12]

@babel/types: https://babel.docschina.org/docs/en/babel-types

d20e6a701f63f109db8ada93b8137902.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

e1305e97d729d918e69293c8217ef85a.png

扫码加我微信 ruochuan02、拉你进源码共读

今日话题

假期总是短暂的,下一个小长假就是五一了。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274767.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

同态加密应用_重新设计具有同态性的银行应用

同态加密应用Catering user preference is undoubtedly a never-ending task. End of the day, it takes all sorts to make a world. For that reason, it is deemed important to design with the accent of communicating core business value, and resolving user needs wi…

(字节/华为/美团)前端面经记录冷冷清清的金三银四

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列从毕业…

netflix_Netflix播放按钮剖析

netflixWe will develop a play pause button similar to the one the Netflix video player has.我们将开发一个类似于Netflix视频播放器的播放暂停按钮。 Since Swift has replaced Objective-C as the default development language for iOS, the same will apply to Swift…

TypeScript 终极初学者指南

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列在过去…

标记偏见_如何(巧妙地)扭曲视觉效果以支持您的偏见叙事

标记偏见Data is important — it is the logical justification for world-changing decisions. Unfortunately, arrays of numbers don’t tell as interpretable a story as a picture does, providing an insatiable need for data visualizations.数据很重要-这是改变世界…

高瓴投资,顶配创业团队,dora 诚招前端 / Flutter

dora 是一个可以跨越设计稿&#xff0c;直接生成应用的新一代设计工具。让任何 Creator 都能轻松构建个性化的网站和应用&#xff0c;无需编写一行代码。通过自主研发的全新技术&#xff0c;我们为用户打造了完全自由度的设计与开发体验&#xff0c;足以满足任何复杂场景的个性…

猎鹰spacex_SpaceX:简单,美观的界面是未来

猎鹰spacex重点 (Top highlight)A photo has been floating around the internet showing how the interior of the new Dragon spacecraft differs from the Space Shuttle. The difference is staggering, but not entirely suprprising. After all the Shuttle started oper…

开发交接文档_为开发人员创造更好的设计交接体验

开发交接文档It’s 2020. We’re supposed to have flying cars and space travel. We should at least have our process for design handoff nailed down at this point.现在是2020年。我们应该有飞行汽车和太空旅行。 在这一点上&#xff0c;我们至少应该确定我们的设计移交…

​Cookie 从入门到进阶:一文彻底弄懂其原理以及应用

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列Cooki…

ui设计师常用的设计工具_2020年应该使用哪个UI设计工具?

ui设计师常用的设计工具重点 (Top highlight)It’s 2020, the market today is saturated with UI design tools. Ever since Sketch app came out with its sleek, simple, and efficient tool to craft user interface design, many companies have followed suit to take a …

你不知道的vscode之空间控制

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列王志远…

lynda ux_UX心态

lynda uxI have had the pleasure of training and mentoring several UX people at the beginning of their careers.在职业生涯的初期&#xff0c;我很高兴接受培训和指导。 Whatever your background or experience, I’ve found repeatedly that there are some key miles…

什么 Leader 值得追随?

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。目前建有江西|湖南|湖北 籍 前端群&#xff0c;想进群的可以加我微信 ruochuan12 进群。历…

pico8 掌机_使用Pico-8构建自己的复古游戏

pico8 掌机An example of the kinds of pixel animations people make in Pico-8.人们在Pico-8中制作的各种像素动画的示例。 Are you a fan of old school video games? What if I told you there’s an NES-style game devkit with the sound/sprite/code tools all built i…

实用 JavaScript 调试技巧

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外…

数据挖掘 点击更多 界面_6(更多)技巧,可快速改善用户界面

数据挖掘 点击更多 界面重点 (Top highlight)Creating beautiful, usable, and efficient UIs takes time, with many design revisions along the way.创建漂亮&#xff0c;可用和高效的UI需要花费时间&#xff0c;并且在此过程中进行了许多设计修订。 Making those constant…

Koa在实际的业务场景中,路由如何做分割?【文末留言送书】

大家好&#xff0c;我是若川。文末留言送书&#xff0c;具体规则文末说明。另外为了鼓励大家多写源码共读笔记&#xff0c;我会在写了5次及以上笔记的作者群里也抽奖送这本书。以后也会有更多福利倾斜。导读&#xff1a;Koa是一个Node框架&#xff0c;在Node开源社区中&#xf…

设计模式_设计

设计模式Thanks for my colleague WanChing‘s help to prepare this sharing article. E-Commerce app collects plentiful products from various brands. Each brand has its brand signature colors and public image. This article introduces how we made a single page …

使用 GTD 优化自己的工作和生活

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外…

模仿不再受宠若惊

If you haven’t heard of the Jio-Zoom plagiarism clash, you’re probably living under a rock (which may not be a bad idea given the state of the world right now). The turf war between Jio Meet and Zoom began when the Indian telecom giant ripped off the Chi…