1

作为一个后端学习前端,我决的主要应该讲解学习的方法,而不是具体的技巧

show time

过年回来终于把第一个demo写完了,根据学堂在线的api,写了一个瀑布流的知识点照片墙一样的东西,鼠标hover的时候在图片切换时有某种direction效果,现在还有些东西需要完善。

  1. 菊花转起来

  2. 打开时瀑布流默认在右边有个很大的留白(估计是 freewall 我没用对)

  3. 懒加载加了但是貌似没效果

预览图

demo 美国vps 图慢 可能被墙 能不能看随缘

源码 tag为2016-02-16-demo。预警: bower npm的东西我都放到git中了(被坑过),比较大,

开始讲解

作为一个后端学习前端,我决的主要应该讲解学习的方法,而不是具体的技巧

思考做点什么

扒了下学堂在线首页,发现是几种列表的集合,几种不同类型课程列表,知识点列表,帖子列表。然而课程点进去是具体的课程,要播放视频还要加课,略微烦躁;帖子没什么意思,看不出什么酷炫的效果;知识点点进去之后发现有个汇总页,而且每分页,可见数据量可以,而且都有图片什么的,点一个进去发现不用注册就可以看视频。嘿嘿嘿,那么就可以做一些事情了。
图片描述

做什么呢,我扒了扒之前收藏夹里面的东西 codrops 上刚好看到两个有点意思的东西

全屏video播放

一个有意思的鼠标经过hover

第二个东西启发我想到了拉钩 上面如下图位置的鼠标hover效果,然后我就想这么多有图的知识点为毛不做个照片墙,鼠标移动过去标题出现就用拉钩的这个效果,点进去开始播视频,好像有点意思。

图片描述

基础数据准备

找api,没找到一次获取全部知识点的api,但是找到另外一组

  1. 获得知识点tag: http://www.xuetangx.com/api/v2/fragment/tags

  2. 根据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的文档很烂,然而他源码很短,所以当时我直接看的源码而没看文档。

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,各种打包工具;诚然这些东西是为了更好的用户体验,更好的开发效率。

个人感觉到的框架缺点:

  1. 学习成本都很重,如果面临项目紧任务重,新人加进来根本无法干活;

  2. 而且一旦你用了某一种东西之后,灵活性就非常严重的下降。例如mvc,假设后端有一个接口设计的不规范,前端就会蛋疼的要命,这就是用框架会造成的后果,你必须按他规范来,否则你就要自己做一些事情擦屁股;

  3. 单页式MVC非常差的可读性,当ajax操作变多的时候,dom操作你都需要放在js里面,这也是单页式MVC框架里面view的事情,他们会用各种前端的模板语言,然后controll里面会说我用的哪一个模板,然后一个controll可能又有几个controll组成,项目大起来之后,这个地方的可读性非常的差。这时候就会怀念以前一个页面一个html在里面写一写js的时候了,虽然会重复写一些东西(违反DRY原则)。

当然可能是我前端的功力不够,在这瞎扯了,有可能有更好的目录结构或者什么能解决这些问题,然而不能让一个新手或者中手很快上手的东西还是不能吸引我。我还是更喜欢手撸jquery css,最少你让一个学生看1天就能直接来做事情,先不说做的好不好,做一周他应该会有进步,而框架不行,框架最少卡半个月。

我想用的框架

很简单,让我更好的用jquery

  1. 有个model,model的概念仅仅是做一个api的封装,也就是简化jquery ajax请求发参数以及接受后实例化的事情

  2. ajax请求能封装一下,jquery的ajax用起来还是比较丑的

  3. 用coffee,懒得写js,烦

  4. 小,我可以很方便的读源码

这完全可以自己定义一个class来做,摆着不重复造轮子的想法找才找到的spine,这玩意儿很符合我的需求(backbone也很相似)

  1. spine的model封装了ajax, model没有 model和collection的定义,刚好就是符合我的第一点需求(虽然spine他支持model上面的很多其他功能,然而对我就够了)

  2. spine的controller+view共同做一件事情,显示,controller处理一些逻辑,比如click事件,ajax前后的dom变化,这个东西就是把以前写在js的逻辑按模块分了一下,有用。关于controller的大小,我目前更倾向于一个html页面一个app,html中每个不同的模块可能会有不同的controller(或者就1个controll)。

  3. view这个东西我仅仅会用作ajax请求后的以前写在success里面的那一坨html,其他的非html元素直接放在html中,而不放在view里面,这样每个页面的html依然分隔可见。

  4. 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上面一些东西的讲解:

  1. el,是指jquery通过哪个element选中一个dom元素,之后再这个controller里面所有的render,append等等改变dom的方法都会在这个el选中的dom上面操作

  2. 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选好了。

照片墙freewall
图片懒加载
拉钩的hover

视频的东西还没做,看心情再说。

一些推荐的网址什么的

某个设计有关的网站 然而我并不看

学习css layout一个不错的网站


D咄咄
1.7k 声望257 粉丝

Life is to short, please use python.