JSX
Описание
JSX — это расширение синтаксиса JavaScript, позволяющее встраивать HTML-теги непосредственно в код JavaScript. Он не является частью стандарта JavaScript и не обязателен для разработки приложений, но может быть предпочтительным синтаксисом для вас или вашей команды.
function MyComponent() {
return {
view: () => m('main', [m('h1', 'Hello world')]),
};
}
// можно записать как:
function MyComponent() {
return {
view: () => (
<main>
<h1>Hello world</h1>
</main>
),
};
}
При использовании JSX вы можете вставлять JavaScript-выражения внутрь JSX-тегов, используя фигурные скобки:
var greeting = 'Hello';
var url = 'https://google.com';
var link = <a href={url}>{greeting}!</a>;
// выводит <a href="https://google.com">Hello!</a>
Компоненты можно использовать, указывая имя компонента с заглавной буквы или обращаясь к нему как к свойству:
m.render(document.body, <MyComponent />)
// эквивалентно m.render(document.body, m(MyComponent))
<m.route.Link href="/home">Go home</m.route.Link>
// эквивалентно m(m.route.Link, {href: "/home"}, "Go home")
Настройка
Рекомендуемый способ использования JSX — с помощью плагина Babel.
Babel требует npm, который автоматически устанавливается при установке Node.js. После установки npm создайте папку проекта и выполните следующую команду:
npm init -y
Если вы хотите использовать Webpack и Babel вместе, перейдите к разделу ниже.
Чтобы установить Babel как отдельный инструмент, используйте эту команду:
npm install @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
Создайте файл .babelrc
:
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "m",
"pragmaFrag": "'['"
}
]
]
}
Чтобы запустить Babel, настройте npm-скрипт. Откройте package.json
и добавьте следующую запись в раздел "scripts"
:
{
"name": "my-project",
"scripts": {
"babel": "babel src --out-dir bin --source-maps"
}
}
Теперь вы можете запустить Babel, выполнив эту команду:
npm run babel
Использование Babel с Webpack
Если вы еще не установили Webpack в качестве сборщика модулей, используйте эту команду:
npm install webpack webpack-cli --save-dev
Вы можете интегрировать Babel с Webpack, выполнив следующие шаги.
npm install @babel/core babel-loader @babel/preset-env @babel/plugin-transform-react-jsx --save-dev
Создайте файл .babelrc
:
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "m",
"pragmaFrag": "'['"
}
]
]
}
Затем создайте файл с именем webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './bin'),
filename: 'app.js',
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /\/node_modules\//,
use: {
loader: 'babel-loader',
},
},
],
},
resolve: {
extensions: ['.js', '.jsx'],
},
};
Если вы знакомы с Webpack, учтите, что добавление параметров Babel непосредственно в раздел babel-loader
файла webpack.config.js
приведет к ошибке. Вместо этого укажите их в отдельном файле .babelrc
.
Эта конфигурация предполагает, что файл исходного кода для точки входа приложения находится в src/index.js
, и он выведет бандл в bin/app.js
.
Чтобы запустить сборщик модулей, настройте npm-скрипт. Откройте package.json
и добавьте следующую запись в раздел "scripts"
:
{
"name": "my-project",
"scripts": {
"start": "webpack --mode development --watch"
}
}
Теперь вы можете запустить сборщик модулей, выполнив следующую команду в командной строке:
npm start
Продакшен сборка
Чтобы сгенерировать минифицированный файл, откройте package.json
и добавьте новый npm-скрипт с именем build
:
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p"
}
}
Вы можете использовать хуки в своей рабочей среде, чтобы автоматически запускать скрипт сборки для production. Вот пример для Heroku:
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p",
"heroku-postbuild": "webpack -p"
}
}
Предоставление m
глобально доступным
Чтобы получить доступ к m
глобально из всего вашего проекта, сначала импортируйте webpack
в ваш webpack.config.js
следующим образом:
const webpack = require('webpack');
Затем создайте новый плагин в свойстве plugins
объекта конфигурации Webpack:
{
plugins: [
new webpack.ProvidePlugin({
m: 'mithril',
}),
];
}
Смотрите документацию Webpack для получения дополнительной информации о ProvidePlugin
.
Отличия от React
JSX в Mithril отличается от JSX в React некоторыми важными нюансами.
Соглашения о регистре для атрибутов и свойств style
React требует использования camelCase для имен DOM-свойств вместо HTML-атрибутов, за исключением data-*
и aria-*
. Например, использование className
вместо class
и htmlFor
вместо for
. В Mithril более принято использовать имена HTML-атрибутов в нижнем регистре. Mithril всегда пытается установить атрибуты, если свойство не существует, что более интуитивно соответствует HTML. Обратите внимание, что в большинстве случаев имена свойств DOM и HTML-атрибутов либо одинаковы, либо очень похожи. Например, value
/checked
для inputs и глобальный атрибут tabindex
против свойства elem.tabIndex
для HTML-элементов. Очень редко они отличаются чем-то большим, чем регистр: свойство elem.className
для атрибута class
или свойство elem.htmlFor
для атрибута for
— одни из немногих исключений.
Аналогично, React всегда использует имена свойств стиля в camelCase, представленные в DOM через свойства elem.style
(например, cssHeight
и backgroundColor
). Mithril поддерживает как это, так и имена CSS-свойств в стиле kebab-case (например, height
и background-color
) и обычно предпочитает последнее. Только cssHeight
, cssFloat
и свойства с префиксами поставщиков отличаются чем-то большим, чем регистр.
DOM events
React использует camelCase для именования обработчиков событий: onClick
для события click
, onSubmit
для события submit
. Некоторые дополнительно изменяются, поскольку они представляют собой несколько слов, объединенных вместе. Например, onMouseMove
прослушивает события mousemove
. Mithril не выполняет это преобразование регистра, а просто добавляет on
к имени нативного события, поэтому вы добавите прослушиватели для onclick
и onmousemove
, чтобы прослушивать эти два события соответственно. Это гораздо больше соответствует схеме именования HTML и гораздо более интуитивно понятно, если вы пришли из HTML или vanilla DOM.
React поддерживает планирование прослушивателей событий во время фазы захвата (capture phase) (в первом проходе, снаружи внутрь, в отличие от фазы всплытия (bubble phase) по умолчанию, идущей внутрь наружу во втором проходе), добавляя Capture
к этому событию. Mithril в настоящее время не имеет такой функциональности, но она может появиться в будущем. Если это необходимо, вы можете вручную добавлять и удалять свои собственные прослушиватели в lifecycle hooks.
JSX vs hyperscript
JSX и hyperscript — это два разных синтаксиса для описания vnodes, каждый из которых имеет свои особенности:
JSX гораздо более понятен, если вы пришли из HTML/XML и вам удобнее описывать DOM-элементы с помощью такого синтаксиса. Он также немного чище во многих случаях, поскольку использует меньше знаков препинания, а атрибуты содержат меньше визуального шума, поэтому многим людям его гораздо легче читать. И, конечно, многие распространенные редакторы предоставляют поддержку автозаполнения для DOM-элементов так же, как и для HTML. Однако для его использования требуется дополнительный этап сборки, поддержка редакторов не так широка, как в обычном JS, и он значительно более многословен. Он также немного более многословен при работе с большим количеством динамического контента, потому что вам нужно использовать интерполяции для всего.
Hyperscript может быть более предпочтительным, если у вас больше опыта в серверном JavaScript, чем в HTML/XML. Он более лаконичен с меньшим количеством избыточности и предоставляет удобный CSS-подобный синтаксис для статических классов, идентификаторов и других атрибутов. Его также можно использовать без этапа сборки, хотя вы можете добавить его, если хотите. И с ним немного легче работать при большом количестве динамического контента, потому что вам не нужно ничего "интерполировать". Однако краткость затрудняет чтение для некоторых людей, особенно для тех, у кого меньше опыта и кто пришел из HTML/CSS/XML, и я не знаю ни одного плагина, который автоматически завершает части селекторов hyperscript, такие как идентификаторы, классы и атрибуты.
Вы можете увидеть, как особенности вступают в игру в более сложных деревьях. Например, рассмотрим это дерево hyperscript, адаптированное из реального проекта @dead-claudia с некоторыми изменениями для ясности и удобочитаемости:
function SummaryView() {
let tag, posts;
function init({ attrs }) {
Model.sendView(attrs.tag != null);
if (attrs.tag != null) {
tag = attrs.tag.toLowerCase();
posts = Model.getTag(tag);
} else {
tag = undefined;
posts = Model.posts;
}
}
function feed(type, href) {
return m('.feed', [
type,
m('a', { href }, m('img.feed-icon[src=./feed-icon-16.gif]')),
]);
}
return {
oninit: init,
// Чтобы тег правильно сравнивался при изменении маршрута.
onbeforeupdate: init,
view: () =>
m('.blog-summary', [
m('p', 'My ramblings about everything'),
m('.feeds', [
feed('Atom', 'blog.atom.xml'),
feed('RSS', 'blog.rss.xml'),
]),
tag != null
? m(TagHeader, { len: posts.length, tag })
: m('.summary-header', [
m('.summary-title', 'Posts, sorted by most recent.'),
m(TagSearch),
]),
m(
'.blog-list',
posts.map(post =>
m(
m.route.Link,
{
class: 'blog-entry',
href: `/posts/${post.url}`,
},
[
m(
'.post-date',
post.date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
),
m('.post-stub', [
m('.post-title', post.title),
m('.post-preview', post.preview, '...'),
]),
m(TagList, { post, tag }),
]
)
)
),
]),
};
}
Вот точный эквивалент приведенного выше кода, использующий JSX. Вы можете увидеть, как различаются эти два синтаксиса только в этом бите, и какие компромиссы применяются.
function SummaryView() {
let tag, posts;
function init({ attrs }) {
Model.sendView(attrs.tag != null);
if (attrs.tag != null) {
tag = attrs.tag.toLowerCase();
posts = Model.getTag(tag);
} else {
tag = undefined;
posts = Model.posts;
}
}
function feed(type, href) {
return (
<div class="feed">
{type}
<a href={href}>
<img class="feed-icon" src="./feed-icon-16.gif" />
</a>
</div>
);
}
return {
oninit: init,
// Чтобы тег правильно дифференцировался при смене маршрута.
onbeforeupdate: init,
view: () => (
<div class="blog-summary">
<p>My ramblings about everything</p>
<div class="feeds">
{feed('Atom', 'blog.atom.xml')}
{feed('RSS', 'blog.rss.xml')}
</div>
{tag != null ? (
<TagHeader len={posts.length} tag={tag} />
) : (
<div class="summary-header">
<div class="summary-title">Posts, sorted by most recent</div>
<TagSearch />
</div>
)}
<div class="blog-list">
{posts.map(post => (
<m.route.Link class="blog-entry" href={`/posts/${post.url}`}>
<div class="post-date">
{post.date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<div class="post-stub">
<div class="post-title">{post.title}</div>
<div class="post-preview">{post.preview}...</div>
</div>
<TagList post={post} tag={tag} />
</m.route.Link>
))}
</div>
</div>
),
};
}
Советы и хитрости
Преобразование HTML в JSX
В Mithril.js валидный HTML обычно является валидным JSX. Для того чтобы все просто работало, требуется немного больше, чем просто вставка необработанного HTML. Единственное, что вам обычно нужно сделать, это изменить значения свойств без кавычек, такие как attr=value
, на attr="value"
, и изменить самозакрывающиеся теги, такие как <input>
, на <input />
. Это связано с тем, что JSX основан на XML, а не на HTML.
При использовании hyperscript вам часто необходимо преобразовать HTML в синтаксис hyperscript, чтобы использовать его. Чтобы ускорить этот процесс, вы можете использовать созданный сообществом конвертер HTML в Mithril-шаблон, чтобы сделать большую часть работы за вас.