简单应用
让我们开发一个简单的应用程序,展示使用 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>
行声明这是一个 HTML5 文档。第一个 charset
meta 标签声明文档的字符编码,viewport
meta 标签定义了移动浏览器如何缩放页面。title
标签包含将在应用程序的浏览器选项卡上显示的文本,script
标签指定了控制应用程序的 JavaScript 文件的路径。
我们可以将整个应用程序创建在一个 JavaScript 文件中,但这会使后续浏览代码库变得困难。相反,让我们将代码拆分成_模块_,然后将这些模块打包成一个 bundle bin/app.js
。
有很多种打包工具可供选择,但大多数都通过 npm 进行分发。实际上,包括 Mithril 在内的大多数现代 JavaScript 库和工具都是以这种方式分发的。要下载 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;
在本教程中,我们将向 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 要求启用 Cookie)。
m.request
调用返回一个 Promise,其 resolve 的结果是来自端点的数据。默认情况下,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+ 代码转换为兼容 IE 浏览器的 ES5 代码,也可以将 JSX (一种内联 HTML 风格的语法扩展) 转换为相应的 hyperscript 调用。
- 您可以使用 ESLint 进行简单的 linting,而无需任何特殊插件。
- 您可以使用 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
代表一个 CSS 类。未指定标签时,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,因此我们仍然应该像以前一样在屏幕上看到人员列表。
#!
代码段称为哈希bang,是实现客户端路由的常用字符串。可以通过 m.route.prefix
来配置此字符串。某些配置需要支持服务器端更改,因此在本教程的其余部分中,我们将继续使用哈希bang。
让我们为应用程序添加另一个用于编辑用户的路由。首先,让我们创建一个名为 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
,并保留了相同的 CSS 类和子元素。我们添加了一个 href
属性,指向我们希望跳转的路由。这意味着点击链接会改变 URL 中 hashbang #!
后面的部分 (从而在不重新加载 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 元素的布局。
让我们创建一个文件 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 组件实例的 vnode 的引用(即,由 m(Layout)
调用返回的 vnode)。 因此,vnode.children
引用该 vnode 的所有子节点。
让我们再次更新样式:
/* 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()
调用中使用组件而非选择器字符串。 在 /list
路由中,我们有 m(Layout, m(UserList))
。 这意味着有一个根 vnode,它表示 Layout
的一个实例,并且它有一个 UserList
vnode 作为其唯一的子节点。
在 /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 聊天室。