React, Web Components, Angular і jQuery — друзі навіки. Універсальні JavaScript-компоненти

image
Ця стаття про те, як написати універсальний JavaScript-компонент, який можна буде використовувати
  • як React-компонент;
  • як Preact-компонент;
  • як Angular-компонент;
  • як Web Component;
  • jQuery функцію для рендеринга в DOMElement;
  • як нативну функцію для рендеринга в DOMElement.
Навіщо і кому це потрібно
Світ JavaScript-розробки дуже фрагментований. Є десятки популярних фреймворків, велика частина з яких абсолютно несумісні один з одним. В таких умовах розробники JavaScript-компонентів і бібліотек, вибираючи один конкретний фреймворк, автоматично відмовляються від дуже великої аудиторії, що даний фреймворк не використовує. Це серйозна проблема, і в статті запропоновано її рішення.
Як все буде реалізовано
  1. Напишемо React-компонент.
  2. Використовуючи JavaScript-бібліотеки preact і preact-compat, які разом працюють точно так само як React і при цьому важать жалюгідні 20 кілобайт, напишемо обгортки для всього іншого.
  3. Налаштуємо складання з допомогою Webpack-а.
Пишемо код компонента
Для прикладу розробимо Donut Chart такого виду:
Donut Chart
Тут нічого дивного ми не побачимо — просто код.
import React from 'react';

export default class DonutChart extends React.Component {
render() {
const { radius, holeSize, text, value, total, backgroundColor, valueColor } = this.props;
const r = radius * (1 - (1 - holeSize)/2);
const width = radius * (1 - holeSize);
const circumference = 2 * Math.PI * r;
const strokeDasharray = ((value * circumference) / total) + '' + circumference;
const transform = 'rotate(-90' + radius + ',' + radius + ')';
const fontSize = r * holeSize * 0.6;
return (
<div style = {{ textAlign: 'center', fontFamily: 'sans-serif' }}>
<svg width = {radius * 2 + 'px'} height = {radius * 2 + 'px'}>
<circle 
r = {r + 'px'} cx = {radius + 'px'} cy = {radius + 'px'}
transform = {transform} fill = 'none'
stroke = {backgroundColor} strokeWidth = {width}
/>
<circle
r = {r + 'px'} cx = {radius + 'px'} cy = {radius + 'px'}
transform = {transform} fill = 'none'
stroke = {valueColor} strokeWidth = {width}
strokeDasharray = {strokeDasharray}
/>
<text
x = {radius + 'px'} y = {radius + 'px' }dy = {fontSize/3 + 'px'}
textAnchor = 'middle' fill = {valueColor} fontSize = {fontSize + 'px'}
>
{~~(value * 1000 / total) / 10}%
</text>
</svg>
<div style = {{ marginTop: '10px' }}>
{text}
</div>
</div>
);
}
}

DonutChart.defaultProps = {
holeSize : 0.8,
radius : 65,
backgroundColor : '#d1d8e7',
valueColor : '#49649f'
};

Що повинно вийти в результаті
Codepen Collection
Налаштовуємо збірку Webpack-ом
Базовий Webpack-конфіг
var webpack = require('webpack');

module.exports = {
output: {
path: './dist'
},
resolve: {
extensions: [", '.js'],
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: [
'останні',
'stage-0',
'react'
],
plugins: [
'transform-react-remove-prop-types',
'transform-react-constant-elements'
]
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': "'production'"
}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurrenceOrderPlugin(),
new webpack.optimize.AggressiveMergingPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false },
comments: false,
sourceMap: true,
mangle: true,
minimize: true
})
]
};

Додаємо в package.json скрипти для складання проекту
"scripts": {
"build:preact": "node ./scripts/build-as-preact-component.js",
"build:react": "node ./scripts/build-as-react-component.js",
"build:webcomponent": "node ./scripts/build-as-web-component.js",
"build:vanila": "node ./scripts/build-as-vanila-component.js",
"build:jquery": "node ./scripts/build-as-jquery-component",
"build:angular": "node ./scripts/build-as-angular-component",
"build": "npm run build:preact && npm run build:react && npm run build:webcomponent && npm run build:vanila && npm run build:jquery && npm run build:angular"
}

Збірка Webpack-му і обгортка для Web Components
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

config.resolve.alias = {
'react': 'preact-compat',
'react-dom': 'preact-compat'
};
config.entry = './src/DonutChartWebComponent.js';
config.output.filename = 'DonutChartWebComponent.js';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Обгортка
import React from 'react';
import ReactDOM from 'react-dom';
import DonutChart from './DonutChart';

const proto = Object.create(HTMLElement.prototype, {
attachedCallback: {
value: function() {
const mountPoint = document.createElement('span');
this.createShadowRoot().appendChild(mountPoint);
const props = {
radius : +this.getAttribute('radius') || undefined,
holeSize : +this.getAttribute('hole-size') || undefined,
text : this.getAttribute('text') || undefined,
value : +this.getAttribute('value') || undefined,
total : +this.getAttribute('total') || undefined,
backgroundColor : this.getAttribute('background-color') || undefined,
valueColor : this.getAttribute('value-color') || undefined
};
ReactDOM.render((
<DonutChart {...props}/>
), mountPoint);
}
}
});
document.registerElement('donut-chart', {prototype: proto});

Приклад

<donut-chart value="39.6" total="100" text="Hello Web Components"></donut-chart>

Результат


Збірка Webpack-му і обгортка для Angular
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

config.resolve.alias = {
'react': 'preact-compat',
'react-dom': 'preact-compat'
};
config.entry = './src/DonutChartAngularComponent.js';
config.output.filename = 'DonutChartAngularComponent.js';
config.output.library = 'DonutChart';
config.output.libraryTarget = 'umd';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Обгортка
import React from 'react';
import ReactDOM from 'react-dom';
import DonutChart from './DonutChart';

const module = angular.module('future-charts-example', []);

module.directive('donutChart', function() {
return {
restrict: 'E',
link: function(scope, element, attrs) {
const props = {
radius : +attrs['radius'] || undefined,
holeSize : +attrs['hole-size'] || undefined,
text : attrs['text'] || undefined,
value : +attrs['value'] || undefined,
total : +attrs['total'] || undefined,
backgroundColor : attrs['background-color'] || undefined,
valueColor : attrs['value-color'] || undefined
};
ReactDOM.render((
<DonutChart {...props}/>
), element[0]);
}
};
});

Приклад
<body ng-app="future-charts-example">
<donut-chart value="89.6" total="100" text="Hello Angular"></donut-chart>
</body>

Результат

Збірка Webpack-му і обгортка для jQuery
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

config.resolve.alias = {
'react': 'preact-compat',
'react-dom': 'preact-compat'
};
config.entry = './src/DonutChartJQueryComponent.js';
config.output.filename = 'DonutChartJQueryComponent.js';
config.output.library = 'DonutChart';
config.output.libraryTarget = 'umd';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Обгортка
import React from 'react';
import ReactDOM from 'react-dom';
import DonutChart from './DonutChart';

jQuery.fn.extend({
DonutChart: function(props) {
this.each(
function () {
ReactDOM.render((
<DonutChart {...props}/>
), this);
}
);
}
});

Приклад
$('#app').DonutChart({
value : 42.1,
total : 100,
text : 'Hello jQuery'
});

Результат

Збірка Webpack-му і обгортка для VanilaJS (використання з нативної функції)
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

config.resolve.alias = {
'react': 'preact-compat',
'react-dom': 'preact-compat'
};
config.entry = './src/DonutChartVanilaComponent.js';
config.output.filename = 'DonutChartVanilaComponent.js';
config.output.library = 'DonutChart';
config.output.libraryTarget = 'umd';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Обгортка
import React from 'react';
import ReactDOM from 'react-dom';
import DonutChart from './DonutChart';

module.exports = function DonutChartVanilaComponent(mountPoint, props) {
ReactDOM.render((
<DonutChart {...props}/>
), mountPoint);
};

Приклад
DonutChart(document.getElementById('app'), {
value : 57.4,
total : 100,
text : 'Hello Vanila'
});

Результат

Збірка Webpack-ом для React
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

var react = {
root: 'React',
commonjs2: 'react',
commonjs: 'react'
};

var reactDom = {
root: 'ReactDOM',
commonjs2: 'react-dom',
commonjs: 'react-dom'
};

config.externals = {
'react': react,
'react-dom': reactDom
};
config.entry = './src/DonutChartUMD.js';
config.output.filename = 'DonutChartReact.js';
config.output.library = 'DonutChart';
config.output.libraryTarget = 'umd';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Результат


Збірка Webpack-ом для Preact
Модифікація базового Webpack-конфига і збірка
var webpack = require('webpack');
var config = require('./webpack.config');
var statsConfig = require('./statsConfig');

var preactCompat = {
root: 'preactCompat',
commonjs2: 'preact-compat',
commonjs: 'preact-compat'
};

config.externals = {
'react': preactCompat,
'react-dom': preactCompat
};
config.entry = './src/DonutChartUMD.js';
config.output.filename = 'DonutChartPreact.js';
config.output.library = 'DonutChart';
config.output.libraryTarget = 'umd';

webpack(config).run(function (err, stats) {
console.log(stats.toString(statsConfig));
});

Результат

Висновок
Скільки в підсумку буде важити кожен з варіантів:








React Preact VanilaJS jQuery Angular Web Components Код компонента (3кб) Код компонента (3кб) Код компонента (3кб) Код компонента (3кб) Код компонента (3кб) Код компонента (3кб) Обгортка (1кб) Обгортка(1кб) Обгортка(1кб) Обгортка (1кб) preact.min.js (3кб) preact.min.js (3кб) preact.min.js (3кб) preact.min.js (3кб) preact-compat.min.js (18кб) preact-compat.min.js (18кб) preact-compat.min.js (18кб) preact-compat.min.js (18кб) 3кб 3кб 25кб 25кб 25кб 25кб
Оверхед в 20 кілобайт за можливість використовувати React-компоненти в будь-яких інших фреймворках або в якості Web Components — це прекрасний результат. Якщо ви розробляєте якісь React-компоненти, знайте — ви можете зробити їх доступними всім і кожному — це дуже просто. Сподіваюся, що цей туторіал допоможе зробити світ хоча б трішки краще і скоротить страшну фрагментацію всесвіту JavaScript-розробки.
Джерело: Github, Codepen, NPM
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.