JSX
描述
JSX 是一种 JavaScript 语法扩展,允许您在 JavaScript 代码中编写类似 HTML 的标签。它不是 JavaScript 标准,也不是构建应用程序的必要条件,但如果您或您的团队喜欢,可以使用它。
function MyComponent() {
return {
view: () => m('main', [m('h1', 'Hello world')]),
};
}
// 可以写成:
function MyComponent() {
return {
view: () => (
<main>
<h1>Hello world</h1>
</main>
),
};
}
使用 JSX 时,可以通过花括号在 JSX 标签中插入 JavaScript 表达式:
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 选项添加到 webpack.config.js
的 babel-loader
部分会导致错误,因此您需要将它们包含在单独的 .babelrc
文件中。
此配置假定应用程序入口点的源代码文件位于 src/index.js
中,打包后的文件将输出到 bin/app.js
。
要运行打包器,请设置一个 npm 脚本。打开 package.json
并在 "scripts"
下添加此条目:
{
"name": "my-project",
"scripts": {
"start": "webpack --mode development --watch"
}
}
现在,您可以通过从命令行运行以下命令来运行打包器:
npm start
生产构建
要生成一个经过最小化的文件,打开 package.json
并添加一个新的名为 build
的 npm 脚本:
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p"
}
}
您可以使用生产环境中的钩子来自动运行生产构建脚本。这是一个 Heroku 的示例:
{
"name": "my-project",
"scripts": {
"start": "webpack -d --watch",
"build": "webpack -p",
"heroku-postbuild": "webpack -p"
}
}
使 m
全局可访问
为了从您的所有项目中全局访问 m
,首先需要在您的 webpack.config.js
中导入 webpack
,如下所示:
const webpack = require('webpack');
然后在 Webpack 配置对象的 plugins
属性中创建一个新插件:
{
plugins: [
new webpack.ProvidePlugin({
m: 'mithril',
}),
];
}
有关 ProvidePlugin
的更多信息,请参阅 Webpack 文档。
与 React 的差异
Mithril 中的 JSX 与 React 的 JSX 相比,有一些细微但重要的差异。
属性和样式属性大小写约定
React 要求您对除 data-*
和 aria-*
属性之外的所有属性使用驼峰式 DOM 属性名称,而不是 HTML 属性名称。例如,使用 className
而不是 class
,使用 htmlFor
而不是 for
。在 Mithril 中,通常使用小写的 HTML 属性名称。如果属性不存在,Mithril 总是会降级到设置属性,这与 HTML 更加直观一致。请注意,在大多数情况下,DOM 属性和 HTML 属性名称要么相同,要么非常相似。例如,输入框的 value
/checked
和 tabindex
全局属性与 HTML 元素上的 elem.tabIndex
属性。它们很少在大小写之外有所不同:class
属性的 elem.className
属性或 for
属性的 elem.htmlFor
属性是少数例外。
类似地,React 总是使用通过 elem.style
属性(如 cssHeight
和 backgroundColor
)在 DOM 中公开的驼峰式样式属性名称。Mithril 同时支持这种方式和 kebab-cased CSS 属性名称(如 height
和 background-color
),并且更推荐使用后者。只有 cssHeight
、cssFloat
和带有供应商前缀的属性在大小写之外有所不同。
DOM 事件
React 将所有事件处理程序的第一个字符大写:onClick
监听 click
事件,onSubmit
监听 submit
事件。有些事件在将多个单词连接在一起时会被进一步更改。例如,onMouseMove
监听 mousemove
事件。Mithril 不执行此大小写映射,而是将 on
前置到原生事件,因此您将添加 onclick
和 onmousemove
的监听器来分别监听这两个事件。这与 HTML 的命名方案更加紧密地对应,如果您来自 HTML 或原生 DOM 背景,则更加直观。
React 支持在捕获阶段(在第一阶段,从外向内,而不是默认的冒泡阶段,在第二阶段从内向外)调度事件监听器,方法是将 Capture
附加到该事件。Mithril 目前缺乏这种功能,但将来可能会获得此功能。如果这是必要的,您可以在生命周期钩子中手动添加和删除您自己的监听器。
JSX vs hyperscript
JSX 和 hyperscript 是两种不同的语法,您可以用来指定 vnode(虚拟节点),它们各有优劣:
如果您来自 HTML/XML 背景,并且更喜欢使用这种语法来指定 DOM 元素,那么 JSX 更容易上手。在许多情况下,它也稍微简洁一些,因为它使用的标点符号更少,并且属性包含的视觉噪音更少,因此许多人发现它更容易阅读。当然,许多常见的编辑器都像支持 HTML 一样,为 DOM 元素提供自动完成支持。但是,它需要额外的构建步骤才能使用,编辑器支持不如普通 JS 广泛,而且它比较冗长。在处理大量动态内容时,代码也会变得更长,因为您必须对所有内容使用插值。
如果您来自不涉及太多 HTML 或 XML 的后端 JS 背景,那么 Hyperscript 会更容易上手。它更简洁,冗余更少,并且为静态类、ID 和其他属性提供了类似 CSS 的语法糖。它也可以在完全没有构建步骤的情况下使用,尽管您可以根据需要自行添加一个。并且在面对大量动态内容时,它更容易使用,因为您不需要“插值”任何内容。但是,对于某些人来说,这种简洁的写法确实会降低可读性,尤其是那些经验不足且来自前端 HTML/CSS/XML 背景的人,而且我不知道有任何插件可以自动完成 hyperscript 选择器的各个部分,如 ID、类和属性。
您可以在更复杂的树中看到这些优劣如何发挥作用。例如,考虑这个 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,
// To ensure the tag gets properly diffed on route change.
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,
// To ensure the tag gets properly diffed on route change.
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 模板转换器来完成大部分工作。