专栏首页拂晓风起浅谈HTML5单页面架构(三)—— 回归本真:自定义路由 + requirejs + zepto + underscore

浅谈HTML5单页面架构(三)—— 回归本真:自定义路由 + requirejs + zepto + underscore

前两篇简单讨论了requirejs+angular和requirejs+backbone的架构,这两个架构,估计也是国内最热门的做法。

浅谈HTML5单页面架构(一)——requirejs + angular + angular-route

浅谈HTML5单页面架构(二)——backbone + requirejs + zepto + underscore

不过,这一篇,我想进一步探讨一下这两个框架的优缺点,另外,再进一步,抛开这两个框架,回到本真,自己搞个简单的路由一样可以实现单页面。

这个对于刚做前端开发的新同学来说就最好不过了,如果一来到岗位就一大堆angular、backbone、requirejs,看资料都看一两周。其实大家最熟悉的东西还是那个美元$,用美元能解决的问题,就不要麻烦到angular、backbone大爷了。

事先说明,由于我的业务范围窄,不一定能把angular和backbone的功能都用一遍,所以以下的分析可能以偏概全,欢迎大家讨论。

angular优点:

  • 强大的数据双向绑定
  • View界面层组件化
  • 内置的强大服务(例如表单校验)
  • 路由简单

angular缺点:

  • 引入的js较大,对移动端来说有点吃不消
  • 语法复杂,学习成本高

backbone优点:

  • 引入的js较小
  • 清晰MVC分层
  • Model层事件机制
  • 路由简单而且便于扩展

backbone缺点:

  • MVC有点死板,有时候觉得累赘
  • 没有双向绑定,界面修改只能靠自己
  • view切换时,没有足够便捷的事件通知(要自己监听route)

其实,这两个框架都非常优秀,但是,在实际业务中,不一定百试百灵,因为有一些移动端的单页面web,业务就很简单,只是路由分别切换到几个子模块,每个子模块基本都是拉一次数据,展示给用户,很少用户交互从而修改数据,改变视图的功能。

对于这种情况,使用angular未免有点杀鸡用牛刀的感觉,而backbone虽然小巧了不少,但是模型的功能也是浪费的。

所以,在这里,我想探讨一下,能否抛开这两个框架,只索取我们基本所需,建立一个更简单的架构呢?

经验看来,一些类库是必不可少的:

  • requirejs:模块划分
  • zepto:移动端的jquery
  • underscore:便捷的基础方法,包括模版template、each、map等等
  • 路由库:这里先使用director.js,然而这玩意并没有backbone和angular的路由好用,文章最后再来探讨这个问题

自己做一套最简单的架构,思想非常简单:

  1. 启动程序
  2. 监听路由
  3. 路由变化,映射到对应的处理逻辑,加载对应的模块
  4. 模块加载完成,修改dom,也就是视图
  5. 页面跳转时,移除上一个模块,加载下一个模块,也就是回到第3点

简单的思路,让架构非常简洁明了,新团队成员来到能够轻松上手,而angular和backbone的架构,少说得2、3天才能融入一个已有项目中去。

接下来,我们具体看看怎么做。

第一步,还是index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Underscore & Director & Requirejs</title>
</head>

<body>
<div id="container"></div>
<script data-baseurl="./" data-main="main.js" src="libs/require.js" id="main"></script>
</body>
</html>

这个跟前两篇没什么差别。requirejs引入main.js作为程序入口

第二步,main.js配置requirejs的依赖关系,并启动webapp

(function (win) {
    //配置baseUrl
    var baseUrl = document.getElementById('main').getAttribute('data-baseurl');

    /*
     * 文件依赖
     */
    var config = {
        baseUrl: baseUrl,           //依赖相对路径
        paths: {                    //如果某个前缀的依赖不是按照baseUrl拼接这么简单,就需要在这里指出
            director: 'libs/director',
            zepto: 'libs/zepto.min',
            underscore: 'libs/underscore',
            text: 'libs/text'             //用于requirejs导入html类型的依赖
        },
        shim: {                     //引入没有使用requirejs模块写法的类库。
            underscore: {
                exports: '_'
            },
            zepto: {
                exports: '$'
            },
            director: {
                exports: 'Router'
            }
        }
    };

    require.config(config);
    require(['zepto', 'router', 'underscore'], function($, router, _){
        win.appView = $('#container');      //用于各个模块控制视图变化
        win.$ = $;                          //暴露必要的全局变量,没必要拘泥于requirejs的强制模块化
        win._ = _;
        router.init();                      //开始监控url变化
    });


})(window);

director.js没有AMD写法,还是按照shim的方式引入。另外,由于$和_的使用率太高,所以这里直接公开为全局变量。

除此之外,还加了appView变量,目的是方便各个子模块修改界面。

第三步,router.js配置路由

这里使用的路由类库是director(https://github.com/flatiron/director),相对精简的路由,但其实对于我们这个程序来说,貌似还不够精简。先凑合着吧。

director官网给出的示例也相当简单,就是“路径”对应“函数”,非常清晰而且实用的方式。

      var author = function () { console.log("author"); };
      var books = function () { console.log("books"); };
      var viewBook = function (bookId) {
        console.log("viewBook: bookId is populated: " + bookId);
      };

      var routes = {
        '/author': author,
        '/books': [books, function() {
          console.log("An inline route handler.");
        }],
        '/books/view/:bookId': viewBook
      };

      var router = Router(routes);

      router.init();

来看看我们自己的版本:

define(['director', 'underscore'], function (Router, _) {

    //先设置一个路由信息表,可以由html直出,纯字符串配置
    var routes = {
        'module1': 'module1/controller1.js',
        'module2/:name': 'module2/controller2.js'     //director内置了普通必选参数的写法,这种路由,必须用路径“#module2/kenko”才能匹配,无法缺省
//        'module2/?([^\/]*)/?([^\/]*)': 'module2/controller2.js'    //可缺省参数的写法,其实就是正则表达式,括号内部分会被抽取出来变成参数值。backbone做得比较好,把这个语法简化了
                                                                    //  “ /?([^\/]*) ”  这样的一段表示一个可选参数,接受非斜杠/的任意字符
    };

    var currentController = null;

    //用于把字符串转化为一个函数,而这个也是路由的处理核心
    var routeHandler = function (config) {
        return function () {
            var url = config;
            var params = arguments;
            require([url], function (controller) {
                if(currentController && currentController !== controller){
                    currentController.onRouteChange && currentController.onRouteChange();
                }
                currentController = controller;
                controller.apply(null, params);
            });
        }
    };

    for (var key in routes) {
        routes[key] = routeHandler(routes[key]);
    }

    return Router(routes);
});

这里把director的路由配置修改了一下,原来只能接受<String, Function>这样的key value对,但参考之前backbone篇,更好方式应该是让路由表尽量只有字符串配置,不要写逻辑(函数)。

所以,上述代码中,多了一个routeHandler,目的就是建立闭包,把string(配置)转换为一个闭包函数。

结果,运行效果就是,遇到一个路由,就根据配置加载对应的子模块代码。后续实际执行什么,由子模块自己决定。这样main/router就能彻底跟子模块解耦。

第四步,建立一个模块

tpl.html

<div>
    Here is module 1. My name: <%=name %><br>
    <a href="#module2/fromModule1">turn to module 2</a>
</div>

controller1.js

define(['text!module1/tpl.html'], function (tpl) {

    var controller = function () {
        appView.html(_.template(tpl, {name: 'kenko'}));
    };
    return controller;
});

我觉得能实现业务逻辑的前提下,越简单的架构就越好,便于传承和维护。

controller就是这个子模块要做的逻辑,appView是整个视图根节点,想怎么玩就怎么玩,这对于不熟悉angular、backbone的同学最爽不过了。

这里重点是利用了requirejs做模块化和依赖加载,并用了underscore的模版库template。

第五步,再做一个模块,加上一些销毁接口

tpl.html

<div>
    Here is module 2. My name: <%=name %><br>
    <button>click me!</button>
    <a href="#module1">turn to module 1</a>
</div>

controller2.js

define(['text!module2/tpl.html'], function (tpl) {

    var controller = function (name) {
        appView.html(_.template(tpl, {name: name?name:'vivi'}));

        $('button').on('click', function clickHandler() {
            alert('hello');
        });

        controller.onRouteChange = function () {
            console.log('change');      //可以做一些销毁工作,例如取消事件绑定
            $('button').off('click');   //解除所有click事件监听
        };
    };

    return controller;
});

至此,整个简单的框架就完成了。

大道至简,我非常喜欢这样简单的架构。希望对新手朋友有所帮助。

最后,关于director的路由,要吐槽一下,这个并没有backbone那些这么好用,它没有内置的缺省参数写法,需要自己理解正则表达式,写复杂的([?*。参照上边router.js的代码。

路由匹配的本质,其实是正则表达式的exec匹配和提取参数。我后续会再整一个简单好用的路由,参考backbone的模式,猛击这里:http://www.cnblogs.com/kenkofox/p/4650824.html

本文代码:https://github.com/kenkozheng/HTML5_research/tree/master/UnderscoreRequireJS

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一个类似backbone路由的纯净route ( 前端路由 客户端路由 backbone路由 )

    用户1258909
  • 浅谈HTML5单页面架构(二)——backbone + requirejs + zepto + underscore

    用户1258909
  • JS:指定FPS帧频,requestAnimationFrame播放动画

    用户1258909
  • 利用jieba和wordcloud从新闻中生成词云

    wordcloud 是一个python实现的高效词频可视化工具,除了可以使用各种mask和颜色提供个性化的掩膜,还可以通过api便捷的挑战获得个性化的词云输出。...

    菲宇
  • 学界丨从科学把妹说起,为何博弈论能成为深度学习的未来?

    AI 科技评论按:作者Carlos Perez是一名软件开发者,著有《深度学习的设计模型》一书。他在这篇文章中提及了我们熟悉的概念——博弈论,并认为这一概念将会...

    AI科技评论
  • 跨平台开源项目Ngui【基准性能测试报告】

    这是一个GUI的排版显示引擎和跨平台的GUI应用程序开发框架,基于NodeJS/OpenGL,这也是第一个在移动端Android/iOS融合NodeJS的前端G...

    IMWeb前端团队
  • 跨平台开源项目Ngui【基准性能测试报告】

    Ngui简介 这是一个GUI的排版显示引擎和跨平台的GUI应用程序开发框架,基于NodeJS/OpenGL,这也是第一个在移动端Android/iOS融合Nod...

    IMWeb前端团队
  • PHP设计模式——复合模式

    Agnes在沃尔玛工作了一年多;她一开始担任的是库存助理,最近被提升为库存业务员。Agnes作为库存业务员的主要工作是进行每日库存记录。在上班的第三天,经理很担...

    Lemon黄
  • Android 移动直播(LiteAV)直播播放如何自定义渲染

    开发者可能因为场景需要,希望自己做视频画面地渲染,以便满足点赞动画、送礼物动画等需求。

    腾讯云-yyuanchen
  • 基于GitHub Issues的评论系统--gitment

    最近在做个人博客网站,需要评论系统,比较流行的几个第三方评论系统:多说,友言,网易云跟帖,gitment,之前使用过多说,感觉挺好用的,但是现在...

    逆月翎

扫码关注云+社区

领取腾讯云代金券