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

в 4:59, , рубрики: angular, components, html, javascript, jquery, preact, React, ReactJS, web components, Блог компании DevExpress, будущее, Программирование

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: [
                        '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((
                <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

Автор: DevExpress

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js