JSX
描述
JSX 是一種語法擴展,讓您能夠在 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,而 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 的差異
相較於 React 的 JSX,Mithril 中的 JSX 有一些細微但重要的差異。
屬性和樣式屬性大小寫慣例
React 要求使用駝峰式 DOM 屬性名稱,而非 HTML 屬性名稱,但 data-*
和 aria-*
屬性除外。例如,使用 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 與 hyperscript
JSX 和 hyperscript 是兩種不同的語法,您可以用於指定 vnode(虛擬節點),它們各有優缺點:
若您有 HTML/XML 背景,並且更喜歡使用這種語法指定 DOM 元素,那麼 JSX 會更容易上手。它也稍微乾淨一些,因為它使用的標點符號較少,並且屬性包含的視覺雜訊較少,因此許多人發現它更容易閱讀。當然,許多常見的編輯器都提供對 DOM 元素的自動完成支持,就像它們對 HTML 一樣。但是,還需額外的建置步驟方能使用,編輯器支持不如普通 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 範本轉換器來為您完成大部分工作。