这是将BEM方法集成到React世界中的故事。 您将阅读的材料是基于Yandex开发人员开发俄罗斯最大,负载最大的服务的经验-Yandex.Search。 我们之前从未如此详细,深入地谈论过我们这样做的原因,而没有其他方面谈到过激励我们的动机以及我们真正想要的是什么。 局外人在发布会上发布了发布和评论。 只有在场外才能听到类似的声音。 作为合著者,我很愤慨,因为每次我谈论图书馆的新版本时,外界信息的匮乏。 但是这次我们将分享所有细节。

每个人都听说过BEM方法论。 带下划线的CSS选择器。 讨论组件方法时要牢记CSS CSS选择器的编写方式。 但是本文中不会涉及CSS。 只有JS,只有铁杆!
为了了解这种方法为何出现以及Yandex当时面临什么问题,我建议您熟悉 BEM 的历史 。
序言
BEM实际上是从CSS中强大的连接性和嵌套的拯救中诞生的。 但是,将style.css
表分为每个块,元素或修饰符的文件不可避免地导致了类似的JavaScript代码结构。
2011年,开放源代码获得了i-bem.js
的第一批提交,该 i-bem.js
与bem-xjst
模板引擎bem-xjst
。 两种技术均源于XSLT ,并为当时流行的将业务逻辑和组件表示分离的想法服务。 在外部世界,这是Handlebars和Underscore的美好时光 。
bem-xjst
是另一种类型的模板引擎。 为了增加我对标准化方法的体系结构的了解,我强烈推荐Sergei Berezhnoy的报告 。 bem-xjst
可以在在线沙箱中尝试使用bem-xjst
模板引擎。
由于Yandex搜索服务的特殊性,用户界面是使用数据构建的。 搜索结果页面对于每个查询都是唯一的。

通过参考搜索查询

通过参考搜索查询

通过参考搜索查询
当分为块,元素和修饰符的划分扩展到文件系统时,这仅允许为每个用户请求的每个页面收集尽可能多的必需代码。 但是如何?
src/components ├── ComponentName │ ├── _modName │ │ ├── ComponentName_modName.tsx — │ │ └── ComponentName_modName_modVal.tsx — │ ├── ElementName │ │ └── ComponentName-ElementName.tsx — ComponentName │ ├── ComponentName.i18n — │ │ ├── ru.ts — │ │ ├── en.ts — │ │ └── index.ts — │ ├── ComponentName.test — │ │ ├── ComponentName.page-object.js — Page Object │ │ ├── ComponentName.hermione.js — │ │ └── ComponentName.test.tsx — unit- │ ├── ComponentName.tsx — │ ├── ComponentName.scss — │ ├── ComponentName.examples.tsx — Storybook │ └── README.md —
现代组件目录结构
与其他公司一样,在Yandex中,界面开发人员负责前端,前端由浏览器中的客户端部分和Node.js
上的服务器部分组成Node.js
服务器部分处理“大”搜索的数据并在其上加上模板。 主数据处理将JSON转换为BEMJSON , bem-xjst
模板引擎的数据结构。 模板引擎围绕树的每个节点并在其上强加一个模板。 由于主要转换发生在服务器上,并且由于分为小实体,所以节点对应于文件,因此在模板生成期间,我们将代码推送到仅在当前页面上使用的浏览器中。
下面是BEMJSON节点与文件系统上文件的对应关系。
module.exports = { block: 'Select', elem: 'Item', elemMods: { type: 'navigation' } };
src/components ├── Select │ ├── Item │ │ _type │ │ ├── Select-Item_type_navigation.js │ │ └── Select-Item_type_navigation.css
YModules
模块化系统负责隔离浏览器中JavaScript代码的组件。 它允许您同步和异步将模块交付给浏览器。 可以在此处找到有关组件如何与YModules
和i-bem.js
一起使用的示例。 如今,对于大多数开发人员而言, webpack
和未发布的动态导入标准都可以做到这一点 。
一套BEM方法论,声明性模板引擎和带有模块化系统的JS框架使解决任何问题成为可能。 但是随着时间的流逝,动态性已经进入用户界面。
新希望
在2013年, React迷上了开源。 实际上,Facebook早在2011年就开始使用它。 詹姆斯·朗(James Long)在JS Conf US会议上的笔记中说:
最后两节令人惊讶。 第一个是由两名Facebook开发人员提供的,他们宣布了Facebook React 。 我没有记很多笔记,因为我对这个想法有多么糟糕感到震惊。 本质上,他们创建了一种称为JSX的语言,该语言使您可以将XML嵌入JavaScript中以创建实时响应式用户界面。 XML格式 在JavaScript中。
React已经改变了设计Web应用程序的方法。 它变得如此流行,以至于今天您找不到没有听说过React的开发人员。 但是另一件事很重要:应用程序变得不同, SPA进入了我们的生活。
Yandex开发人员在技术方面具有特殊的美感,这是公认的。 有时很奇怪,很难与之抗争,但从来没有没有道理。 当React 在GitHub上广为人知时 ,许多熟悉Yandex网络技术的人坚持认为:Facebook赢了,放弃您的技术,并在一切都太晚之前运行重写React上的一切。 了解两件事很重要。
首先,没有战争。 公司在创建地球上最好的框架方面没有竞争。 如果一家公司开始以相同的生产力减少在基础架构任务上花费的时间(读钱),那么每个人都会从中受益。 编写框架来编写框架是没有意义的。 最好的开发人员创建工具,以最佳方式解决公司的任务。 公司,服务,目标-都是不同的。 因此,工具多种多样。
其次,我们正在寻找一种以自己希望的方式使用React的方法。 具有上述技术所提供的所有功能。
人们普遍认为使用React的代码默认情况下是快速的。 如果您也这样认为,那您就深深地误会了。 在大多数情况下,React唯一要做的就是帮助与DOM进行最佳交互。
在版本16之前,React有一个致命缺陷。 它比服务器上的bem-xjst
慢10倍。 我们负担不起这种浪费。 Yandex的响应时间是关键指标之一。 想象一下,当您要求甜酒配方时,您会得到比平时慢10倍的答案。 即使您对网络开发有所了解,您也不会为借口感到高兴。 我们可以说些什么,例如“但使开发人员与DOM通信变得更加方便”。 在此处加上实施价格与利润的比率-您自己将做出唯一正确的决定。
幸运的是,开发人员是陌生的人。 如果无法解决问题,那么这并不是丢弃所有内容的理由...
倒挂
我们坚信我们可以战胜React的缓慢发展。 我们已经有一个快速的模板引擎。 您所需bem-xjst
使用bem-xjst
在服务器上生成HTML,并在客户端上“强制” React接受此标记作为其自身。 这个想法很简单,没有什么预示着失败。
在15以下的版本中,React使用哈希和验证了标记的有效性,该算法将所有优化转化为南瓜。 为了使React确信标记的有效性,有必要为每个节点设置一个id,并计算所有节点的哈希和。 它还意味着要支持bem-xjst
模板:针对客户端的React和针对服务器的bem-xjst
。 使用id安装进行简单的速度测试可以清楚地表明继续进行毫无意义。
bem bem-xjst
是一个非常被低估的工具。 查看Glory Oliyanchuk主要维护者的报告,亲自看看 。 bem-xjst
基于一种体系结构,该体系结构允许您将一种模板语法用于源树的不同转换。 与React非常相似,不是吗? 如今,此功能允许react-sketchapp
诸如react-sketchapp
工具。
开箱即用bem-xjst
包含两种类型的转换:HTML和JSON。 任何足够勤奋的开发人员都可以编写自己的引擎,以将模板转换为任何东西。 我们教bem-xjst
将数据树转换为对HyperScript函数的调用序列。 这意味着与React以及虚拟DOM算法的其他实现(例如Preact)完全兼容。
HyperScript函数调用生成的详细介绍
由于React模板需要布局和业务逻辑共存,因此我们不得不将i-bem.js
的逻辑引入我们的模板中,而这并不是为此而设计的。 对于他们来说,这是不自然的。 他们走的不一样。 对了!
下面是在一个运行时中融合不同世界的示例。
block('select').elem('menu')( def()(function() { const React = require('react'); const Menu = require('../components/menu/menu'); const MenuItem = require('../components/menu-item/menu-item'); const _select = this.ctx._select; const selectComponent = _select._select; return React.createElement.apply(React, [ Menu, { mix: { block : this.block, elem : this.elem }, ref: menu => selectComponent._menu = menu, size: _select.mods.size, disabled: _select.mods.disabled, mode: _select.mods.mode, content: _select.options, checkedItems: _select.bindings.checkedItems, style: _select.bindings.popupMenuWidth, onKeyDown: _select.bindings.onKeyDown, theme: _select.mods.theme, }].concat(_select.options.map(option => React.createElement( MenuItem, { onClick: _select.bindings.onOptionCheck, theme: _select.mods.theme, val: option.value, }, option.content) )) ); }) );
当然,我们有自己的程序集。 如您所知,最快的操作是字符串连接。 bem-xjst
引擎建立在其上,组件建立在其上。 块,元素和修饰符的文件位于文件夹中,而装配只需要按正确的顺序粘贴文件即可。 使用这种方法,您可以并行粘合JS,CSS和模板以及实体本身。 也就是说,如果您在一个项目中有四个组件,在笔记本电脑上有四个内核,并且组装一个组件技术需要一秒钟,那么构建项目将需要两秒钟。 在这里,应该更加清楚我们如何设法仅将必要的代码推入浏览器。
这一切对我们来说都是ENB 。 我们仅在运行时收到了最后一棵用于标准化的树,并且由于组件之间的依赖性必须提早出现才能收集包,因此该功能由鲜为人知的deps.js
技术deps.js
。 它允许您在组件之间建立依赖关系图,然后收集器可以按所需顺序粘贴代码,从而绕过该图。
React版本16停止朝这个方向工作,服务器上模板的执行速度是相等的 。 在生产设备上,差异变得难以察觉。
节点: v8.4.0
儿童: 5K
渲染器 | 平均时间 | 运算/秒 |
---|
预设v8.2.6 | 66.235毫秒 | 15 |
bem-xjst v8.8.4 | 71.326毫秒 | 14 |
反应 v16.1.0 | 73.966毫秒 | 14 |
使用下面的链接,您可以恢复该方法的历史记录:
我们还尝试了其他吗?

动机
在故事的中间,谈论促使我们前进的因素会很有用。 在一开始就值得这样做,但是-谁记得那只老眼睛作为礼物。 为什么我们需要所有这些? BEM可以带来React无法做的事情? 几乎每个人都提出的问题。
分解
组件的功能逐年变得更加复杂,并且变体的数量增加。 这由if
或switch
构造表示,结果,代码库不可避免地增长,结果,组件的重量以及使用这种组件的项目增加了。 React组件逻辑的主要部分包含在render()
方法中。 要更改组件的功能,必须重写大多数方法,这不可避免地导致高度专用组件的数量呈指数增长。
每个人都知道material-ui , fabric-ui和react-bootstrap库 。 通常,所有带有组件的知名库都具有相同的缺点。 假设您有多个项目,并且所有项目都使用相同的库。 您使用相同的组件,但是变化形式不同:这里有带有复选框的选择,没有,带有按钮的蓝色按钮,没有按钮的红色按钮。 该库带给您的CSS和JS的权重在所有项目中都相同。 但是为什么呢? 组件的各种变体都嵌入在组件本身内部,并随组件一起提供,无论您是否想要。 对我们来说这是不可接受的。
Yandex还拥有自己的带有组件的库-Lego。 它适用于约200个服务。 我们是否要在Yandex.Health中以相同的价格使用Lego in Search? 你知道答案。
为了支持多个平台,大多数情况下,它们为每个平台创建一个单独的版本,或者为一个自适应版本创建一个。
各个版本的开发需要更多资源:平台越多,工作量就越大。 在不同版本中保持产品属性的同步状态将引起新的困难。
自适应版本的开发使代码变得复杂,增加了重量,降低了产品的速度,并且平台之间存在适当的差异。
我们是否希望我们的父母/朋友/同事/孩子在移动设备上使用台式机版本,从而降低互联网速度并降低生产率? 你知道答案。
实验
如果您正在为大量受众开发项目,则需要确保每一个更改。 A / B实验是获得这种信心的一种方法。
组织实验代码的方式:
- 项目的分支和生产中服务实例的创建;
- 代码库中的点条件。
如果项目有大量冗长的实验,则分支代码库将导致大量成本。 有必要使每个分支机构都与实验保持最新:端口更正的错误和产品功能。 代码库分支使相交的实验多次复杂化。
点条件可以更灵活地工作,但会使代码库复杂化:实验条件可以影响项目的不同部分。 大量情况会通过增加浏览器的代码量来降低性能。 必须删除条件,使代码基本化或完全删除失败的实验。
在搜索中〜针对不同受众的100种在线实验,以各种组合形式出现。 您可以自己看到它。 请记住,也许您注意到了该功能,但是一周后它神奇地消失了。 我们是否要以维护500,000行的活动代码库的数百个分支为代价来测试产品理论,而这些分支每天约有60个开发人员进行更改? 你知道答案。
全球变化
例如,您可以创建从库继承自Button
的CustomButton
组件。 但是继承的CustomButton
不适用于包含Button
的库中的所有组件。 一个库可能具有一个由Input
和Button
构建的Search
组件。 在这种情况下,继承的CustomButton
不会出现在Search
组件内。 我们是否要手动Button
使用Button
的整个代码库?

漫长的创作之路
我们决定改变策略。 在以前的方法中,他们以Yandex技术为基础,并试图让React在此基础上工作。 新的策略则相反。 bem-react-core项目就是这样产生的。
别说了 为什么要做出反应?
我们在其中看到了摆脱HTML显式初始呈现的机会,并在稍后的运行时从手动支持JS组件的状态中摆脱了机会-实际上,将BEMHMTL模板和JS组件合并为一种技术成为可能。
最初,我们计划将bem-xjst
所有最佳实践和属性bem-xjst
到React之上的库中。 首先引起您注意的是签名,或者,如果您愿意,还可以描述组件的语法。
你做了什么,有JSX!
第一个版本是在继承的基础上构建的-一个有助于实现类和继承的库。 就像您中有些人记得的那样,当时JavaScript的原型原型没有类,也没有super
。 通常,它们仍然不存在,更确切地说,这些不是首先要想到的类。 inherit
完成了ES2015标准中的类现在可以做的所有事情,以及所谓的黑魔法:多重继承和原型合并而不是重建链,这对性能产生了积极影响。 如果您认为像在Node.js中继承那样看起来很有意义,那么您不会误会,但它们的工作原理有所不同。
以下是模板bem-react-core@v1.0.0
的语法bem-react-core@v1.0.0
。
App-Header.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', attrs: { role: 'heading' }, content() { return ' '; } });
App-Header@desktop.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', tag: 'h1', attrs() { return { ...this.__base(...arguments), 'aria-level': 1 }, }, content() { return ` ${this.__base(...arguments)} h1`; } });
App-Header@touch.js
import { decl } from 'bem-react-core'; export default decl({ block: 'App', elem: 'Header', tag: 'h2', content() { return ` ${this.__base(...arguments)} `; } });
index.js
import ReactDomServer from 'react-dom/server'; import AppHeader from 'b:App e:Header'; ReactDomServer.renderToStaticMarkup(<AppHeader />);
output@desktop.html
<h1 class="App-Header" role="heading" aria-level="1">A h1</h2>
output@touch.html
<h2 class="App-Header" role="heading"> </h2>
可以在此处找到用于更复杂组件的设备模板。
由于类是对象,并且使用JavaScript处理对象最方便,因此语法是合适的。 语法后来迁移到其主脑bem-xjst
。
该库是对象声明的全局存储库-执行decl
函数的结果,实体的一部分:块,元素或修饰符。 BEM提供了独特的命名机制,因此适合在保管库中创建密钥。 所得的React组件在其使用地点被粘上。 诀窍是在导入模块时可以找到decl
。 这样就可以使用简单的导入列表来指示每个特定位置需要组件的哪些部分。 但是请记住:组件很复杂,有很多部分,导入列表很长,开发人员很懒。
导入魔术
如您所见,在代码示例中,有import AppHeader from 'b:App e:Header'
行import AppHeader from 'b:App e:Header'
。
您违反了标准! 不可能! 只是行不通!
首先,导入标准并未以“必须在导入行中有通往实际模块的路径”这一精神进行操作。 其次,是使用Babel转换的语法糖。 第三, import txt from 'raw-loader!./file.txt';
webpack import txt from 'raw-loader!./file.txt';
奇怪的import txt from 'raw-loader!./file.txt';
标点构造import txt from 'raw-loader!./file.txt';
由于某种原因,他们没有打扰任何人。
因此,我们的代码块存在于两个平台中: desktop
, touch
。
import Hello from 'b:Hello';
Hello
, applyDecls
, inherit
, React-.
Babel, , . webpack, , .
, :
:
- TypeScript/Flow;
- React- ;
- - ;
- .
bem-react-core@v1.0.0
, .
import { Elem } from 'bem-react-core'; import { Button } from '../Button'; export class AppHeader extends Elem { block = 'App'; elem = 'Header'; tag() { return 'h2'; } content() { return ( <Button> </Button> ); } }
, . , , TypeScript/Flow. , inherit
«» , , .
:
— webpack Babel;
— ;
— , .
HOC , .
import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { Block, Elem, withMods } from 'bem-react-core'; interface IButtonProps { children: string; } interface IModsProps extends IButtonProps { type: 'link' | 'button'; }
, .
withMods
, (), . , , withMods , . . , , , ( ) . . , , — , .
, :
- . , . , TS. , . ES5 TS super , . , TS , .
- . TS ES6 Babel ES5. , npm- . , Babel.
:
- , . , . : DOM-. HOC, . withMods .
- (, , ) . SFC .
- CSS-. CSS- JS- . , , .
v2.
, . . , , 1 2. .
— . CSS- HOC, — dependency injection .
React:
. . React.ComponentType
-. HOC compose .
.
dependency injection, React.ContextAPI
. , , . , . DI — HOC, . . , , .
, , . , , 4 , 1.5Kb
.
. 感谢那些读到最后的人。 , React . .