Marko.js-ebay.com的前端

Marko.js不如Angular,React.js,Vue.js或Svelte流行。 Marko.js是一个ebay.com项目,自2015年以来已成为开源资源。实际上,正是在此库中构建了ebay.com前端,这使我们可以得出有关其对开发人员的实际价值的结论。



Marko.js的情况有点类似于Ember.js框架的情况,尽管它实际上是几个高负载站点(例如Linkedin)的前端,但一般的开发人员对此几乎一无所知。就Marko.js而言,可以说他根本不知道。



Marko.js的速度非常快,尤其是在渲染服务器端时。当涉及到服务器渲染时,Marko.js的速度可能与其悠闲的对手保持遥不可及,这是有客观原因的。我们将在建议的材料中讨论它们。



SSR优先框架



Marko.js可以成为经典前端(具有服务器端渲染),单页应用程序(具有客户端渲染)以及同构/通用应用程序(稍后将讨论其示例)的基础。但仍然可以将Marko.js视为SSR优先的库,也就是说,主要专注于服务器渲染。 Marko.js与其他组件框架的区别在于服务器端组件不构建DOM,而是将DOM序列化为字符串,但将其实现为输出流。为了清楚说明其含义,我将列出一个简单的服务器组件:



// Compiled using marko@4.23.9 - DO NOT EDIT
"use strict";

var marko_template = module.exports = require("marko/src/html").t(__filename),
    marko_componentType = "/marko-lasso-ex$1.0.0/src/components/notFound/index.marko",
    marko_renderer = require("marko/src/runtime/components/renderer");

function render(input, out, __component, component, state) {
  var data = input;

  out.w("<p>Not found</p>");
}

marko_template._ = marko_renderer(render, {
    ___implicit: true,
    ___type: marko_componentType
  });

marko_template.meta = {
    id: "/marko-lasso-ex$1.0.0/src/components/notFound/index.marko"
  };


服务器组件不应与客户端组件相同的想法似乎很自然。在此基础上,最初构建了Marko.js库。我可以假设在其他框架的情况下,这些框架最初是作为面向客户端的,服务器端呈现而构建的,已被粘贴到已经非常复杂的代码库中。这是在体系结构上存在缺陷的决定的地方,在DOM的服务器端重新创建了DOM,以便现有的客户端代码可以不变地重用。



它为什么如此重要



随着Angular,React.js,Vue.js的广泛使用以及积极的时刻,观察到了单页应用程序创建的进展,揭示了面向客户端的体系结构的一些致命错误。早在2013年,Airbnb的Spike Brehm发表了一篇程序化文章,其相关部分的标题为“美中不足”。同时,所有负面因素都影响了企业:



  • 第一页的加载时间增加;
  • 内容未被搜索引擎索引;
  • 残疾人无障碍环境。


作为替代方案,最终创建了用于开发同构/通用应用程序的框架:Next.js和Nust.js。然后另一个因素发挥作用-性能。每个人都知道,在加载复杂的计算时,node.js并不是那么好。而且,当我们在服务器上创建DOM然后开始对其进行序列化时,node.js很快就会消失。是的,我们可以吊起无限数量的node.js副本。但是也许尝试在Marko.js中做同样的事情?



如何开始使用Marko.js



初次接触时,我建议按照文档中的命令使用命令启动npx @marko/create --template lasso-express



结果,我们将为使用配置的Express.js服务器和Lasso链接器(此链接器由ebay.com开发并且最易于集成)进行的项目进一步开发奠定基础。



Marko.js中的组件通常位于扩展名为.marko的文件的/ components目录中。组件代码很直观。如文档所述,如果您知道html,那么您就会知道Marko.js。



该组件在服务器上呈现,然后在客户端上水合。也就是说,在客户端上,我们不会收到静态的html,而是具有状态和事件的完整的客户端组件。



在开发模式下启动项目时,热重装有效。



要构建复杂的应用程序,除了组件库外,我们很可能还需要其他东西,例如,路由,存储,用于创建同构/通用应用程序的框架。遗憾的是,这里的问题与React.js的开发人员在早期面临的问题相同-没有现成的解决方案和众所周知的方法。因此,到目前为止的所有内容都可以称为关于基于Marko.js构建应用程序的对话的简介。



建立同构/通用应用程序



就像我说的那样,关于Marko.js的文章并不多,因此下面的所有内容都是我实验的成果,部分是基于与其他框架的合作。



Marko.js允许您动态(即以编程方式)设置和更改标签或组件的名称-这就是我们将要使用的名称。让我们匹配路线-组件名称。由于Marko.js中没有开箱即用的路由(很有趣的是,它是如何在ebay.com上构建的),我们将使用该软件包,仅用于这种情况-通用路由器:



const axios = require('axios');
const UniversalRouter = require('universal-router');

module.exports = new UniversalRouter([
  { path: '/home', action: (req) => ({ page: 'home' }) },
  {
    path: '/user-list',
    action: async (req) => {
      const {data: users} = await axios.get('http://localhost:8080/api/users');
      return { page: 'user-list', data: { users } };
    }
  },
  {
    path: '/users/:id',
    action: async (req) => {
      const {data: user} = await axios.get(`http://localhost:8080/api/users/${req.params.id}`);
      return { page: 'user', data: { req, user } };
    }
  },
  { path: '(.*)', action: () => ({ page: 'notFound' }) }
])


通用路由器软件包的功能非常简单。它解析url字符串,并使用已解析的字符串调用异步函数action(req),例如,在其中我们可以访问已解析的字符串参数(req.params.id)。由于action(req)函数是异步调用的,因此我们可以在此处使用API​​请求初始化数据。



您还记得,在上一节中,一个团队创建了一个项目npx @marko/create --template lasso-express让我们将其作为同构/通用应用程序的基础。为此,我们将稍微更改server.js文件



app.get('/*', async function(req, res) {
    const { page, data } = await router.resolve(req.originalUrl);
    res.marko(indexTemplate, {
            page,
            data,
        });
});


我们还将更改已加载页面的模板:



<lasso-page/>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>Marko | Lasso + Express</title>
    <lasso-head/>
    <style>
      .container{
        margin-left: auto;
        margin-right: auto;
        width: 800px;
    }
    </style>
  </head>
  <body>
    <sample-header title="Lasso + Express"/>
    <div class="container">
      <router page=input.page data=input.data/>
    </div>
    <lasso-body/>
    <!--
    Page will automatically refresh any time a template is modified
    if launched using the browser-refresh Node.js process launcher:
    https://github.com/patrick-steele-idem/browser-refresh
    -->
    <browser-refresh/>
  </body>
</html>


<router />组件正是负责加载动态组件的部分,我们从页面属性中的路由器获得名称。



<layout page=input.page>
  <${state.component} data=state.data/>
</layout>

import history from '../../history'
import router from '../../router'

class {
  onCreate({ page, data }) {
    this.state = {
      component: require(`../${page}/index.marko`),
      data
    }
    history.listen(this.handle.bind(this))
  }

  async handle({location}) {
    const route = await router.resolve(location);
    this.state.data = route.data;
    this.state.component = require(`../${route.page}/index.marko`);
  }
}


传统上,Marko.js具有this.state,它的更改会导致组件视图的更改,这就是我们正在使用的状态。



您还必须自己实施历史记录工作:



const { createBrowserHistory } = require('history')
const parse = require('url-parse')
const deepEqual = require('deep-equal')
const isNode = new Function('try {return !!process.env;}catch(e){return false;}') //eslint-disable-line
let history

if (!isNode()) {
  history = createBrowserHistory()
  history.navigate = function (path, state) {
    const parsedPath = parse(path)
    const location = history.location
    if (parsedPath.pathname === location.pathname &&
      parsedPath.query === location.search &&
      parsedPath.hash === location.hash &&
      deepEqual(state, location.state)) {
      return
    }
    const args = Array.from(arguments)
    args.splice(0, 2)
    return history.push(...[path, state, ...args])
  }
} else {
  history = {}
  history.navigate = function () {}
  history.listen = function () {}
}

module.exports = history


最后,应该有一个导航源可以拦截链接单击事件并在页面上调用导航:



import history from '../../history'

<a on-click("handleClick") href=input.href><${input.renderBody}/></a>

class {
  handleClick(e) {
    e.preventDefault()
    history.navigate(this.input.href)
  }
}


为了方便学习材料,我在存储库中展示了结果



apapacy@gmail.com

2020年11月22日



All Articles