3

这里我谈论的不仅仅是服务器端构建web组件, 而是你能用来构建服务器的web组件。

简单回顾一下,web组件是一套提案标准, 提供了一种模块化的方式,把UI和功能一起打包成可复用的、声明式的组件。这些组件可以很简单的被共享、组合成一个完整的应用。如今,它们已经被广泛用于前端开发。那么后端就不需要了么?Polymer Project 已经表明,web组件不仅对UI有利,对原生功能也有同样的好处。我们来看看如何使用这些组件,讨论下它们主要的优点:

  • 声明式

  • 模块化

  • 通用性

  • 共享性

  • 可调试性

  • 更小的学习曲线

  • 客户端结构

声明式

首先,取得声明式的服务器。以下是一个用 Express Web Components 快速构建的 Express.js 应用

<link rel="import" href="bower_components/polymer/polymer.html">
<link rel="import" href="bower_components/express-web-components/express-app.html">
<link rel="import" href="bower_components/express-web-components/express-middleware.html">
<link rel="import" href="bower_components/express-web-components/express-router.html">

<dom-module id="example-app">
    <template>
        <express-app port="5000">
            <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
            <express-middleware callback="[[notFound]]"></express-middleware>
        </express-app>
    </template>

    <script>
        class ExampleAppComponent {
            beforeRegister() {
                this.is = 'example-app';
            }

            ready() {
                this.indexHandler = (req, res) => {
                    res.send('Hello World!');
                };

                this.notFound = (req, res) => {
                    res.status(404);
                    res.send('not found');
                };
            }
        }

        Polymer(ExampleAppComponent);
    </script>
</dom-module> 

你不必单独把路由写在 JS 里面,可以直接在 HTML 里面声明。实际上,你还可以给路由构建一个视觉层级,这样比单纯写在 JS 里面更加清晰易懂。看上面这个例子,所有与这个 Express 应用相关的结点/中间件都被嵌套在 element 里面,而且这些中间件都按他们被写入 HTML 的顺序,链接到这个app。路由也很容易被嵌套。每个中间件甚至都可以被写到 element 里面,而不必链接到路由上面。

模块化

我们已经在 ExpressNode.js 中实现过模块化了,但个人感觉模块化web组件会更好维护。一起来看下这个用 Express web组件实现模块化自定义元素的例子

<!--index.html-->

<!DOCTYPE html>

<html>
    <head>
        <script src="../../node_modules/scram-engine/filesystem-config.js"></script>
        <link rel="import" href="components/app/app.component.html">
    </head>

    <body>
        <na-app></na-app>
    </body>

</html> 

所有事情都在 index.html 这个文件开始:

<!--components/app/app.component.html-->

<link rel="import" href="../../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-app.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-middleware.html">
<link rel="import" href="../api/api.component.html">

<dom-module id="na-app">
    <template>
        <express-app port="[[port]]" callback="[[appListen]]">
            <express-middleware callback="[[morganMW]]"></express-middleware>
            <express-middleware callback="[[bodyParserURLMW]]"></express-middleware>
            <express-middleware callback="[[bodyParserJSONMW]]"></express-middleware>
            <na-api></na-api>
        </express-app>
    </template>

    <script>
        class AppComponent {
            beforeRegister() {
                this.is = 'na-app';
            }

            ready() {
                const bodyParser = require('body-parser');
                const morgan = require('morgan');

                this.morganMW = morgan('dev'); // log requests to the console

                // configure body parser
                this.bodyParserURLMW = bodyParser.urlencoded({ extended: true });
                this.bodyParserJSONMW = bodyParser.json();

                this.port = process.env.PORT || 8080; //set our port

                const mongoose = require('mongoose');
                mongoose.connect('mongodb://@localhost:27017/test'); // connect to our database

                this.appListen = () => {
                    console.log(`Magic happens on port ${this.port}`);
                };
            }
        }

        Polymer(AppComponent);
    </script>
</dom-module> 

首先,我们在 8080 或者 process.env.PORT 端口监听这个 Express 应用,然后声明三个中间件和一个自定义元素。 我希望直觉告诉你那三个中间件在里面没有任何东西时就会被执行,这恰好就是它们的工作方式。

<!--components/api/api.component.html-->

<link rel="import" href="../../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-middleware.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-router.html">
<link rel="import" href="../bears/bears.component.html">
<link rel="import" href="../bears-id/bears-id.component.html">

<dom-module id="na-api">
    <template>
        <express-router path="/api">
            <express-middleware callback="[[allMW]]"></express-middleware>
            <express-middleware method="get" path="/" callback="[[indexHandler]]"></express-middleware>
            <na-bears></na-bears>
            <na-bears-id></na-bears-id>
        </express-router>
    </template>

    <script>
        class APIComponent {
            beforeRegister() {
                this.is = 'na-api';
            }

            ready() {
                // middleware to use for all requests with /api prefix
                this.allMW = (req, res, next) => {
                    // do logging
                    console.log('Something is happening.');
                    next();
                };

                // test route to make sure everything is working (accessed at GET http://localhost:8080/api)
                this.indexHandler = (req, res) => {
                    res.json({ message: 'hooray! welcome to our api!' });
                };
            }
        }

        Polymer(APIComponent);
    </script>
</dom-module> 

该组件里面的所有中间件在 /api 中都是可用的。再来看看下面这个例子:

<!--components/bears/bears.component.html-->

<link rel="import" href="../../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-middleware.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-route.html">

<dom-module id="na-bears">
    <template>
        <express-route path="/bears">
            <express-middleware method="post" callback="[[createHandler]]"></express-middleware>
            <express-middleware method="get" callback="[[getAllHandler]]"></express-middleware>
        </express-route>
    </template>

    <script>
        class BearsComponent {
            beforeRegister() {
                this.is = 'na-bears';
            }

            ready() {
                var Bear = require('./models/bear');

                // create a bear (accessed at POST http://localhost:8080/bears)
                this.createHandler = (req, res) => {
                    var bear = new Bear();      // create a new instance of the Bear model
                    bear.name = req.body.name;  // set the bears name (comes from the request)

                    bear.save(function(err) {
                        if (err)
                            res.send(err);
                        res.json({ message: 'Bear created!' });
                    });
                };

                // get all the bears (accessed at GET http://localhost:8080/api/bears)
                this.getAllHandler = (req, res) => {
                    Bear.find(function(err, bears) {
                        if (err)
                            res.send(err);
                        res.json(bears);
                    });
                };
            }
        }

        Polymer(BearsComponent);
    </script>
</dom-module> 
<!--components/bears-id/bears-id.component.html-->

<link rel="import" href="../../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-middleware.html">
<link rel="import" href="../../../../bower_components/express-web-components/express-route.html">

<dom-module id="na-bears-id">
    <template>
        <express-route path="/bears/:bear_id">
            <express-middleware method="get" callback="[[getHandler]]"></express-middleware>
            <express-middleware method="put" callback="[[updateHandler]]"></express-middleware>
            <express-middleware method="delete" callback="[[deleteHandler]]"></express-middleware>
        </express-route>
    </template>

    <script>
        class BearsIdComponent {
            beforeRegister() {
                this.is = 'na-bears-id';
            }

            ready() {
                var Bear = require('./models/bear');

                // get the bear with that id
                this.getHandler = (req, res) => {
                    console.log(req.params);
                    Bear.findById(req.params.bear_id, function(err, bear) {
                        if (err)
                            res.send(err);
                        res.json(bear);
                    });
                };

                // update the bear with this id
                this.updateHandler = (req, res) => {
                    Bear.findById(req.params.bear_id, function(err, bear) {
                        if (err)
                            res.send(err);
                        bear.name = req.body.name;
                        bear.save(function(err) {
                            if (err)
                                res.send(err);
                            res.json({ message: 'Bear updated!' });
                        });
                    });
                };

                // delete the bear with this id
                this.deleteHandler = (req, res) => {
                    Bear.remove({
                        _id: req.params.bear_id
                    }, function(err, bear) {
                        if (err)
                            res.send(err);
                        res.json({ message: 'Successfully deleted' });
                    });
                };
            }
        }

        Polymer(BearsIdComponent);
    </script>
</dom-module> 

如你所见,所有的路由被分到它们各自的组件中,这样更容易被包含于主应用中。index.html 这个文件是一系列浅显易懂的 imports 的起点,引导你穿梭于各个路由之间。

通用性

我喜欢 Javascript 原因之一,就是它可以在客户端和服务端之间共享代码。现今从某种程度上讲已经可以做到这样,但是客户端的库由于缺少 APIs, 仍然无法在服务器端工作,反之亦然。基本上,Node.js 和 浏览器仍然是提供不同 APIs 的不同平台。那么假使你能把这两者结合起来呢?Electron 就是为它而生的。Electron 把 Node.js 和 Chromium 项目结合起来在短时间内运行,允许我们一起使用客户端和服务端的代码。

Scram.js 是一个小工程,能让 Electron 在自由状态下运行,使得我们有可能运行服务端的 web 组件,就像你在运行任何 Node.js 的应用一样。

生产环境上面,我已经有一些基础应用了,是用 Dokku 做的。如果感兴趣,可以查看 Dokku Example

现在我来阐释一下在开发服务端 web组件 过程中,最优雅的一件事情。我使用了这个客户端的JS库 来制作一些特殊的 API 请求。显然这些库通过在客户端提供访问数据库的证书,这样对我们来说是行不通的。我们需要保证那些证书的安全,这就意味着我们需要在服务端处理请求。在Node.js里面去重写这些库的功能将会耗费大量的精力,但是用 ElectronScram.js, 我只需要把库引进来,无需做任何修改,就可以在服务器端运行了。

我只需要把库引进来,无需做任何修改,就可以在服务器端运行了。

我也曾在一些用 JavaScript 构建的移动端app尝试过这种方法。我们使用localForage 作为我们客户端的数据库。这些app使用了分布式数据库设计,用于彼此之间的交流,不用任何中心授权。我希望能在 Node.js 中使用 localForage, 这样我们就能复用模块,不需要做大的改动就可以运行了。

Electron 结合 Scram.js 一起使用,就可以访问 LocalStorage, Web SQL, 和 IndexedDB 了。

我不确定这种性能如何伸展,但至少它是有可能实现的。

现在你也可以使用像在客户端一样,使用 iron-ajaxredux-store-element 这些组件了。我希望这样能使该范例被复用,在客户端良好运行,这样可以免去从客户端到服务端这个过程的时间间距。

共享性

这是web组件固有的优点。web组件主要就是希望能够被共享,跨浏览器使用,而且不用因为框架和库的改变,而重复用同样的解决方案。web组件之所以能被共享,是因为它是基于当前或已经提案的标准,而这些标准所有的主流浏览器都会努力遵守。

这就意味着web组件不依赖任何框架或者库,却能在web平台上广泛使用。

我希望有更多人构建服务端的web组件,像前端组件一样把功能打包起来。我已经从 Express 组件着手了,设想一下还有用 Koa, Hapi.js, Socket.io, MongoDB 等开发的组件。

可调试性

Scram.js 只有一个可选项 -d 允许你调试时打开一个 Electron 窗口。现在你拥有所有 Chrome 的开发工具,可以帮助你调试服务器:断点、打印、查看网络信息等。对我而言,Node.js的服务器端调试是比较次要的。现在恰好打造了这样一个平台:

更小的学习曲线

服务器端web组件有助于平衡后端编程的竞争环境。有很多人,如网页设计,UX设计或者其他职业的人,他们都知道 HTML 和 CSS,但可能不了解 编程, 因为现他们中的一些人,几乎不可能接触到的服务端代码的。但如果服务端web组件能用大家所熟悉的HTML来写,尤其是可以自定义元素的语义化语言,它们将有可能更好地与服务端代码一起运行。至少可以降低我们的学习曲线。

客户端结构

客户端和服务端apps的结构现在更加接近。每个 app 都会有一个入口文件 index.html, 组件就从这个文件开始引用。这又是另一种统一的方式。过去我们想找到服务端app代码的起点是有点困难的,但 index.html 似乎已经成为前端app标准的起始文件,那为什么后端代码不要也这样做呢?

下面是一个用web组件构建的 客户端app 通用结构的例子:

app/
----components/
--------app/
------------app.component.html
------------app.component.js
--------blog-post/
------------blog-post.component.html
------------blog-post.component.js
----models/
----services/
----index.html

而这下面是一个用web组件构建服务端app通用结构的例子:

app/
----components/
--------app/
------------app.component.html
------------app.component.js
--------api/
------------api.component.html
------------api.component.js
----models/
----services/
----index.html

两个结构可能同样有效,而且我们减少了从客户端到服务端之间必要内容切换的数量, 反之亦然。

可能存在的问题

有可能使之崩溃的最大问题就是它的性能和生产服务器环境下 Electron 的稳定性。话虽这么说,但我并不是预言性能会是一个大问题,因为 Electron 仅仅是加快一个渲染过程来执行 Node.js 的代码, 而且我假定进程会或多或少地运行 Node.js 代码,就像 vanilla Node.js 的过程一样。更大的问题是是否 Chromium 的运行时间足够稳定到一次性持续几个月而不用停下来。

另一个可能出现的问题就是太冗长。在实现相同功能的情况下,用服务端web组件因为所有的标记,要比用 vanilla JavaScript 多一些代码。话虽如此,但我希望代码的易读性可以弥补它冗长的缺点。

测试基准

出于好奇,我做过一些测试来对比两个相同app的性能。一个是用vanilla Node.js 和 Express实现并且运行在该环境下,另一个则是用 Express web 组件构建并且运行在Electron 结合Scram.js 的环境下。下面的图表展示了使用这个库在主线上的一些简单的压力测试。以下是测试的参数:

  • 在本地机器运行

  • 没秒增加100个get请求

  • 运行直到1%的请求返回不成功

  • 在 Node.js app 和 Electron/Scram.js app 运行10次

  • Node.js app

  • 使用 Node.js v6.0.0

  • 使用 Express v4.10.1

  • Electron/Scram.js app

  • 使用 Scram.js v0.2.2

  • 默认设置 (从本地服务器加载起点html)

  • 调试关闭的窗口

  • 使用 Express v4.10.1

  • 使用 electron-prebuilt v1.2.1

  • 用这个库运行: https://github.com/doubaokun/node-ab

  • 用这条命令运行: nab http://localhost:3000 --increase 100 --verbose

以下是以下结果(QPS 代表 "Queries Per Second")

令人惊讶的是,Electron/Scram.js 竟胜于 Node.js。 我们或许应该对这些测试结果持保留的态度,但从这些结果我得出一个结论:使用Electron来做服务端不一定比使用Node.js差,至少在短时间内处理请求的性能方面还是很不错的。这些结果为我之前的陈述 “我不是预言性能会有大问题” 这句话增添了论据。

总结

Web components are awesome. They are bringing a standard declarative component model to the web platform. It’s easy to see the benefits on the client, and there are so many benefits to be gained on the server. The gap between the client and server is narrowing, and I believe that server-side web components are a huge step in the right direction. So, go build something with them!
web 组件了不起。他们给web平台带来了一个标准的声明式组件模板。很容易看出客户端的好处,服务端也可以获得很多好处。客户端和服务端的沟正在不断地缩小,我相信,服务端web组件在正确的方向上跨出了一大步。所以,把他们用起来吧!


grace_xhw
1.4k 声望132 粉丝

博观而约取,厚积而薄发