博客

  • libGDX基于RoboVM对IOS平台的支持

    libGDX基于RoboVM对IOS平台的支持

    Libgdx这款跨平台的游戏开发引擎,我之前的很多文章都介绍过了,可以点击Libgdx查看。

    Libgdx在0.99版本正式加入对IOS的支持。

    Libgdx之前其实也是支持IOS的,只是需要先转化成C#,然后使用收费的工具实现。

    而现在Libgdx使用了RoboVM,这是开源免费的。目前已经有超过百款的Libgdx游戏发布到App Store了。

    RoboVM

    RoboVM将Java的字节码转为本地代码,并封装了UI和各种API。这样可以最大程度重用你原有的Java代码。

    RoboVM需要一个Mac OS X,虽然在其他平台,比如ubuntu,也是可以使用的。不过为了更好的集成和开发,最好还是入手一个Mac吧。

    RoboVM提供了Eclipse插件,安装地址:http://download.robovm.org/eclipse/

    插件安装

    在安装完成后复制以下代码测试以下:

    import org.robovm.cocoatouch.coregraphics.*;
    import org.robovm.cocoatouch.foundation.*;
    import org.robovm.cocoatouch.uikit.*;
    
    publicclass IOSDemo extends UIApplicationDelegate.Adapter {
    
    private UIWindow window =null;
    privateint clickCount =0;
    
    @Override
    publicbooleandidFinishLaunching(UIApplication application,
    NSDictionary launchOptions) {
    
    final UIButton button = UIButton.fromType(UIButtonType.RoundedRect);
    button.setFrame(new CGRect(115.0f,121.0f,91.0f,37.0f));
    button.setTitle("Click me!", UIControlState.Normal);
    
    button.addOnTouchUpInsideListener(new UIControl.OnTouchUpInsideListener() {
    @Override
    publicvoidonTouchUpInside(UIControl control, UIEvent event) {
    button.setTitle("Click #" + (++clickCount), UIControlState.Normal);
    }
    });
    
    window =new UIWindow(UIScreen.getMainScreen().getBounds());
    window.setBackgroundColor(UIColor.lightGrayColor());
    window.addSubview(button);
    window.makeKeyAndVisible();
    
    returntrue;
    }
    
    publicstaticvoidmain(String[] args) {
    NSAutoreleasePool pool =new NSAutoreleasePool();
    UIApplication.main(args,null, IOSDemo.class);
    pool.drain();
    }
    }

    右键点击Run asiOS Simulator App即可看到效果。

    Libgdx和RoboVM

    因为对于多数开发者而言,你是希望的是发布游戏到App Stroe,所以除了Mac以外,你还需要一个开发者认证。

    建议使用setup-ui工具,这样可以免去不少烦心的配置。

    导入项目后可以直接运行查看效果。

    虽然原理上你可以不会oc就进行相关操作,因为RoboVM还没有完全成熟,遇上问题你需要自己解决。

    对于早期的这个错误,比如java.lang.NoSuchMethodError等等,解决问题所需要的只是升级RoboVM和它的插件。

    使用构建工具是一个不错的选择,比如Gradle。具体的配置可以参考RoboVM Sample iOS App

    主要的配置就是

    apply plugin:'robovm'
    
    buildscript {
    repositories {
    mavenCentral()
    }
    dependencies {
    classpath'com.github.jtakakura:gradle-robovm-plugin:0.0.5'
    }
    }

    第三方服务的集成

    Libgdx对IOS的支持中另外一个问题就是第三方SDK的集成。特别是对广告和应用内付费的集成。

    这些问题其实是RoboVM的,而不是Libgdx,所以寻求帮助时需要明确对象。

    robovm-ios-bindings是一个较好的选择,它支持大量第三方服务,如:

    • adcolony
    • admob
    • chatboost
    • crashlytics
    • facebook
    • flurry
    • gamecenter

    使用步骤如下:

    • 下载这个项目
    • 导入Eclipse
    • 将你需要的依赖加入Eclipse
    • 打开robovm.xml
    • 从项目中复制你需要的配置到robovm.xml中
    • 在代码中使用

    该项目的包名为org.robovm.bindings.bindingname,比如admob就是org.robovm.bindings.admob.sample

    对于每种服务都有例子,可以放心大胆使用。

    遇到问题怎么办

    首先区分问题是属于Libgdx-RoboVM-backend还是RoboVM的。

    如果属于RoboVM的去RoboVM的社区,如果是Libgdx的先尝试nightly版本,如果问题依旧请前往英文社区提交。

    大部分问题都是繁琐且细致的,在QQ群询问很难得到准确的帮助。

    参考

    使用RoboVM发布Libgdx游戏

    RoboVM官网

    GDX Super Jumper

  • 模块化的Grunt配置

    Grunt是Javascript任务运行工具,它可以让一切自动化。

    简而言之,它可以帮你完成代码压缩,代码编译,单元测试,代码规范验证等必须但高度重复的工作。

    极大程度简化你的工作,同时又保证你的代码质量。

    Grunt的同类工具很多,但是Grunt的一大特点就是生态庞大,有各种插件可以选择,而且都是可以通过npm安装的。

    GruntFile

    Gruntfile.js文件都是位于项目根目录中的一个JavaScript文件,并且它也应该与你的项目源文件一起提交。

    一个Gruntfile由下面几部分组成:

    • wrapper函数
    • 项目和任务配置
    • 加载的Grunt插件和任务
    • 自定义任务
    • 一个Gruntfile示例

    比如下面的例子,它从package.json中读取配置,再调用grunt-contrib-uglify插件来压缩源文件,同时根据读取的配置生成一个标语注释。同时注册默认任务为uglify任务。

    module.exports =function(grunt){
    
    grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    uglify: {
    options: {
    banner:'/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */n'
    },
    build: {
    src:'src/<%=pkg.name %>.js',
    dest:'build/<%= pkg.name %>.min.js'
    }
    }
    });
    
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.registerTask('default', ['uglify']);
    }

    这个配置还不足以让你困扰,但是当你的任务让比较复杂时,配置文件本身就是一种负担了。

    插件加载

    Grunt的插件机制需要你在配置文件中加载你所需的插件。

    比如:

    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-qunit');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-shell');
    grunt.loadNpmTasks('grunt-sass');
    grunt.loadNpmTasks('grunt-recess');
    grunt.loadNpmTasks('grunt-sizediff');
    grunt.loadNpmTasks('grunt-svgmin');
    grunt.loadNpmTasks('grunt-contrib-concat');

    其实这个问题比较好解决,因为Grunt的插件一般是已grunt开头的,所以我们只需要遍历node_module,需要可能的包,然后加载它们就可以了。

    当然遍历目录是可行,不过最简单的还是直接读取package.json文件,然后判断它们的名字是否符合要求

    var multimatch=require('mulimatch'),
    config = options.config;
    var pattern ='grunt-*';
    multimatch(config['dependencies'],patterns).forEach(grunt.loadNpmTasks);

    这样就可以自动加载Grunt插件了。

    当然我们完全没有必要自己写这个加载器,直接使用load-grunt-tasks就可以了。

    使用方法:

    require('load-grunt-tasks')(grunt);

    将任务配置模块化

    从上面的操作可以看出grunt.loadNpmTasks的效果,Grunt还提供了grunt.loadTasks(name)函数来加载项目文件夹中的配置。

    比如上面的例子可以改为:

    min
    └── uglify.js

    这样使用grunt.loadTasks('min')来加载配置。

    而uglify.js的内容应该是

    module.exports = {
    options: {
    banner:'/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> */n'
    },
    build: {
    src:'src/<%=pkg.name %>.js',
    dest:'build/<%= pkg.name %>.min.js'
    }
    }

    在package.json中配置参数

    还有一个需要注意的是Gruntfile不应该有过多依赖的参数,比如项目版本等等。

    这些信息应该写入package.json中,而不是写入Gruntfile.js中。

    比如angular项目就将cdn版本信息写入package.json,并在Grunt执行时读取。

    这样可以保证参数更新,也增加Gruntfile的美感。

    参考链接

  • 海外博客使用CDN加速

    海外博客使用CDN加速

    因为在祖国的怀抱中自建博客很麻烦,所以最后还是选择全套购买海外的东西。

    不过博客主要的访客还是祖国的,所以这响应速度还是比较重要的。

    Hexo的一个好处就是全站静态化,生成以后再压缩一下,轻松加愉快。

    参考了网上很多博主的文章,决定CDN是个不错的东西,可以考虑一下。

    百度加速乐

    因为本身空间不错,访问速度也很快,所以国外的CDN基本就不考虑了,英文折腾起来太麻烦。

    国内的话看了一下挺多的,对于未备案的网站也可以分配海外节点。

    因为百度的云引擎BAE不错,所以对百度的服务期待很高。

    百度的CDN是百度加速乐,免费版3G/h,30万PV/日。

    这个数据大家别觉得特别好…这么大的量一般站点还用不上。

    我选用的别名接入,接入以后可以看大部分请求被香港节点处理了。速度有轻微提升,感觉还可以,就用着吧…然后就遇到坑爹的400(unknown virtual host)错误了。

    400错误

    我当时还琢磨这估计是抽风一下吧,刷新无数次都没有用。

    在百度经验和加速乐的论坛看了看,说需要等等或者删除再创建一下。

    确实管用,不过几分钟以后又会出现400错误,这样折腾不起呀,考虑到速度提升也不大,果断放弃之。

    毕竟是百度的服务,估计也不会太差。不过国内节点必须备案…所以也就只是看看。

    安全宝

    安全宝是一家名为星云融创的公司提供的,同样是未备案分配海外节点。

    安全宝的海外节点在美国,不过速度还挺快的。

    使用方法差不多,不过接入和解析生效都是人工审核的,一般一个小时左右。

    安全宝使用起来比较流畅,速度提升也比较明显,本站现在使用的就是它,安全宝把节点换到了日本的,基本不能用。不是速度慢,而是基本无法访问,慎用之。

    域名界面

    不过这个数据统计我就有些疑惑了。

    我是在购买了主机刚刚迁移完博客就去申请的服务,凌晨时分接入的。

    第二天早上就提示我拦截了恶意攻击

    控制面板的恶意攻击提示

    要不要这样…我一个静态博客,才搬迁就有攻击…

    不过一看就知道是自动攻击,我主机上一个php文件都没有。

  • Hexo模板系统和pacman的修改

    Hexo模板系统和pacman的修改

    Hexo的原理是解析_posts中的md格式文件,然后根据模板的解析规则进行解析。

    我使用的风格修改自pacman,主要它看着比官方默认的主题要充实很多。

    但是毕竟是别人做好的,还是有很多不合自己意的,所以还是需要稍微修改一下。

    Hexo模板系统

    要做出修改,首先要看看Hexo的模板系统是怎么实现的。

    Hexo使用的是ejs,类似的东西就多了,比如Jade,swig,doT等等。

    ejs效率比较慢(相比其他的),这里有一个测评,也没有Jade有那么多特性。不过好处也是明显的,不需要太多的时间和精力就可以掌握。

    在Hexo中有post、page等不同的布局,而选用哪种布局是在md文件中声明的。

    Hexo首先解析md文件,然后根据layout.ejs判断布局类型,再转发给其他布局文件。在布局中可以引入其他文件,比如

    <%- partial('_partial/header')%>

    这样每一块内容都是单独的,方便二次使用,也可以几个不同布局引用一个代码片段。

    简要流程

    修改几处英文

    我没有从官方风格慢慢改,而是选用了pacman,修改了主题色以后感觉就不错了,不过有几处英文显示感觉不舒服。

    Hexo是支持多语言的,在_config.xml中配置就可以了,一般使用zh-CN。

    首先看看文章末尾的上一篇和下一篇

    language中没有对于这个的定义,首先我们补充nextpostprevpostzh-CN.yml

    nextpost:下一篇
    prevpost:上一篇

    然后修改post文件夹中的pagination.ejs,将PREVIOUS和NEXT替换掉

    <nav class="article-nav clearfix">
    <% if (page.prev){ %>
    <div class="prev" >
    <a href="<%-%20config.root%20%><%-%20page.prev.path%20%>" title="<%= page.prev.title %>">
    <strong><%= __('prevpost') %>:</strong><br/>
    <span>
    <% if (page.prev.title){ %><%= page.prev.title %><% } else { %>(no title)<% } %></span>
    </a>
    </div>
    <% } %>
    <% if (page.next){ %>
    <div class="next">
    <a href="<%-%20config.root%20%><%-%20page.next.path%20%>" title="<%= page.next.title %>">
    <strong><%= __('nextpost') %>:</strong><br/>
    <span><% if (page.next.title){ %><%= page.next.title %><% } else { %>(no title)<% } %>
    </span>
    </a>
    </div>
    <% } %>
    </nav>

    在Hexo中,多语言支持是通过在语言文件中定义,然后在模板中通过<%= --('name')%>来使用的。

    然后是搜索框的提示文字:

    因为语言文件中已经有了定义,所以直接修改header.ejs文件

    <inputtype="search"id="search"autocomplete="off"name="q"maxlength="20"placeholder="<%= __('search') %>" />

    还有一个问题是在上下页的时候,修改方法类似

    分页标题修改

    还有一处小瑕疵是在分页页面那里,对于文章列表的所有页面,它们的标题都是一样的,而一般而言应该是第%d页 | blog.title

    对应的设置在head.ejs中,

    <%
    var title = page.title;
    
    if (is_archive()){
    title ='Archives';
    
    if (is_month()){
    title +=': ' + page.year +'/' + page.month;
    }elseif (is_year()){
    title +=': ' + page.year;
    }
    }elseif (is_category()){
    title ='Category: ' + page.category;
    }elseif (is_tag()){
    title ='Tag: ' + page.tag;
    }
    %>
    <title><% if (title){ %><%= title %> | <% } %><%= config.title %></title>

    首先判断页面类型,然后构造标题,如果没有标题,就使用默认配置中的标题。

    很明显,分页页面没有被处理。而其中诸如is_archive()的定义在helper/index.js中,这其中没有关于分页页数的判断方法。

    最后参考modernist风格才找到答案。

    <%
    var title = [];
    if (page.current >1) title.push(__('page', page.current));
    if (page.title) title.push(page.title);
    if (page.category) title.push(page.category);
    if (page.tag) title.push(page.tag);
    if (page.archive){
    if (page.year) title.push(__('archive_b', page.year + (page.month ?'/' + page.month :'')));
    else title.push(__('archive_a'));
    }
    title.push(config.title);
    %>
    <title><%= title.join(' | ') %></title>

    通过判断page.current来处理。所以我们在head.ejs中的代码修改为

    <%
    var title = page.title;
    
    if (is_archive()){
    title ='Archives';
    
    if (is_month()){
    title +=': ' + page.year +'/' + page.month;
    }elseif (is_year()){
    title +=': ' + page.year;
    }
    }elseif (is_category()){
    title ='Category: ' + page.category;
    }elseif (is_tag()){
    title ='Tag: ' + page.tag;
    }elseif(page.current >1){
    title ='第'+page.current+'页';
    }
    %>
    <title><% if (title){ %><%= title %> | <% } %><%= config.title %></title>

    效果:

  • 通过ssh使用git更新hexo内容

    Hexo每次新增一篇文章都会引起很多页面的变动。

    比如page页面,因为增加一篇文章,所以分页情况都改变了,这里会有将近10个页面的变化。

    其次是tag和categories目录,因为文章一般都会指定tag和categories,所以这里也会有几个页面的变动。

    最后就是文章页面本身了。

    这么算下来新增一篇文章会更新十多个页面,还有若干图片。

    每次使用ftp更新都很慢,虽然可以设置各种跳过规则,但是对比文件列表也是会花费相当的时间。加之hexo提供的ftp发布比一般的ftp客户端还慢(ftp发布工具是给予lftp的,只适用于linux)。

    我本来准备做一个版本管理工具,或者让hexo只生成最近几天的文章(issue链接)已减少更新量。

    不过折腾起来挺费劲的,最后作罢了。

    刚才突然想到我需要的其实就是一个git而已,加上主机本身支持ssh,所以决定使用在服务器搭建一个git服务器来更新网站。

    创建Git仓库

    其实创建Git仓库挺简单的,因为git自身就支持ssh方式的连接,我们只需要在服务端建立一个Git仓库,然后将页面生成到Git仓库中就行了。

    在目标目录执行命令:

    git init
    git add .
    git commit -m'init git repo with remote files'

    初始化仓库并将现有文件全部添加到其中。

    然后在本地执行命令将库拉去回来

    git clone username@host:~/path/to/www

    也可以直接在本地执行初始化一个Git仓库,然后添加一个remote

    git init
    git remote add web username@host:~/path/to/www

    之后只需要用Hexo生成页面,然后本地git add .之后push就可以了。

    receive.denyCurrentBranch问题

    当然没有那么顺利,执行push以后报错

    remote: error: refusing toupdate checked out branch: refs/heads/master
    remote: error:Bydefault, updating thecurrent branchin a non-bare repository
    remote: error:is denied, because it will make theindexandwork tree inconsistent
    remote: error:with what you pushed,and will require'git reset --hard'tomatch
    remote: error: thework treeto HEAD.

    直接添加配置就OK了。

    git config receive.denyCurrentBranch ignore

    之后就可以顺利提交了。

    当然你也可以使用

    git push web +master:refs/heads/master

    然后…然后什么都没有发生。

    添加hook

    我们还需要添加一个hook,以便我们提交以后将真正的内容签出。

    Git的hook有以下几种:

    • applypatch-msg
    • post-update
    • pre-rebase
    • commit-msg
    • pre-applypatch
    • update
    • post-commit
    • pre-commit
    • post-receive
    • prepare-commit-msg

    提交后自动签出自然需要的是post-receive

    在创建.git/hooks/post-receie,内容为

    export GIT_DIR=$(pwd)
    cd ..
    git checkout-f
    git submodule update --init --recursive

    如果你是*uix系统,不要忘记赋予权限

    chmod +x .git/hooks/post-receive

    这样就一切OK了。

    每次更新后push一下就可以了。

    去除不必要的更新

    有些更新是不必要,比如我最开始在博客底部添加了最后更新时间,这样每次生成都需要更新所有页面,这样很不划算,果断删除之。

    用Git来更新真的很快…FTP更新是坑呀。

  • 从博客园迁移到Hexo

    备注:该文章写于2014年,相关思路可以作为参考,但是hexo的版本更新相关导致相关工具无法使用了。

    很早以前就开始使用博客园的服务,虽然有这样那样的问题,但是始终还是觉得不错。

    一来不用自己维护空间什么的,二来博客园的原创文章很好被收录,偶尔写写文章也不操心。

    大概从去年起将操作系统全面切换到ubuntu,没有想到最不习惯的居然是没有live writer。博客园的在线编辑器实在太难用了,反复折腾决定用node-webkit快速的做一个linux下的编辑器。

    就在这之间接触到了Hexo,最终决定从博客园迁移出来,自建博客。

    这里分享一下迁移中的各种收获。

    Hexo

    Hexo是基于Nodejs的静态博客工具。所谓静态博客区别于Wordress之类的博客系统主要是并不是由动态语言在服务端生成,而是使用者在使用完博客后生成全站静态文件。

    Hexo的优点其实我没有啥深刻的体会,这是我接触到的第一款静态博客工具。对我而言它的优势有三:

    • 书写方便,使用markdown书写。

    很多人觉得md很潮,其实我很现实的,只要好用就行了。用md来写不需要专用软件支持,一个记事本就好了。

    • 容易保存原始资料

    在博客园的时候就有定期备份数据的习惯,倒也不是害怕哪天它突然关闭了,只是决定数据在自己手中有一份比较安全。

    Hexo的原始资料可以纳入版本管理之中,还是挺方便的。

    • 使用方便,易于扩展

    Hexo在设计上预留了扩充点,而且本身原理很简单,实在不行也是可以自己修改的。这样如果有不合意的地方可以自己动手完成。接下来我也会谈到我为了迁移做的一个迁移插件。

    准备工作

    Hexo要求nodejs环境,具体安装大家随意搜索以下就可以找到。而npm是nodejs的包管理器,默认也会自动安装上。
    然后运行

    npm install -g hexo
    hexo init <blogFolder>

    初始化完成以后运行hexo server可以看到效果。

    基本的配置在_config.xml文件中,有部分配置也会在对应的主题目录的_config.xml中。

    从博客园迁移数据

    博客园是由一个未公开的API的,不过API中没有提供分类和标签,所以不适合迁移时的数据获取,所以还是抓取网页比较值得信赖。

    首先来看看插件列别。

    Hexo将插件分类7类。分别是生成,渲染,迁移,标签,辅助,其他。

    从博客园迁移数据当然是属于(Migrator)。

    原理上将是比较简单的:

    • 从列表中获取所有博文的地址。
    • 抓取博文内容并转为适合Hexo的格式。
    • 将博文中的图片保存到source中,并更改对应文件中的引用路径。
    • 将转化好的博文写入文件系统。

    我原本一直觉的Javascript属于很二流的语言,直到我接触了nodejs,然后看了一些javascript的书籍后我才发现这东西很不错的。

    nodejs的各种库的数量之大让人振奋,在npmjs中可以找到几乎所有需要的库。

    比如将html转为markdown,下载图片,解析html等等。

    具体的实现很简单,大家有兴趣可以看看hexo-migrator-cnblogs

    使用的时候首先安装它:

    npm install hexo-migrator-cnblogs --save

    然后在博客根目录中运行

    hexo migrate cnblogs <username>

    这里的用户不是你登录博客园后台的用户名,而是你博客的短名,比如我的是htynkn。

    等待程序执行完成,这可能会花费一些时间,特别是有大量图片需要下载的时候,所以请保持网络通畅,耐心等待。

    数据迁移后的一些处理

    虽然插件节省了很多功夫,但是对于每篇文章还是预览检查一下。

    目前有几个已知的问题需要手动处理一下:

    • 代码高亮

    在博客园中代码高亮有好几种,包括后台提供的和livewriter提供的,所有有时候会无法识别代码类型,这样可能会导致高亮效果不明显,可以手动补全。

    • 页面变形

    因为迁移插件使用了to-markdown,它可能会错误判断标签闭合,导致页面变形,这个情况不常见,不过还是有的。

    迁移后的配置

    刚才的迁移步骤只是迁移了博文和图片,博客的配置并没有完成。

    _config.xml中配置博客的名称、地址等等。

    然后就是根据使用习惯替换统计代码,留言系统和分享代码。这涉及到改theme中的样式片段。如果嫌麻烦,可以使用一些改好的主题。比如我当期这个主题是修改自pacman

    它使用多说和jiathis,基本不需要再修改了。

    不过pacman的一些语言设置有问题,可能需要手动修改以下,比如下一页的显示在languages中有配置,但是在实际中没有使用。

    发布和上传

    Hexo支持发布博客到github,这样很方便速度也很快,还支持域名绑定。

    我最初也想托管到github的,但是想想sf的结局,还是自己买了云主机。

    云主机支持FTP和ssh,刚才用FTP上传,稳定性之差令人发指,文件基本不完成。

    所以改成sshfs挂在主机目录到本地,然后rsync之。Hexo支持rsync发布,所以可以直接hexo deploy搞定。

    不过每次挂在以下有些慢,最后改成lftp直接mirror,添加上 --ignore-time--use-cache之后速度还可以接受。

  • 使用Gradle自动发布Java Web到SAE

    使用Gradle自动发布Java Web到SAE

    现在像SAE这类的应用引擎已经比较多了,百度和腾讯都提供了相似的平台。

    我很早的时候就开始用SAE,当时还为了迁就SAE学习了PHP(当时只支持PHP和另外一个什么语言)。后来SAE支持Java了,版本是6,容器是Jetty 7.4,而常用的框架也基本能跑。

    代码的部署使用svn,稍微有点麻烦。最近在做一个Java Web的练习,代码放在github上,每次上传到SAE很烦。项目使用Gradle管理,所以琢磨着怎么把自动发布到SAE这个事情交给Gradle来做。

    流程分析

    先来看一下SAE这边。

    建立默认版本,版本号为1。然后通过代码管理页面上传一个war包。

    进入svn仓库看一看

    其实就是把对应的war包放在版本号目录的根目录就可以了。

    而调用

    gradle war

    就可以生成war包,而文件地址通过以下方法获得

    war.archivePath

    这样就只需要checkout原有代码,然后将新的war拷贝过去,然后commit就行了。

    直接调用SVN命令

    Gradle有一种任务类型是Exec,可以直接在命令行调用命令。

    比如我们的checkout操作就可以这样

    task checkoutRepo(type:Exec) {
    workingDir'build/'
    commandLine'svn','checkout',repo_path,"svn"
    }

    这样代码仓库就被签出到build/svn目录下了。

    在使用copy指令

    copy{
    from war.archivePath
    into"build/svn/"+numberVersion
    }

    当然,gradle生成的war包的名字组成是

    ${baseName}-${appendix}-${version}-${classifier}.${extension}

    如果要重命名的话,可以使用rename,比如利用重命名去掉version信息

    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" +version,'')
    }
    }

    然后再次提交

    workingDir'build/svn'
    
    commandLine'svn','commit','-m',"Built By Gradle in"+newDate()

    为了方便调试还可以导出命令运行的输出,提交部分的任务如下

    task commitWarToSae(type:Exec) {
    workingDir'build/svn'
    commandLine'svn','commit','-m',"Built By Gradle in"+newDate()
    
    standardOutput=new ByteArrayOutputStream()
    
    ext.output= {
    return standardOutput.toString()
    }
    }

    这样勉强可以完成任务,但是问题是我的运行环境不一定包含了svn。我平时用的也比较少,不想安装它。这样只有使用其他方法了。

    在Gradle中调用SVNKit

    SVNKit 是一个纯 Java 的 SVN 客户端库,使用 SVNKit 无需安装任何 SVN 的客户端,支持各种操作系统。使用它一方面不需要考虑安装问题,另一方面SVNKit在maven仓库中有,不需要手动管理,最新版本为1.7.8

    首先在buildscript中配置SVNKit依赖

    buildscript {
    repositories {
    mavenLocal()
    mavenCentral()
    }
    dependencies {
    classpath(
    'org.tmatesoft.svnkit:svnkit:1.7.8',
    'org.tmatesoft.svnkit:svnkit-cli:1.7.8'
    )
    }
    }

    然后引用cli中的SVN,这样就可以直接调用程序的方法。

    import org.tmatesoft.svn.cli.SVN;

    调用时使用

    SVN.main(String[] args)

    这是可以直接将上文的调用svn的地方全部改为调用SVN.main,但是这之前还有一个问题。

    SVN.main实质上只有一句话,调用了svn目录下的SVN类,而这个类继承了AbstractSVNLauncher。而AbstractSVNLauncher的代码中包含了错误处理

    publicvoidfailure() {
    setCompleted();
    try {
    System.exit(1);
    }catch (SecurityException se) {
    
    }
    }
    
    publicvoidsuccess() {
    setCompleted();
    try {
    System.exit(0);
    }catch (SecurityException se) {
    
    }
    }

    对于System.exit()的调用明显是我们不希望的,即使SVNKit的运行出现了错误,我们依然期望其他任务继续运行。

    通过Java的SecurityManager来达到目的

    def _disableSystemExitCall = {
    System.setSecurityManager(
    new SecurityManager() {
    @Override
    publicvoidcheckPermission(java.security.Permission perm) {}
    
    @Override
    publicvoidcheckExit(int status) {thrownew SecurityException(); }
    }
    );
    };
    
    def _enableSystemExitCall = { System.setSecurityManager(null); };
    
    def doSvn = { String... aSvnArgs ->
    _disableSystemExitCall();
    try {
    SVN.main(aSvnArgs as String[]);
    }finally {
    _enableSystemExitCall();
    }
    };

    配置完成了,先来测试一下帮助信息的显示

    task('showSvnHelp') << {
    doSvn("help")
    }

    运行

    gradle showSvnHelp

    可以看到效果。

    剩下的事情就简单了,把所有事情放到一个新的任务中去,命名为uploadWarToSae

    task('uploadWarToSae') << {
    ext {
    repo = YOUR_REPO_PATH
    username = YOUR_USER_NAME
    password = YOUR_PASSWORD
    numberVersion = YOUR_NUMBER_VERSION
    svnPath ='build/svn'
    }
    
    delete(svnPath)
    
    doSvn("checkout", repo, svnPath,"--username", username,"--password", password)
    
    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" +version,'')
    }
    }
    
    doSvn("commit","-m","Built By Gradle in" +new Date(),"--username", username,"--password", password, svnPath)
    }

    试着运行看看,你可以会遇到一下问题

    • Server certificate verification failed
    • Fingerprint: * (R)eject, accept (t)emporarily or accept (p)ermanently?
    • failed: SSL error: certificate verify failed

    从svn的帮助中可以找到这几个指令:—no-auth-cache —non-interactive —trust-server-cert

    将它们加入doSvn中(其实也可以在调用的时候添加)

    def doSvn= {String... aSvnArgs->
    _disableSystemExitCall();
    List<string> cmds= aSvnArgs as LinkedList<string>;
    cmds.add("--no-auth-cache");
    cmds.add("--non-interactive");
    cmds.add("--trust-server-cert")
    try {
    SVN.main(cmds asString[]);
    } finally {
    _enableSystemExitCall();
    }
    };

    这样就没有问题了。

    如果你觉得用户名和密码直接写入build.gradle不安全,你也可以考虑将私密信息写入gradle.properties中去。

    集成Travis

    Travis是一个和Github集成的非常好的CI。使用它你需要在项目中配置.travis.yml文件。

    我希望让travis自动将每次的更改打包后上传到SAE中,当然直接调用

    gradle uploadWarToSae

    就行了。但是私密信息写在哪里?

    Travis提供了利用RSA密匙加密的方法。具体细节参考文末链接。

    travis encrypt-a env.global USERNAME=username PASSWORD=password

    这样就可以在命令行中调用了,比如

    gradle uploadWarToSae -Pusername=$USERNAME -Ppassword=$PASSWORD

    然后改造uploadWarToSae方法,让其可以接受参数。

    task('uploadWarToSae') << {
    if (project.hasProperty('username') &&project.hasProperty("password")) {
    ext {
    repo ='https://svn.sinaapp.com/blackjack/'
    numberVersion =1
    svnPath ='build/svn'
    }
    delete(svnPath)
    doSvn("checkout", repo, svnPath,"--username", username,"--password", password)
    
    copy {
    from war.archivePath
    into svnPath +"/" + numberVersion +"/"
    rename { String fileName ->
    fileName.replace("-" + version,'')
    }
    }
    
    doSvn("commit","-m","Built in" +new Date(),"--username", username,"--password", password, svnPath)
    }else {
    thrownew InvalidUserDataException("没有提供sae的安全邮箱和密码,部署失败")
    }
    }

    运行效果如下:

    结语

    如果使用的是BAE的话,官方提供了maven的部署插件的…而SAE不知道有没有提供,反正从官方文档中没有看到。

    附上几个参考地址:

    AbstractSVNLauncher.java

    Preventing System.exit() from API

    disabling System.exit()

    encryption-keys

  • 注入器和发布库–AngularJS学习笔记(三)

    注入器和发布库–AngularJS学习笔记(三)

    AngularJS的一大特性就是Module的加载和依赖注入,本文将分析一下loader.js和最后这些代码文件是怎么组织和运行的。

    Loader.js

    该文件中只有setupModuleLoader函数,当然它的返回值是一个函数,包含了angular.Module的API。

    首先是位置,这些配置和模块保存在哪里

    var $injectorMinErr = minErr('$injector');
    var ngMinErr = minErr('ng');
    
    function ensure(obj, name, factory) {
    return obj[name] || (obj[name] = factory());
    }
    
    var angular = ensure(window,'angular',Object);

    前两句建立两个错误提示用的对象,这个在前一篇文章有介绍。

    接下来声明的函数用于新建对象,return语句利用惰性求知保证了对象的存在性。如果存在就直接返回,否则利用提供的函数新建。

    下一句就声明了angular对象,传入的参数是window,angular字符串和Object。

    所有浏览器都支持 window 对象。而全局变量是 window 对象的属性。 全局函数是 window 对象的方法。

    这句话将angular绑定在window对象上。当然这是的angular只是一个普通的Object对象。

    而最后它将变成

    [])

    这些扩充不是在loader.js中完成的,而是在AngularPublic.js中进行的。

    再继续看这句

    angular.$$minErr = angular.$$minErr || minErr;

    刚才说到了minErr是一个很方便的东西,它可以提供更丰富的错误消息处理。而ngResource模块中的第一句就使用了

    var $resourceMinErr = angular.$$minErr('$resource');

    所以这里将minErr暴露出去。

    接下来是

    return ensure(angular,'module',function() {
    ...
    });

    这里将名为module的对象绑定到angular上,也就是window.angular.module

    接下来看看模块的具体处理。首先是判断你的模块名称是否正确。

    var assertNotHasOwnProperty =function(name, context) {
    if (name ==='hasOwnProperty') {
    throw ngMinErr('badname','hasOwnProperty is not a valid {0} name', context);
    }
    };
    
    assertNotHasOwnProperty(name,'module');

    其实就是判断名字是不是module ⊙﹏⊙b汗。

    然后是判断是否提供了依赖声明,现有模块中是否已经有了,如果有了重置之。

    if (requires && modules.hasOwnProperty(name)) {
    modules[name] =null;
    }

    modules是该方法中的一个变量

    var modules = {};

    然后是判断是否声明了依赖,如果没有报错。当然如果你的模块不需要依赖其他的,使用[]即可。

    if (!requires) {
    throw $injectorMinErr('nomod',"Module '{0}' is not available! You either misspelled " +
    "the module name or forgot to load it. If registering a module ensure that you " +
    "specify the dependencies as the second argument.", name);
    }

    然后声明了两个数组

    var invokeQueue = [];
    var runBlocks = [];

    第一个是调用队列,在createInjector中会被调用。而第二个是函数数组,其中的函数将在注入器之后被调用,通过run方法注册需要的运行的函数。

    而invokeQueue的添加是invokeLater函数

    function invokeLater(provider, method, insertMethod) {
    returnfunction() {
    invokeQueue[insertMethod ||'push']([provider, method,arguments]);
    return moduleInstance;
    };
    }

    将传入的参数放入这个数组的头部。可以看出loader只是记录了需要的依赖,需要的调用等等,并没有真正执行,所以取名叫invokeLater而不是invoke了。

    AngularPublic.js

    刚才已经看到了,通过ensure函数我们暴露了一个angular,但是它只是一个Object,并没有相关的功能,而这些扩展就是AngularPublic做的了。

    首先是关于版本的东西

    var version = {
    full:'"NG_VERSION_FULL"',
    major:"NG_VERSION_MAJOR",
    minor:"NG_VERSION_MINOR",
    dot:"NG_VERSION_DOT",
    codeName:'"NG_VERSION_CODENAME"'
    };

    其中的字符串由Grunt负责替换,而具体内容是解读package.json所得。

    然后是publishExternalAPI函数了。

    先将一些基本的方法和函数暴露出去

    extend(angular, {
    'bootstrap': bootstrap,
    'copy': copy,
    'extend': extend,
    'equals': equals,
    'element': jqLite,
    'forEach': forEach,
    'injector': createInjector,
    'noop':noop,
    'bind':bind,
    'toJson': toJson,
    'fromJson': fromJson,
    'identity':identity,
    'isUndefined': isUndefined,
    'isDefined': isDefined,
    'isString': isString,
    'isFunction': isFunction,
    'isObject': isObject,
    'isNumber': isNumber,
    'isElement': isElement,
    'isArray': isArray,
    'version': version,
    'isDate': isDate,
    'lowercase': lowercase,
    'uppercase': uppercase,
    'callbacks': {counter:0},
    '$$minErr': minErr,
    '$$csp': csp
    });

    这也是我们看到的window.angular的内容。

    然后是获得模块加载器,就是文章前部分分析的内容

    angularModule = setupModuleLoader(window);

    将在ngLocal和ng对象。

    try {
    angularModule('ngLocale');
    }catch (e) {
    angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
    }
    angularModule('ng', ['ngLocale'], ['$provide',
    function ngModule($provide) {
    ...
    }
    ]);

    注意一下SanitizeUriProvider,它在ng中,是一个内部服务,主要处理链接,“编译”的时候会用到它,所以先初始化它。

    $provide.provider({
    $$sanitizeUri: $$SanitizeUriProvider
    });

    然后是就是常用的指令、处理器等等

    $provide.provider('$compile', $CompileProvider).
    directive({
    a: htmlAnchorDirective,
    input: inputDirective,
    textarea: inputDirective,
    form: formDirective
    ...
    });
    $provide.provider({
    $anchorScroll: $AnchorScrollProvider,
    $animate: $AnimateProvider,
    $browser: $BrowserProvider
    ...
    });

    我最开始以为这就结束了…然后才发现这还是一个函数呀…谁调用了它…结果在angular.suffix中

    bindJQuery();
    publishExternalAPI(angular);
    jqLite(document).ready(function() {
    angularInit(document, bootstrap);
    });
    })(window,document);

    这段代码没有闭合,因为还有一个angular.prefix文件

    (function(window, document, undefined) {

    那么这两个文件的内容是怎么合并到最终的js文件中的呢?

    是Grunt干的!

    想想我们的grunt package命令,然后看看lib/grunt/utils.js文件。

    wrap:function(src, name){
    src.unshift('src/' + name +'.prefix');
    src.push('src/' + name +'.suffix');
    return src;
    }

    就是它将.prefix文件盒.suffix文件添加到合并后的js文件中去的。

    在Gruntfile.js配置如下:

    src: util.wrap([files['angularSrc']],'angular')

    这样,在package的时候运行到build任务下的angular时,这两个文件就会合并到源文件之中了。

    结语

    这些是src根目录下的文件,而src下的目录中的文件就是各个方面的东西了,比如ng,ngAnimate,ngCookies等等。

    几个参考:

    Gruntfile.js文件

    关于provider的博文

    module的一个小分析文档

    what is angular loader for

  • 启动和JQuery绑定–AngularJS学习笔记(二)

    启动和JQuery绑定–AngularJS学习笔记(二)

    上一篇简单的分析了AngularJS的项目结构,后面就开始分析具体的源代码了。

    从angularFiles.js中的定义可以看出有几个文件直接位于src根目录,并不是隶属于某个模块。这几 个分别是minErr.js,Angular.js,loader.js,AngularPublic.js,jqLite.js,apis.js。

    这几个提供了AngularJS中很基础的工具,比如angular.equals等,在文档中它们中的部分被归属于 Global API中。

    本文主要看看AngularJS的启动和JQuery绑定。

    minErr.js

    Javascript中有Error对象,用于表示运行时错误,但是它的功能比较单一,一般使用

    newError(message)

    但是它的功能比较单一。而minErr是一个提供更丰富信息的工具。这样生成的错误信息将包含模块名 称和其他信息,同时还提供了模板功能。比如

    var exampleMinErr = minErr('example');
    var detailMinErr=exampleMinErr('one','This {0} is {1}',"v1","v2");
    detailMinErr.stack

    显示如下:

    screenshot13

    可以看出错误还提供了对应了文档地址,不过其中的NG_VERSION_FULL看着有点奇怪,这里应该显示诸如1.2.7这样的版本号。在 lib/grunt/utils.js中有一个处理,将NG_VERSION_FULL、NG_VERSION_MAJOR等替换了。

    function(src, NG_VERSION, strict){
    var processed = src
    .replace(/"NG_VERSION_FULL"/g, NG_VERSION.full)
    .replace(/"NG_VERSION_MAJOR"/, NG_VERSION.major)
    .replace(/"NG_VERSION_MINOR"/, NG_VERSION.minor)
    .replace(/"NG_VERSION_DOT"/, NG_VERSION.dot)
    .replace(/"NG_VERSION_CDN"/, NG_VERSION.cdn)
    .replace(/"NG_VERSION_CODENAME"/, NG_VERSION.codename);
    if (strict !==false) processed =this.singleStrict(processed,'nn',true);
    return processed;
    }

    这里的NG_VERSION信息是getVersion从package.json文件的version值中读取的。

    function(){
    if (version)return version;
    
    var package =JSON.parse(fs.readFileSync('package.json','UTF-8'));
    var match = package.version.match(/^([^-]*)(?:-(.+))?$/);
    var semver = match[1].split('.');
    
    var fullVersion = match[1];
    
    if (match[2]) {
    fullVersion +='-';
    fullVersion += (match[2] =='snapshot') ? getSnapshotSuffix() : match[2];
    }
    
    version = {
    full: fullVersion,
    major: semver[0],
    minor: semver[1],
    dot: semver[2].replace(/rcd+/,''),
    codename: package.codename,
    cdn: package.cdnVersion
    };
    
    return version;
    }

    现在来看看minErr的源码,其实主要是模板替换功能。

    message = prefix + template.replace(/{d+}/g,function (match) {
    ...
    }

    这里的prefix是module名称组成而成的,而这里使用了replace方法,匹配诸如{0},{1}的字符串,而 /g命令表示全局替换。而 function(match)是根据匹配的字符串生成对应的替换字符串。

    function (match) {
    var index = +match.slice(1, -1), arg;
    
    if (index +2 < templateArgs.length) {
    arg = templateArgs[index +2];
    if (typeof arg ==='function') {
    return arg.toString().replace(/ ?{[sS]*$/,'');
    }elseif (typeof arg ==='undefined') {
    return'undefined';
    }elseif (typeof arg !=='string') {
    return toJson(arg);
    }
    return arg;
    }
    return match;
    }

    因为传入的参数实质上是类似{0},{1}这样的字符串,所以首先用slice(1,-1)去掉两端的大括号,然 后比较剩余的数字范围是否正确,这里的+2是因为传入的参数中第一个是code码,第二个是模板,从第 三个开始才是替换用的字符串。这里还针对不同的类型有不同的处理。

    Angular.js

    这个文件涉及到了AngularJS的初始化,还提供了若干基础方法,比如大小写转化,浏览器适配, forEach,Uid生成,浅复制,深复制等。

    其实这些基础方法比较简单,这里只分析nextUid方法。调用它可以生成一个供Angular使用的唯一ID 。初始的uid为

    uid = ['0','0','0'];

    Angular的uid不是单纯的数字,而是字母和数字的组合,这样就不会增长太快,也不会超值。

    function nextUid() {
    var index = uid.length;
    var digit;
    
    while(index) {
    index--;
    digit = uid[index].charCodeAt(0);
    if (digit ==57/*'9'*/) {
    uid[index] ='A';
    return uid.join('');
    }
    if (digit ==90/*'Z'*/) {
    uid[index] ='0';
    }else {
    uid[index] =String.fromCharCode(digit +1);
    return uid.join('');
    }
    }
    uid.unshift('0');
    return uid.join('');
    }

    首先是获取uid的长度,然后获取最后一位,如果是9就变成“A”,如果是Z就变成0,不 然就是直接+1。如果长度不够了就增加uid数组长度。这样首先生成的就是001,然后是002…然后 是00Z,接下来就是010。nextUid方法在Scope中会被使用。

    然后AngularJS的启动了,这里就涉及了两个比较重要的方法:angularInit和bootstrap。

    AngularJS官方文档的第一个列子就是一个数据绑定的例子,显示Hello {yourName}。

    例子中html标签中添加了执行ng-app,文档说这是告知AngularJS,这里是它的管理范围,而实现就在angularInit中。

    ng-app是一个自动初始化的指令,而且只能有一个自动初始化,其他的需要调用angular.bootstrap 启动。

    function angularInit(element, bootstrap) {
    var elements = [element],
    appElement,
    module,
    names = ['ng:app','ng-app','x-ng-app','data-ng-app'],
    NG_APP_CLASS_REGEXP =/sng[:-]app(:s*([wd_]+);?)?s/;
    
    function append(element) {
    element && elements.push(element);
    }
    
    forEach(names,function(name) {
    names[name] =true;
    append(document.getElementById(name));
    name = name.replace(':',':');
    if (element.querySelectorAll) {
    forEach(element.querySelectorAll('.' + name), append);
    forEach(element.querySelectorAll('.' + name +':'), append);
    forEach(element.querySelectorAll('[' + name +']'), append);
    }
    });
    
    forEach(elements,function(element) {
    if (!appElement) {
    var className =' ' + element.className +' ';
    var match = NG_APP_CLASS_REGEXP.exec(className);
    if (match) {
    appElement = element;
    module = (match[2] ||'').replace(/s+/g,',');
    }else {
    forEach(element.attributes,function(attr) {
    if (!appElement && names[attr.name]) {
    appElement = element;
    module = attr.value;
    }
    });
    }
    }
    });
    if (appElement) {
    bootstrap(appElement,module ? [module] : []);
    }
    }

    该函数首先匹配指令本身,有4种ng:app,ng-app,x-ng-app,data-ng-app。我最开始挺好奇的为什么 有四种写法,只有一种不是挺好的嘛。网上搜寻了一番发现了原因。

    首先是data-ng-app。在html5标准中定义了data-为自定义属性,这样页面就可以通过验证。x-也 是基于相似的原理。不过ng:app是为啥我确实没有找到…望知道的朋友点拨一下。ng:app是为了兼容XML,格式为namespace:name,其他三种依次为:none,HTML5,xHtml。

    然后依次匹配这四种执行,直到appElement被赋值位置。确定了html元素位置以后通过正则表达式解 析module的名称,因为这是可选参数,所以如果没有提供就自动使用[]。当然具体的初始化实际上是有 bootstrap函数完成的。

    首先是doBootstrap函数:

    var doBootstrap =function() {
    element = jqLite(element);
    
    if (element.injector()) {
    var tag = (element[0] ===document) ?'document' : startingTag(element);
    throw ngMinErr('btstrpd',"App Already Bootstrapped with this Element '{0}'", tag);
    }
    
    modules = modules || [];
    modules.unshift(['$provide',function($provide) {
    $provide.value('$rootElement', element);
    }]);
    modules.unshift('ng');
    var injector = createInjector(modules);
    injector.invoke(['$rootScope','$rootElement','$compile','$injector','$animate',
    function(scope, element, compile, injector, animate) {
    scope.$apply(function() {
    element.data('$injector', injector);
    compile(element)(scope);
    });
    }]
    );
    return injector;
    };

    首先先将元素转为Jquery对象(如果没有,则有jqLite提供Jquery的一个功能子集),然后判断是否 有注入器了,是否已经初始化了。然后判断是否有modules,如果没有就为空。然后根据modules生成注 入器,然后编译。

    jqLite.js

    刚才启动的时候提到了将元素转为JQuery对象,但是很明显JQuery很流行,但不代表必须使用它。 AngularJS提供了jqLite,它提供了一个JQuery的子集,包含了addClass(),after(),append()等。当然 针对AngularJS的特定,它还提供了controller(),injector()等方法。

    首先有个小适配,针对不同的浏览器对应事件的处理不同做了一个兼容:

    addEventListenerFn = (window.document.addEventListener
    ?function(element, type, fn) {element.addEventListener(type, fn,false);}
    :function(element, type, fn) {element.attachEvent('on' + type, fn);}),
    removeEventListenerFn = (window.document.removeEventListener
    ?function(element, type, fn) {element.removeEventListener(type, fn,false); }
    :function(element, type, fn) {element.detachEvent('on' + type, fn); });

    其实主要是IE的问题,如果没有提供addEventListener方法,那么就使用attachEvent方法。

    如果调用jqLite(element)方法,但是传入的是一个String的话那么就会自动生成一个html元素并插 入其中。

    if (isString(element)) {
    var div =document.createElement('div');
    // Read about the NoScope elements here:
    // http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx
    div.innerHTML ='<div> </div>' + element;// IE insanity to make
    
    NoScope elements work!
    div.removeChild(div.firstChild);// remove the superfluous div
    jqLiteAddNodes(this, div.childNodes);
    var fragment = jqLite(document.createDocumentFragment());
    fragment.append(this);// detach the elements from the temporary DOM div.
    }else {
    jqLiteAddNodes(this, element);
    }

    如果没有使用JQuery,那么当然好,所有功能由jqLite提供。如果使用JQuery,那么这个适配其实是 由Angular.js处理的。

    function bindJQuery() {
    // bind to jQuery if present;
    jQuery =window.jQuery;
    // reset to jQuery or default to us.
    if (jQuery) {
    jqLite = jQuery;
    extend(jQuery.fn, {
    scope: JQLitePrototype.scope,
    isolateScope: JQLitePrototype.isolateScope,
    controller: JQLitePrototype.controller,
    injector: JQLitePrototype.injector,
    inheritedData: JQLitePrototype.inheritedData
    });
    // Method signature:
    // jqLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments)
    jqLitePatchJQueryRemove('remove',true,true,false);
    jqLitePatchJQueryRemove('empty',false,false,false);
    jqLitePatchJQueryRemove('html',false,false,true);
    }else {
    jqLite = JQLite;
    }
    angular.element = jqLite;
    }

    如果直接引用了JQuery,那么就会有一个window.jQuery。同时根据JQuery扩展的原理,将 controller(),injector()等方法加入JQuery中,然后调用jqLitePatchJQueryRemove将JQuery的remove 等和$destroy等绑定。

    结语

    还有loader.js,AngularPublic.js,apis.js没有分析,估摸着是下一篇的内容。

    附上几个参考:

    minError.js源码

    JavaScript replace() 方法

    关于data-*的规定

    关于IE的attachEvent和IE11支持addEventListener

  • 源代码目录结构–AngularJS学习笔记(一)

    源代码目录结构–AngularJS学习笔记(一)

    最近开始接触AngularJS,确实是一个相当不错的东西,可以把很多东西简化掉。又对于其中的双向绑定等的实现很好奇,加之正在学习Javascript的东西,所以觉得从源代码这块开始深入学习AngularJs。

    获取AngularJs源代码

    虽然可以直接通过阅读最终的js文件来学习,不过总觉得这样入手不太容易,而项目源代码本身就是一块一块的,同时又配有测试,从这里开始学习的话应该比较容易一些。

    AngularJs的源码在github上,地址是:https://github.com/angular/angular.js

    实现先获取到代码:

    git clone https://github.com/angular/angular.js.git

    下载需要一点点时间…特别是你访问github速度比较慢的时候…

    目录结构和构建工具

    首先来看看项目有哪些文件夹:

    • src目录是项目的源代码,也是学习的主要对象
    • test目录是对应的测试
    • closure中的文件是Google Closure Compiler使用的
    • scripts目录中有一些脚本,主要用于bower、travis等的任务使用

    再来看看文件:

    • angularFiles.js中定义了AngularJs中每个某块所包含的js文件地址,很多地方使用到了这个文件配置。
    • bower.json是包管理工具的配置文件,更多介绍点击Bower
    • check-size.sh将angular.min.js文件压缩,然后显示文件大小…(说实话,我不是太清楚这有什么意义)
    • changelog.js是一个nodejs脚本,它可以从git仓库中获取更新日志。通过
    node changelog.js

    运行脚本。如果提示[Error: Cannot find module ‘qq’],可以通过npm安装

    npm install qq

    changelog.spec.js是对应的测试文件。

    • compare-master-to-stable.js也是一个nodejs脚本,它可以比较稳定版和当前版本的区别。
    • gen_docs.sh会使用Jasmine进行测试,然后调用docs/src/中的gen-docs.js生成文档。
    • Gruntfile.js是Grunt的配置文件,更多介绍参考Grunt
    • init-repo.sh是一个初始化脚本,它会检查nodejs,npm,karam并且挂上git hook。
    • karam-* 这些文件时karam的配置文件。Karam是一个Javascript测试执行过程管理工具。更多信息参考Karam
    • package.json 是npm的配置文件,它管理了nodejs的依赖。

    总的来说,AngularJs用Jasmine做单元测试,Karma自动化完成单元测试,Grunt做项目管理,npm管理Nodejs的依赖,Bower管理javascript的包依赖。

    检查环境

    获取到源代码以后我们需要运行一下测试什么的..首先是保证你有nodejs、npm、grunt:

    sudo apt-get install nodejs
    sudo npm install -g grunt-cli
    npm install -g bower

    然后安装nodejs依赖

    npm install

    安装javascript依赖

    bower install

    最后运行单元测试

    grunt test:unit

    效果如下图:

    如果要打包的话执行

    grunt package

    Gruntfile.js

    Gruntfile.js是Grunt的配置文件,这里有一份中文文档,大家可以参考:http://www.gruntjs.org/

    Gruntfile.js中定义了很多任务,对于学习最关键的是build任务了:

    build:{
    scenario: {
    dest:'build/angular-scenario.js',
    src: [
    'bower_components/jquery/jquery.js',
    util.wrap([files['angularSrc'], files['angularScenario']],'ngScenario/angular')
    ],
    styles: {
    css: ['css/angular.css','css/angular-scenario.css']
    }
    },
    angular:{
    dest: 'build/angular.js',
    src: util.wrap([files['angularSrc']],'angular'),
    styles: {
    css: ['css/angular.css'],
    generateCspCssFile: true,
    minify: true
    }
    },
    loader:{
    dest: 'build/angular-loader.js',
    src: util.wrap(files['angularLoader'],'loader')
    },
    touch:{
    dest: 'build/angular-touch.js',
    src: util.wrap(files['angularModules']['ngTouch'],'module')
    },
    mocks:{
    dest: 'build/angular-mocks.js',
    src: util.wrap(files['angularModules']['ngMock'],'module'),
    strict: false
    },
    sanitize:{
    dest: 'build/angular-sanitize.js',
    src: util.wrap(files['angularModules']['ngSanitize'],'module')
    },
    resource:{
    dest: 'build/angular-resource.js',
    src: util.wrap(files['angularModules']['ngResource'],'module')
    },
    animate:{
    dest: 'build/angular-animate.js',
    src: util.wrap(files['angularModules']['ngAnimate'],'module')
    },
    route:{
    dest: 'build/angular-route.js',
    src: util.wrap(files['angularModules']['ngRoute'],'module')
    },
    cookies:{
    dest: 'build/angular-cookies.js',
    src: util.wrap(files['angularModules']['ngCookies'],'module')
    },
    "promises-aplus-adapter":{
    dest:'tmp/promises-aplus-adapter++.js',
    src:['src/ng/q.js','lib/promises-aplus/promises-aplus-test-adapter.js']
    }
    },

    这里多次用到了files这个变量,而这个量是通过

    varfiles =require('./angularFiles').files

    从angularFiles.js文件中获取到的。比如files[‘angularSrc’]中就有60个文件:

    screenshot12

    结语

    终于找到学习的入口了,接下来就可以通过学习files[‘angularSrc’]中定义的文件来学习AngularJS这个强大的框架了。

    附上几个链接:

    http://nodejs.org/

    http://gruntjs.com/

    http://bower.io/

    http://karma-runner.github.io/

    http://pivotal.github.io/jasmine/