作为一个后端学习前端,我决的主要应该讲解学习的方法,而不是具体的技巧
show time
过年回来终于把第一个demo写完了,根据学堂在线的api,写了一个瀑布流的知识点照片墙一样的东西,鼠标hover的时候在图片切换时有某种direction效果,现在还有些东西需要完善。
菊花转起来
打开时瀑布流默认在右边有个很大的留白(估计是 freewall 我没用对)
懒加载加了但是貌似没效果
demo 美国vps 图慢 可能被墙 能不能看随缘
源码 tag为2016-02-16-demo。预警: bower npm的东西我都放到git中了(被坑过),比较大,
开始讲解
作为一个后端学习前端,我决的主要应该讲解学习的方法,而不是具体的技巧
思考做点什么
扒了下学堂在线首页,发现是几种列表的集合,几种不同类型课程列表,知识点列表,帖子列表。然而课程点进去是具体的课程,要播放视频还要加课,略微烦躁;帖子没什么意思,看不出什么酷炫的效果;知识点点进去之后发现有个汇总页,而且每分页,可见数据量可以,而且都有图片什么的,点一个进去发现不用注册就可以看视频。嘿嘿嘿,那么就可以做一些事情了。
做什么呢,我扒了扒之前收藏夹里面的东西 codrops 上刚好看到两个有点意思的东西
第二个东西启发我想到了拉钩 上面如下图位置的鼠标hover效果,然后我就想这么多有图的知识点为毛不做个照片墙,鼠标移动过去标题出现就用拉钩的这个效果,点进去开始播视频,好像有点意思。
基础数据准备
找api,没找到一次获取全部知识点的api,但是找到另外一组
根据tag id获得对应知识点列表: http://www.xuetangx.com/api/v2/fragment/tag/1/
gulp 和 spine 我分别在上面两个帖子中讲过 先搞定gulp 喜闻乐见的跨域问题,有些东西就不说了。 尤其是model获得数据这里我就不再赘述。
先看下最后的目录结构, course有关的东西都没用,之前留下的先留着。
根据上个帖子讲的,准备tag和知识点的两个model
class FragmentTag extends Spine.Model
@configure "Tag", "id", "key"
@extend Spine.Model.Ajax
@url: "/api/v2/fragment/tags"
@beforeFromJSON: (data) ->
data['tags']
module.exports = FragmentTag
class Fragment extends Spine.Model
@configure "Fragment", "tag_id"
@extend Spine.Model.Ajax
url: () =>
"#{Spine.Model.host}/api/v2/fragment/tag/#{@tag_id}/"
@beforeFromJSON: (data) ->
data['fragments']
module.exports = Fragment
上一个帖子没有提到的东西beforeFromJSON
,因为这两个api直接返回的是这样下面第一端代码这种结构, 而不是第二段代码的结构,我想要拿到每一个model实体是需要剥离外面那一层的。
{
tags: [
{},
{}
]
}
{
"fragments": [
{},
{}
]
}
而不是
[
{},
{}
]
那么怎么做呢,两种选择,看文档或者看源码,个人感觉spine的文档很烂,然而他源码很短,所以当时我直接看的源码而没看文档。
思考流程,这是个ajax操作,肯定在ajax.coffee里面,然后要拿数据肯定已经success了,搜success,两处搜索结果,都在不同的两个recordResponse中,其中这行代码告诉我 @record.trigger('ajaxSuccess', @record, @model.fromJSON(data), status, xhr, settings)
,trigger的ajaxSuccess看起来就是正常jquery ajax成功的回调(传的参数就像),data他传的是@model.fromJSON(data)
,所以一定是在model的这个方法里面,接着我退出vim grep了下fromJSON, 发现在spine.coffee里面,显然是beforeFromJSON这个方法做的我想要的事情。
@beforeFromJSON: (objects) -> objects
@fromJSON: (objects) ->
return unless objects
if typeof objects is 'string'
objects = JSON.parse(objects)
objects = @beforeFromJSON(objects)
if Array.isArray(objects)
for value in objects
if value instanceof this
value
else
new @(value)
else
return objects if objects instanceof this
new @(objects)
上面这一段是我在写或者学东西时候的一个思路,我个人依然感觉讲下思路比直接讲技术会好很多,尤其是广大的新同学看到会知道如何从完全没看过学习一个东西,记住你不只有文档,记住源码大于文档(这就是为什么python比c系java系语言好的一个重要因素,你随时可以看各种框架的源码)。
文档中可以看到configure是定义这个model上面有哪些属性的,写过backbone的人都知道,model有两种类型,一个是个体model,一个是结合collection,然而spine并没有,这里我要中间插一句我对框架这种东西的理解。
框架是什么
前端的MVC框架出了很多很多,我这里就不说了,我个人的感觉是,除非你写某些大型项目,否则得不偿失。首先框架非常重,框架中号称更好的代码可读性,维护性等等,都拼不过这个框架的学习成本。
以前我们对于一个页面,针对每个不同地方的需求写一些js,做一些ajax请求,处理一些dom结束了;现在前端有了更多的追求,单页应用,MVC,各种打包工具;诚然这些东西是为了更好的用户体验,更好的开发效率。
个人感觉到的框架缺点:
学习成本都很重,如果面临项目紧任务重,新人加进来根本无法干活;
而且一旦你用了某一种东西之后,灵活性就非常严重的下降。例如mvc,假设后端有一个接口设计的不规范,前端就会蛋疼的要命,这就是用框架会造成的后果,你必须按他规范来,否则你就要自己做一些事情擦屁股;
单页式MVC非常差的可读性,当ajax操作变多的时候,dom操作你都需要放在js里面,这也是单页式MVC框架里面view的事情,他们会用各种前端的模板语言,然后controll里面会说我用的哪一个模板,然后一个controll可能又有几个controll组成,项目大起来之后,这个地方的可读性非常的差。这时候就会怀念以前一个页面一个html在里面写一写js的时候了,虽然会重复写一些东西(违反DRY原则)。
当然可能是我前端的功力不够,在这瞎扯了,有可能有更好的目录结构或者什么能解决这些问题,然而不能让一个新手或者中手很快上手的东西还是不能吸引我。我还是更喜欢手撸jquery css,最少你让一个学生看1天就能直接来做事情,先不说做的好不好,做一周他应该会有进步,而框架不行,框架最少卡半个月。
我想用的框架
很简单,让我更好的用jquery
有个model,model的概念仅仅是做一个api的封装,也就是简化jquery ajax请求发参数以及接受后实例化的事情
ajax请求能封装一下,jquery的ajax用起来还是比较丑的
用coffee,懒得写js,烦
小,我可以很方便的读源码
这完全可以自己定义一个class来做,摆着不重复造轮子的想法找才找到的spine,这玩意儿很符合我的需求(backbone也很相似)
spine的model封装了ajax, model没有 model和collection的定义,刚好就是符合我的第一点需求(虽然spine他支持model上面的很多其他功能,然而对我就够了)
spine的controller+view共同做一件事情,显示,controller处理一些逻辑,比如click事件,ajax前后的dom变化,这个东西就是把以前写在js的逻辑按模块分了一下,有用。关于controller的大小,我目前更倾向于一个html页面一个app,html中每个不同的模块可能会有不同的controller(或者就1个controll)。
view这个东西我仅仅会用作ajax请求后的以前写在success里面的那一坨html,其他的非html元素直接放在html中,而不放在view里面,这样每个页面的html依然分隔可见。
spine的源码是用coffee写的,非常短,而且每个模块分开,可读性特别好
继续讲解,html骨架
跟spine无关的我决的就是这个html骨架,因为spine我看来就是让我更有调理的用jquery的一种东西,如下代码,及时我裸写jquery html也是这么定,我会把ajax返回的东西经过一些逻辑处理,修改<div id="fragment-list"></div>
这个dom元素。
所以spine仅仅就是干了jquery的活儿,代码看起来更有条理而已。
<!DOCTYPE html>
<html lang="zh-CN" >
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>那些年我们看过的知识点</title>
<link rel="stylesheet" href="/assets/vendor/marx/css/marx.min.css" type="text/css" />
<link rel="stylesheet" href="/assets/css/main.css" type="text/css" />
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico" />
</head>
<body>
<h1> 那些年我们看过的知识点 </h1>
<div id="header-bar">
<a href="http://weibo.com/duoduo369/home" target="_blank">
<img class="icon-weibo" src="/assets/images/weibo.png" />
</a>
<a href="https://github.com/duoduo369/leaning-frontend" target="_blank">
<img class="icon-weibo" src="/assets/images/github.png" />
</a>
</div>
<div id="fragment-list"></div>
<!-- JavaScripts -->
<script src="/assets/js/vendor.js"></script>
<script src="/assets/vendor/spine/lib/spine.js"></script>
<script src="/assets/vendor/spine/lib/route.js"></script>
<script src="/assets/vendor/spine/lib/ajax.js"></script>
<script src="/assets/js/main.js"></script>
</body>
</html>
几个controller
controller+view是做显示的,记住这一点。
controller上面一些东西的讲解:
el,是指jquery通过哪个element选中一个dom元素,之后再这个controller里面所有的render,append等等改变dom的方法都会在这个el选中的dom上面操作
className,写的话会给el添加一个class
首先是APP controller, el为body,会给body添加一个class app, Route那里暂时无视,这个demo中没用。
APP级别的el选body的原因是:controller虽然可以在里面初始化其他的controller,但是子controller的el必须这个时候已经在浏览器里面初始化过了,否则jquery自然选中不了,当你有复杂的dom嵌套(尤其是有ajax操作时)注意这里。
比如下面,假设我@fragmentListController = new FragmentListController()
的el是在@courseListController = new CourseListController()
中的,如果我把这行注释掉#@courseListController = new CourseListController()
,那么页面什么都不会发生,因为FragmentListController选不中他的dom元素(页面没有)。
config = require "./config.coffee"
utils = require './utils.coffee'
global.lazy = require('lazyloadjs')()
utils.set_model_host()
#Courses = require './controllers/main'
CourseListController = require './controllers/courses'
FragmentListController = require './controllers/fragments'
class App extends Spine.Controller
el: 'body'
className: 'app'
constructor: ->
super
#@courseListController = new CourseListController()
@fragmentListController = new FragmentListController()
Spine.Route.setup()
$ ->
new App()
FragmentListController, 这个controller做的事情就是ajax取得知识点列表数据,并且已照片墙的形式渲染到'#fragment-items'
这个dom上。
这段demo的结构来自 文档 这里,为毛呢,因为我需要先获得所有的tag,然后根据不同的tag获得对应tag的知识点list,然后append到页面上去,这就导致了会发tag个数的ajax请求,多个请求导致我无法再一个controller里面完成一个render(或者我科学的办法我没想到,总之当时实验了半天)。
config = require '../config'
FragmentTag = require '../models/fragment_tags'
Fragment = require '../models/fragment'
freewall = require 'freewall'
sliphover = require 'sliphover'
class FragmentItem extends Spine.Controller
el: '#fragment-items'
constructor: ->
super
throw "@item required" unless @item
@item.bind("refresh change", @render)
$.get @item.url(), (items) =>
@render(Fragment.fromJSON(items))
template: (items) ->
require('../views/fragment-item')(items)
render: (items) =>
@item = items if items
@append(@template(@item)) if @item
@
class FragmentListController extends Spine.Controller
el: '#fragment-list'
className: 'fragments'
constructor: ->
super
# 注释掉下面这行放到render最后做,因为FragmentItemController
# 需要FragmentListController先render一次才行,因为item中的el
# 是由ListControlller动态生成的
#FragmentTag.bind("refresh", @add_fragments)
FragmentTag.bind("refresh", @render)
FragmentTag.fetch()
add_fragments: =>
tags = FragmentTag.all()
fragments = (new FragmentItem(item: new Fragment(tag_id: tag.id)) for tag in tags)
@add_fragment fragment for fragment in fragments
add_fragment: (item) =>
@append(item.render())
init_freewall: =>
wall = new freewall.Freewall("#fragment-items")
wall.reset
selector: '.fragment-box'
duration: 100
animate: true
reverse: true
cellW: 300
cellH: 300
onResize: ->
wall.refresh()
wall.fitWidth()
$(window).trigger("resize")
@el.sliphover
caption: 'data-caption'
withLink: true
template: (items) ->
require('../views/fragments')(items)
render: =>
items = FragmentTag.all()
@html(@template(items))
@add_fragments()
@init_freewall()
module.exports = FragmentListController
所以有两个controller,FragmentItem 是处理每个tag返回的东东,这么说吧,用jquery解释,既然有多次异步调用,那么每次回来的时候都要做一波dom处理,FragmentItem就是干这个事情的。
我们先讲解FragmentListController,constructor里面先FragmentTag.bind("refresh", @render)
然后fetch的,为什么,文档的例子,并且,看了下源码fetch完事儿后@model.refresh(record, options)
调用model的refresh, 而refresh会trigger两个event:refresh和change
refresh: (atts) ->
atts = @constructor.fromJSON(atts)
# ID change, need to do some shifting
if atts.id and @id isnt atts.id
@changeID(atts.id)
# go to the source and load attributes
@constructor.irecords[@id].load(atts)
@trigger('refresh', this)
@trigger('change', this, 'refresh')
this
这样当FragmentTag就把他的refresh事件绑定到了FragmentListController的render上面,然而为什么要绑定而不直接调用,render调用FragmentTag.all()
时候源码是这样的@all: -> @cloneArray(@records)
,而@records又是在ajax回调完成后才有的,也就是说完成前调用all这玩意儿一直是空的,所以要等model完成ajax后trigger的那个event,然后在取all才有值。
render: =>
items = FragmentTag.all()
@html(@template(items))
@add_fragments()
@init_freewall()
然后这个的view长这个样子,framents.eco, 我想在html的<div id="fragment-list"></div>
这个dom里面在套一层来包裹
<div id="fragment-items"></div>
然后add_fragments根据取回来的FragmentTag 初始化了一堆FragmentItem,FragmentItem的构造跟FragmentListController很相似,不同的是我是直接用jquery调用的render,为毛呢,因为不同的tag的url不同,然而我又没找到如何调用fetch,所以就这么写了,但是感觉无所谓,不要被框架框死。注意我调用render的时候调了下Fragment.fromJSON,这样render的时候items就是一堆Fragment对象,跟fetch没什么区别这样。
@item.bind("refresh change", @render)
$.get @item.url(), (items) =>
@render(Fragment.fromJSON(items))
fragment-item.eco, 这个没什么说的
<% for fragment in @: %>
<div class='fragment-box'>
<a href="<%= fragment.web_address %>" target="_blank">
<img
src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
data-src="<%= fragment.cover_image %>"
title="<%= fragment.title %>"
alt="<%= fragment.title %>"
data-caption="<%= fragment.title %>"
onload="lazy(this)" />
</a>
</div>
<% end %>
所以一组稍微有点复杂的ajax请求,渲染完成了(先调用tag,然后根据tag id的列表调用对应的fragment)。这样页面就有了基本的html数据,剩下的是样式,这个我就不想说什么了。
关于插件
前端最大的好处是基本你要的效果都有现成的,你把某个效果翻译成英语,然后github一搜,按照星星数排序选前几个看看挑一个就行,然后根据例子撸。
按照这种思路,demo中的js选好了。
视频的东西还没做,看心情再说。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。