簡單應用程式
讓我們開發一個簡單應用程式,展示在使用 Mithril 時需要處理的大多數主要功能。
最終結果的互動範例可以在這裡
首先,讓我們為應用程式建立一個入口點。建立一個名為 index.html
的檔案:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Application</title>
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
<!doctype html>
行表示這是一個 HTML 5 文件。第一個 charset
meta 標籤表示文件的編碼,而 viewport
meta 標籤決定了行動瀏覽器應如何縮放頁面。title
標籤包含要在此應用程式的瀏覽器標籤上顯示的文字,而 script
標籤表示控制應用程式的 JavaScript 檔案的路徑。
我們可以在單個 JavaScript 檔案中建立整個應用程式,但這樣做會使以後難以瀏覽程式碼庫。相反,讓我們將程式碼分成_模組_,並將這些模組組裝成一個_套件_ bin/app.js
。
有很多方法可以設定打包工具,但大多數都是透過 npm 發布的。事實上,大多數現代 JavaScript 函式庫和工具都是以這種方式發布的,包括 Mithril。要下載 npm,請安裝 Node.js;npm 會隨之自動安裝。安裝 Node.js 和 npm 後,開啟命令列並執行以下命令:
npm init -y
如果 npm 安裝正確,將會建立一個 package.json
檔案。此檔案將包含一個骨架專案元資料描述檔案。您可以隨意編輯此檔案中的專案和作者資訊。
要安裝 Mithril.js,請按照安裝頁面中的說明進行操作。建立具有 Mithril.js 安裝的專案骨架後,我們就可以建立應用程式了。
首先建立一個模組來儲存狀態。讓我們建立一個名為 src/models/User.js
的檔案
// src/models/User.js
var User = {
list: [],
};
module.exports = User;
現在新增從伺服器載入資料的程式碼。為了與伺服器通訊,我們可以使用 Mithril.js 的 XHR 實用程式 m.request
。首先,我們在模組中包含 Mithril.js:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
};
module.exports = User;
接下來,我們建立一個將觸發 XHR 呼叫的函式。讓我們稱之為 loadList
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
// TODO:發出 XHR 呼叫
},
};
module.exports = User;
然後,我們可以新增一個 m.request
呼叫來發送 XHR 請求。在本教學中,我們將對 REM (DEAD LINK, FIXME: https //rem-rest-api.herokuapp.com/) API 進行 XHR 呼叫,這是一個專為快速原型設計而設計的模擬 REST API。此 API 從 GET https://mithril-rem.fly.dev/api/users
端點傳回使用者清單。讓我們使用 m.request
發出 XHR 請求,並使用該端點的回應來填入我們的資料。
注意:可能必須啟用第三方 Cookie 才能使 REM 端點正常運作。
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
method
選項是一個 HTTP 方法。為了從伺服器檢索資料而不對伺服器造成副作用,我們需要使用 GET
方法。url
是 API 端點的位址。withCredentials: true
行表示我們正在使用 Cookie(這是 REM API 的要求)。
m.request
呼叫傳回一個 Promise,該 Promise 解析為端點的資料。預設情況下,Mithril.js 假設 HTTP 回應本文採用 JSON 格式,並自動將其剖析為 JavaScript 物件或陣列。.then
回呼會在 XHR 請求完成時執行。在這種情況下,回呼會將 result.data
陣列指派給 User.list
。
請注意,我們在 loadList
中也有一個 return
陳述式。這是使用 Promise 時的一般良好做法,它允許我們註冊更多回呼以在 XHR 請求完成後執行。
這個簡單的模型公開了兩個成員:User.list
(使用者物件的陣列)和 User.loadList
(使用伺服器資料填入 User.list
的方法)。
現在,讓我們建立一個視圖模組,以便我們可以顯示來自我們的 User 模型模組的資料。
建立一個名為 src/views/UserList.js
的檔案。首先,讓我們包含 Mithril.js 和我們的模型,因為我們需要同時使用兩者:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
接下來,讓我們建立一個 Mithril.js 元件。元件是一個包含 view
方法的物件:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
// TODO:在此處新增程式碼
},
};
預設情況下,Mithril.js 視圖是使用 hyperscript 描述的。Hyperscript 提供了一種簡潔的語法,可以比 HTML 更自然地縮排複雜的標籤,並且由於其語法只是 JavaScript,因此可以利用大量的 JavaScript 工具生態系統。例如:
- 您可以使用 Babel 將 ES6+ 轉換為 ES5 以用於 IE,並將 JSX(一種內嵌的類似 HTML 的語法擴充)轉換為適當的 hyperscript 呼叫。
- 您可以使用 ESLint 進行輕鬆的程式碼檢查,而無需特殊外掛程式。
- 您可以使用 Terser 或 UglifyJS(僅限 ES5)輕鬆縮小您的程式碼。
- 您可以使用 Istanbul 進行程式碼涵蓋率分析。
- 您可以使用 TypeScript 進行輕鬆的程式碼分析。(有 社群支援的類型定義可用,因此您無需自行建立。)
讓我們從 hyperscript 開始,並建立一個項目清單。Hyperscript 是使用 Mithril.js 的慣用方式,但 JSX 的運作方式非常相似。
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m('.user-list');
},
};
".user-list"
字串是一個 CSS 選擇器,正如您所期望的那樣,.user-list
代表一個類別。未指定標籤時,div
是預設值。因此,此視圖等效於 <div class="user-list"></div>
。
現在,讓我們參考我們先前建立的模型 (User.list
) 中的使用者清單,以動態迴圈處理資料:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
由於 User.list
是一個 JavaScript 陣列,並且由於 hyperscript 視圖只是 JavaScript,因此我們可以使用 .map
方法迴圈處理該陣列。這會建立一個 vnode 陣列,該陣列表示一個 div
清單,每個 div
都包含使用者的姓名。
當然,問題在於我們從未呼叫 User.loadList
函式。因此,User.list
仍然是一個空陣列,此視圖將會呈現空白頁面。由於我們希望在呈現此元件時呼叫 User.loadList
,因此我們可以利用元件生命週期方法:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m('.user-list-item', user.firstName + ' ' + user.lastName);
})
);
},
};
請注意,我們向元件新增了一個 oninit
方法,該方法參考 User.loadList
。這表示當元件初始化時,會呼叫 User.loadList,從而觸發 XHR 請求。當伺服器傳回回應時,User.list
會被填入。
另請注意,我們沒有執行 oninit: User.loadList()
(結尾帶有括號)。不同之處在於 oninit: User.loadList()
會立即呼叫該函式一次,但 oninit: User.loadList
僅在元件呈現時才呼叫該函式。這是一個重要的差異,也是 JavaScript 新手開發人員常犯的錯誤。立即呼叫該函式,表示 XHR 請求會在評估原始程式碼後立即觸發,即使元件從未呈現。此外,如果元件被重新建立(透過在應用程式中來回導覽),則該函式不會像預期的那樣再次被呼叫。
讓我們從我們先前建立的進入點檔案 src/index.js
呈現視圖:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.mount(document.body, UserList);
m.mount
呼叫將指定的元件 (UserList
) 呈現到 DOM 元素 (document.body
) 中,從而清除先前存在的任何 DOM。現在,在瀏覽器中開啟 HTML 檔案應會顯示一個人員姓名清單。
現在,清單看起來相當普通,因為我們尚未定義任何樣式。因此,讓我們新增一些樣式。首先,讓我們建立一個名為 styles.css
的檔案,並將其包含在 index.html
檔案中:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Application</title>
<link href="styles.css" rel="stylesheet" />
</head>
<body>
<script src="bin/app.js"></script>
</body>
</html>
現在我們可以在設定 UserList
元件的樣式:
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
現在重新載入瀏覽器視窗應會顯示一些樣式化的元素。
為應用程式加入路由功能。
路由表示將螢幕繫結到唯一的 URL,以建立從一個「頁面」轉到另一個「頁面」的能力。Mithril.js 專為單頁應用程式設計,因此這些「頁面」不一定是傳統意義上的不同 HTML 檔案。相反,單頁應用程式的路由會維持同一個 HTML 檔案,但透過 JavaScript 變更應用程式的狀態。用戶端路由的好處是可以避免頁面轉換之間出現空白螢幕閃爍,並且在與 Web 服務導向架構(即,以 JSON 形式下載資料而不是下載預先呈現的詳細 HTML 區塊的應用程式)結合使用時,可以減少從伺服器傳送下來的資料量。
我們可以透過將 m.mount
呼叫變更為 m.route
呼叫來新增路由:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
m.route(document.body, '/list', {
'/list': UserList,
});
m.route
呼叫指定應用程式將被呈現到 document.body
中。"/list"
引數是預設路由。這表示如果使用者進入不存在的路由,他們將被重新導向到該路由。{"/list": UserList}
物件宣告了現有路由的地圖,以及每個路由解析為的元件。
現在重新整理瀏覽器中的頁面應會將 #!/list
附加到 URL,以表示路由正在運作。由於該路由呈現 UserList,因此我們仍然應該像以前一樣在螢幕上看到人員清單。
#!
片段稱為 hashbang,它是用於實作用戶端路由的常用字串。可以透過 m.route.prefix
設定此字串。某些設定需要支援伺服器端變更,因此我們將在本教學的其餘部分繼續使用 hashbang。
讓我們為我們的應用程式新增另一個路由,用於編輯使用者。首先,讓我們建立一個名為 views/UserForm.js
的模組
// src/views/UserForm.js
module.exports = {
view: function () {
// TODO:實作檢視
},
};
然後我們可以從 src/index.js
「require
」 這個新模組
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
});
最後,我們可以建立一個參考它的路由:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
m.route(document.body, '/list', {
'/list': UserList,
'/edit/:id': UserForm,
});
請注意,新路由中包含 :id
。這是一個路由參數;您可以將其視為萬用字元;路由 /edit/1
將解析為 UserForm
,其 id
為 "1"
。/edit/2
也會解析為 UserForm
,但其 id
為 "2"
。依此類推。
實作 UserForm
元件,以便它可以回應這些路由參數:
// src/views/UserForm.js
var m = require('mithril');
module.exports = {
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]'),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]'),
m('button.button[type=submit]', 'Save'),
]);
},
};
讓我們向 styles.css
新增更多樣式:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
現在,此元件不會執行任何操作來回應使用者事件。讓我們在 src/models/User.js
的 User
模型中新增程式碼。以下是目前的程式碼:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
};
module.exports = User;
讓我們新增程式碼以允許我們載入單個使用者
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
};
module.exports = User;
請注意,我們新增了一個 User.current
屬性和一個 User.load(id)
方法,該方法會填入該屬性。我們現在可以使用這個新方法填入 UserForm
視圖:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m('form', [
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', { value: User.current.lastName }),
m('button.button[type=submit]', 'Save'),
]);
},
};
與 UserList
元件類似,oninit
會呼叫 User.load()
。還記得我們在 "/edit/:id": UserForm
路由上有名為 :id
的路由參數嗎?路由參數會成為 UserForm
元件的 vnode 的屬性,因此路由到 /edit/1
會使 vnode.attrs.id
的值為 "1"
。
現在,讓我們修改 UserList
視圖,以便我們可以從那裡導覽到 UserForm
:
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: User.loadList,
view: function () {
return m(
'.user-list',
User.list.map(function (user) {
return m(
m.route.Link,
{
class: 'user-list-item',
href: '/edit/' + user.id,
},
user.firstName + ' ' + user.lastName
);
})
);
},
};
在這裡,我們將 .user-list-item
vnode 換成具有該類別和相同子元素的 m.route.Link
。我們加入的 href
指向目標路由。這表示按一下連結會變更 hashbang #!
之後的 URL 部分(從而變更路由而不卸載目前的 HTML 頁面)。在幕後,它使用 <a>
標籤來實作連結,確保一切正常運作。
如果您重新整理瀏覽器中的頁面,您現在應該能夠按一下某個人並被帶到一個表單。您也應該能夠按下瀏覽器中的「上一頁」按鈕,從表單返回到人員清單。
表單本身在您按下「儲存」時,預設並不會自動儲存。 讓我們讓這個表單能夠運作:
// src/views/UserForm.js
var m = require('mithril');
var User = require('../models/User');
module.exports = {
oninit: function (vnode) {
User.load(vnode.attrs.id);
},
view: function () {
return m(
'form',
{
onsubmit: function (e) {
e.preventDefault();
User.save();
},
},
[
m('label.label', 'First name'),
m('input.input[type=text][placeholder=First name]', {
oninput: function (e) {
User.current.firstName = e.target.value;
},
value: User.current.firstName,
}),
m('label.label', 'Last name'),
m('input.input[placeholder=Last name]', {
oninput: function (e) {
User.current.lastName = e.target.value;
},
value: User.current.lastName,
}),
m('button.button[type=submit]', 'Save'),
]
);
},
};
我們為兩個輸入框新增了 oninput
事件處理器,以便在使用者輸入時更新 User.current.firstName
和 User.current.lastName
屬性。
此外,我們設定當按下「儲存」按鈕時,會呼叫 User.save
方法。 讓我們實作這個方法:
// src/models/User.js
var m = require('mithril');
var User = {
list: [],
loadList: function () {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users',
withCredentials: true,
})
.then(function (result) {
User.list = result.data;
});
},
current: {},
load: function (id) {
return m
.request({
method: 'GET',
url: 'https://mithril-rem.fly.dev/api/users/' + id,
withCredentials: true,
})
.then(function (result) {
User.current = result;
});
},
save: function () {
return m.request({
method: 'PUT',
url: 'https://mithril-rem.fly.dev/api/users/' + User.current.id,
body: User.current,
withCredentials: true,
});
},
};
module.exports = User;
在 save
方法中,我們使用 PUT
HTTP 方法來向伺服器發送更新後的資料。
現在嘗試編輯應用程式中某個使用者的名稱。 儲存變更後,您應該可以在使用者清單中看到變更。
目前,我們只能透過瀏覽器的返回按鈕回到使用者清單。 理想情況下,我們希望有一個選單,或者更廣泛地說,一個可以放置全域 UI 元素的佈局(Layout)。
讓我們建立一個檔案 src/views/Layout.js
:
// src/views/Layout.js
var m = require('mithril');
module.exports = {
view: function (vnode) {
return m('main.layout', [
m('nav.menu', [m(m.route.Link, { href: '/list' }, 'Users')]),
m('section', vnode.children),
]);
},
};
這個元件很簡單,它有一個 <nav>
包含指向使用者清單的連結。 類似於我們對 /edit
連結所做的,這個連結使用 m.route.Link
來建立可路由的連結。
請注意還有一個 <section>
元素,它的子元素是 vnode.children
。 vnode
是一個指向代表 Layout 元件實例的虛擬節點的引用(也就是 m(Layout)
呼叫所返回的虛擬節點)。 因此,vnode.children
指的是這個虛擬節點的所有子節點。
讓我們再次更新樣式表:
/* styles.css */
body,
.input,
.button {
font: normal 16px Verdana;
margin: 0;
}
.layout {
margin: 10px auto;
max-width: 1000px;
}
.menu {
margin: 0 0 30px;
}
.user-list {
list-style: none;
margin: 0 0 10px;
padding: 0;
}
.user-list-item {
background: #fafafa;
border: 1px solid #ddd;
color: #333;
display: block;
margin: 0 0 1px;
padding: 8px 15px;
text-decoration: none;
}
.user-list-item:hover {
text-decoration: underline;
}
.label {
display: block;
margin: 0 0 5px;
}
.input {
border: 1px solid #ddd;
border-radius: 3px;
box-sizing: border-box;
display: block;
margin: 0 0 10px;
padding: 10px 15px;
width: 100%;
}
.button {
background: #eee;
border: 1px solid #ddd;
border-radius: 3px;
color: #333;
display: inline-block;
margin: 0 0 10px;
padding: 10px 15px;
text-decoration: none;
}
.button:hover {
background: #e8e8e8;
}
讓我們變更 src/index.js
中的路由器設定,將我們的版面配置加入其中:
// src/index.js
var m = require('mithril');
var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');
var Layout = require('./views/Layout');
m.route(document.body, '/list', {
'/list': {
render: function () {
return m(Layout, m(UserList));
},
},
'/edit/:id': {
render: function (vnode) {
return m(Layout, m(UserForm, vnode.attrs));
},
},
});
我們將每個元件替換為 RouteResolver(基本上是一個具有 render
方法的物件)。 render
方法可以像編寫常規元件視圖一樣,使用巢狀的 m()
呼叫。
需要注意的是,元件如何在 m()
呼叫中被用作選擇器字串。 這表示有一個根虛擬節點代表 Layout
的一個實例,並且它有一個 UserList
虛擬節點作為其唯一的子節點。
在 /edit/:id
路由中,還有一個 vnode
參數,它將路由參數傳遞到 UserForm
元件中。 因此,如果 URL 是 /edit/1
,則此處的 vnode.attrs
為 {id: 1}
,並且此 m(UserForm, vnode.attrs)
等效於 m(UserForm, {id: 1})
。 等效的 JSX 程式碼將是 <UserForm id={vnode.attrs.id} />
。
重新整理瀏覽器頁面,現在您將在應用程式的每個頁面上看到全域導航。
本教學到此結束。
在本教學中,我們完成了建立一個非常簡單的應用程式的過程,這個應用程式讓我們可以在其中列出伺服器中的使用者並單獨編輯它們。 作為額外練習,請嘗試自行實作使用者的建立與刪除功能。
若您想查看更多 Mithril.js 程式碼範例,請查看 examples 頁面。 如果您有任何疑問,請隨時加入 Mithril.js 聊天室。