程式設計師必知的前端演進史

github發表於2015-12-03

細細整理了過去接觸過的那些前端技術,發現前端演進是段特別有意思的歷史。人們總是在過去就做出未來需要的框架,而現在流行的是過去發明過的。如,響應式設計不得不提到的一個缺點是:它只是將原本在模板層做的事,放到了樣式(CSS)層來完成

複雜度同力一樣不會消失,也不會憑空產生,它總是從一個物體轉移到另一個物體或一種形式轉為另一種形式。

如果六、七年前的行動網路速度和今天一樣快,那麼直接上的技術就是響應式設計,APP、SPA就不會流行得這麼快。儘管我們可以預見未來這些領域會變得更好,但是更需要的是改變現狀。改變現狀的同時也需要預見未來的需求。

程式設計師必知之前端演進史

(題圖來自:cuelogic.com)

什麼是前端?

維基百科是這樣說的:前端(front-end)和後端(back-end)是描述程式開始和結束的通用詞彙。前端作用於採集輸入資訊,後端進行處理。計算機程式的介面樣式,視覺呈現屬於前端。

這種說法給人一種很模糊的感覺,但是他說得又很對,它負責視覺展示。在MVC結構或者MVP中,負責視覺顯示的部分只有View層,而今天大多數所謂的View層已經超越了View層。前端是一個很神奇的概念,但是而今的前端已經發生了很大的變化。

你引入了Backbone、Angluar,你的架構變成了MVP、MVVM。儘管發生了一些架構上的變化,但是專案的開發並沒有因此而發生變化。這其中涉及到了一些職責的問題,如果某一個層級中有太多的職責,那麼它是不是加重了一些人的負擔?

前端演進史

過去一直想整理一篇文章來說說前端發展的歷史,但是想著這些歷史已經被人們所熟知。後來發現並非如此,大抵是倖存者偏見——關注到的都知道這些歷史。

資料-模板-樣式混合

在有限的前端經驗裡,我還是經歷了那段用Table來作樣式的年代。大學期間曾經有償幫一些公司或者個人開發、維護一些CMS,而Table是當時幫某個網站更新樣式接觸到的——ASP.Net(maybe)。當時,我們啟動這個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>[站外圖片上傳中……(9)]</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開發者寫出來的程式碼,我想會得到相同的結論。有一些語言可以讓寫程式的人Happy,但是看的人可能就不Happy了。做事的方法不止一種,但是不是所有的人都要用那種方法去做。

過去的那些程式設計師都是真正的全棧程式設計師,這些程式設計師不僅僅做了前端的活,還做了資料庫的工作。

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的《企業應用架構模式》。該書中文譯版出版的時候是2004年,那時對於系統的分層是

層次 職責
表現層 提供服務、顯示資訊、使用者請求、HTTP請求和命令列呼叫。
領域層 邏輯處理,系統中真正的核心。
資料層 與資料庫、訊息系統、事物管理器和其他軟體包通訊。

化身於當時最流行的Spring,就是MVC。人們有了iBatis這樣的資料持久層框架,即ORM,物件關係對映。於是,你的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層,如下圖:

程式設計師必知之前端演進史
Service MVC

然而對於Domain相關的Service應該放在哪一層,總會有不同的意見:

程式設計師必知之前端演進史
MVC Player

程式設計師必知之前端演進史
MS MVC

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層應該做的事是:

  • 處理請求的引數
  • 渲染和重定向
  • 選擇Model和Service
  • 處理Session和Cookies

業務是善變的,昨天我們可能還在和對手競爭誰先推出新功能,但是今天可能已經合併了。我們很難預見業務變化,但是我們應該能預見Controller是不容易變化的。在一些設計裡面,這種模式就是Command模式。

View層是一直在變化的層級,人們的品味一直在更新,有時甚至可能因為競爭對手而產生變化。在已經取得一定市場的情況下,Model-Service-Controller通常都不太會變動,甚至不敢變動。企業意識到創新的兩面性,要麼帶來死亡,要麼佔領更大的市場。但是對手通常都比你想象中的更聰明一些,所以這時開創新的業務是一個更好的選擇

高速發展期的企業和發展初期的企業相比,更需要前端開發人員。在使用者基數不夠、業務待定的情形中,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出現了,並帶來了更多的挑戰。隨後,解析度從1024×768變成了176×208,開發人員不得不面臨這些挑戰。當時所需要做的僅僅是修改View層,而View層隨著iPhone的出現又發生了變化。

程式設計師必知之前端演進史
WAP 網站

這是一個短暫的歷史,PO還需要為手機使用者製作一個怎樣的網站?於是他們把桌面版的網站搬了過去變成了移動版。由於網路的原因,每次都需要重新載入頁面,這帶來了不佳的使用者體驗。

幸運的是,人們很快意識到了這個問題,於是就有了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策略,上面這些程式碼是我部落格過去的程式碼。對於桌面版和移動版都是不同的模板和不同的JS、CSS。

程式設計師必知之前端演進史
移動版網頁

在這一時期,桌面版和移動版的程式碼可能在同一個程式碼庫中。他們使用相同的程式碼,呼叫相同的邏輯,只是View層不同了。但是,每次改動我們都要維護兩份程式碼。

隨後,人們發現了一種更友好的移動版應用——APP。

APP與過渡期API

這是一個艱難的時刻,過去我們的很多API都是在原來的程式碼庫中構建的,即桌面版和移動版一起。我們已經在這個程式碼庫中開發了越來越多的功能,系統開發變得臃腫。如《Linux/Unix設計思想》中所說,這是一個偉大的系統,但是它臃腫而又緩慢。

我們是選擇重新開發一個結合第一和第二系統的最佳特性的第三個系統,還是繼續臃腫下去。我想你已經有答案了。隨後我們就有了APP API,構建出了部落格的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。但是這無法改變我們需要Client Side Render和Server Side Render的兩種Render方式,除非我們可以像淘寶一樣不需要考慮SEO——因為它不那麼依靠搜尋引擎帶來流量。

這時,我們還是基於類MVC模式。只是資料的獲取方式變成了Ajax,我們就犯了一個錯誤——將大量的業務邏輯放在前端。這時候我們已經不能再從View層直接訪問Model層,從安全的角度來說有點危險。

如果你的View層還可以直接訪問Model層,那麼說明你的架構還是MVC模式。之前我在Github上構建一個Side Project的時候直接用View層訪問了Model層,由於Model層是一個ElasticSearch的搜尋引擎,它提供了JSON API,這使得我要在View層處理資料——即業務邏輯。將上述的JSON API放入Controller,儘管會加重這一層的複雜度,但是業務邏輯就不再放置於View層。

如果你在你的View層和Model層總有一層介面,那麼你採用的就是MVP模式——MVC模式的衍生(PS:為了區別別的事情,總會有人取個表意的名稱)。

一夜之前,我們又回到了過去。我們離開了JSP,將View層變成了Template與Controller。而原有的Services層並不是只承擔其原來的責任,這些Services開始向ViewModel改變。

一些團隊便將Services抽成多個Services,美其名為微服務。傳統架構下的API從下圖

程式設計師必知之前端演進史
API Gateway

變成了直接呼叫的微服務:

程式設計師必知之前端演進史
Micro Services

對於後臺開發者來說,這是一件大快人心的大好事,但是對於應用端/前端來說並非如此。呼叫的服務變多了,在應用程式端進行功能測試變得更復雜,需要Mock的API變多了。

Hybird與ViewModel

這時候遇到問題的不僅僅只在前端,而在App端,小的團隊已經無法承受開發成本。人們更多的注意力放到了Hybird應用上。Hybird應用解決了一些小團隊在開發初期遇到的問題,這部分應用便交給了前端開發者。

前端開發人員先熟悉了單純的JS + CSS + HTML,又熟悉了Router + PageView + API的結構,現在他們又需要做手機APP。這時候只好用熟悉的jQuer Mobile + Cordova。

隨後,人們先從Cordova + jQuery Mobile,變成了Cordova + Angular的 Ionic。在那之前,一些團隊可能已經用Angular代換了Backbone。他們需要更好的互動,需要data binding。

接著,我們可以直接將我們的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在我們的迭代裡更新著,可能是一星期,可能是兩星期,又或者是一個月。誰說APP內自更新不是一件壞事,但是APP的提醒無時無刻不在干擾著人們的生活,噪聲越來越多。不要和使用者爭奪他們手機的使用權

一次構建,跨平臺執行

在我們需要學習C語言的時候,GCC就有了這樣的跨平臺編譯。

在我們開發桌面應用的時候,QT有就這樣的跨平臺能力。

在我們構建Web應用的時候,Java有這樣的跨平臺能力。

在我們需要開發跨平臺應用的時候,Cordova有這樣的跨平臺能力。

現在,React這樣的跨平臺框架又出現了,而響應式設計也是跨平臺式的設計。

響應式設計不得不提到的一個缺點是:他只是將原本在模板層做的事,放到了樣式(CSS)層。你還是在針對著不同的裝置進行設計,兩種沒有什麼多大的不同。複雜度不會消失,也不會憑空產生,它只會從一個物體轉移到另一個物體或一種形式轉為另一種形式。

React,將一小部分複雜度交由人來消化,將另外一部分交給了React自己來消化。在用Spring MVC之前,也許我們還在用CGI程式設計,而Spring降低了這部分複雜度,但是這和React一樣降低的只是新手的複雜度。在我們不能以某種語言的方式寫某相關的程式碼時,這會帶來諸多麻煩。

RePractise

如果你是一隻辛勤的蜜蜂,那麼我想你應該都玩過上面那些技術。你是在練習前端的技術,還是在RePractise?如果你不花點時間整理一下過去,順便預測一下未來,那麼你就是在白搭。

前端的演進在這一年特別快,Ruby On Rails也在一個合適的年代裡出現,在那個年代裡也流行得特別快。RoR開發效率高的優勢已然不再突顯,語法靈活性的副作用就是執行效率降低,同時後期維護難——每個人超程式設計了自己。

如果不能把Controller、Model Mapper變成ViewModel,又或者是Micro Services來解耦,那麼ES6 + React只是在現在帶來更高的開發效率。而所謂的高效率,只是相比較而意淫出來的,因為他只是一層View層。將Model和Controller再加回View層,以後再拆分出來?

現有的結構只是將View層做了View層應該做的事。

首先,你應該考慮的是一種可以讓View層解耦於Domain或者Service層。今天,桌面、平板、手機並不是唯一使用者裝置,雖然你可能在明年統一了這三個平臺,現在新的裝置的出現又將裝置分成兩種型別——桌面版和手機版。一開始桌面版和手機版是不同的版本,後來你又需要合併這兩個裝置。

其次,你可以考慮用混合Micro Services優勢的Monolithic Service來分解業務。如果可以舉一個成功的例子,那麼就是Linux,一個混合核心的“Service”。

最後,Keep Learning。我們總需要在適當的時候做出改變,儘管我們覺得一個Web應用程式碼庫中含桌面版和移動版程式碼會很不錯,但是在那個時候需要做出改變。

對於複雜的應用來說,其架構肯定不是隻有純MVP或者純MVVM這麼簡單的。如果一個應用混合了MVVM、MVP和MVC,那麼他也變成了MVC——因為他直接訪問了Model層。但是如果細分來看,只有訪問了Model層的那一部分才是MVC模式。

模式,是人們對於某個解決方案的描述。在一段程式碼中可能有各種各樣的設計模式,更何況是架構。

相關文章