使用acmd构建jade/sass/livescript开发环境

之前写过一个browserSync的脚本编译jade/sass/coffee,在多个项目中拷贝脚本文件比较麻烦,业余封装了一个npm插件,这里介绍一下用法。
项目地址:https://github.com/thinkjs/autocommand-cli,插件使用typescript开发,欢迎有兴趣的同学一起完善。

环境搭建

1.创建目录

mkdir example && cd example
mkdir _source _source/jade _source/sass _source/live
mkdir html html/css html/js

_source目录存放源代码,_source/jade、_source/sass、_source/live分别存放jade、sass、livescript源文件。html目录存放生成结果,html、html/css、html/js分别存放html、css、js文件。

2.安装npm包依赖

echo '{}' > package.json
npm install thinkjs/autocommand-cli#latest jade node-sass livescript --save-dev

如果网络不好,建议使用cnpm替代

3.编辑package.json加以下内容:

"scripts": {
  "acmd": "acmd",
  "start": "acmd watch",
  "build": "acmd run"
},

完整的package.json如下

{
  "scripts": {
    "acmd": "acmd",
    "start": "acmd watch",
    "build": "acmd run"
  },
  "devDependencies": {
    "autocommand-cli": "thinkjs/autocommand-cli#latest",
    "jade": "^1.11.0",
    "livescript": "^1.5.0",
    "node-sass": "^3.9.3"
  }
}

4.创建acmd配置文件并修改

npm run acmd -- config -i
默认在当前目录下创建名为_config的配置文件,编辑该配置文件
修改file字段内容为:
"file": ["_source/**/*.{jade,sass,ls}"],

修改define字段内容为:

"define": {
  "_source/": {
    ".jade": {
      "file": "html/#{fileName}.html",
      "command": "jade -Po html/ #{file}"
    },
    ".sass": {
      "file": "html/css/#{fileName}.css",
      "command": "node-sass --output-style compact #{file} html/css/#{fileName}.css"
    },
    ".ls": {
      "file": "html/js/#{fileName}.js",
      "command": "lsc -co html/js/ #{file}"
    }
  }
},

修改 browserSync > init下的server为:

"server": {
  "baseDir": "html/"
},

完整配置文件如下:

{
  // 侦听的文件
  "file": ["_source/**/*.{jade,sass,ls}"],
  // 过滤
  "ignore": ["_*.*", "node_modules/"],
  // 变量
  "variable": {
    "localBin": "~/node_modules/.bin"
  },
  // 环境变量
  "environment": {
    ":PATH": "#{localBin}"
  },
  // 定义
  "define": {
    "_source/": {
      ".jade": {
        "file": "html/#{fileName}.html",
        "command": "jade -Po html/ #{file}"
      },
      ".sass": {
        "file": "html/css/#{fileName}.css",
        "command": "node-sass --output-style compact #{file} html/css/#{fileName}.css"
      },
      ".ls": {
        "file": "html/js/#{fileName}.js",
        "command": "lsc -co html/js/ #{file}"
      }
    }
  },
  // browserSync配置
  "browserSync": {
    // 初始化配置
    "init": {
      "server": {
        "baseDir": "html/"
      },
      "open": false,
      "ui": false
    },
    // 启动livereload
    "reload": true
  }
}
// vim: se sw=2 ts=2 sts=2 ft=javascript et:

开发

1.编译jade文件

创建_source/jade/index.jade,内容如下:

doctype html
html
  head
    title jade page
  body
    h1 hello world

在terminal中运行 npm start,浏览器访问 http://localhost:3000,可查看页面。

2.编译sass文件

创建_source/sass/style.sass,内容如下:

html, body
  padding: 0
  margin: 0
  font-size: 16px

h1
  color: green

编辑_source/jade/index.jade加入对style.sass的引用,在head中插入以下内容:
link(href="css/style.css" rel="stylesheet")
保存查看浏览器,内容已自动刷新。

3.编译livescript文件

创建_source/sass/script.ls,内容如下:
console.log \hello

编辑_source/jade/index.jade加入对script.ls的引用,在body中插下以下内容:
script(src="js/index.js")
保存可查看浏览器中的控制台输出。

进阶用法

1.编译所有文件

有时需要对项目的所有源文件进行编译,使用如下命令:
npm run build

2.编译某一类型文件

比如需要编译所有的sass文件,可以使用如下命令:
npm run build -- -f _source/sass/*.sass

3.使用glob表达式匹配文件

-f选项支持glob表达式,使用引号括起即可,例如编译jade和sass可以使用如下命令:
npm run build -- -f '_source/**/*.{jade,sass}'

4.使用通道

如果使用git管理文件,需要编译git中已经修改的文件,可以使用如下命令:
git status -uno | awk '{print $2}' | npm run build -- -o
使用find查找sass进行编译,命令如下:
find _source -type f | grep *.sass | npm run build -- -o

小结

autocommand-cli项目最初的出发点是为了锻炼一下typescript的能力,经过半年的迭代,功能已经逐渐完善。命令行参数解析、文件改动侦听、http服务器功能都使用现成npm插件实现。前端的工具链不断完善,各种工具层出不穷,花大量时间造一个自己用起来的顺手的工具还是有点奢侈:)

使用JS重写API拦截运营商广告

近期公司站点通过联通4G网站访问时被运营商插入广告代码,通过手机抓包定位到运营商值入的广告代码。

植入的广告代码通过在固定url的请求中添加内容插行一段js,通过body.appendChild在页面引入一段js代码。想到一个应急方案,通过改写appendChild来拦截广告,经测试有效。代码如下:

(function() {
var rootEl = top.document.body;
var originAppendChild = rootEl.appendChild;
try {
    rootEl.appendChild = function (elem) {
    var src = '';
    if (typeof elem == 'object' && elem != null && elem.getAttribute) {
        src = elem.getAttribute('src');
    }
    if (typeof src == 'string' && src.length) {
        if ( src.match(/^(?:http(?:s)?:)?\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)
            || src.match(/cifenqi\.com/) != null) {
        return;
        }
    }
    originAppendChild.apply(rootEl, [].slice.call(arguments, 0));
    }
}
catch(e) { }
})();

ISP广告干的是非法勾当多使用ip地址,因此直接屏蔽ip地址的元素。此方法只能应急可被绕过,难怪某度、某宝纷纷启用全站https。

NERDTree插件API小记

之前因为vim上的TFS插件一直不好使,自己写了一个简单的插件在vim上签出TFS源文件。为了在NERDTree上面集成了菜单功能,研究了下NERDTree插件。年底忙里偷闲整理了一些关于NERDTree的知识点,分享出来。

1. 添加NERDTree文件菜单
在vim插件目录创建一个nerdtree_plugin文件夹,在其中新建vim脚本文件,使用NERDTreeAddMenuItem函数添加一项按钮项。参数格式:
{'text': 'menu text', 'shortcut': 'short key', 'callback':'callback function name'}
示例:
call NERDTreeAddMenuItem({ 'text': 'TFS Check (o)ut', 'shortcut': 'o', callback': 'TFSCheckoutCallback' })

2. 获取当前文件节点
let currentNode = g:NERDTreeFileNode.GetSelected()

3.获取文件路径
let currentFile = currentNode.path._strForGlob()

4.更新文件状态
cal currentNode.path.refresh()
cal NERDTreeRender()
# 当我们通过currentNode.path.refresh() 可以刷新文件节点,之后通过NERDTreeRender重绘获得更新。当通过操作修改了文件的属性后需要通过此操作更新文件状态。

附插件git地址:https://github.com/thinkjs/TFSCheckout
NERDTree FileNode参考:https://github.com/scrooloose/nerdtree/blob/5.0.0/lib/nerdtree/tree_file_node.vim
NERDTree Pathp参考:https://github.com/scrooloose/nerdtree/blob/5.0.0/lib/nerdtree/path.vim
NERDTree Public API参考:https://github.com/scrooloose/nerdtree/blob/5.0.0/plugin/NERD_tree.vim#L164

 

基于browser-sync的前端预处理器工作流

之前使用预处理器一直是基于vim插件autocommand,最近和同事协作开发,vim入门太陡峭,抽了点时间用browser-sync实现了类似功能。具体功能如下:自动检测jade、sass、livescript等源码文件的改动,编译对应的html、css、js,更新改动到浏览器中打开的页面。

明确几个定义:
1. browser-sync nodejs插件,官方说明 Time-saving synchronised browser testing。
2. 前端预处理器 jade、sass、livescript。
3. 工作流 使用browser-sync整合jade、sass、livescript完成开发的一套流程。

依赖:
nodejs
 npm package: jade、livescript、browser-sync
ruby
 gem package: sass

工作目录:

workspace/
  _source/
    jade/
    sass/
    live/
  html
    css/
    js/
    img/

说明:_source目录下存放预处理器源代码,html目录存放生成的html、css、js等。

在workspace目录下创建 browserSync.ls 具体代码如下:

path = require \path
exec = require \child_process .exec
browserSync = require \browser-sync .create!

# config {{{
outputDir = 'html'
cssOutputDir = "#outputDir/css"
jsOutputDir = "#outputDir/js"

reloadWatchFile = ''
  # "#outputDir/*.html"
  # "#jsOutputDir/*.js"
  # "#outputDir/img/*.*"

compileWatchFile =
  "_source/jade/*.jade"
  "_source/sass/*.sass"
  "_source/live/*.ls"

autoCompileFile = true
# }}}
# getTimeToken {{{
getTimeToken = ->
  currDate = new Date()
  hours = currDate.getHours()
  minutes = currDate.getMinutes()
  seconds = currDate.getSeconds()
  if hours < 10
    hours = "0#hours"
  if minutes < 10
    minutes = "0#minutes"
  if seconds < 10
    seconds = "0#seconds"
  "#hours:#minutes:#seconds"
# }}}
# compileTask {{{
getCompileCmdAndFileName = (file, ext) ->
  filename = path.basename file, ext

  switch ext
  case '.jade' then
    compileFileName = "#outputDir/#{filename}.html"
    cmd = "jade -Po #outputDir #file"
  case '.sass' then
    compileFileName = "#cssOutputDir/#{filename}.css"
    cmd = "sass --sourcemap=none --style compact #file|sed '/^@charset/d'>#compileFileName"
  case '.coffee' then
    compileFileName = "#jsOutputDir/#{filename}.js"
    cmd = "coffee --no-header -bco #jsOutputDir #file"
  case '.ls' then
    compileFileName = "#jsOutputDir/#{filename}.js"
    cmd = "lsc --no-header -bco #jsOutputDir #file"
  default
    compileFileName = cmd = ''
  [cmd, compileFileName]

compileTask = (file, ext, reload) !->
  cmdIndex = -1
  try
    [cmd, filename] = getCompileCmdAndFileName file, ext

  if not cmd or not filename
    console.log "cmd not define. file: #file ext: #ext"

  # exec callback
  execCallback = (err, stdo, stde) !->
    if err is null and not stde
      if cmdIndex is -1
        console.log "[#{getTimeToken!}] compiled #filename"
        reload filename if reload
      else
        execCmd()
    else
      console.log err || stde

  # execute command
  do execCmd = !->
    if Array.isArray cmd
      currCmd = cmd[++cmdIndex]
      if cmd.length <= cmdIndex+1
        ``cmdIndex = -1;``
    else
      currCmd = cmd

    if currCmd
      exec currCmd, execCallback

compileCallback = (file) !->
  ext = path.extname file
  filename = path.basename file

  # ignore partial file
  if filename.charAt(0) is '_'
    return undefined

  switch ext
  case '.jade', '.coffee', '.ls', '.sass'
    #   compileTask file, ext
    # case '.sass'
    compileTask file, ext, browserSync.reload
  default
    console.log 'unknown file type.'
# }}}
# browserSync {{{
browserSync.init do
  server:
    baseDir: outputDir
    index: \index.html
  open: false

if reloadWatchFile and reloadWatchFile.length
  browserSync.watch reloadWatchFile
  .on \change, browserSync.reload

wacher = browserSync.watch compileWatchFile
.on \change, compileCallback
# auto compile file
if autoCompileFile
  wacher.on \add, compileCallback
# }}}

# vim: set sw=2 ts=2 sts=2 et fdm=marker:

使用方法如下:
cd workspace
lsc browserSync.ls

* lsc 是 livescript的命令,使用npm 全局安装 livescript 后方可使用。

配置说明:
outputDir        html文件的输出目录,web server的根目录。
cssOutputDir     css文件输出目录。
jsOutputDir      js文件输出目录。
reloadWatchFile  检测改动的目录,如果文件发生变更,会更新到打开的页面中。html/css/js已自动更新,默认可留空。
compileWatchFile 监测的源码文件,如果发生变化执行编译任务。
autoCompileFile  自动编译文件,设为true时,每次启动会把所有的源码文件编译一遍。

用vim正则环视优化vim-coloresque插件

vim-coloresque是一款高亮插件,将css/sass/less中的颜色用背景色高亮出来方便预览。用了一段时间发现代码补全一定程度上受影响,自己对插件进行了一些优化,修改后git地址:https://github.com/thinkjs/vim-coloresque

看插件github上的issue,插件中对isk这个选项的设置影响了补全,全并且影响了vim全局的keyword识别,直接注释掉isk+=-、isk+=#、 isk+=# 解决了补全问题但针对colorDict的高亮识别又发生错误,例如样式名为 .red或.common-red-btn,此处的red会被高亮。作者使用"\<"单词边界来识别colorDict,修改isk以后单词边界识别出错高亮出错,想到用正则表达式来调整此处的colorDict匹配。

匹配规则如下:
1. 前字符为行首或空白或:(冒号)
2. 后字符为行尾或空白或;(分号)

正则如下:
'\(^\|\s\|:\)\@<=\c'._color.'\($\|\s\|;\)\@='

vim中使用@<=作逆序环视、@=做顺序环视(vim中使用:h @<=查看详细介绍),修改后效果不错,符合我的应用场景。diff代码如下:

From f162c7a118caba4af655741feeae11720329bb4e Mon Sep 17 00:00:00 2001
From: leaf
Date: Wed, 21 Jan 2015 16:27:40 +0800
Subject: [PATCH] remove "isk" change, improve color dict regexp

---
 after/syntax/css/vim-coloresque.vim | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/after/syntax/css/vim-coloresque.vim b/after/syntax/css/vim-coloresque.vim
index c80a9a5..b60f863 100644
--- a/after/syntax/css/vim-coloresque.vim
+++ b/after/syntax/css/vim-coloresque.vim
@@ -122,9 +122,9 @@ function! s:VimCssInit(update)
     if a:update==1
         call s:ClearMatches()
     endif
-    :set isk+=-
-    :set isk+=#
-    :set isk+=.
+    " :set isk+=-
+    " :set isk+=#
+    " :set isk+=.
 
     if len(keys(b:color_pattern))>0
         call s:RestoreColors()
@@ -301,7 +301,10 @@ function! s:AdditionalColors()
   "let w:colorDictRegExp = '\(' 
   for _color in keys(w:colorDict)
     "let w:colorDictRegExp.='\<'._color.'\>\|' 
-    call s:MatchColorValue(strpart(w:colorDict[tolower(_color)], 1), '\<\c'._color.'\>')
+    " old
+    " call s:MatchColorValue(strpart(w:colorDict[tolower(_color)], 1), '\<\c'._color.'\>')
+    " new regexp
+    call s:MatchColorValue(strpart(w:colorDict[tolower(_color)], 1), '\(^\|\s\|:\)\@<=\c'._color.'\($\|\s\|;\)\@=')
   endfor
   "let w:colorDictRegExp=strpart(w:colorDictRegExp, 0, len(w:colorDictRegExp)-2).'\)\c'
 endfunction
-- 
1.7.11.1

vim缩进参考线

编辑缩进嵌套的文件时想找到对应的层级比较困难,写了一个函数,使用cc选项设定一条辅助线,标识到指定的缩进层级。代码如下:

" --------------------------------------------------
" [参考线切换] {{{
" --------------------------------------------------
fu! ReferenceLine(t)
	if exists('w:ccnum')
		let ccnum=w:ccnum
	elsei exists('b:ccnum')
		let ccnum=b:ccnum
	else
		let ccnum=0
	en
	let oldcc=ccnum
	" let ccc=&cc
	" ec oldcc
	let ccc=','.&cc.','
	" add/sub
	if a:t=='add' || a:t=='sub'
		" check old cc
		if match(ccc, ','.oldcc.',')<0
			let oldcc=0
			let ccnum=0
		en
		" step
		let csw=&sw
		if a:t=='add'
			let ccnum=ccnum + csw
		elsei a:t=='sub'
			let ccnum=ccnum - csw
			if ccnum < 0 | let ccnum=0 | en
		en
		if oldcc > 0 | let ccc=substitute(ccc, ','.oldcc.',', ',', '') | en
		let ccc=ccc.ccnum
		" ec ccc
		" ec ccnum
		let ccc=substitute(ccc, '^0,\|,0,\|,0$', ',', 'g')
		let ccc=substitute(ccc, '^,\+\|,\+$', '', 'g')
		" ec ccc
		let w:ccnum=ccnum
		let b:ccnum=ccnum
		exec "setl cc=".ccc
	" del
	elsei a:t=='del'
		let ccc=substitute(ccc, ','.oldcc.',', ',', '')
		let ccc=substitute(ccc, '^,\+\|,\+$', '', 'g')
		" ec ccc
		let w:ccnum=0
		let b:ccnum=0
		exec "setl cc=".ccc
	en
endf
nn <silent> <A-u> :call ReferenceLine('sub')<CR>
nn <silent> <A-o> :call ReferenceLine('add')<CR>
" }}}

使用方法:
Alt+o 增加参考线、Alt+u 减少参考线,最后两行是键映射,可以按照需求自行修改。
设定参考线后,如果想要设定cc做列宽参照,请使用set cc+=<num>来设定。

效果如下:左侧的是缩进辅助线,右侧的是手动设置的列宽参照。

win平台配置vim-addon-manager

vim-addon-manager是一款功能非常强大的vim插件管理工具,但其依赖git, hg, svn, curl, unzip等工具在win平台配置较繁琐,一直没在win平台中使用这种方案。这两天在试着在win平台配置,分享一下配置过程。

1. 依赖程序
1.1 git 主页 http://msysgit.github.io/
下载地址:https://github.com/msysgit/msysgit/releases/download/Git-1.9.2-preview20140411/Git-1.9.2-preview20140411.exe

1.2 svn 主页 http://www.sliksvn.com/
下载地址:http://www.sliksvn.com/pub/Slik-Subversion-1.8.9-x64.msi

1.3 hg 主页 http://mercurial.selenic.com/
下载地址:http://mercurial.selenic.com/release/windows/Mercurial-3.0.exe

msysgit包中带有curl以及unzip等工具

2. 添加环境变量
编辑vim配置文件($VIM/.vimrc)为vim指定相关环境变量(将C:\Program Files替换成自己的安装路径)

if(has('win32') && match($PATH,'SlikSvn')<0)
  let $PATH=$PATH.';C:\Program Files\SlikSvn\bin;C:\Program Files\Mercurial;C:\Program Files\Git\bin'
en

3. 下载vim-addon-manager并配置
https://github.com/MarcWeber/vim-addon-manager 获取源代码到插件目录,在vimrc中添加配置。
附官方建议配置:

fun! SetupVAM()
  let c = get(g:, 'vim_addon_manager', {})
  let g:vim_addon_manager = c
  let c.plugin_root_dir = expand('$HOME', 1) . '/.vim/vim-addons'
  " most used options you may want to use:
  " let c.log_to_buf = 1
  " let c.auto_install = 0
  let &rtp.=(empty(&rtp)?'':',').c.plugin_root_dir.'/vim-addon-manager'
  if !isdirectory(c.plugin_root_dir.'/vim-addon-manager/autoload')
    execute '!git clone --depth=1 git://github.com/MarcWeber/vim-addon-manager '
        \       shellescape(c.plugin_root_dir.'/vim-addon-manager', 1)
  endif
  call vam#ActivateAddons([], {'auto_install' : 0})
endfun

call SetupVAM()
VAMActivate matchit.zip vim-addon-commenting

不建议将所有配置内容都放在vimrc中,向大家推荐我整理的vim配置分割方案:http://zshou.i11r.com/posts/39266.html

devise配置登录超时和单处登陆

1.配置用户超时
<Project>/config/initializers/devise.rb
编辑 # config.timeout_in = 30.minutes 这一行,修改为想要配置的时间

<Project>/app/models/user.rb
配置devise :database_authenticatable, :registerable,这一行,加上 :timeoutable

2.设置用户单处登陆
原理:devise在用户登陆后会设置users表中的current_sign_in_at字段为最后一次登陆的时间,用户每次登陆后在session中保存current_sign_in_at字段,在application中校验session中current_sign_in_at字段,如果为空或者小于current_user对象的current_sign_in_at则强制注销
1) 在application_controller基类中定义after_sign_in_path_for方法在当中设置session对象

  def after_sign_in_path_for(resource)
    session[:current_sign_in_at] = current_user.current_sign_in_at
    root_path
  end

2) 定义has_signed取代:authenticate_user!判断用户登陆

  def has_signed
    unless user_signed_in?
      redirect_to root_path
    else
      if session[:current_sign_in_at] == nil or session[:current_sign_in_at] < current_user.current_sign_in_at
        redirect_to destroy_user_session_path
      end
    end
  end

3)在需要判断用户登陆的controller中添加before_action :has_signed过滤器。

rails判断浏览器head设置多语言

给rails系统加i18n,根据浏览器的头去识别用户语言。记录一下相关实现:

1. 添加i18n配置
# config/application.rb
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
config.i18n.available_locales = [:en, :cn]
config.i18n.default_locale = :cn

2. 获取语言设置

# app/controller/application_controller.rb
before_action :set_locale
# set local {{{
def set_locale
  I18n.locale = get_locale
end
# }}}

# locale {{{
def get_locale
  locale = I18n.default_locale

  if params[:locale]
    locale = params[:locale].downcase
  elsif cookies[:locale]
    locale = cookies[:locale]
  elsif locale = get_accept_language
    locale
  end
  # set cookies
  if cookies[:locale] != locale
    cookies[:locale] = locale
  end

  locale
end

def get_accept_language
  begin
    first_lang = request.env['HTTP_ACCEPT_LANGUAGE'].split(';').first
    default_lang = first_lang[0,2].downcase

    if default_lang == 'zh'
      first_lang[3,2].downcase
    else
      default_lang
    end
  rescue
    nil
  end
end
# }}}

说明:locale的值优先从url地址读取,次之从cookie,最后从浏览器头判断。英文浏览器的头为Accept-Language:en-US,en;,取en即前两位。中文浏览器头为Accept-Language:zh-CN,zh;,取cn即3-5位,转为小写。

3.编辑语言文件
# config/locales/en.yml
en:
 hello: "Hello world"
# config/locales/cn.yml
cn:
 hello: "你好"

4. 在模板文件中应用
# app/views/home/index.html.erb
<p><%= t('hello'); %></p>
<ul>
  <% I18n.available_locales.each.each do |locale| %>
    <li><%= link_to locale, params.merge(locale: locale) %></li>
  <% end %>
</ul>
说明:点击生成的链接可以手动切换语言,测试的时候方便一些。

devise使用ajax登录

devise默认已经有较好的ajax支持了,只需要简单的配置一下即可使用ajax登录/注册。

1. 修改config/application.rb加下以下配置,启用json输出。

# devise respond_to json
config.to_prepare do
  DeviseController.respond_to :html, :json
end

2. 修改form标签,添加data-remote, data-type,设置id

<%= form_for(resource, :as => resource_name, :url => session_path(resource_name), :remote => true,
 html: {id: 'ajax_user_signin', data: {type: :json}}) do |f| %>

3. 设置ajax头

$.ajaxSetup({
  beforeSend: function(xhr){
    var token;
    token = $('meta[name="csrf-token"]').attr('content');
    if (token) {
      xhr.setRequestHeader('X-CSRF-Token', token);
    }
  }
});

4. js处理代码

$('#ajax_user_signin').on('ajax:complete', function(e, xhr, type){
  if (type === 'success') {
    location.href = '/';
  } else {
    try {
      alert(xhr.responseJSON.error);
      // this.reset();
    } catch (e$) {
      e = e$;
    }
  }
});

关于注册:
注册的方法和登录基本相同,给form_for添加data-remote、data-type和id等属性。需要注意的是注册返回的表单验证是多项错误,使用xhr.resonseJSON.errors获取错误集合。