rails 中使用 jstree 使用记录 创建于:30 Nov 2014

环境: 基于jstree 3.0.0,rails: 4.1.4

jsTree介绍

  1. Jquery插件
  2. 在网页中生成具有交互功能的树状结构
  3. 开源、免费
  4. 容易扩展、配置
  5. 支持HTML和JSON格式的数据源
  6. 支持AJAX动态获取数据

在rails中配置

  1. 在http://www.jstree.com/ 主页下载jstree插件源码
  2. 把js, css及图片资源放到对应的rails项目目录中
// 32px.png 40px.png 和 throbber.gif 放到images目录
// src/themes/style.css 重命名为jstree-style.css.scss放到stylesheets目录
// src目录下的js文件: jstree.js放入javascripts文件夹
// 其余的都是jstree的插件,有需要就放,没有就可以不放进来:jstree.checkbox.js jstree.contextmenu.js jstree.dnd.js jstree.search.js jstree.sort.js jstree.state.js jstree.types.js jstree.unique.js jstree.wholerow.js
// 在application.js中设置js文件的加载顺序,不然会出现js错误。jstree需要在插件之前,实例如下:
// //=require jquery
// //=require jquery_ujs
// //=require jquery-ui
// //=require jstree.vakata
// //=require jstree
// //=require jstree.contextmenu
// .
// .
// .
// //= require_tree

ps: 对于jstree.vakata.js文件,我这边的实践是,只有引入了这个文件,jstree的有些功能才能正常进行,但是作者也在这里有过说明,说是不需要引入: https://github.com/vakata/jstree/issues/123

一棵简单的树,有html预设置的节点和少量js组成:

<!--views/home/simple_tree.erb-->
<div id="mytree-html">
  <ul>
    <li data-jstree='{"type": "fold"}'>文件夹1</li>
    <li data-jstree='{"type": "fold"}'>文件夹2
      <ul>
        <li data-jstree='{"icon": "/assets/file.png"}'>文件1</li>
      </ul>
    </li>
  </ul>
</div>

<script>
  $('#mytree-html').bind('loaded.jstree', function(e, data) {
    // 当jstree被load完成后,打开所有的节点
    data.instance.open_all();
  });
  $('#mytree-html').jstree({
    "types": {
      "fold": {
        "icon": ".<%= asset_path('fold.png') %>"
      },
      "file": {
        "icon": ".<%= asset_path('file.png') %>"
      }
    },
    'plugins': ['html_data', 'types', 'themes']
  });
</script>

一颗复杂的树,通过后台的json数据自动生成,同时添加了可以自由的拖拽节点,节点右键菜单功能

通过JSON数据生成树的基本设置:

  $('#mytree').jstree({
    'core': {
      // check_callbackc参数用于Contextmenu 和Drag & drop 插件,设置true后才能起作用
      'check_callback': true,
      'data': {
        // 后台JSON数据的地址,当jstree载入时,会自动从这个地址获取json数据,然后生成树状结构
        'url': '/home/tree_data',
        'data': function(node) {
          return {node_id: node.id}
        }
      }
    },
    // types插件设置,但是在我的实践中,这个怎么也没法起效,有待进一步研究
    'types': {
      '#': {
        'valid_children': ['fold', 'file']
      },
      'fold': {
        'icon': "<%= asset_path('fold.png') %>",
        'valid_children': ['fold', 'file']
      },
      'file': {
        'icon': "<%= asset_path('file.png') %>",
        'valid_children': []
      }
    },
    'plugins': ['types', 'dnd', 'contextmenu', 'wholerow'],
    // 右键自定义菜单设置,customMenu是一个函数,下面马上就会讲到
    'contextmenu': {
      'items': customMenu
    }
  });

右键自定义菜单处理:

  // 自定义右键菜单
  function customMenu(node) {
    var items = {
      'createSubFold': {
        'label': '新建子目录',
        'action': function(obj) {
          var inst = $.jstree.reference(obj.reference);
          var node = inst.get_node(obj.reference);
          inst.create_node(node, {}, 'last', function(new_node) {
            setTimeout(function() {inst.edit(new_node);}, 0);
          });
        }
      },
      'createfile': {
        'label': '新建文件',
        'action': function(obj) {
          var inst = $.jstree.reference(obj.reference);
          var node = inst.get_node(obj.reference);
          $.ajax({
            url: '/home/new_file',
            data: {fold_id: node.id},
            success: function(data, textStatus, jqXHR) {
              console.log("new file success!");
            }
          });
        }
      },
      'rename': {
        'label': '重命名',
        'action': function(obj) {
          var inst = $.jstree.reference(obj.reference);
          var node = inst.get_node(obj.reference);
          inst.edit(node);
        }
      },
      'edit': {
        'label': '编辑',
        'submenu': {
          'cut': {
            'label': '剪切',
            'action': function(obj) {}
          },
          'copy': {
            'label': '复制',
            'action': function(obj) {}
          },
          'paste': {
            'label': '粘帖',
            'action': function(obj) {}
          },
        }
      },
      'destroy': {
        'label': '删除',
        'action': function(obj) {
          var inst = $.jstree.reference(obj.reference);
          var node = inst.get_node(obj.reference);
          $.ajax({
            url: '/home/destroy_node',
            type: 'DELETE',
            data: {id: node.id}
          });
        }
      }
    };
    // 当树的节点是文件时,右键菜单没有新建子文件夹
    // 通过json格式传过来的li_attr或a_attr我们可以轻松的实现不同的节点,拥有不同的右键菜单
    if (node.li_attr.class == 'file') {
      delete items.createSubFold;
    }
    return items;
  }

处理树的节点移动事件

  $('#mytree').bind('move_node.jstree', function(e, data) {
    // 当页面树的节点位置发生变化后,同时需要更新后端存储的树的结构
    $.ajax({
      url: '/home/move_node',
      type: 'POST',
      data: {id: data.node.id, old_parent: data.old_parent, old_position: data.old_position, parent: data.parent, position: data.position},
      success: function(data, textStatus, jqXHR) {
        console.log('move node successfully!');
      }
    });
  });

处理树中节点的左键单击选中事件

  // 实现的功能,通过左键点击节点,可以显示节点的详细信息
  $('#mytree').bind('select_node.jstree', function(e, data) {
    // 只有左键选中node才触发ajax操作
    if (data.event.which == 1) {
      if (data.node.li_attr.class == 'file') {
        $.ajax({
          url: '/home/show_file',
          data: {id: data.node.id}
        });
      }
      else {
        $.ajax({
          url: '/home/show_fold',
          data: {id: data.node.id}
        });
      }
    }
  });

处理树中节点的重命名事件

  // 前端重命名后,需要同时处理后端的重命名,同时,创建一个子目录时,也是调用这个callback
  $('#mytree').bind('rename_node.jstree', function(e, data) {
    $.ajax({
      url: '/home/node_rename',
      type: 'POST',
      data: {id: data.node.id, new_name: data.node.text},
      success: function(data, textStatus, jqXHR) {
        console.log("rename successfully!");
      }
    });
  });

实现双击打开或关闭当前节点

$('#mytree').delegate('a', 'dbclick', function(e) {
  $('#mytree').jstree('toggle_node', this);
  e.preventDefault();
  return false;
};

刷新局部的jstree

$('#jstree-demo').jstree(true).refresh_node('#' + node_id);

刷新整颗树

$('#jstree-demo').jstree(true).refresh();

使用效果参见:https://github.com/beyondalbert/jstree-demo

参考:
http://www.jstree.com/

jquery使用小记 创建于:30 Nov 2014

jquery常用接口

jquery选择器
// 选中所有元素
$("*");
// 根据类名选择元素
$('.class-demo');
// 根据元素名字来选择元素
$('div');
// 根据元素id来选择元素
$('#id-demo');
// 组合选择多种类型的元素
$('#id-demo, div, .class-demo');
// 根据元素的属性来选择元素,更多属性选择器用法见:
// http://api.jquery.com/category/selectors/attribute-selectors/
$("a[href='/test_demo']");
.resize:当元素大小变化时,触发这个事件,如果绑定到了window元素,则会在浏览器大小变化的时候触发,实例:
$(window).resize(function() {
    console.log("浏览器宽度为:" + $(window).width());
});
.hover:当鼠标hover到元素上时,然后离开这个元素,触发这个事件,实例:
$('#target-element-id').hover(function() {
    // 悬浮到目标元素上时,触发这个函数中的事件
    console.log("mouse enter target element");
}, function() {
    // 鼠标离开目标元素时,触发这个函数中的事件
    console.log("mouse leave target element");
});
.click: 当元素被单击时,触发的事件;
.dblclick():当元素被双击时触发的事件,实例:
$("#target-element-id").click(function() {
    console.log("target element is clicked!");
});
$("#target-element-id").dblclick(function() {
    console.log("target element is double clicked!");
});
.change:当元素的value发生变化的时候,触发的事件,该事件只对input, textarea, select这三种类型的元素value变化会触发,对于select boxes, checkboxes和radio button,这个事件会会立刻触发,一旦值变化,但对于其他的元素,则是要等到元素失去焦点后才能触发,实例:
$('#target-select-id').change(function() {
    var selectedItemValue = $(this).children('option:selected').val();
    console.log("change the value to: " + selectedItemValue);
});
.addClass .removeClass: 添加和删除目标元素的css类,实例:
$('#target-element-id').addClass("foo");
$('#target-element-id').removeClass("foo");
.serialize():把要提交的表单中的输入框的值编码成字符串,使之可以在url中传输;也可以对单个的表单进行编码,实例:
// 一般用于ajax中,设置需要提交的data
// 同时,一般会阻止默认的表单form提交: event.preventDefault();
var submitData = $("#target-form-id").serialize();

ps: 表单form中的表单都需要有name属性,不然这个函数不会把表单的值编码到最终的字符串

.text() .html():前者得到目标元素所有子元素的文本内容,或者设置目标元素的文本内容;后者得到目标元素所有子元素的html内容,或者设置目标元素的html内容
.val():获取或设置匹配选择器的元素的值,一般是针对表单中的元素值的获取

html内容:

<div id="target-id">
    <div>test1</div>
    test2
    <input id="input-id" value="test!">
</div>

js实例:

// 输出 test1 test2
$('#target-id').text();
// 得到
// <div>test1</div>
// test2
$('#target-id').html();
// 设置目标元素的text
$('#target-id').text('test3');
// 设置目标元素的html内容
$('#target-id').html(htmlContent);
// 获取input框的值
$('#input-id').val();
// 设置input框的值
$('#input-id').val();
data(): 获取或设置元素中的data值,一般用于不能设置value值的元素,通过设置data值,可以给该元素设置value
<div id="demo-id" data-msg="test msg" data-demo="test demo">
// 输出test msg
$('#demo-id').data('msg');
// 设置data-msg的值
$('#demo-id').data('msg', 'test change msg');
.clone():克隆目标元素以及其所有子元素,默认不会克隆绑定在子元素上的事件,除非参数中设置true,实例:
var cloneTargetElement = $('#target-element-id').clone();
// 通常用于复制后,在append到其他的元素
cloneTargetElement.appendTo("#another-element");
// 如需复制子元素上的事件,需要添加true选项
var cloneTargetElement = $('#target-element-id').clone(true);
.after 在目标元素后添加参数中的元素,添加的元素是目标元素的兄弟元素
.appendTo 在目标元素中,添加参数中的元素,添加的元素是目标元素的子元素
.append 把目标元素添加到参数中的元素中去,目标元素是参数元素的子元素

实例:

$('#target-id').after(cloneTargetElement);
$('#target-id').append(cloneTargetElement);
cloneTargetElement.appendTo("#target-id");
.prop:获取或设置dom对象的属性值
.attr:获取或设置html页面元素的属性值

实例:

<input id="check-demo" type="checkbox" checked="checked">
// 返回true
$('#check-demo').prop('checked');
// 返回“checked”
$('#check-demo').attr('checked');
// 设置checkbox为未选中状态
$('#check-demo').prop('checked', false);
.each:遍历jquery或得的dom对象,实例:
<ul>
    <li>foo</li>
    <li>bar</li>
</ul>
$('li').each(function(index) {
    console.log(index + ":" + $(this).text());
});

ps: 一般如果是jquery获取的一个dom数组,建议用这个方式来遍历,如果是纯js的数组,建议用forEach方式来遍历

var arrayDemo = [1, 2, 3];
arrayDemo.forEach(function(element, index, array) {
    console.log("a[" + index + "] = " + element);
});
.filter: 过滤选中的dom数组,实例:
$('li').filter(function(index) {
    return index % 3 === 2
});
.find:查找当前元素下的所有元素,返回匹配选择器的元素
.children:查找当前元素下的第一层子元素,返回匹配选择器的元素

实例:

$('#target-id').find(".demo-class");
$('#target-id').children(".demo-class");
.scrollTo:jquery的一个插件,用于html内容的滚动,demo可以看链接: http://jsfiddle.net/beyondalbert/ofjr2m7j/
.remove: 删除一个html元素,实例:
<div class="container">
    <div class="hello">Hello</div>
    <div class="goodbye">Goodbye</div>
</div>
// 删除选中的元素
$('.hello').remove();
// 删除选中元素中,符合选择器的子元素
$('.container').remove('.goodbye');
.show() .hide(): 显示和隐藏目标元素
$('#target-id').show();
$('#target-id').hide();
// 比较复杂的用法:
// 控制显示的快慢:用“slow”(600)和“fast”(200),或者是数值,以毫秒为单位
$('#target-id').show("slow", function() {
    // 显示结束后,需要处理的逻辑写在这里
});
.prev(): 获取目标元素的前面一个元素
$('#target-id').prev();

// 目标节点之后的兄弟节点选择,以下选择id为target的元素之后的div兄弟节点
$('#target ~ div');
.before(): 在目标元素的前面插入html
.insertBefore(): 把目标元素插入到指定的元素后面
$('#target-id').before("<p>test</p>");
$("<p>test</p>").insertBefore('#target-id');
给还没有渲染的元素捆绑事件
  $(document).on('click', '#target-element', function() {
    console.log('test');
  });

环境版本: webmock: 1.20.4 rspec-rails: 3.0.2

为什么要mock外部的api依赖

  1. 外部依赖不可控,会导致我们自己的测试结果也不可控
  2. 外部依赖的api会使我们的测试变慢
  3. 不会产生过多的无用外部数据

webmock整合到Rspec的配置

  1. 在Gemfile中添加gem
group :test do
     gem 'webmock'
end
  1. 在spec_helper.rb中添加
require 'webmock/rspec'

ps: 默认情况下,只要添加了webmock,当前应用所有的外部依赖都被阻断了,除非你在spec_helper.rb中打开

# 打开所有的外部链接
WebMock.allow_net_connect!

# 或者只允许部分外部链接,并且允许正则匹配
WebMock.disable_net_connect!(:allow => [/example.org/, /google/])

直接mock一个特定的外部链接

直接在 it 语句中使用:

it "should return project hash" do
    stub_request(:get, "http://www.example.com/api/projects/1.json").to_return(:body => {id: 1, name: "test project"}.to_json)实力
    res = JSON.parse(Net::HTTP.get(URI('http://www.example.com/api/projects/1.json')))
    expect(res).to eq({id: 1, name: "test project"})
end

更多的mock实例可以参加github:
https://github.com/bblimke/webmock

通过rack应用,在模拟一个外部的api服务依赖,把所有api服务全部发送到自己假造的rack服务上

应用场景:你的应用非常多的依赖于一个外部的api服务,通过上面的简单mock,会重复写多个mock,并且测试代码也会变得分散无法很好的维护。

  1. 在每个测试开始前,设置mock的api到特定的rack server(这里用sinatra作为rack server)
    先在Gemfile中添加sinatra这个gem:
group :development, :test do
    gem 'sinatra'
end

在spec_helper.rb中添加如下的代码:

config.before(:each) do
    stub_request(:any, /example.com/).to_rack(FakeServer)
end
  1. 在spec/support/下添加rack应用: fake_server.rb
# spec/support/fake_server.rb
require 'sinatra/base'
class FakeApiServer < Sinatra::Base
    get '/api/projects/1.json' do
        content_type :json
        status 200
        {id: 1, name: "test project"}.to_json
    end
end
  1. 在测试中的使用:
it "should return project hash" do
    # 下面的url会自动的发送的我们的fake server上,然后返回fake server上设置的返回值
    res = JSON.parse(Net::HTTP.get(URI('http://www.example.com/api/projects/1.json')))
    expect(res).to eq({id: 1, name: "test project"})
end

参考:
http://robots.thoughtbot.com/how-to-stub-external-services-in-tests
https://github.com/bblimke/webmock

Rails Rspec 使用简明指南 创建于:28 Nov 2014

基于版本: rspec_rails: 3.0.4 factory_girl_rails: 4.5.0

Rails中设置

  1. Gemfile中添加如下gem,然后bundle install
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails'
end
  1. 设置测试数据库
# config/database.yml
test:
  adapter: mysql2
  encoding: utf8
  reconnect: false
  pool: 5
  socket: /var/run/mysqld/mysqld.sock
  database: demo_test
  username: demo
  password: "****"
  1. 安装rspec

在rails demo的根目录下运行如下的命令:

rails g rspec:install

该命令会生成如下的结果:

create  .rspec
create  spec
create  spec/spec_helper.rb
  1. 改变rspec的输出,让人更容易看结果

在.rspec文件中加如下代码:

--format documentation

替换自带的test框架,设置自动生成rspec测试文件

在config/application.rb中添加一下配置

  config.generators do |g|
    g.test_framework :rspec,
      :fixtures => true,
      :view_specs => false,
      :helper_specs => true,
      :routing_specs => false,
      :controller_specs => true,
      :request_specs => true
    g.fixture_replacement :factory_girl, :dir => "spec/factories"
  end

Rspec自动生成命令

rails g rspec:model project
# 将生成一下文件:
# spec/models/project_spec.rb
# spec/factories/projects.rb

#还能用于一下内容的自动生成:
rails g rspec:controller project
rails g rspec:helper project
# 等等,具体参见:https://www.relishapp.com/rspec/rspec-rails/docs/generators

Model spec demo

require "rails_helper"

Rspec.describe Project, :type => :model do
    # model的实例方法测试
    describe "#issue_count" do
        it "返回项目中的任务个数" do
            project = FactoryGirl.create(:project, :with_issues)
            expect(project.issue_count).to eq(3)
        end
    end
    
    # model的类方法测试
    describe ".total_count" do
        it "返回所有项目的数量" do
            expect(Project.total_count).to eq(1)
        end
    end
end

To Be Continue

Git常用操作简记 创建于:13 Aug 2014

基本操作

git clone git链接 #克隆remote代码
git clone -b branch名字 git链接 #指定remote的分支克隆
git branch #查看本地分支
git branch -d [name] #删除分支, -d选项只能删除已经参与了合并的分支,对于未有合并的分支是无法删除的。如果想强制删除一个分支,可以使用-D选项
git push origin --delete branch_name #删除remote分支
git branch -r #查看remote分支
git branch b-name #创建分支
git checkout b-name #切换到另外的分支
git checkout -b b-name #切换到remote的分支
git push origin b-name #本地branch push到remote
git config -l #查看本地git配置信息

进阶操作

git tag -a v1.0.0 -m "后台第一个版本!" #创建tag
git tag #列出本地的tag
git push origin --tags #本地所有tag一起push到remote
git push origin v1.0.0 #push本地特定的tag到remote

git log --pretty=format:'%h : %s' --topo-order --graph #log显示

#合并多个commit
git rebase -i HEAD~4 #对最近的4次进行合并
#rebase操作参考: http://blog.chinaunix.net/uid-27714502-id-3436706.html

#stash用法:
git stash #备份当前的工作区的内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致。同时,将当前的工作区内容保存到Git栈中。
git stash pop #从Git栈中读取最近一次保存的内容,恢复工作区的相关内容。由于可能存在多个Stash的内容,所以用栈来管理,pop会从最近的一个stash中读取内容并恢复。
git stash list #显示Git栈内的所有备份,可以利用这个列表来决定从那个地方恢复。
git stash clear #清空Git栈。此时使用gitg等图形化工具会发现,原来stash的哪些节点都消失了。
git stash apply stash@{1} #就可以将你指定版本号为stash@{1}的工作取出来

git reset --hard commit_id #回到某个版本
git reset --hard HEAD~3 #会将最新的3次提交全部重置,就像没有提交过一样

git remote set-url origin url #切换git url
git config --global user.email "your_email@example.com" #设置全局的用户邮件地址
git config user.email "your_email@example.com" #设置当前git库的用户邮件地址

#分支1 merge 到 分支2
git checkout branch2
git fetch origin
git merge origin/branch1 #merge完后需要可能需要手动处理conflict
git push origin branch2

github托管代码(linux系统)

  • 注册github;
  • 安装git:
sudo apt-get install git
  • 生成github公钥和私钥:
ssh-keygen -C "github帐号" -f ~/.ssh/github
  • 然后把~/.ssh/github目录下的github.pub文件中的公钥拷贝到github网站个人设置中的SSH keys中新添加的ssh中
  • 测试是否设置正确:
ssh -T git@github.com

Hi XXX! You’ve successfully authenticated, but GitHub does not provide shell access. #显示这个表示设置成功

  • 完成配置,可以从githubclone代码了
  • 本地创建git库,然后push到github
# 先进入需要push的目录
git init
git commit -m "first commit"
git remote add origin git@github.com:xxx/xxx.git
git push -u origin master

基于版本: fancory_girl_rails: 4.5.0

factory_girl是什么?

  • A Replacement for Fixtures
  • Provides a Simple DSL
  • Keeps Tests Focused & Readable
  • Builds Objects Instead of Database Records

创建一个factory

一般一个model对象对应一个factories目录下的文件,比如有一个项目model,则建立以下factories文件:
Rails.root/spec/factories/projects.rb

一个最简单的factory:

# Rails.root/spec/factories/projects.rb
FactoryGirl.define do
  factory :project do
    name "test project"
    description "test descriprion"
  end
end

调用如下:

@project = FactoryGirl.create(:project)
# 或者:
@project = FactoryGirl.build(:project)

重复创建不同内容的对象

FactoryGirl.define do
  sequence :name do |n|
    "test#{n} project"
  end
  factory :project do
    name
    description "test descriprion"
  end
end

@project = FactoryGirl.create_list(:project, 5)
# => test1_project
# => test2_project
# => .
# => .
# => .

重复利用对象的现有attributes

使用trat实现

FactoryGirl.define do
  factory :project do
    name "test project"
    description "test descriprion"
  end
  
  trait :with_archived do
    is_archived true
  end
end

# 调用
project = FactoryGirl.create(:project, :with_archived)
project.is_archived # => true

使用继承的方式实现

FactoryGirl.define do
    factory :project do
        name "test project"
        description "test description"
        
        factory :archived_project do
            is_archived true
        end
    end
end
# 或者:
FactoryGirl.define do
    factory :project do
        name "test project"
        description "test description"
    end
    factory :archived_project, parent: :project do
        is_archived true
    end
end

# 调用:
archived_project = FactoryGirl.create(:archived_project)
archived_project.is_archived # => true

创建多个相互关联的对象

直接添加

# Rails.root/spec/factories/versions.rb
FactoryGirl.define do
  factory :version do
    project
  end
end

# 在创建version时,会自动创建version关联的project,前提是已经定义好了project的FactoryGirl
version = FactoryGirl.create(:version)

通过callback实现

FactoryGirl.define do
  factory :project do
    name "test project"
  end
  
  trait :with_version do
    after :create do |project|
      FactoryGirl.create :version, project: project
    end
  end
end

# 调用
project = FactoryGirl.create(:project, :with_version)

使用transient block来自定义创建的对象内容

# Rails.root/spec/factories/projects.rb
FactoryGirl.define do
  factory :project do
    transient do
      name_flag false
    end
    name { name_flag ? "test project with flag" : "test project" }
  end
  trait :with_versions do
    transient do
      number_of_versions 3
    end
    after :create do |project, evaluator|
      FactoryGirl.create_list :version, evaluator.number_of_versions, project: project
    end
  end
end

# 调用如下:
project = FactoryGirl.create(:project, name_flag: true)
project.name # => "test project with flag"
FactoryGirl.create(:project, :with_versions, number_of_versions: 5, name_flag: true)

参考链接

http://www.slideshare.net/gabevanslv/factory-girl-15924188
http://arjanvandergaag.nl/blog/factory_girl_tips.html
http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md