Mithril.js上的样本Simple Notes SPA

Mithril.js是用于构建客户端Web应用程序的不受欢迎的工具。几乎没有有关此主题的出版物。在本文中,我想展示如何在Mithril上制作一个小型应用程序。该应用程序将基于此出版物翻译

什么是米特里尔


Mithril是一个反应式框架,旨在创建SPA(单页Web应用程序)。总而言之,这只是JavaScript和API函数的13个签名。此外,还有一个秘银流库,该库不包含在秘银中,而是单独使用。秘银的核心包括路由应用程序和处理XHR请求。中心概念是抽象-虚拟节点(vnode)。虚拟节点只是具有一些属性集的js对象。虚拟节点由特殊功能m()创建。接口的当前状态存储在虚拟节点列表(虚拟DOM)中。在最初呈现应用程序页面时,虚拟DOM被转换为DOM。当DOM API事件处理程序启动时,当m.request()承诺完成时,并且URL发生更改(沿着应用程序的路径导航)时,会生成一个新的虚拟DOM数组,与旧版本比较,并且更改后的节点更改了浏览器DOM。除了DOM事件和m.request()请求的完成之外,还可以使用m.redraw()函数手动调用重绘。

秘银盒中没有开箱即用的类似HTML的模板,没有JSX支持,尽管您可以根据需要将所有这些与各种插件结合使用。我不会在这里利用这些机会。

如果m()的第一个参数是字符串(例如'div'),则该函数返回一个简单的虚拟节点,因此,将在DOM中显示一个HTML标签。

<div></div>

如果m()的第一个参数是对象或返回该对象的函数,则此类对象必须具有view()方法,并且此类对象称为组件。反过来,组件的view()方法应始终返回m()函数(或类型为[m(),]的数组)。因此,我们可以构建组件对象的层次结构。很明显,最终所有组件都返回简单的vnode节点。

虚拟节点和组件都具有生命周期方法,它们被称为相同的oninit(),oncreate(),onbeforeupdate()等。在页面渲染的特定时间点将调用这些方法中的每一个。

您可以将参数作为对象传递给虚拟节点或组件,该参数应该是m()函数的第二个参数。您可以使用vnode.attrs表示法在节点内获得指向该对象的链接。 m()函数的第三个参数是该节点的后代,可以通过vnode.children链接进行访问。除m()函数外,m.trust()函数还返回简单节点。

秘银的作者没有提供任何特殊的应用程序设计模式,尽管他建议避免使用一些不成功的解决方案,例如“太厚”的组件或操纵组件树的后代。作者也没有提供特殊的方式或方法来控制应用程序整体或组件的状态。尽管文档指出您不应使用节点本身的状态,但请对其进行操作。

秘银的所有这些功能似乎非常不便,并且框架似乎尚未完成,没有特别的建议,没有状态/存储,没有reduce /事件分配器,没有模板。通常,请尽力而为。

您需要的示例


我们将使用:


前端服务器在这里并不重要,它只需要给客户端提供index.html以及脚本和样式文件。

我们不会在节点模块中安装mithril,并将应用程序代码和框架绑定到一个文件中。应用程序代码和秘银将分别上传到页面。

我不会描述这些工具的安装过程,尽管我可以说说postgREST,只是下载二进制文件,将其放在单独的文件夹中,创建一个test.conf配置文件,如下所示:

db-uri = "postgres://postgres:user1@localhost:5432/testbase"
server-port= 5000
# The name of which database schema to expose to REST clients
db-schema= "public"
# The database role to use when no client authentication is provided.
# Can (and probably should) differ from user in db-uri
db-anon-role = "postgres" 

同时,您的postgesql集群应具有testbase base和user1。在此测试基础中,创建一个表:

-- Postgrest sql notes table 
create table if not exists notes (
id serial primary key,
title varchar(127) NOT NULL,
content text NOT NULL,
created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed timestamp with time zone,
ddel smallint default 0
)

使用以下命令启动postgREST服务器:

postgrest test.conf

启动服务器后,它将显示有关连接到数据库以及它将在哪个端口上侦听客户端的信息性消息。

规划项目


因此,如果大致了解mitril的工作原理,则需要弄清楚如何制作该应用程序。这是计划:

  1. 我们将应用程序数据存储在本地对象中,我们称之为模型
  2. API应用程序将存储在单独的文件中
  3. 我们会将应用程序路由存储在单独的文件中
  4. 路径文件将是构建应用程序的入口点。
  5. 每个单独的组件(我将使用组件渲染方案)以及与之关联的功能将存储在单独的文件中
  6. 呈现模型数据的每个组件都可以访问模型。
  7. 组件DOM事件处理程序位于组件中

因此,我们不需要自定义事件,而是本地DOM事件,这些事件的回调位于组件中。我将使用两种方式绑定。也许不是每个人都喜欢这种方法,但也不是每个人都喜欢redux或vuex。而且,单向结合技术也可以使用秘银丝在米特里尔中实现。但是在这种情况下,这是一个冗余解决方案。

应用程序文件夹结构




公用文件夹将由前端服务器提供,其中有一个index.html文件以及带有样式和脚本的目录。

src文件夹包含路由器的根目录和API定义,以及两个目录,分别用于模型和视图。

在项目的根目录下有一个rollup.config配置文件,该项目是使用以下命令构建的:

rollup –c

为了避免让读者沉迷github.com上提供的大量代码我将仅对主要实现元素进行评论,以演示二尖瓣的惯用方法。

API和路由器


API代码:

// used by backend server
export const restApi= {
  notes: { url: 'notes', editable: ['add', 'edit', 'del'] }
}

// used by routers
export const appApi = {
  root: "/",
}
// used by navBar
// here array of arrays though it may be hash eg
export const appMenu = [
  [`${appApi.root}`, 'Home'],
  [`${appApi.root}`, 'About'],
  [`${appApi.root}`, 'Contacts'],
]

我为REST服务器和路由器定义了API。

路由器:

import { restApi, appApi } from './appApi';
import { moModel } from './model/moModel';
import { vuView, vuApp } from './view/vuApp';
import { vuNavBar } from './view/vuNavBar';

// application router
const appRouter = { [appApi.root]: {
  render() {
    const view = m(vuApp, {
      model: moModel.getModel( restApi.notes ),
    });
    return vuView( {menu: vuNavBar}, view);
  }
}};

// once per app
m.route(document.body, "/", appRouter);

此处,路由器安装在文档主体上。路由器本身是一个描述路由和将在此路由上使用的组件的对象,render()函数应返回vnode。

appApi对象定义了所有有效的应用程序路由,而appMenu对象定义了该应用程序的所有可能的导航元素。

渲染函数在调用时会生成一个应用程序模型,并将其传递给根节点。

应用模式


存储相关数据的结构称为模型。返回模型的函数的源代码:

getModel(
    {url=null, method="GET", order_by='id', editable=null} = {}
  ) {
/**
  * url - string of model's REST API url
  * method - string of model's REST method
  * order_by - string "order by" with initially SELECT 
  * editable - array defines is model could changed
*/
    const model = {
      url: url,
      method: method,
      order_by: order_by,
      editable: editable,
      key: order_by, // here single primary key only
      list: null, // main data list 
      item: {}, // note item
      error: null, // Promise error
      save: null, // save status
      editMode: false, // view switch flag
      word: '' // dialog header word
    };  
    model.getItem= id => {
      model.item= {};
      if ( !id ) {
        model.editMode= true;
        return false;
      }
      const key= model.key;
      for ( let it of model.list ) {
        if (it[key] == id) {
          model.item= Object.assign({}, it);
          break;
        }
      }
      return false;
    };
    return model;
  },

在这里它被初始化,并返回模型对象。对象内部的链接可能会更改,但是指向对象本身的链接保持不变。

除了getModel函数之外,全局moModel对象还具有m.request()mitry函数的包装函数,这些函数包括getList(模型)和formSubmit(事件,模型,方法)。model参数实际上是对模型对象的引用,event是在提交表单时生成的事件对象,method是我们要用来保存注释的HTTP方法(POST是新注释,PATCH,DELETE是旧的)。

表示


视图文件夹包含负责呈现单个页面元素的功能。我将其分为4部分:

  • vuApp-应用程序的根组件,
  • vuNavBar-导航栏,
  • vuNotes-注释列表,
  • vuNoteForm-笔记编辑表单,
  • vuDialog-HTML对话框元素

路由器确定在单个路由上返回了vuView(菜单,视图)

,该函数的定义:

export const vuView= (appMenu, view)=> m(vuMain, appMenu, view);

这只是返回vuMain组件的包装器,如果appMenu对象足够复杂,以至于嵌套对象的结构类似于外部对象,那么这种包装器是返回具有不同导航元素和子组件的组件的合适方法(您只需要编写更少的代码) 。

VuMain组件:

const vuMain= function(ivnode) {
  // We use ivnode as argument as it is initial vnode
  const { menu }= ivnode.attrs;
  return { view(vnode) {
    // IMPORTANT !
    // If we use vnode inside the view function we MUST provide it for view
    return [
      m(menu),
      m('#layout', vnode.children)
    ];
  }};
};

这仅返回导航的vnode和页面的实际内容。

在下文中,在可能定义组件的地方,我将使用闭包。在页面的初始呈现期间,闭包被调用一次,并在本地存储所有传递给对象的链接以及它们自己的功能的定义。

关闭作为定义应该总是返回一个组件。

实际上是应用程序内容组件:

export const vuApp= function(ivnode) {
  const { model }= ivnode.attrs;
  //initially get notes
  moModel.getList( model );
  
  return { view() { 
    return [
      m(vuNotes, { model }),
      m(vuNoteForm, { model }),
      vuModalDialog(model)
    ];
  }};
}

由于我们已经有了一个模型,因此当我调用闭包时,我想从数据库中获取整个注释列表。页面上将包含三个组件:

  • vuNotes-带“添加”按钮的注释列表,
  • vuNoteForm-笔记编辑表单,
  • vuModalDialog-我们将以模态显示的对话框元素,并在必要时猛击。

由于每个组件都需要知道如何绘制自身,因此我们将链接传递给每个组件中的模型对象。

组件注释清单:

//Notes List
export const vuNotes= function(ivnode) {
  const { model }= ivnode.attrs;
  const _display= ()=> model.editMode ? 'display:none': 'display:block';
  const vuNote= noteItem(model); // returns note function
  
  return { view() {
    return model.error ? m('h2', {style: 'color:red'}, model.error) :
    !model.list ? m('h1', '...LOADING' ) :
    m('div', { style: _display() }, [
      m(addButton , { model } ),
      m('.pure-g', m('.pure-u-1-2.pure-u-md-1-1',
        m('.notes', model.list.map( vuNote ) )
      ))
    ]);
  }};
}

布尔editMode标志存储在模型对象中;如果标志值为true,则显示编辑形式,否则-注释列表。您可以将测试提高一个级别,但是每次切换标志时,虚拟节点的数量和实际DOM节点的数量都会改变,这是不必要的工作。

在这里,我们习惯于使用mitril,我们生成一个页面,使用三元运算符检查模型中属性的存在与否。

这是返回注释显示功能的闭包:

const noteItem= model=> {
  // click event handler
  const event= ( msg, word='', time=null)=> e=> {
    model.getItem(e.target.getAttribute('data'));
    if ( !!msg ) {
      model.save= { err: false, msg: msg };
      model.word= word;
      if ( !!time )
        model.item.completed= time;
      vuDialog.open();
    } else {
      model.editMode= true;
    }
  };
  // trash icon's click handler
  const _trash= event('trash', 'Dlelete');
  
  // check icon's click handler
  const _check= event('check', 'Complete',
    // postgre timestamp string
    new Date().toISOString().split('.')[0].replace('T', ' '));
  
  // edit this note
  const _edit= event('');
  
  const _time= ts=> ts.split('.')[0];
  
  // Single Note 
  const _note= note=> m('section.note', {key: note.id}, [
    m('header.note-header', [ m('p.note-meta', [
      // note metadata
      m('span', { style: 'padding: right: 3em' }, `Created: ${_time( note.created )}`),
      note.completed ? m('span', `  Completed: ${_time( note.completed )}`) : '', 
      // note right panel 
      m('a.note-pan', m('i.fas.fa-trash', { data: note.id, onclick: _trash } )),
      note.completed ? '' : [
        m('a.note-pan', m('i.fas.fa-pen', {data: note.id, onclick: _edit } )),
        m('a.note-pan', m('i.fas.fa-check', { data: note.id, onclick: _check} ))
      ]
    ]),
      m('h2.note-title', { style: note.completed ? 'text-decoration: line-through': ''}, note.title)
    ]),
    m('.note-content', m('p', note.content))
  ]);
  return _note;
}

所有点击处理程序均在本地定义。该模型不知道note对象的结构,我只定义了key属性,通过它我们可以从model.list数组中选择所需的元素。但是,组件必须确切知道其绘制的对象的结构。

我不会提供用于编辑笔记的表单的代码的全文;我们只是单独查看表单提交处理程序:

// form submit handler
  const _submit= e=> {
    e.preventDefault();
    model.item.title= clean(model.item.title);
    model.item.content= clean(model.item.content);
    const check= check_note(model.item);
    if ( !!check ) {
      model.save= { err: true, msg: check }; 
      model.word= 'Edit';
      vuDialog.open();
      return false;
    } 
    return moModel.formSubmit(e, model, _method() ).then(
      ()=> { model.editMode=false; return true;}).catch(
      ()=> { vuDialog.open(); return false; } );
  };

由于定义发生在闭包中,因此我们有一个到模型的链接,并且我们返回了对结果进行后续处理的Promise:在正常完成请求或出现错误的情况下禁止显示表格-打开带有错误文本的对话框。

每次您访问后端服务器时,便笺列表都会重新读取。在此示例中,尽管可以执行此操作,但无需调整内存中的列表。

可以在存储库中查看对话框组件,唯一需要强调的是,在这种情况下,我使用对象文字来定义组件,因为我希望窗口打开和关闭功能可用于其他组件。

结论


我们使用javascript和mithril.js编写了一个小型SPA应用程序,试图坚持使用该框架的惯用法。我想再次注意,这只是javascript代码。也许不太干净。该方法允许您封装一小段代码,隔离组件的机制并为所有组件使用通用状态。

All Articles