
Эта статья о том, как написать универсальный 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 ( <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' };
Что должно получиться в итоге
Настраиваем сборку 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(( <DonutChart {...props}/> ), mountPoint); } } }); document.registerElement('donut-chart', {prototype: proto});
Пример использования
<donut-chart value="39.6" total="100" text="Hello Web Components"></donut-chart>
Результат
Сборка 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(( <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
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 (использование из нативной функции)
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
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
ссылка на оригинал статьи https://habrahabr.ru/post/316358/

Добавить комментарий