受 Mithril.js 启发的零学习成本极易用框架

  • 框架名称: mj.js (https://github.com/ahui2016/mj.js)
  • 在介绍本框架之前,我不得不恳求:请先别带着偏见小瞧本框架的能力。
  • 本框架是很容易被小瞧、被误解的,因为基于 jQuery, 第一版一共由两个函数共约 10 行代码构成。

真组件

本框架中的组件是真组件,因为:

  • 生成组件不需要写 HTML 或 JSX, 而是完全使用 JavaScript
  • 组件可以有自己的方法,可调用组件的方法使组件发生变化
  • 组件可以套娃,即组件内可以嵌套另一个组件
  • 组件与组件之间可以交流,互相调用或互相传递数据

第一版

以下两个函数就是本框架的全部代码了。(这是第一版,后来做了一点扩展,但第一版更简洁易懂,有利于讲解)

// 函数名 m 来源于 Mithril.js, 也可以理解为 make 的简称,用来创建一个元素。
function m(obj) {
  if (typeof obj == 'string') {
    return $(document.createElement(obj));
  }
  return obj.view();
}

// 函数名 cc 意思是 create a component, 用来创建一个简单的组件。
function cc(name, id, elements) {
  if (!id) id = '' + Math.round(Math.random() * 100000000);
  const vnode = m(name).attr('id', id);
  if (elements) vnode.append(elements);
  return {id: '#'+id, raw_id: id, view: () => vnode};
}

简单示例 (与 Mithril 对比)

Mithril 创建一个 component 是这样:

const Hello = {
    view: () => m("div", [
        m("h1", {class: "title"}, "My first app"),
        m("button", "A button"),
    ])
}

使用 mj.js 创建一个 component 是这样:

const Hello = {
    view: () => m('div').append([
        m('h1').attr({class: 'title'}).text('My first app'),
        m('button').text('A button'),
    ])
}

也可以这样:

const Hello = cc('div', 'id-123', [
    m('h1').attr({class: 'title'}).text('My first app'),
    m('button').text('A button'),
]);

可见,与 Mithril 的写法非常相似,同时又完全是 jQuery 的基本操作,对于已经会用 jq 的人来说,学习成本接近零。

新版

不久前我写了《超简单易用的 Java Web 框架 - Javalin》及其续篇《Javalin网站框架介绍之二 - 数据库》。

这两篇文章主要讲解了后端的程序,而前端我选择 mj.js, 完整的(包括前后端)可以运行的代码在这里 => github.com/ahui2016/monostich/tree/c34e88afa0 ... 这是 mj.js 的一个真实使用事例。

其中前端部分,重点关注的文件是:

  • mj.js 框架本身,比第一版多加了几行代码,但仍然非常简单。
  • mj-util.js 用 mj.js 制作的一些常用组件
  • add-entry.js 一个用 mj.js 制作的页面,含有表单,可向后端发送 post 请求。

组件之间的相互嵌套与交流

在 mj-util.js 文件中有这样的函数:

function createInput(type = "text") {
    return cc("input", { attr: { type: type } });
}

function createFormItem(inputComp, name) { /* ... */ }

在 add-entry.js 里是这样使用 createInput 函数的:

const NotesInput = createInput();
const CmdInput = createInput();
const SubmitBtn = cc('button', {text: 'Submit'});

const Form = cc('form', { children: [
  createFormItem(NotesInput, 'Notes'),
  createFormItem(CmdInput, 'Command'),
  m(SubmitBtn).on('click', event => {
    const body = {
      notes: valOf(NotesInput),
      cmd: valOf(CmdInput),
    };
    axios.post('/api/add-entry', body).then(() => { /* ... */ });
  }),
]});

可见, SubmitBtn 是一个组件, Form 也是一个组件, SubmitBtn 可以嵌套进 Form 里,例如 Form = cc('form', {children: [ m(SubmitBtn) ]}) 效果相当于:

<form>
    <button>Submit</button>
</form>

另外 NotesInput 等组件也同样嵌套到 Form 里,然后在 SubmitBtn 的 click 事件中用第三方库 axios 向后端发送请求即可。

(同时也可以看到,在 SubmitBtn 的 click 事件中可以直接获取 NotesInput 的数据。在该例子中我使用了函数 valOf, 另外也可以给 NotesInput 添加实现类似功能的方法,然后用类似 NotesInput.val() 的方式获取数据。)

在后端,用类似这样的代码接收该表单的请求 (参考 github.com/ahui2016/monostich/.../Handle.java):

app.post("/api/add-entry", ctx -> {
    var data = ctx.bodyAsClass(EntryForm.class);
    var entry = new Entry(
            db.getNextId(),
            data.notes(),
            data.cmd(),
            Util.now());
    db.insertEntry(entry);
    ctx.status(200);
});

其中 db.insertEntry() 详见 《Javalin网站框架介绍之二 - 数据库》中的 DB.java 部分内容。

如上所述,前端用 mj.js 不写 HTML, 完全用 JS 写组件,组件可以嵌套、可以有自己的方法、可以互相交流。

写好前后端代码后,用 mvn package 命令生成 jar 文件,用 java -jar XXX.jar 命令启动程序,通过浏览器访问 http://127.0.0.1:7070/add-entry.html 即可看到效果,可通过网页表单向后端发送请求,向数据库写入数据。(其中 add-entry.html 的源码见这里: github.com/ahui2016/monostich/.../public/index.html)

TypeScript

另外我也做过 mj.js 的 TypeScript 版本,如有兴趣可参考这个项目的源码 (前端 mj.js, 后端 Go) https://github.com/ahui2016/dictplus

总结

  • mj.js 极致简单,非常直观,对于已经用过 jQuery 的人来说学习成本约等于零。
  • 完全不使用 npm 或类似工具链,用 <script src="jquery.js"></script> 引用 jQuery 就可以直接写代码了。
  • 完全用 JavaScript 写组件,不需要写 HTML 或 JSX
  • 组件可以复用、可以嵌套、可以有自己的方法、可以相互交流
  • 可以配合 Bootstrap 之类的前端框架使用
  • 不适合用来做有很多即时交互的 web app, 但如果用来做博客、论坛之类的前端是完全够用的,而且页面是由一个个 "真组件" 搭建起来的,学习成本低、条理很清晰、可维护性强。
3