妙评

妙评

前端演进史

作者/ 黄峰达

细细整理了过去接触过的那些前端技术,发现前端演进是段特别有意思的历史。人们总是习惯提早就做出未来需要的框架,而现在流行的是过去的过去发明过的。比如,不得不提到响应式设计的一个缺点:它只是将原本在模板层做的事,放到了样式(CSS)层来完成。复杂度同力一样不会消失,也不会凭空产生,它只是从一个物体转移到另一个物体或者从一种形式转为另一种形式而已。

如果六、七年前的移动网络速度和今天的一样快,响应式设计无疑将成为当今流行的技术,也就不会有APP、SPA如此快速得发展。尽管我们可以预见这些领域未来会变得更好,但也需要改变现状。改变现状的同时也需要预见未来的需求。

什么是前端?

维基百科是这样说的:前端(Front-end)和后端(back-end)是描述进程开始和结束的通用词汇。前端作用于采集输入信息,后端进行处理。计算机程序的界面样式和视觉呈现属于前端。

这种说法给人一种很模糊的感觉,但是说得又很对,前端确实负责视觉展示。在MVC结构或者MVP中,负责视觉显示的部分只有View层,而今天大多数所谓的View层已经超越了View层。前端是一个很神奇的概念,如今的前端已经发生了很大的变化。

如果引入Backbone、Angluar,你的架构就变成了MVP、MVVM。尽管一些架构发生了变化,但是项目的开发并没有因此而发生变化。这其中涉及一些职责的问题,如果某一个层级中有太多的职责,它是不是加重了一些人的负担?

前端演进史

过去,我一直想整理一篇文章来说说前端发展的历史,但是想着这些历史已经被人们所熟知,便就此作罢。后来我发现实际情况并非如此。

数据 - 模板 - 样式混合

在有限的前端经验里,我还是经历了那段用Table作样式的年代。大学期间,我曾经有偿帮一些公司或者个人开发维护CMS,而Table是当时更新网站样式接触到的。启动这个CMS时,用到的是一个名为 aspweb.exe 的程序。

<TABLE cellSpacing=0 cellPadding=0 width=910 align=center border=0>
  <TBODY>
  <TR>
    <TD vAlign=top width=188><TABLE cellSpacing=0 cellPadding=0 width=184 align=center border=0>
        <TBODY>
        <TR>
          <TD><IMG src="Images/xxx.gif" width=184></TD></TR>
        <TR>
          <TD>
            <TABLE cellSpacing=0 cellPadding=0 width=184 align=center
            background=Images/xxx.gif border=0>

虽然我在HEAD里找到了DIV + CSS的雏形,当时却仍然是一个Table的年代。

<LINK href="img/xxx.css" type=text/css rel=stylesheet>

人们一直在说前端很难,问题是你学过么?

人们一直在说前端很难,问题是你学过么?

人们一直在说前端很难,问题是你学过么?

也许,你也一直在说CSS不好写,但是CSS真的不好写么?人们总在说JS很难用,但是你学过么?只在需要的时候才去学,那肯定很难。你不曾花时间去专门学习一门语言,但是却能直接写出可以work的代码,这说明语言还是很容易上手的。如果看过一些有经验的Ruby、Scala、Emacs Lisp开发者写出来的代码,我想你会得到相同的结论。

过去的程序员其实是真正的全栈程序员,他们不仅做了前端的工作,还做了数据库的工作。

Set rs = Server.CreateObject("ADODB.Recordset")
sql = "select id,title,username,email,qq,adddate,content,Re_content,home,face,sex from Fl_Book where ispassed=1 order by id desc"
rs.open sql, Conn, 1, 1
fl.SqlQueryNum = fl.SqlQueryNum + 1

在这个ASP文件里,我们从数据库里查找出了数据,然后Render出HTML。如果可以看到历史版本,我想我们能看到作者将style=""的代码一个一个地放到css文件中。

这里也免不了有动态生成JavaScript代码的方法:

show_other = "<SCRIPT language=javascript>"
show_other = show_other & "function checkform()"
show_other = show_other & "{"
show_other = show_other & "if (document.add.title.value=='')"
show_other = show_other & "{"

请尽情地嘲笑吧,然后再看一段代码:

import React from "react";
import { getData } from "../../common/request";
import styles from "./style.css";


export default class HomePage extends React.Component {
  componentWillMount() {
    console.log("[HomePage] will mount with server response: ", this.props.data.home);
  }

  render() {
     let { title } = this.props.data.home;

     return (
       <div className={styles.content}>
         <h1>{title}</h1>
         <p className={styles.welcomeText}>Thanks for joining!</p>
       </div>
    );
  }

  static fetchData = function(params) {
    return getData("/home");
  }
}

10年前的代码和10年后的代码相比,似乎并没有太多的变化。不同的是,数据层已经被独立出去。如果你的component也混合了数据层,即直接查询数据库而不是调用数据层接口,那么你就需要好好思考下这个问题。你只是在追随潮流,还是在改变。用一个View层更换一个View层,用一个Router换一个Router的意义在哪?

Model - View - Controller

人们在不断地反思这其中的复杂过程,以整理出一些好的架构模式。其中不得不提到Martin Folwer的《企业应用架构模式》,该书对于系统的分层如下:

  • 表现层:提供服务、显示信息、用户请求、HTTP请求和命令行调用。

  • 领域层:逻辑处理,系统中真正的核心。

  • 数据层:与数据库、消息系统、事物管理器和其他软件包通讯。

MVC就化身于当时最流行的Spring,ORM(对象关系映射)处理iBatis这样的数据持久层框架。于是,package就会有这样的几个文件夹:

|____ mappers
|____ model
|____ service
|____ utils
|____ controller

在mappers层,我们可以做如下的数据库相关查询:

@Insert(
        "INSERT INTO users(username, password, enabled) " +
           "VALUES (#{userName}, #{passwordHash}, #{enabled})"
)
@Options(keyProperty = "id", keyColumn = "id", useGeneratedKeys = true)
void insert(User user);

model文件夹和mappers文件夹都是数据层的一部分,只是两者的职责不同。

public String getUserName() {
    return userName;
}

public void setUserName(String userName) {
    this.userName = userName;
}

最后两者都需要在Controller,又称为ModelAndView中进行处理。

@RequestMapping(value = {"/disableUser"}, method = RequestMethod.POST)
public ModelAndView processUserDisable(HttpServletRequest request, ModelMap model) {
    String userName = request.getParameter("userName");
    User user = userService.getByUsername(userName);
    userService.disable(user);
    Map<String,User> map = new HashMap<String,User>();
    Map <User,String> usersWithRoles= userService.getAllUsersWithRole();
    model.put("usersWithRoles",usersWithRoles);
    return new ModelAndView("redirect:users",map);
}

多数时候,Controller不应该直接属于数据层的一部分,而将业务逻辑放在Controller层又是一种错误,这时就有了Service层。

{%}

然而,人们对于Domain相关的Service应该放在哪一层,总会有不同的意见:

{%}

{%}

Domain(业务)是一个相当复杂的层级。一个合理的Controller只做自己应该做的事,不会处理业务相关的代码:

if (isNewnameEmpty == false && newuser == null){
    user.setUserName(newUsername);
    List<Post> myPosts = postService.findMainPostByAuthorNameSortedByCreateTime(principal.getName());

    for (int k = 0;k < myPosts.size();k++){
        Post post = myPosts.get(k);
        post.setAuthorName(newUsername);
        postService.save(post);
    }
    userService.update(user);
    Authentication oldAuthentication = SecurityContextHolder.getContext().getAuthentication();
    Authentication authentication = null;
    if(oldAuthentication == null){
        authentication = new UsernamePasswordAuthenticationToken(newUsername,user.getPasswordHash());
    }else{
        authentication = new UsernamePasswordAuthenticationToken(newUsername,user.getPasswordHash(),oldAuthentication.getAuthorities());
    }
    SecurityContextHolder.getContext().setAuthentication(authentication);
    map.clear();
    map.put("user",user);
    model.addAttribute("myPosts", myPosts);
    model.addAttribute("namesuccess", "User Profile updated successfully");
    return new ModelAndView("user/profile", map);
}

在Controller层应该做的事包括:

1. 处理请求的参数

2. 渲染和重定向

3. 选择Model和Service

4. 处理Session和Cookies

业务是善变的。也许昨天我们还在和对手竞争谁先推出新功能,今天可能就已经和对手合并了。我们很难预见业务的变化,但却能够预见Controller是不容易变化的。在一些设计里面,这种模式就是Command模式。

随着品味的不断变化更新,有时甚至可能因为竞争对手的变化,View层一直处于变化当中。竞争对手通常比我们想象中的更聪明,所以开创新的业务是一个更好的选择。

处于高速发展期的企业与初期企业相比,更需要前端开发人员。对于发展初期的企业来说,它们的用户基数不够、业务待定,因此View只要可用并美观就行了。但是对于高速发展的企业来讲,它们可能会有大量的业务代码存放在View层:

<c:choose>
    <c:when test="${ hasError }">
    <p class="prompt-error">
        ${errors.username} ${errors.password}
    </p>
    </c:when>
    <c:otherwise>
    <p class="prompt">
        Woohoo, User <span class="username">${user.userName}</span> has been created successfully!
    </p>
    </c:otherwise>
</c:choose>

前端开发人员需要修改JSP、PHP文件,因此我需要去了解这些Template:

{foreach $lists as $v}
<li itemprop="breadcrumb"><span{if(newest($v['addtime'],24))} style="color:red"{/if}>[{fun date('Y-m-d',$v['addtime'])}]</span><a href="{$v['url']}" style="{$v['style']}" target="_blank">{$v['title']}</a></li>
{/foreach}

有时,像Django这种自称为Model - Template - View的框架,更容易让人理解它的意图:

{% for blog_post in blog_posts.object_list %}
{% block blog_post_list_post_title %}
<section class="section--center mdl-grid mdl-grid--no-spacing mdl-shadow--2dp mdl-cell--11-col blog-list">
{% editable blog_post.title %}
<div class="mdl-card__title mdl-card--border mdl-card--expand">
    <h2 class="mdl-card__title-text">
        <a href="{{ blog_post.get_absolute_url }}"  itemprop="headline">{{ blog_post.title }} › </a>
    </h2>
</div>
{% endeditable %}
{% endblock %}

前端人员真正接触的是View层和Template层,但是MVC并没有说明这一点。

从网页版到移动版

Wap的出现,使得分辨率从1024x768变成176×208,开发人员不得不修改View层。等iPhone出现后,开发人员又必须对View层再次做出改变。

{%}

于是,开发人员把网页版的网站搬了过去,变成了移动版。由于网络的原因,每次都需要重新加载页面,这给用户带来了极其不佳的体验。

幸运的是,人们很快意识到了这个问题,于是出现了SPA。如果当时的移动网络速度可以更快的话,我想很多SPA框架就不存在了。

在讲解jQuery Mobile之前,我们先来看看两个不同版本的代码。下面来自一个手机版本的blog详情页:

<ul data-role="listview" data-inset="true" data-splittheme="a">
    {% for blog_post in blog_posts.object_list %}
        <li>
        {% editable blog_post.title blog_post.publish_date %}
        <h2 class="blog-post-title"><a href="{% url "blog_post_detail" blog_post.slug %}">{{ blog_post.title }}</a></h2>
        <em class="since">{% blocktrans with sometime=blog_post.publish_date|timesince %}{{ sometime }} ago{% endblocktrans %}</em>
        {% endeditable %}
        </li>
    {% endfor %}
</ul>

而这个是网页版本的片段:

{% for blog_post in blog_posts.object_list %}
{% block blog_post_list_post_title %}
{% editable blog_post.title %}
<h2>
    <a href="{{ blog_post.get_absolute_url }}">{{ blog_post.title }}</a>
</h2>
{% endeditable %}
{% endblock %}
{% block blog_post_list_post_metainfo %}
{% editable blog_post.publish_date %}
<h6 class="post-meta">
    {% trans "Posted by" %}:
    {% with blog_post.user as author %}
    <a href="{% url "blog_post_list_author" author %}">{{ author.get_full_name|default:author.username }}</a>
    {% endwith %}
    {% with blog_post.categories.all as categories %}
    {% if categories %}
    {% trans "in" %}
    {% for category in categories %}
    <a href="{% url "blog_post_list_category" category.slug %}">{{ category }}</a>{% if not forloop.last %}, {% endif %}
    {% endfor %}
    {% endif %}
    {% endwith %}
    {% blocktrans with sometime=blog_post.publish_date|timesince %}{{ sometime }} ago{% endblocktrans %}
</h6>
{% endeditable %}
{% endblock %}

可以看出,人们所做的只是重载View层,但这也是一个有效的SEO策略。

{%}

在这一时期,网页版和移动版的代码可能在同一个代码库中。他们使用相同的代码,调用相同的逻辑,只是View层不同。但是,每次改动我们都要维护两份代码。

随后,人们发现了一种更友好的移动版应用——APP。

APP与过渡期API

很多过去的API都是在原来的代码库中构建的,即网页版和移动版一起。随着代码库中功能的开发,系统变得越来越臃肿。就像《Linux/Unix设计思想》中所说的,这是一个伟大的系统,但是它臃肿而又缓慢。

我们是选择重新开发一个结合了前两个系统的最佳特性的第三个系统,还是继续臃肿下去。我想你已经有答案了。随后我们就有了APP API,构建了博客的APP。

{%}

人们很喜欢使用APP,因为与移动版网页相比,APP的响应速度更快而且更流畅。对于服务器来说,也是一件好事,因为请求变少了。

但是并非所有的人都会下载APP,因为有时人们只是想看看上面有没有需要的东西。对于需求不大的应用,人们只是访问访问网站。

有了APP API之后,我们可以向网页提供API。之后,我们开始设想有一个好好的移动版。

过渡期SPA

Backbone诞生于2010年,和响应式设计出现在同一个年代,也似乎在同一个时代里火了起来。如果CSS3能早点流行开来,Backbone似乎就没有登上舞台的机会了。

我们可以用Ajax向后台请求API,然后Mustache Render调出。因为JavaScript在模块化上的缺陷,所以我们用Require.JS来进行模块化。

我尝试对博客进行SPA设计时的代码如下:

define([
    'zepto',
    'underscore',
    'mustache',
    'js/ProductsView',
    'json!/configure.json',
    'text!/templates/blog_details.html',
    'js/renderBlog'
],function($, _, Mustache, ProductsView, configure, blogDetailsTemplate, GetBlog){

    var BlogDetailsView = Backbone.View.extend ({
        el: $("#content"),

        initialize: function () {
            this.params = '#content';
        },

         getBlog: function(slug) {
            var getblog = new GetBlog(this.params, configure['blogPostUrl'] + slug, blogDetailsTemplate);
             getblog.renderBlog();
         }
    });

    return BlogDetailsView;
});

从API获取数据,结合Template来Render出Page。除非我们可以像淘宝一样不需要考虑SEO,否则这无法改变我们需要Client Side Render和Server Side Render的两种Render方式。这是因为淘宝不依靠搜索引擎带来流量。

虽然数据的获取方式变成了Ajax,我们还是基于类MVC模式。这会导致我们将大量的业务逻辑放在前端,从而不能再从View层直接访问Model层。

如果你的View层还可以直接访问Model层,这说明你的架构还是MVC模式。之前我在Github上构建了一个Side Project,可以直接用View层访问Model层,因为Model层是一个ElasticSearch的搜索引擎,它提供了JSON API,这使得我要在View层处理数据——即业务逻辑。如果将上述的JSON API放入Controller,尽管会加重这一层的复杂度,但是业务逻辑就不再放置于View层。

如果View层和Model层之间总有一层接口,这说明你采用的就是MVP模式(MVC模式的衍生)。

一夜之前,我们又回到了过去。我们离开了JSP,将View层变成了Template与Controller。而原有的Services层并不是只承担其原来的责任,这些Services开始向ViewModel改变。

于是,一些团队便将Services抽成多个Services,美其名为“微服务”。传统架构下的API如下图所示:

{%}

变成了直接调用的微服务:

{%}

对于后台开发者来说,这是一件大快人心的好事,但是对于应用端/前端来说却并非如此。因为调用的服务变多,意味着应用程序端进行的功能测试更加复杂,需要Mock的API也就变多了。

Hybrid与ViewModel

这一时期,问题不止出现在前端同样会出现在App端。因此很多小团队就无法承受开发成本。好在Hybrid应用解决了一些小团队在开发初期遇到的问题。

前端开发人员先熟悉了单纯的JS + CSS + HTML,又熟悉了Router + PageView + API的结构,现在他们又需要做手机APP。所以只好用熟悉的jQuer Mobile + Cordova了。

随后,人们从Cordova + jQuery Mobile变成了Cordova + Angular的 Ionic。在这之前,一些团队可能已经用Angular代换了Backbone。

接着,我们可以直接将Angular代码从前端移到APP,比如下面这种博客APP的代码:

.controller('BlogCtrl', function ($scope, Blog) {
   $scope.blogs = null;
   $scope.blogOffset = 0;
   //
   $scope.doRefresh = function () {
     Blog.async('https://www.phodal.com/api/v1/app/?format=json').then(function (results) {
       $scope.blogs = results.objects;
     });
     $scope.$broadcast('scroll.refreshComplete');
     $scope.$apply()
   };

   Blog.async('https://www.phodal.com/api/v1/app/?format=json').then(function (results) {
     $scope.blogs = results.objects;
   });

   $scope.loadMore = function() {
     $scope.blogOffset = $scope.blogOffset + 1;
     Blog.async('https://www.phodal.com/api/v1/app/?limit=10&offset='+ $scope.blogOffset * 20 +  '&format=json').then(function (results) {
       Array.prototype.push.apply($scope.blogs, results.objects);
       $scope.$broadcast('scroll.infiniteScrollComplete');
     })
   };
 })

但是我们遇到了网页版的用户授权问题,于是发明了JWT——Json Web Token。结果时间轴又错了,人们总是超前一个时期就做错了未来可能是正确的决定。

由于WebView在一些早期的Android手机上出现了性能问题,于是人们开始考虑替换方案。不同的解决方案如下:

1. React Native

2. 新的WebView——Crosswalk

于是,一些开发人员开始追捧React Native这样的框架。但是,他们并没有预见到用户对APP的厌恶趋势。APP在迭代里更新着,可能是一星期,可能是两星期,又或者是一个月。这些内设的自动更新提醒无时无刻不在干扰着用户的生活,噪声越来越多。千万不要和用户争夺他们手机的使用权!

一次构建,跨平台运行

我们需要学习C语言的时候,GCC就有了这样的跨平台编译。

我们开发桌面应用的时候,QT就有了这样的跨平台能力。

我们构建Web应用的时候,Java就有了这样的跨平台能力。

我们需要开发跨平台应用的时候,Cordova就有了这样的跨平台能力。

现在,React这样的跨平台框架又出现了。

React,将一小部分复杂度交由人来消化,将另外一部分交给了React自己来消化。在Spring MVC之前,我们也许还在用CGI编程,但Spring的出现,降低了这部分的复杂度。(虽然它和React一样降低的只是新手的复杂度。)

RePractise

如果你是一只辛勤的蜜蜂,我想你应该已经玩过了上面的那些技术。你是在练习前端的技术,还是在RePractise?如果不花点时间整理过去,顺便预测未来,所有的都是无用功。

前端的演进特别快,Ruby On Rails在一个合适的年代出现、流行。RoR的高效率优势已然不再凸显。

如果不能把Controller、Model Mapper变成ViewModel,又或者是Micro Services来解耦,ES6 + React只能带来高的开发效率。而所谓的高效率,只是相比较而来的意淫结果,并不能持久。

未来,View层要突破自己。

首先,考虑可以让View层解耦于Domain或者Service层的方法,以统一桌面、平板、手机这三个用户设备。

其次,考虑用混合Micro Services优势的Monolithic Service来分解业务。如果要举一个成功的例子,这就是Linux,一个混合内核的“Service”。

最后,Keep Learning。总要在适当的时候做出改变!

本文选自图灵社区