Упрощаем «жизнь»
Лебедев Константин
Задача
Project/README
npm install
Не заыбвайте про
preinstall
—  Установка registry на локальное хранилище
postinstall
—   Добавление githook'ов
—  pre-commit: grunt dev
—  post-merge: npm install
—   jam install — обновление jam-пакетов
—  Структура вложенности блоков
—  Путь к файлу и строка (ссылка, открывает файл в IDE)
—  Примеры использования с текущими параметрами
—  «Глаз» — открыть GUI c блоком для просмотра
Feast GUI
Что произошло?
—  spa-meetup-2.html — шаблон блока
—  spa-meetup-2.scss — стили
—  spa-meetup-2.js — описание поведения
—  spa-meetup-2.spec.js — спецификация, на основе которой строятся
примеры использования
./node_modules/feast/bin/assist create-block 
--path=path/to/ui-blocks/ 
01.
02.
<div> </div> .spa-meetup-2 {
padding: 10px;
background: red;
}
import feast from "feast";
import template from "feast-tpl!./spa-meetup-2.html";
import styleSheet from "feast-css!./spa-meetup-2.css";
 
export default feast.Block.extend({
name: "spa-meetup-2",
template,
styleSheet
});
01. 01.
02.
03.
04.
01.
02.
03.
04.
05.
06.
07.
08.
09.
avito-spa-meetup-2.html
<div>
<div bem:elem="picture">
<b:avatar src="..."/>
</div>
<div bem:elem="details">
<div bem:elem="description"><!-- описание --></div>
<b:button remit:click="close" text="Done"/>
</div>
</div>
01.
02.
03.
04.
05.
06.
07.
08.
09.
Как это работает?
2012 / Fest
Fest
—  XML -> (NODEJS) -> JSFUNC_TPL -> STRING
—  Вывод ошибок (файл + строка)
—  Трансформация на сервере ~1ms
—  Запуск «Главной» на Fest
—  Интеграция в «Почту», запуск «Календаря»
—  Разработка внутреннего UI Toolkit (библиотека блоков)
—  Запуск новой версии «Почты» и «Облака» на базе Toolkit
Fest / Проблемы
Возвращает строку
Feast
—  XHTML (CLIENT) -> JSFUNC_TPL -> JSON -> CITOJS -> VDOM
—  GUI для просмотра и редактирования блоков (Live coding
HTML/CSS/JS)
—  Инспектор блоков проекта
—  Визуализация связей
Live coding
—  BrowserSync
—  Webpack
—  node-watch + socket.io
Server
"use strict";
const fs = require("fs");
const http = require("http");
const watch = require("node-watch");
const app = http.createServer((req, res) => {
res.writeHead(200, {"Content-Type": "html/text"});
res.end();
});
const socketIO = require("socket.io")(app);
app.listen(2016, () => {
watch("path/to", {recursive: true}, (file) => {
fs.readFile(file, (err, content) => {
const ext = file.split(".").pop();
socketIO.emit(`file-changed:${ext}`, {file, content});
});
});
});
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
15.
16.
17.
Client
<script src="//cdnjs/socket.io"></script>
<script>
const socket = io();
socket.on("file-changed:html", ({file, content}) => {
// ...
});
</script>
01.
02.
03.
04.
05.
06.
07.
<div class="btn">
Click me!
</div>
XML Parser
{
name: "div",
attrs: {class: "btn"},
file: "path/to/button.html",
line: 1,
children: [{
name: "#text",
value: "Click me!",
file: "path/to/button.html",
line: 2
}]
01.
02.
03.
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
<fn:for data="attrs.items" as="key">...</fn:for>
"fn:for": {
scope: true,
required: ["data"],
expressions: ["data"],
prepare: (node, attrs) => ({
as: attrs.as || "$value",
key: attrs.key || "$index",
data: attrs.data
}),
toCode: () => [
"__each($data, @@.children, function (&as, &key) {", // начало
 
"});" // конец
];
}
}
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
15.
16.
17.
Оновление шаблона
socket.on("file-changed:html", ({file, content}) => {
feast.Block.all.forEach(Block => {
if (file === Block.prototype.template.file) {
const template = feast.parse(content, file);
Block.setTemplate(template);
Block.getInstances().forEach(block => block.render());
}
});
01.
02.
03.
04.
05.
06.
07.
08.
CSS Modules
CSS Modules: PostCSS + React
import React from "react";
import styles from "./button.css";
 
export default class Button extends React.Component {
render() {
return (
<button className={styles.btn}>
<span className={styles.icon}>
<Icon name={this.props.icon}/>
</span>
<span className={styles.text}>{this.props.value}</span>
</button>
);
}
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
PostCSS + React + react-css-modules
import React from "react";
import CSSModules from "react-css-modules";
import styles from "./button.css";
 
class Button extends React.Component {
render () {
return <button styleName="btn">
<span styleName="icon">
<Icon name={this.props.icon}/>
</span>
<span styleName="text">{this.props.value}</span>
</button>;
}
}
 
export default CSSModules(Button, styles);
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
15.
16.
@decorator
import React from "react";
import CSSModules from "react-css-modules";
import styles from "./button.css";
 
@CSSModules(styles)
export default class Button extends React.Component {
render () {
return <button styleName="btn">
<span styleName="icon">
<Icon name={this.props.icon}/>
</span>
<span styleName="text">{this.props.value}</span>
</button>;
}
}
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
15.
Angular2
@Component({
selector: "my-app",
template: `<div class="app">{{text}}</div>`,
styles: [`.app { ... }`] // .app[_ngcontent-mjn-1] { }
})
export class App {
// ...
}
01.
02.
03.
04.
05.
06.
07.
08.
Feast
import feast from "feast";
import template from "feast-tpl!./button.html";
import styleSheet from "feast-css!./button.css";
 
export default feast.Block.extend({
name: "button",
template,
styleSheet
});
01.
02.
03.
04.
05.
06.
07.
08.
09.
CSS Module «на коленке»
function toCSSModule(file, cssText) {
const classes = {};
cssText = cssText.replace(R_CSS_SELECTOR, (_, name) => {
classes[name] = name + "-" + simpleHash(file + name);
return "." + classes[name];
});
return {file, cssText, classes};
}
01.
02.
03.
04.
05.
06.
07.
08.
CSS Module «на коленке»
function simpleHash(str) {
let idx = str.length;
let hash = UNIQ_SEED;
 
while (idx--) {
hash = (hash * 33) ^ str.charCodeAt(idx);
}
 
return (hash >>> 0).toString(36);
}
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
CSS Module «на коленке»
{
file: "path/to/button.css"
cssText: "...",
classes: {
"button": "button-v9m8ae"
}
}
01.
02.
03.
04.
05.
06.
07.
JS / Hot Reload
JS / Hot Reload
class Foo {
constructor(value) {
this.value = value;
}
log() {
console.log(`Foo: ${this.value}`, this instanceof Foo);
}
}
01.
02.
03.
04.
05.
06.
07.
08.
JS / Hot Reload
var foo = new Foo(123);
foo.log(); // "Foo: 123", true
 
replaceClass(Foo, class NewFoo {
constructor(value) {
this.value = value;
}
log() {
console.log(`NewFoo: ${this.value}`, this instanceof NewFoo);
}
});
 
foo.log(); // "NewFoo: 123", true
foo instanceof Foo; // true
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
JS / Hot Reload
function replaceClass(OldClass, NewClass) {
const newProto = NewClass.prototype;
OldClass.prototype.__proto__ = newProto;
 
// Static
Object.keys(NewClass).forEach(name => {
OldClass[name] = NewClass[name];
});
 
// Prototype
Object.getOwnPropertyNames(newProto).forEach(name => {
OldClass.prototype[name] = newProto[name];
});
}
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
JS / Hot Reload / Проблема
replaceClass(Foo, class NewFoo {
/* ... */
});
 
foo.constructor === Foo; // false
01.
02.
03.
04.
05.
JS / Hot Reload / Проблема / Решение
—  Webpack — оборачивает создание класса в специальный фраппер
—  Использовать обвязку для создания классов, например
createClass('MyClassName', {...});
Live Coding
—  React — Webpack/React Hot loader/Redux
—  Angular2 — mgechev/angular2-hot-loader (экспериментально)
—  Так же Hot Relaod есть для Ember, Vue, RiotJS и т.п.
Что ещё?
Extentions / DevTools
lahmatiy/component-inspector
// app/packages/require.config.js
var packages = [{
name: "suggests",
location: "packages/suggests"
}, {
name: "push-manager",
location: "packages/push-manager"
}];
require.config({packages: packages});
Пакеты: npm/bower/jam
jam install suggests --save
jam install push-manager --save
// app/some-controller.js
import suggests from "suggests";
import PushManager from "push-manager";
 
// ...
01.
02.
01.
02.
03.
04.
05.
06.
07.
08.
09.
01.
02.
03.
04.
05.
logger
Istanbul
Не протестировано
↓
Заключение
—  Документируйте процессы
—  Ищите инструменты и создавайте их сами
—  Не описывайте «шаги», пишите утилиты, которые их выполнят
—  Старайтесь писать универсальные компоненты, неотягощенные
сложной логикой
The End
github.com/RubaXa
@ibnRubaXa

Avito / SPA Meetup 2

  • 1.
  • 2.
  • 7.
  • 9.
    npm install Не заыбвайтепро preinstall —  Установка registry на локальное хранилище postinstall —   Добавление githook'ов —  pre-commit: grunt dev —  post-merge: npm install —   jam install — обновление jam-пакетов
  • 14.
    —  Структура вложенности блоков —  Путьк файлу и строка (ссылка, открывает файл в IDE) —  Примеры использования с текущими параметрами —  «Глаз» — открыть GUI c блоком для просмотра
  • 16.
  • 20.
    Что произошло? —  spa-meetup-2.html —шаблон блока —  spa-meetup-2.scss — стили —  spa-meetup-2.js — описание поведения —  spa-meetup-2.spec.js — спецификация, на основе которой строятся примеры использования ./node_modules/feast/bin/assist create-block --path=path/to/ui-blocks/ 01. 02.
  • 21.
    <div> </div> .spa-meetup-2 { padding:10px; background: red; } import feast from "feast"; import template from "feast-tpl!./spa-meetup-2.html"; import styleSheet from "feast-css!./spa-meetup-2.css";   export default feast.Block.extend({ name: "spa-meetup-2", template, styleSheet }); 01. 01. 02. 03. 04. 01. 02. 03. 04. 05. 06. 07. 08. 09.
  • 25.
    avito-spa-meetup-2.html <div> <div bem:elem="picture"> <b:avatar src="..."/> </div> <divbem:elem="details"> <div bem:elem="description"><!-- описание --></div> <b:button remit:click="close" text="Done"/> </div> </div> 01. 02. 03. 04. 05. 06. 07. 08. 09.
  • 26.
  • 27.
  • 28.
    Fest —  XML -> (NODEJS)-> JSFUNC_TPL -> STRING —  Вывод ошибок (файл + строка) —  Трансформация на сервере ~1ms —  Запуск «Главной» на Fest —  Интеграция в «Почту», запуск «Календаря» —  Разработка внутреннего UI Toolkit (библиотека блоков) —  Запуск новой версии «Почты» и «Облака» на базе Toolkit
  • 29.
  • 30.
    Feast —  XHTML (CLIENT) ->JSFUNC_TPL -> JSON -> CITOJS -> VDOM —  GUI для просмотра и редактирования блоков (Live coding HTML/CSS/JS) —  Инспектор блоков проекта —  Визуализация связей
  • 31.
  • 32.
    Server "use strict"; const fs= require("fs"); const http = require("http"); const watch = require("node-watch"); const app = http.createServer((req, res) => { res.writeHead(200, {"Content-Type": "html/text"}); res.end(); }); const socketIO = require("socket.io")(app); app.listen(2016, () => { watch("path/to", {recursive: true}, (file) => { fs.readFile(file, (err, content) => { const ext = file.split(".").pop(); socketIO.emit(`file-changed:${ext}`, {file, content}); }); }); }); 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17.
  • 33.
    Client <script src="//cdnjs/socket.io"></script> <script> const socket= io(); socket.on("file-changed:html", ({file, content}) => { // ... }); </script> 01. 02. 03. 04. 05. 06. 07.
  • 34.
    <div class="btn"> Click me! </div> XMLParser { name: "div", attrs: {class: "btn"}, file: "path/to/button.html", line: 1, children: [{ name: "#text", value: "Click me!", file: "path/to/button.html", line: 2 }] 01. 02. 03. 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11.
  • 35.
    <fn:for data="attrs.items" as="key">...</fn:for> "fn:for":{ scope: true, required: ["data"], expressions: ["data"], prepare: (node, attrs) => ({ as: attrs.as || "$value", key: attrs.key || "$index", data: attrs.data }), toCode: () => [ "__each($data, @@.children, function (&as, &key) {", // начало   "});" // конец ]; } } 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16. 17.
  • 36.
    Оновление шаблона socket.on("file-changed:html", ({file,content}) => { feast.Block.all.forEach(Block => { if (file === Block.prototype.template.file) { const template = feast.parse(content, file); Block.setTemplate(template); Block.getInstances().forEach(block => block.render()); } }); 01. 02. 03. 04. 05. 06. 07. 08.
  • 37.
  • 38.
    CSS Modules: PostCSS+ React import React from "react"; import styles from "./button.css";   export default class Button extends React.Component { render() { return ( <button className={styles.btn}> <span className={styles.icon}> <Icon name={this.props.icon}/> </span> <span className={styles.text}>{this.props.value}</span> </button> ); } 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14.
  • 39.
    PostCSS + React+ react-css-modules import React from "react"; import CSSModules from "react-css-modules"; import styles from "./button.css";   class Button extends React.Component { render () { return <button styleName="btn"> <span styleName="icon"> <Icon name={this.props.icon}/> </span> <span styleName="text">{this.props.value}</span> </button>; } }   export default CSSModules(Button, styles); 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15. 16.
  • 40.
    @decorator import React from"react"; import CSSModules from "react-css-modules"; import styles from "./button.css";   @CSSModules(styles) export default class Button extends React.Component { render () { return <button styleName="btn"> <span styleName="icon"> <Icon name={this.props.icon}/> </span> <span styleName="text">{this.props.value}</span> </button>; } } 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14. 15.
  • 41.
    Angular2 @Component({ selector: "my-app", template: `<divclass="app">{{text}}</div>`, styles: [`.app { ... }`] // .app[_ngcontent-mjn-1] { } }) export class App { // ... } 01. 02. 03. 04. 05. 06. 07. 08.
  • 42.
    Feast import feast from"feast"; import template from "feast-tpl!./button.html"; import styleSheet from "feast-css!./button.css";   export default feast.Block.extend({ name: "button", template, styleSheet }); 01. 02. 03. 04. 05. 06. 07. 08. 09.
  • 43.
    CSS Module «наколенке» function toCSSModule(file, cssText) { const classes = {}; cssText = cssText.replace(R_CSS_SELECTOR, (_, name) => { classes[name] = name + "-" + simpleHash(file + name); return "." + classes[name]; }); return {file, cssText, classes}; } 01. 02. 03. 04. 05. 06. 07. 08.
  • 44.
    CSS Module «наколенке» function simpleHash(str) { let idx = str.length; let hash = UNIQ_SEED;   while (idx--) { hash = (hash * 33) ^ str.charCodeAt(idx); }   return (hash >>> 0).toString(36); } 01. 02. 03. 04. 05. 06. 07. 08. 09. 10.
  • 45.
    CSS Module «наколенке» { file: "path/to/button.css" cssText: "...", classes: { "button": "button-v9m8ae" } } 01. 02. 03. 04. 05. 06. 07.
  • 46.
    JS / HotReload
  • 47.
    JS / HotReload class Foo { constructor(value) { this.value = value; } log() { console.log(`Foo: ${this.value}`, this instanceof Foo); } } 01. 02. 03. 04. 05. 06. 07. 08.
  • 48.
    JS / HotReload var foo = new Foo(123); foo.log(); // "Foo: 123", true   replaceClass(Foo, class NewFoo { constructor(value) { this.value = value; } log() { console.log(`NewFoo: ${this.value}`, this instanceof NewFoo); } });   foo.log(); // "NewFoo: 123", true foo instanceof Foo; // true 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14.
  • 49.
    JS / HotReload function replaceClass(OldClass, NewClass) { const newProto = NewClass.prototype; OldClass.prototype.__proto__ = newProto;   // Static Object.keys(NewClass).forEach(name => { OldClass[name] = NewClass[name]; });   // Prototype Object.getOwnPropertyNames(newProto).forEach(name => { OldClass.prototype[name] = newProto[name]; }); } 01. 02. 03. 04. 05. 06. 07. 08. 09. 10. 11. 12. 13. 14.
  • 50.
    JS / HotReload / Проблема replaceClass(Foo, class NewFoo { /* ... */ });   foo.constructor === Foo; // false 01. 02. 03. 04. 05.
  • 51.
    JS / HotReload / Проблема / Решение —  Webpack — оборачивает создание класса в специальный фраппер —  Использовать обвязку для создания классов, например createClass('MyClassName', {...});
  • 52.
    Live Coding —  React —Webpack/React Hot loader/Redux —  Angular2 — mgechev/angular2-hot-loader (экспериментально) —  Так же Hot Relaod есть для Ember, Vue, RiotJS и т.п.
  • 53.
  • 54.
  • 55.
  • 56.
    // app/packages/require.config.js var packages= [{ name: "suggests", location: "packages/suggests" }, { name: "push-manager", location: "packages/push-manager" }]; require.config({packages: packages}); Пакеты: npm/bower/jam jam install suggests --save jam install push-manager --save // app/some-controller.js import suggests from "suggests"; import PushManager from "push-manager";   // ... 01. 02. 01. 02. 03. 04. 05. 06. 07. 08. 09. 01. 02. 03. 04. 05.
  • 57.
  • 58.
  • 59.
  • 63.
    Заключение —  Документируйте процессы —  Ищите инструментыи создавайте их сами —  Не описывайте «шаги», пишите утилиты, которые их выполнят —  Старайтесь писать универсальные компоненты, неотягощенные сложной логикой
  • 64.