React, Web Components, Angular и jQuery — друзья навеки. Универсальные JavaScript-компоненты
Эта статья о том, как написать универсальный JavaScript-компонент, который можно будет использовать
- как React-компонент;
- как Preact-компонент;
- как Angular-компонент;
- как Web Component;
- как jQuery функцию для рендеринга в DOMElement;
- как нативную функцию для рендеринга в DOMElement.
Зачем и кому это нужно
Мир JavaScript-разработки очень фрагментирован. Есть десятки популярных фреймворков, большая часть из которых абсолютно несовместима друг с другом. В таких условиях разработчики JavaScript-компонентов и библиотек, выбирая один конкретный фреймворк, автоматически отказываются от очень большой аудитории, которая данный фреймворк не использует. Это серьезная проблема, и в статье предложено ее решение.
Как все будет реализовано
- Напишем React-компонент.
- Используя JavaScript-библиотеки preact и preact-compat, которые вместе работают точно так же как React и при этом весят жалкие 20 килобайт, напишем обертки для всего остального.
- Настроим сборку с помощью Webpack-а.
Пишем код компонента
Для примера разработаем 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 (
{text}
);
}
}
DonutChart.defaultProps = {
holeSize : 0.8,
radius : 65,
backgroundColor : '#d1d8e7',
valueColor : '#49649f'
};
Что должно получиться в итоге
Настраиваем сборку 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: [
'latest',
'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
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((
), mountPoint);
}
}
});
document.registerElement('donut-chart', {prototype: proto});
Пример использования
Результат
Сборка Webpack-ом и обертка для Angular
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((
), element[0]);
}
};
});
Пример использования
Результат
Сборка Webpack-ом и обертка для jQuery
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((
), this);
}
);
}
});
Пример использования
$('#app').DonutChart({
value : 42.1,
total : 100,
text : 'Hello jQuery'
});
Результат
Сборка Webpack-ом и обертка для VanilaJS (использование из нативной функции)
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((
), mountPoint);
};
Пример использования
DonutChart(document.getElementById('app'), {
value : 57.4,
total : 100,
text : 'Hello Vanila'
});
Результат
Сборка Webpack-ом для React
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
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
Комментарии (2)
5 декабря 2016 в 08:41
0↑
↓
А что насчет биндинга в ангуляре?5 декабря 2016 в 08:53
0↑
↓
Не думаю, что разработчики на ангуляре будут с неистовством мешать в проекты компоненты написанные на реакте. Разумнее было бы написать компонент на ваниле и адаптировать под конкретный фреймворк или библиотеку.