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 (
            
{~~(value * 1000 / total) / 10}%
{text}
); } } 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: [
                        '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
Модификация базового 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((
                
            ), mountPoint);
        }
    }
});
document.registerElement('donut-chart', {prototype: proto});

Пример использования



Результат



Сборка 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((
                
            ), element[0]);
        }
    };
});

Пример использования



    












Результат



Сборка 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((
                    
                ), 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((
        
    ), 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

Комментарии (2)

  • 5 декабря 2016 в 08:41

    0

    А что насчет биндинга в ангуляре?
  • 5 декабря 2016 в 08:53

    0

    Не думаю, что разработчики на ангуляре будут с неистовством мешать в проекты компоненты написанные на реакте. Разумнее было бы написать компонент на ваниле и адаптировать под конкретный фреймворк или библиотеку.

© Habrahabr.ru