前端,从网页到应用—— 初识前端框架
Posted 三水清
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前端,从网页到应用—— 初识前端框架相关的知识,希望对你有一定的参考价值。
下面是文章内容:
本系列文章,记录我转行做前端至今所经历的所有工作、项目经历,以及所经历的技术演进路线过程中的思考与实践,希望对初入行做前端方向的童鞋们有所帮助。
这个系列可能会有些杂乱,毕竟工作也有些年头了,加上之前并没怎么写过文章,如有错误之处欢迎大家指出,转载请注明出处及作者,谢谢。
By 天翔Skyline
2012年的夏天,被某朋友忽悠转行来北京做前端(结果坑没了),寻找机会N久之后,机缘巧合入职了一家初创公司,做了一个神奇的网站叫师兄帮帮忙,也是职业生涯中算是第一个正式web项目了。
当时整个网站基于Django搭建,也是很多刚入行的前端er们最熟悉的工作---根据后端吐的数据套模板,俗称页面仔。开发环境前后端强耦合,没有测试机,本地完整clone前后端代码以及copy一份线上数据库到本地,直接作为开发环境。
作为一个刚入行的小白,接手项目的时候我是完全懵逼的,项目之间全量引入bootstrap + 通篇jQuery插件拼装,定制化样式全部使用inline-style,各种面向过程的js代码,这…这跟书上教我的不一样啊
<span class="step-links" style="float:left;margin-left:300px">
<span style="width:50px;display:block;float:left;margin-top:6px;">
{% if msgs.has_previous %}
<a href="?page={{ msgs.previous_page_number }}">上一页</a>
{% endif %}
</span>
<span class="current" style="margin-top:6px;float:left">
第{{ msgs.number }}页/共{{ msgs.paginator.num_pages }}页
</span>
<span style="width:50px;display:block;float:left;margin-top:6px;margin-left:11px">
{% if msgs.has_next %}
<a href="?page={{ msgs.next_page_number }}">下一页</a>
{% endif %}
</span> {% if msgs.has_other_pages %}
<span style="color:#39C;float:left;margin-top:6px">转到</span>
<input type="text" style="height:13px;width:13px;margin:5px 5px 0px 5px;border-radius:1px;float:left" maxlength="2">
<a style="cursor:pointer;float:left;margin-top:6px" onclick="go($(this));">Go</a> {% endif%}
</span>
(随手粘上来一段当时还没来的及改的代码,大概就是这种风格…槽点满满)
与此同时,老大说:我们帖子下面的评论不能每次点击都刷屏翻页啊,我们改成动态获取 + 分页吧,还有那个什么,帖子列表,也改成无限加载,还有那个什么回复的时候,能不能先把回复打上去啊,咱们这个速度有点慢,用户体验不是很好,还有那个什么什么…..
总结下来,老大想要的其实就是在原有网页基础上增加动态化获取。大概整理了下思路以及不断的骚扰朋友讨教之后,首先是需要整理了下模块结构以及简单拆分下公共样式,抽取util函数等,于是我开始引入Grunt做简单的一些打包合并压缩工作(此处网上有很多文章了,省略之,后续会对我所经历的工程化有一些加详细的介绍)。对于老大的新需求上,虽然简单的通过ajax+拼装html可以解决,但是没有合适的分层,代码组织上会非常凌乱。
简单来分析了一下现状,对应问题可以通过模型驱动的方式来解决。此时的我需要一个Model层在前端上进行数据的管理以及变化监听,需要一个模板引擎来解决Model --> Page的拼装,需要一个View层来声明行为(事件绑定),以及通过某种机制建立与Model变化的响应。此时正当 Backbone.js
比较火,于是…直接边学边引入进来使用。
举个简单的例子,为了兼容SEO,第一屏数据一定是页面直出的,后续更新时,前端提供当前 currentPage
后,拿到 taskList
,merge回当前 taskList
并去重(当时的情况局限);当删除任务的时候,又需要同步将页面中相应的片段删除掉。
简化下刚开始写的代码如下:
$('#task-container').on('click', '[data-action-get-topic]', function () {
api.getNewTask({
page: currentPage
}, function (list) {
currentPage++;
for (var i = 0; i < list.length; i++) {
if (!$('#topic-' + list[i].id).length) {
$('#topic-list').append([
'<li class="topic-item" data-id="' + list[i].id + '">',
list[i].content '</li>'
].join(''));
}
}
});
});
$('#task-container').on('click', '[data-action-delete-topic]', function (event) {
var me = this;
var id = $(event.target).closet('topic-item').data('id');
confirm('确定要删除帖子吗?', function () {
api.deleteTask({
id: id
}, function (data) {
if (data.success) {
$('#topic-' + id).remove();
}
});
});
});
简化了下当时的处理代码如下:
// 暂时省略了当时当时taskListView部分
var taskListModel = new Backbone.Model.extend({
defaults: {
nowGroupId: '',
currentPage: 1
}
});
var Task = Backbone.Model.extend({
defaults: {
isFromAjax: false
}
});
var TaskCollections = Backbone.Collection.extend({
model: Task,
initialize: function () {
var $topicList = $('#topic-list');
this.collection.on('add', function (model) {
if (model.get('isFromAjax')) {
$topicList.append(topicTemplate.render(model));
}
});
this.collection.on('remove', function (model) {
$('#topic-' + model.get('id')).remove();
});
}
});
var TaskView = Backbone.View.extend({
events: {
'click [data-action-get-topic]': 'getTask',
'click [data-action-delete-topic]': 'deleteTask'
},
initialize: function () {
var me = this;
this.$el.find('.topic-item').forEach(function (el) {
var id = el.find('[data-id]');
var content = el.find('[data-content]');
if (!me.collection.get(id)) {
me.collection.add({
id: id,
content: content
});
}
});
},
getTask: function () {
var me = this;
var currentPage = taskListModel.get('currentPage');
api.getNewTask({
page: currentPage
}, function (list) {
taskListModel.set('currentPage', currentPage);
for (var i = 0; i < list.length; i++) {
me.collection.add({
id: list[i].id,
content: list[i].content,
isFromAjax: true
});
}
});
},
deleteTask: function (event) {
var me = this;
var id = $(event.target).closet('topic-item').data('id');
confirm('确定要删除帖子吗?', function () {
api.deleteTask({
id: id
}, function (data) {
if (data.success) {
me.collection.remove(me.collection.find(id));
}
});
});
}
});
var taskCollection = new TaskCollections();
new TaskView({
el: $('#task-container'),
collection: taskCollection
});
(代码超级简陋,请容忍一个当时刚入行不久的我吧……)
简单来说:
建立 taskView
与 taskCollection
的时机是任务页面加载完毕的时机,在 taskCollection
初始化的时候去页面爬一遍task信息塞进去(为了SEO),后续异步获取的时候,根据 isFromAjax
以及 id
来判断该任务之前是否存在于 taskCollection
来进行判断是否生成html插入当前页面。删除时,根据后端返回结果,删除对应任务。
可以看到,虽然代码里面还有很多很多很多的问题,但是对于如何去拆分各个代码片断的职责,已经有了一个雏形轮廓的结构了。对于 currentPage
这类的全局变量,以及一些模块进行抽象,选择了拆分不同的 Model
来存储而非直接保存在全局的一个变量上;对于原有面向过程的代码,通过拆分对应职责,塞到对应的Model与View层中,来保证职责尽量清晰。
以上的理解也是当时我对于前端框架的第一次接触,原来前端除了切页面,为了保证代码可维护性,也是需要框架来帮助解决分层等问题的啊~
虽然一定程度上对职责进行了拆分(现在看起来有非常多的不合理之处),但是仍然避免不了很多问题,总结起来基本是以下三个方面:
模块耦合与职责边界,拆分粒度的问题。
比如当时有一个新消息推送,如果在任务A中,有@当前用户的任务,需要同步在右上角消息中心进行显示,且同时在当前页面上放提示有新的任务到达,请点击加载。当时我的解决方案是在
taskView
中直接引用了messageView
并注册监听来自messageView
上的事件,直接导致两个模块间强耦合。
对于代码的模块化组织上,没有什么好的解决方案。
比如css的部分,js的部分,在开发过程中只是简单的拆分了多个模块且进行打包合并引入,并未实现真正意义上的按需加载。
前后端的代码组织上耦合性太高,开发限制较大。
每次需要新增加页面or功能模块的时候,都需要前后端一起商量下目录划分,且开发过程中前端需要依赖于后端的开发进度,才能进行调试。
以及通过简单的Django MVT拆分,并不能有效的解决模块耦合问题。比如为了配合前端显示,后端需要在各个与路由直接绑定的
View
中均引入UserProfileModel
来进行当前登录用户查询。
在这个时期,除了学习以及恶补前端基础知识以外,也主见发现了诸如mvvm等概念的提出,以及前后端分离,模块化等前端层出不穷的解决方案,本打算继续学习下去以及解决这些问题的时候,我们公司……黄了……(蛮子嫖娼被抓我也没辙啊)于是,开始找工作,机缘巧合下,进入在下一家公司后,第一次接触了SPA的开发……
To be Continued...
以上是关于前端,从网页到应用—— 初识前端框架的主要内容,如果未能解决你的问题,请参考以下文章