前端,从网页到应用—— 初识前端框架

Posted 三水清

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端,从网页到应用—— 初识前端框架相关的知识,希望对你有一定的参考价值。


下面是文章内容:


本系列文章,记录我转行做前端至今所经历的所有工作、项目经历,以及所经历的技术演进路线过程中的思考与实践,希望对初入行做前端方向的童鞋们有所帮助。

这个系列可能会有些杂乱,毕竟工作也有些年头了,加上之前并没怎么写过文章,如有错误之处欢迎大家指出,转载请注明出处及作者,谢谢。

By 天翔Skyline

2012年的夏天,被某朋友忽悠转行来北京做前端(结果坑没了),寻找机会N久之后,机缘巧合入职了一家初创公司,做了一个神奇的网站叫师兄帮帮忙,也是职业生涯中算是第一个正式web项目了。

当时整个网站基于Django搭建,也是很多刚入行的前端er们最熟悉的工作---根据后端吐的数据套模板,俗称页面仔。开发环境前后端强耦合,没有测试机,本地完整clone前后端代码以及copy一份线上数据库到本地,直接作为开发环境。

作为一个刚入行的小白,接手项目的时候我是完全懵逼的,项目之间全量引入bootstrap + 通篇jQuery插件拼装,定制化样式全部使用inline-style,各种面向过程的js代码,这…这跟书上教我的不一样啊

 
   
   
 
  1. <span class="step-links" style="float:left;margin-left:300px">

  2.    <span style="width:50px;display:block;float:left;margin-top:6px;">

  3.        {% if msgs.has_previous %}

  4.        <a href="?page={{ msgs.previous_page_number }}">上一页</a>

  5.        {% endif %}

  6.    </span>

  7.    <span class="current" style="margin-top:6px;float:left">

  8.        第{{ msgs.number }}页/共{{ msgs.paginator.num_pages }}页

  9.    </span>

  10.    <span style="width:50px;display:block;float:left;margin-top:6px;margin-left:11px">

  11.        {% if msgs.has_next %}

  12.        <a href="?page={{ msgs.next_page_number }}">下一页</a>

  13.        {% endif %}

  14.    </span> {% if msgs.has_other_pages %}

  15.    <span style="color:#39C;float:left;margin-top:6px">转到</span>

  16.    <input type="text" style="height:13px;width:13px;margin:5px 5px 0px 5px;border-radius:1px;float:left" maxlength="2">

  17.    <a style="cursor:pointer;float:left;margin-top:6px" onclick="go($(this));">Go</a> {% endif%}

  18. </span>

(随手粘上来一段当时还没来的及改的代码,大概就是这种风格前端,从网页到应用(一)—— 初识前端框架…槽点满满)

与此同时,老大说:我们帖子下面的评论不能每次点击都刷屏翻页啊,我们改成动态获取 + 分页吧,还有那个什么,帖子列表,也改成无限加载,还有那个什么回复的时候,能不能先把回复打上去啊,咱们这个速度有点慢,用户体验不是很好,还有那个什么什么…..

总结下来,老大想要的其实就是在原有网页基础上增加动态化获取。大概整理了下思路以及不断的骚扰朋友讨教之后,首先是需要整理了下模块结构以及简单拆分下公共样式,抽取util函数等,于是我开始引入Grunt做简单的一些打包合并压缩工作(此处网上有很多文章了,省略之,后续会对我所经历的工程化有一些加详细的介绍)。对于老大的新需求上,虽然简单的通过ajax+拼装html可以解决,但是没有合适的分层,代码组织上会非常凌乱。

简单来分析了一下现状,对应问题可以通过模型驱动的方式来解决。此时的我需要一个Model层在前端上进行数据的管理以及变化监听,需要一个模板引擎来解决Model --> Page的拼装,需要一个View层来声明行为(事件绑定),以及通过某种机制建立与Model变化的响应。此时正当 Backbone.js比较火,于是…直接边学边引入进来使用。

举个简单的例子,为了兼容SEO,第一屏数据一定是页面直出的,后续更新时,前端提供当前 currentPage后,拿到 taskList,merge回当前 taskList并去重(当时的情况局限);当删除任务的时候,又需要同步将页面中相应的片段删除掉。

简化下刚开始写的代码如下:

 
   
   
 
  1. $('#task-container').on('click', '[data-action-get-topic]', function () {

  2.    api.getNewTask({

  3.        page: currentPage

  4.    }, function (list) {

  5.        currentPage++;

  6.        for (var i = 0; i < list.length; i++) {

  7.            if (!$('#topic-' + list[i].id).length) {

  8.                $('#topic-list').append([

  9.                    '<li class="topic-item" data-id="' + list[i].id + '">',

  10.                    list[i].content '</li>'

  11.                ].join(''));

  12.            }

  13.        }

  14.    });

  15. });

  16. $('#task-container').on('click', '[data-action-delete-topic]', function (event) {

  17.    var me = this;

  18.    var id = $(event.target).closet('topic-item').data('id');

  19.    confirm('确定要删除帖子吗?', function () {

  20.        api.deleteTask({

  21.            id: id

  22.        }, function (data) {

  23.            if (data.success) {

  24.                $('#topic-' + id).remove();

  25.            }

  26.        });

  27.    });

  28. });

简化了下当时的处理代码如下:

 
   
   
 
  1. // 暂时省略了当时当时taskListView部分

  2. var taskListModel = new Backbone.Model.extend({

  3.    defaults: {

  4.        nowGroupId: '',

  5.        currentPage: 1

  6.    }

  7. });

  8. var Task = Backbone.Model.extend({

  9.    defaults: {

  10.        isFromAjax: false

  11.    }

  12. });

  13. var TaskCollections = Backbone.Collection.extend({

  14.    model: Task,

  15.    initialize: function () {

  16.        var $topicList = $('#topic-list');

  17.        this.collection.on('add', function (model) {

  18.            if (model.get('isFromAjax')) {

  19.                $topicList.append(topicTemplate.render(model));

  20.            }

  21.        });

  22.        this.collection.on('remove', function (model) {

  23.            $('#topic-' + model.get('id')).remove();

  24.        });

  25.    }

  26. });

  27. var TaskView = Backbone.View.extend({

  28.    events: {

  29.        'click [data-action-get-topic]': 'getTask',

  30.        'click [data-action-delete-topic]': 'deleteTask'

  31.    },

  32.    initialize: function () {

  33.        var me = this;

  34.        this.$el.find('.topic-item').forEach(function (el) {

  35.            var id = el.find('[data-id]');

  36.            var content = el.find('[data-content]');

  37.            if (!me.collection.get(id)) {

  38.                me.collection.add({

  39.                    id: id,

  40.                    content: content

  41.                });

  42.            }

  43.        });

  44.    },

  45.    getTask: function () {

  46.        var me = this;

  47.        var currentPage = taskListModel.get('currentPage');

  48.        api.getNewTask({

  49.            page: currentPage

  50.        }, function (list) {

  51.            taskListModel.set('currentPage', currentPage);

  52.            for (var i = 0; i < list.length; i++) {

  53.                me.collection.add({

  54.                    id: list[i].id,

  55.                    content: list[i].content,

  56.                    isFromAjax: true

  57.                });

  58.            }

  59.        });

  60.    },

  61.    deleteTask: function (event) {

  62.        var me = this;

  63.        var id = $(event.target).closet('topic-item').data('id');

  64.        confirm('确定要删除帖子吗?', function () {

  65.            api.deleteTask({

  66.                id: id

  67.            }, function (data) {

  68.                if (data.success) {

  69.                    me.collection.remove(me.collection.find(id));

  70.                }

  71.            });

  72.        });

  73.    }

  74. });

  75. var taskCollection = new TaskCollections();

  76. new TaskView({

  77.    el: $('#task-container'),

  78.    collection: taskCollection

  79. });

(代码超级简陋,请容忍一个当时刚入行不久的我吧……)

简单来说:

建立 taskView与 taskCollection的时机是任务页面加载完毕的时机,在 taskCollection初始化的时候去页面爬一遍task信息塞进去(为了SEO),后续异步获取的时候,根据 isFromAjax以及 id来判断该任务之前是否存在于 taskCollection来进行判断是否生成html插入当前页面。删除时,根据后端返回结果,删除对应任务。

可以看到,虽然代码里面还有很多很多很多的问题,但是对于如何去拆分各个代码片断的职责,已经有了一个雏形轮廓的结构了。对于 currentPage这类的全局变量,以及一些模块进行抽象,选择了拆分不同的 Model来存储而非直接保存在全局的一个变量上;对于原有面向过程的代码,通过拆分对应职责,塞到对应的Model与View层中,来保证职责尽量清晰。

以上的理解也是当时我对于前端框架的第一次接触,原来前端除了切页面,为了保证代码可维护性,也是需要框架来帮助解决分层等问题的啊~

虽然一定程度上对职责进行了拆分(现在看起来有非常多的不合理之处),但是仍然避免不了很多问题,总结起来基本是以下三个方面:

  1. 模块耦合与职责边界,拆分粒度的问题。

比如当时有一个新消息推送,如果在任务A中,有@当前用户的任务,需要同步在右上角消息中心进行显示,且同时在当前页面上放提示有新的任务到达,请点击加载。当时我的解决方案是在 taskView中直接引用了 messageView并注册监听来自 messageView上的事件,直接导致两个模块间强耦合。

  1. 对于代码的模块化组织上,没有什么好的解决方案。

比如css的部分,js的部分,在开发过程中只是简单的拆分了多个模块且进行打包合并引入,并未实现真正意义上的按需加载。

  1. 前后端的代码组织上耦合性太高,开发限制较大。

每次需要新增加页面or功能模块的时候,都需要前后端一起商量下目录划分,且开发过程中前端需要依赖于后端的开发进度,才能进行调试。

以及通过简单的Django MVT拆分,并不能有效的解决模块耦合问题。比如为了配合前端显示,后端需要在各个与路由直接绑定的 View中均引入 UserProfileModel来进行当前登录用户查询。

在这个时期,除了学习以及恶补前端基础知识以外,也主见发现了诸如mvvm等概念的提出,以及前后端分离,模块化等前端层出不穷的解决方案,本打算继续学习下去以及解决这些问题的时候,我们公司……黄了……(蛮子嫖娼被抓我也没辙啊)于是,开始找工作,机缘巧合下,进入在下一家公司后,第一次接触了SPA的开发……


To be Continued...




以上是关于前端,从网页到应用—— 初识前端框架的主要内容,如果未能解决你的问题,请参考以下文章

前端开发初识搜狗搜索前端代码鉴赏

前端初识

前端初识

前端初识

前端笔记五初识VUE

前端笔记五初识VUE