Skip to content
Mithril.js 2
Main Navigation 指南API

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

主题

Sidebar Navigation

入门

安装

简单应用

资源

JSX

在旧版浏览器上使用 ES6+

动画

测试

示例

第三方库集成

路径处理

关键概念

虚拟 DOM

组件

生命周期方法

键(Key)

自动重绘系统

杂项

框架对比

从 v1.x 迁移

从 v0.2.x 迁移

API

页面导航

简单应用 ​

让我们开发一个简单的应用程序,展示使用 Mithril 时需要处理的大多数主要功能。

最终结果的交互式示例可以在这里

首先,让我们为应用程序创建一个入口点。创建一个名为 index.html 的文件:

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 后,打开命令行并运行以下命令:

bash
npm init -y

如果 npm 安装成功,将会创建一个 package.json 文件。该文件包含项目的基本元数据描述。你可以自由编辑此文件中的项目和作者信息。


要安装 Mithril.js,请按照安装页面中的说明进行操作。安装了 Mithril.js 的项目骨架后,我们就可以创建应用程序了。

让我们首先创建一个模块来存储状态。我们创建一个名为 src/models/User.js 的文件:

javascript
// src/models/User.js
var User = {
  list: [],
};

module.exports = User;

现在,让我们添加代码从服务器加载一些数据。为了与服务器通信,我们可以使用 Mithril.js 提供的 XHR 工具 m.request。首先,我们在模块中引入 Mithril.js:

javascript
// src/models/User.js
var m = require('mithril');

var User = {
  list: [],
};

module.exports = User;

接下来,我们创建一个函数来触发 XHR 调用。我们将其命名为 loadList:

javascript
// 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 端点正常工作。

javascript
// 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 和我们的模型,因为我们需要同时使用它们:

javascript
// src/views/UserList.js
var m = require('mithril');
var User = require('../models/User');

接下来,让我们创建一个 Mithril.js 组件。组件就是一个包含 view 方法的对象:

javascript
// 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 的工作方式非常相似。

javascript
// 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),并动态地渲染数据:

javascript
// 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,所以我们可以利用组件的生命周期方法:

javascript
// 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 中渲染该视图:

javascript
// 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 文件中:

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 组件设置样式:

css
.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 调用来实现路由:

javascript
// 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 的模块。

javascript
// src/views/UserForm.js

module.exports = {
  view: function () {
    // TODO:实现视图
  },
};

然后,我们可以在 src/index.js 中 require 这个新模块。

javascript
// src/index.js
var m = require('mithril');

var UserList = require('./views/UserList');
var UserForm = require('./views/UserForm');

m.route(document.body, '/list', {
  '/list': UserList,
});

最后,我们可以创建一个引用它的路由:

javascript
// 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 组件,使其能够响应这些路由参数:

javascript
// 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 添加更多样式:

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 模型中添加一些代码。当前代码如下:

javascript
// 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;

让我们添加加载单个用户的代码。

javascript
// 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 视图:

javascript
// 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:

javascript
// 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> 标签来实现链接跳转的,一切都运行良好。

刷新浏览器后,你应该能够点击用户列表中的某个人,并跳转到对应的表单页面。你也可以通过浏览器的后退按钮,从表单页面返回到用户列表。


表单本身在您点击“保存”按钮时仍然不会保存。 让我们使这个表单正常工作:

javascript
// 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 方法。 让我们实现该方法:

javascript
// 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:

javascript
// 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 的所有子节点。

让我们再次更新样式:

css
/* 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 中的路由配置,将布局组件整合进去:

javascript
// 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 聊天室。

Pager
上一页安装
下一页JSX

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors

https://mithril.js.org/simple-application.html

基于 MIT 许可证 发布。

版权所有 (c) 2024 Mithril Contributors