ionic下拍照获取带有GPS的EXIF信息

ionic下使用Apache Cordova Plugin camera插件进行拍照或者从手机相册中选择照片,使用方法也很简单,首先安装cordova-plugin-camera插件:

  1. cordova plugin add cordova-plugin-camera

然后使用下面的代码实现拍照的功能:

  1.  $ionicPlatform.ready(function() {
  2.    $cordovaCamera.getPicture({
  3.      quality: 60, targetWidth: 200, targetHeight: 200,
  4.      saveToPhotoAlbum: false, allowEdit: false,
  5.      encodingType: navigator.camera.EncodingType.JPEG,
  6.      destinationType: navigator.camera.DestinationType.FILE_URI,
  7.      sourceType: navigator.camera.PictureSourceType[CAMERA]
  8.    }).then(function(imageData) {
  9.      // 处理图片数据
  10.    }, function(err) {
  11.    });
  12.  });

如果在iOS下想要获取EXIF信息的话需要首先在config.xml文件中添加如下的配置项:

  1.  <preference name="CameraUsesGeolocation" value="false" />

获取EXIF信息的话需要使用第三方的js库,这里使用Exif.js,代码如下(已经引用Exif.js文件):

  1.  $ionicPlatform.ready(function() {
  2.    $cordovaCamera.getPicture({
  3.      quality: 60, targetWidth: 200, targetHeight: 200,
  4.      saveToPhotoAlbum: false, allowEdit: false,
  5.      encodingType: navigator.camera.EncodingType.JPEG,
  6.      destinationType: navigator.camera.DestinationType.FILE_URI,
  7.      sourceType: navigator.camera.PictureSourceType[CAMERA]
  8.    }).then(function(imageData) {
  9.      // 处理图片数据
  10.      window.resolveLocalFileSystemURL(imageData, function(entry) {
  11.        entry.file(function(file) {
  12.          EXIF.getDate(file, function() {
  13.            console.log(EXIF.pretty(this));
  14.          });
  15.        }, function(err) {
  16.        });
  17.      }, function(err) {
  18.        // file error
  19.      });
  20.    }, function(err) {
  21.    });
  22.  });

不过很奇怪,使用上面的方法在iOS下只能获取部分的EXIF信息,并且无法获取GPS相关的信息,Android下面没有测试。期间又测试了Cordova Exif等几个获取EXIF信息的库,不是根本无法获取到任何EXIF信息,就是获取到的信息不全。

最后在Cordova Exif的issues页面中找到了一个cordova-plugin-camera-with-exif插件,这个插件其实是Cordova Camera Plugin插件的一个扩展,其返回的结果是一个JSON形式的字符串,里面包含的完整的EXIF信息,测试下来在iOS和Android下面都运行正常,就是其最后的JSON字符串结果比较变态,一是没有提供一个合法的JSON对象,二是Android和iOS下面的参数竟然都不一致,需要根据平台进行转换,可以将解析的方法独立成一个service方法。

需要特别注意的是,使用cordova-plugin-camera-with-exif的时候,destinationType只能设置成navigator.camera.DestinationType.FILE_URI,否则在iOS下面会出现闪退,这应该是作者在编译的时候只考虑了FILE_URI的情况。采用这种方式,如果想要获取文件的Base64字符串可以使用FileReader对象进行读取。

参考资料:
Take picture with geo location doesn’t work!
Phonegap / Cordova camera plugin – how to get photo’s date/time stamp?
Cordova 3.6: how can I extract GPS Exif data from photo library in Android?
Camera exif data are always undefined

自动安装gulpfile中所有依赖的模块

在gulpfile.js中定义任务的时候,需要引用不同的模块,这些模块需要使用 npm install –save-dev 命令进行安装,非常的繁琐。可以使用一个名为gulpfile-install的模块,使用下面命令进行安装:

npm install -g gulpfile-install

安装成功之后就可以在项目根目录(存放gulpfile.js的目录)下,直接执行 gulpfile-install 命令就可以自动根据引用的模块进行安装了,非常方便。

使用gulp和cordova hook发布ionic程序

发布ioinc程序主要通过gulp任务和cordova hook方式进行,前者是目前前端最流行的构建工具,后者在cordova(ionic框架底层的实现)构建过程中插入自定义的任务,从而通过对构建过程的干预达到最终发布的目的。通常来说,正式发布ionic程序需要做以下工作:

  • 对js代码进行语法检验(Lint)(cordova hook),保证js代码的正确性,才能在后面进行正确的混淆处理
  • 把html模板文件转换为angular模板(gulp任务),从而加快程序的运行速度
  • 开启agular的严格注入模式(gulp任务),相当于对angular进行lint检验,这一点也是为了保证后面正确的混淆处理
  • 合并css和js文件并替换html中的引用(gulp任务),将代码中的css和js文件进行合并
  • 压缩和混淆代码(cordova hook),因为ionic基于Cordova框架,因此这里使用cordova hook的方式进行,也可以使用gulp任务具体来写

下面分别进行详细的描述:
1. 对js代码进行语法检验(Lint)(cordova hook)
首先在项目根目录下执行下面的命令安装jshint

npm install jshint --save-dev
npm install async --save-dev

然后在工程目录下面创建hooks目录,这里里面是存放cordova hook的文件,然后创建before_prepare目录并将这里的文件放入其中,主要有一个02_jshint.js文件。

2. 把html模板文件转换为angular模板(gulp任务)
首先在项目根目录下执行下面的命令安装gulp-angular-templatecache

npm install gulp-angular-templatecache --save-dev

接下来打开gulpfile.js,增加下面的代码:

  1.  var templateCache = require('gulp-angular-templatecache');
  2.  
  3.  // 在paths下面增加对tempate目录的设置
  4.  var paths = {
  5.    sass: ['./scss/**/*.scss'],
  6.    templatecache: ['./www/templates/**/*.html']
  7.  };
  8.  
  9.  gulp.task('templatecache', function (done) {
  10.    gulp.src(paths.templatecache)
  11.      .pipe(templateCache({standalone:true, root: 'templates'}))
  12.      .pipe(gulp.dest('./www/js'))    // 将处理好的js文件放在www/js下面,方便后面的合并
  13.      .on('end', done);
  14.  });

在app.js中增加对上面自动生成的templates模块的引用:

  1.  angular.module('app', ['ionic', 'controllers', 'templates'])

在index.html中引入对templates.js文件的引用

  1.  <script src="js/templates.js"></script>

3. 开启agular的严格注入模式(gulp任务)
首先在项目根目录下执行下面的命令安装gulp-ng-annotate

npm install gulp-ng-annotate --save-dev

然后编写gulpfile.js文件,增加ng_annotate任务:

  1.  var ngAnnotate = require('gulp-ng-annotate');
  2.  
  3.  // 在paths下面增加ng_annotate的目录
  4.  var paths = {
  5.    sass: ['./scss/**/*.scss'],
  6.    templatecache: ['./www/templates/**/*.html'],
  7.    ng_annotate: ['./www/js/*.js']
  8.  };
  9.  
  10.  // angular注入验证
  11.  gulp.task('ng_annotate', function(done) {
  12.    gulp.src(paths.ng_annotate)
  13.      .pipe(ngAnnotate({single_quotes: true}))
  14.      .pipe(gulp.dest('./www/dist/dist_js/app'))
  15.      .on('end', done);
  16.  });

上面的ng_annotate任务会自动将js中的angular用的引用加上,比如原始代码为

  1.  angular.module('app.controllers', []).controller('LoginCtrl', function($scope) {});

经过上面的插件会自动处理成

  1.  angular.module('app.controllers', []).controller('LoginCtrl', '$scope', function($scope) {});

并将新的文件放在www/dist/dist_js/app目录下,因此接下来需要在index.html文件中修改对原来js的引用并在body标签中添加上ng-strict-di属性:

  1.  <script src="dist/dist_js/app/app.js"></script>
  2.  ...
  3.  
  4.  <body ng-app="app" ng-strict-di>
  5.  ...

4. 合并css和js文件并替换html中的引用(gulp任务)
合并和替换css、js文件需要使用gulp-useref模块,在项目根目录下执行下面命令进行安装:

npm install gulp-useref --save-dev

然后编写gulpfile.js文件增加useref任务:

  1.  var useref = require('gulp-useref');
  2.  
  3.  // 在paths下面增加ng_annotate的目录
  4.  var paths = {
  5.    sass: ['./scss/**/*.scss'],
  6.    templatecache: ['./www/templates/**/*.html'],
  7.    ng_annotate: ['./www/js/*.js'],
  8.    useref: ['./www/*.html']
  9.  };
  10.  
  11.  // useref任务
  12.  gulp.task('useref', function (done) {
  13.    var assets = useref.assets();
  14.    gulp.src(paths.useref)
  15.      .pipe(assets)
  16.      .pipe(assets.restore())
  17.      .pipe(useref())
  18.      .pipe(gulp.dest('./www/dist'))
  19.      .on('end', done);
  20.  });

useref使用特殊的注释标签对标签内的css或js文件进行合并,因此修改index.html文件

  1.  <!-- build:css dist_css/styles.css -->
  2.      <link href="css/style.css" rel="stylesheet">
  3.      <!-- endbuild -->
  4.  
  5.      <!-- build:js dist_js/app.js -->
  6.      <script src="dist/dist_js/app/app.js"></script>
  7.      <script src="dist/dist_js/app/controllers.js"></script>
  8.      <script src="dist/dist_js/app/routes.js"></script>
  9.      <script src="dist/dist_js/app/services.js"></script>
  10.      <script src="dist/dist_js/app/directives.js"></script>
  11.      <script src="dist/dist_js/app/templates.js"></script>
  12.      <!-- endbuild -->

对于不需要合并的资源文件可以不加build:xx注释。

5. 压缩和混淆代码(cordova hook)
最后使用cordova hook的方式压缩和混淆代码,需要用到cordova-uglify和mv模块,在项目根目录下执行下面的命令进行安装:

npm install cordova-uglify --save-dev
npm instal mv --save-dev
npm instal uglify-js --save-dev
npm instal clean-css --save-dev
npm instal ng-annotate --save-dev

然后将这个里面的js文件下载下来放到hooks/after_prepare目录下,在安装cordova-uglify的时候,会自动在hooks/after_prepare目录下创建一个uglify.js文件,因为上面的文件中已经有了060_uglify.js,因此将uglify.js文件删除即可。

经过上面的设置之后,就对整个的发布流程进行了修改和处理,最后还需要做一些后续的工作。首先将上面的gulp任务加入到default和watch任务中,修改gulpfile.js如下:

  1.  gulp.task('default', ['sass', 'templatecache', 'ng_annotate', 'useref']);
  2.  
  3.  
  4.  gulp.task('watch', function() {
  5.    gulp.watch(paths.sass, ['sass']);
  6.    gulp.watch(paths.templatecache, ['templatecache']);
  7.    gulp.watch(paths.ng_annotate, ['ng_annotate']);
  8.    gulp.watch(paths.ng_annotate, ['useref']);
  9.  });

然后还需要在ionic.project文件中加入gulpStartupTasks,让ionic项目启动的时候自动执行gulp任务:

  1. {
  2.   "name": "hello",
  3.   "app_id": "",
  4.   "gulpStartupTasks": [
  5.     "sass",
  6.     "templatecache",
  7.     "ng_annotate",
  8.     "useref",
  9.     "watch"
  10.   ]
  11. }

另外,如果是在linux或mac系统下,还需要使用chmod +x 命令启用hooks目录下面的所有js文件的执行权限。

最后执行正确的ionic命令就会自动执行上述的过程并进行发布,比如ioinc build browser。如果发现Errors in file www/js/xxx.js的提示,则说明代码不太严格,出现了错误,根据提示对代码进行修改,直到执行成功为止。另外,在开发的时候执行ioinc serve browser命令的时候,也会自动监控文件的变化并执行gulp任务。

gulpfile.js文件内容如下:

  1.  var gulp = require('gulp');
  2.  var gutil = require('gulp-util');
  3.  var bower = require('bower');
  4.  var concat = require('gulp-concat');
  5.  var sass = require('gulp-sass');
  6.  var minifyCss = require('gulp-minify-css');
  7.  var rename = require('gulp-rename');
  8.  var sh = require('shelljs');
  9.  var templateCache = require('gulp-angular-templatecache');
  10.  var ngAnnotate = require('gulp-ng-annotate');
  11.  var useref = require('gulp-useref');
  12.  
  13.  var paths = {
  14.    sass: ['./scss/**/*.scss'],
  15.    templatecache: ['./www/templates/**/*.html'],
  16.    ng_annotate: ['./www/js/*.js'],
  17.    useref: ['./www/*.html']
  18.  };
  19.  
  20.  gulp.task('default', ['sass', 'templatecache', 'ng_annotate', 'useref']);
  21.  
  22.  gulp.task('sass', function(done) {
  23.    gulp.src('./scss/ionic.app.scss')
  24.      .pipe(sass())
  25.      .on('error', sass.logError)
  26.      .pipe(gulp.dest('./www/css/'))
  27.      .pipe(minifyCss({
  28.        keepSpecialComments: 0
  29.      }))
  30.      .pipe(rename({ extname: '.min.css' }))
  31.      .pipe(gulp.dest('./www/css/'))
  32.      .on('end', done);
  33.  });
  34.  
  35.  // 将模板进行合并
  36.  gulp.task('templatecache', function (done) {
  37.    gulp.src(paths.templatecache)
  38.      .pipe(templateCache({standalone:true, root: 'templates'}))
  39.      .pipe(gulp.dest('./www/js'))
  40.      .on('end', done);
  41.  });
  42.  
  43.  // angular注入验证
  44.  gulp.task('ng_annotate', function(done) {
  45.    gulp.src(paths.ng_annotate)
  46.      .pipe(ngAnnotate({single_quotes: true}))
  47.      .pipe(gulp.dest('./www/dist/dist_js/app'))
  48.      .on('end', done);
  49.  });
  50.  
  51.  // 压缩合并css/js文件
  52.  gulp.task('useref', function (done) {
  53.    gulp.src(paths.useref)
  54.      .pipe(useref())
  55.      .pipe(gulp.dest('./www/dist'))
  56.      .on('end', done);
  57.  });
  58.  
  59.  gulp.task('watch', function() {
  60.    gulp.watch(paths.sass, ['sass']);
  61.    gulp.watch(paths.templatecache, ['templatecache']);
  62.    gulp.watch(paths.ng_annotate, ['ng_annotate']);
  63.    gulp.watch(paths.useref, ['useref']);
  64.  });
  65.  
  66.  gulp.task('install', ['git-check'], function() {
  67.    return bower.commands.install()
  68.      .on('log', function(data) {
  69.        gutil.log('bower', gutil.colors.cyan(data.id), data.message);
  70.      });
  71.  });
  72.  
  73.  gulp.task('git-check', function(done) {
  74.    if (!sh.which('git')) {
  75.      console.log(
  76.        '  ' + gutil.colors.red('Git is not installed.'),
  77.        '\n  Git, the version control system, is required to download Ionic.',
  78.        '\n  Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.',
  79.        '\n  Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.'
  80.      );
  81.      process.exit(1);
  82.    }
  83.    done();
  84.  });

参考资料:
Production ready apps with Ionic Framework
如何制作一个发布版的ionic应用?
Quick tip: using gulp to customize the ‘serve’, ‘run’ and ‘build’ process for your Ionic framework apps
gulp自动化任务脚本在HybridApp开发中的使用
Can’t get Gulp to run: cannot find module ‘gulp-util’
gulp-useref

Webstorm中设置Ionic工程的sass watcher

这里以Websorm 11为例,其他版本或JetBrains的其他IDE都是类似的。首先在工程下面创建名为config.rb的compass配置文件,针对Ionic工程项目来说,配置文件的内容如下:

  1.  project_path = "www"
  2.  sass_dir = "."
  3.  sass_pathFile.expand_path("scss")
  4.  css_dir = "www/css"
  5.  javascript_dir = "www/js"
  6.  images_dir = 'www/img'

这里需要特别注意的是,不能直接将sass_path设置成scss,而是需要同时设置sass_dir和sass_path,并且sass_path设置成File.expand_path(“scss”)

接下来在File – Settings菜单中打开Settings对话框,首先打开Languages & Frameworks下面的Compass,选择Enable Compass support,将当前项目启用为支持compass,需要确定compass和config指向的文件都是正确的。
20160317141648

然后再依次选择 Tools 下面的 File Watchers,点击右上角的加号并选择Compass SCSS
20160317141931
设置如下:
20160317142219
Arguments的内容是compile -s compressed $ProjectFileDir$ $UnixSeparators($FilePath$)$,其中-s compressed 是compass内置的用于对输出的css进行压缩的参数,也可以在config.rb中进行设置。

保存所有设置之后在编辑scss文件的时候就会自动编译输出成css文件了。

参考资料:
Compile specific file
Using Compass in WebStorm
Using File Watchers

Maven项目无法获取Sonatype Nexus中SNAPSHOT的包

今天在一个Maven项目中调用某个包的SNAPSHOT版本,总是提示找不到该jar包,主仓库使用了局域网中的Sonatype Nexus服务器,项目pom.xml相关配置如下:

  1.  <project>
  2.    <repositories>
  3.      <repository>
  4.        <id>nexus</id>
  5.        <name>Local Nexus Repository</name>
  6.        <url>http://192.168.1.200:8080/content/groups/public/</url>
  7.        <releases>
  8.          <enabled>true</enabled>
  9.        </releases>
  10.        <snapshots>
  11.          <enabled>true</enabled>
  12.        </snapshots>
  13.      </repository>
  14.    </repositories>
  15.  
  16.    <dependencies>
  17.      <dependency>
  18.        <groupId>com.example.tools</groupId>
  19.        <artifactId>example-tools</artifactId>
  20.        <version>1.0.2-SNAPSHOT</version>
  21.      </dependency>
  22.    </dependencies>
  23.  </project>

配置文件中已经启用了snapshots,但maven在下载example-tools-1.0.2-SNAPSHOT.jar的时候总是出错,提示无法找到该包。

首先我登录Nexus服务器,在Repositories中检查Public Repositories下面Configuration已经将Releases和Snapshots(分别代表本地的正式版服务和测试版服务)放到了仓库中,如下图:
nexus-repository

突然想到Maven调用SNAPSHOT版本的时候首先获取maven-metadata.xml文件,然后根据文件中所定义的最后一个SNAPSHOT版本时间戳来决定使用服务器上的具体的哪个jar包,但是在本例中在浏览器中无法直接访问http://192.168.1.200:8080/content/groups/public/com/example/tools/example-tools/1.0.2-SNAPSHOT/maven-metadata.xml,直接显示404错误,按照Sonatype官方文档的指示依次排除各种情况,都没有发现,在最后面加上?describe参数,发现显示的错误是FILE NOT FOUND,但是服务器上这个文件是存在的。后来直接使用snapshots仓库进行访问,即直接访问http://192.168.1.200:8080/content/repositories/snapshots/com/example/tools/example-tools/1.0.2-SNAPSHOT/maven-metadata.xml是可以直接访问的。

但是为什么在Public Repositories和Public Snapshot Repositories中为什么都不能访问呢?后来在Sonatyp Nexus的讨论组中找到了答案,原来我使用的Nexus是1.4,版本太老了,无法识别Maven 3.x之后的这个metadata格式,因此程序就不在Public Repositories中进行公开了,因此需要将Nexus进行升级了。

按照官方的升级文档,可以从1.x升级到2.7.x,如果想升级到最新的2.11的话就需要再从2.7.x进行升级,但是2.7.x目前也足够使用了,于是决定将现在用的1.4升级到2.7.2。

Nexus2.5之后的版本开始使用JDK7了,所以需要先将服务器上的JDK升级到7.0,然后从http://www.sonatype.org/nexus/archived/下载Nexus 2.7.2的zip包,下载完成之后解压,将压缩包中的nexus-2.7.2-03程序复制原来的nexus server安装根目录下,然后修改nexus-2.7.2-03/conf/nexus.properties文件,新的配置文件比原来的要简洁明了很多,根据需要调整配置文件即可。首先执行原来的nexus服务卸载,然后再执行bin/nexus install安装新的nexus服务,再启动服务就可以了,非常简单,这样原来的仓库文件可以继续使用,而且是完全兼容的,根目录下的文件结构如下:
20160119164337

这里需要说明在Windows 2003下面运行 nexus console 命令之后会出现下面的错误:

wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    | 
jvm 1    | 2016-01-19 16:45:15 INFO  [WrapperListener_start_runner] - org.sonatype.nexus.bootstrap.jsw.JswLauncher - Starting with arguments: [./conf/jetty.xml]
jvm 1    | Unable to open process: 拒绝访问。 (0x5)
jvm 1    | 2016-01-19 16:45:15 ERROR [WrapperListener_start_runner] - org.sonatype.nexus.bootstrap.jsw.JswLauncher - Failed to start
jvm 1    | java.lang.NullPointerException: null
jvm 1    | 	at org.sonatype.nexus.bootstrap.jsw.JswLauncher.doStart(JswLauncher.java:53) ~[nexus-bootstrap-2.7.2-03.jar:2.7.2-03]
jvm 1    | 	at org.sonatype.nexus.bootstrap.jsw.WrapperListenerSupport.start(WrapperListenerSupport.java:37) ~[nexus-bootstrap-2.7.2-03.jar:2.7.2-03]
jvm 1    | 	at org.tanukisoftware.wrapper.WrapperManager$12.run(WrapperManager.java:2788) [wrapper-3.2.3.jar:3.2.3]
wrapper  | <-- Wrapper Stopped

这应该是java wrapper的一个bug,不需要理会,只要使用nexus install安装成windows服务然后在启动服务即可。

参考资料:
Maven: Why is the -SNAPSHOT suffix missing from artifact file name?
Troubleshooting Artifact Download Failures
Artifact present in snapshot repo, that repo is in a group, artifact not present in group
Nexus升级小记

兼容浏览器的iframe加载完成事件

在网页中点击某个按钮或链接下载文件的时候,从点击到文件开始下载的过程中有一个过程,如果没有对这个过程进行判断的话,用户会一直不停地进行点击,体验非常差。浏览器没有办法直接获取到下载文件的事件,一个讨巧的方法是可以使用iframe的onload事件,代码如下:

  1.  var url = "http://www.example.com/file.zip";
  2.  var iframe = document.createElement('iframe');
  3.  iframe.src = url;
  4.  iframe.style.display = 'none';
  5.  iframe.onload = function() {
  6.      console.debug('start downloading...');
  7.      document.body.removeAttribute(iframe);
  8.  }
  9.  document.body.appendChild(iframe);

这段代码在Firefox下面可以正常获取到onload的事件,但在Google Chrome和IE下面,如果iframe src属性的链接是一个网页的话,可以触发onload事件,但如果HTTP文件头中包含Content-disposition: attachment…即下载文件的链接的话,不会触发这个事件。浏览器对iframe具体的支持情况可见:Events to expect when dynamically loading iframes in javascript

这一点有点可惜,有人提出了一个利用Cookies的思路,即在服务器端捕获文件下载的进度,然后随时将进度写入到Cookies中,前台javascript轮询Cookies,从而得到文件是否开始进行下载。这种方案详细的可以参考:Detecting the File Download Dialog In the Browser,后台如果使用Spring的话可以通过过滤器来判断文件下载的情况,详细情况见 http://xiaoz5919.iteye.com/blog/1259561

但这种方案实施起来需要后台进行配合,前后台的代码都过于复杂,另外如果浏览器将Cookies功能关闭的话就会出错,后来无意中在Google Chrome下面运行这个Plunker代码,发现可以正常捕获iframe加载后的时间。原始代码是使用的AngularJS,代码如下:

  1.  var url = 'https://github.com/blueimp/jQuery-File-Upload/archive/master.zip';
  2.  var e = angular.element("<iframe id=export style='display:none'></iframe>");
  3.  e.attr('src', url);
  4.  e.load(function() {
  5.      console.debug('start downloading...');
  6.  });
  7.  
  8.  angular.element('body').append(e);

这里使用了element的load事件,即页面实际加载DOM元素的事件。但非常奇怪的是,如果我将上述代码中的url换成其他的下载文件之后依然无法激活这个事件。后来仔细分析对比了二者中的HTTP Response Header的差异,发现需要在服务器端增加下面两个HTTP头文件内容即可:

  1.  // 不让浏览器自动检测文件类型,说明资料:http://drops.wooyun.org/tips/1166
  2.  response.addHeader("X-Content-Type-Options", "nosniff");
  3.  // 提示浏览器不让其在frame或iframe中加载资源的文件内容 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/X-Frame-Options
  4.  response.addHeader("X-Frame-Options", "deny");

如果使用jQuery的话,javascript代码如下:

  1.  var url = 'https://github.com/blueimp/jQuery-File-Upload/archive/master.zip';
  2.  var e = $("<iframe id=export style='display:none'></iframe>");
  3.  e.attr('src', url);
  4.  e.load(function() {
  5.      console.debug('start downloading...');
  6.  });
  7.  
  8.  $('body').append(e);

PS(2017-5-4):最新版本Google Chrome(本地使用的是v58)开始如果将X-Frame-Options设置成deny的话会出现下面的异常信息:

Refused to display ‘https://github.com/blueimp/jQuery-File-Upload/archive/master.zip’ in a frame because it set ‘X-Frame-Options’ to ‘deny’.

并且下载的时候网络连接会出现中断。

参考 http://stackoverflow.com/a/30065720/1805493 所提供的思路,可以设置一个定时器检测readyState的状态,如果为complete或interactive的话说明文件加载完成,并且这种方法不需要在服务器端进行设置,代码如下:

  1.  var url = 'https://github.com/blueimp/jQuery-File-Upload/archive/master.zip';
  2.  var iframe = document.createElement('iframe');
  3.  iframe.src = url;
  4.  iframe.style.display = 'none';
  5.  document.body.appendChild(iframe);
  6.  var timer = setInterval(function() {
  7.      var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
  8.      if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
  9.          console.debug('start downloading...');
  10.          document.body.removeAttribute(iframe);
  11.          clearInterval(timer);
  12.          return;
  13.      }
  14.  }, 1000);

当然,需要在服务器端将X-Frame-Options的头信息去除。

参考资料:
‘load’ event not firing when iframe is loaded in Chrome

Maven install之后无法将YUICompressor插件压缩后的js/css文件放到输出文件夹中的解决方法

YUI Compressor Maven插件可以压缩/合并js或css文件,经常用在Maven项目中,但最近发现在wabapp中执行了 mvn install 命令进行发布之后,终端中显示插件已经执行了压缩的动作,但在输出文件夹或者war包中js和css文件都还是未压缩的原始文件。

项目./pom.xml文件相关内容如下:

  1.  <project>
  2.  
  3.    <!-- 定义全局的变量 -->
  4.    <properties>
  5.      <!-- 编译编码设定 -->
  6.      <build.source.encoding>UTF-8</build.source.encoding>
  7.      <!-- webapp source dir -->
  8.      <webapp.src.dir>${basedir}/src/main/webapp</webapp.src.dir>
  9.      <!-- 默认的js&css输出总目录 -->
  10.      <yuicompressor.output.dir>${project.build.directory}/${project.build.finalName}</yuicompressor.output.dir>
  11.    </properties>
  12.  
  13.    <build>
  14.      <plugins>
  15.          <plugin>
  16.            <groupId>net.alchim31.maven</groupId>
  17.            <artifactId>yuicompressor-maven-plugin</artifactId>
  18.            <version>1.1</version>
  19.            <!-- 对该插件整体的配置信息 -->
  20.            <configuration>
  21.              <!-- 设定编码 -->
  22.              <encoding>${build.source.encoding}</encoding>
  23.              <!-- 输出主目录 where to put the generate files -->
  24.              <outputDirectory>${yuicompressor.output.dir}</outputDirectory>
  25.              <!-- 待处理文件目录 directory where source files will be pulled from -->
  26.              <sourceDirectory>${webapp.src.dir}</sourceDirectory>
  27.              <webappDirectory>${yuicompressor.output.dir}</webappDirectory>
  28.              <!-- 无前缀名,默认为min- -->
  29.              <nosuffix>true</nosuffix>
  30.              <!-- 强制压缩 defalut is false -->
  31.              <force>true</force>
  32.              <!-- [js only] Minify only, do not obfuscate. -->
  33.              <nomunge>false</nomunge>
  34.              <jswarn>false</jswarn>
  35.              <failOnWarning>false</failOnWarning>
  36.              <!-- 将不需要压缩的js或css文件放在packs文件夹下 -->
  37.              <excludes>
  38.                <exclude>**/all.js</exclude>
  39.                <exclude>**/all.css</exclude>
  40.              </excludes>
  41.              <aggregations>
  42.                <aggregation>
  43.                  <!-- insert new line after each concatenation (default: false) -->
  44.                  <insertNewLine>true</insertNewLine>
  45.                </aggregation>
  46.              </aggregations>
  47.            </configuration>
  48.            <!-- js compressor -->
  49.            <executions>
  50.              <execution>
  51.                <id>jsfiles</id>
  52.                <!-- Maven 3.x之后必须指定phase,默认为process-resources -->
  53.                <phase>process-resources</phase>
  54.                <goals>
  55.                  <goal>compress</goal>
  56.                </goals>
  57.                <configuration>
  58.                  <aggregations>
  59.                    <aggregation>
  60.                      <!-- the ultimate javascript file that is served up to the browser -->
  61.                      <output>${webapp.src.dir}/scripts/all.js</output>
  62.                      <!--
  63.                        files to include, path relative to output's directory or
  64.                        absolute path
  65.                      -->
  66.                      <includes>
  67.                        <!-- 压缩的js列表 -->
  68.                        <include>scripts/a.js</include>
  69.                        <include>scripts/b.js</include>
  70.                      </includes>
  71.                    </aggregation>
  72.                  </aggregations>
  73.                </configuration>
  74.              </execution>
  75.              <!-- css -->
  76.              <execution>
  77.                <id>cssfiles</id>
  78.                <!-- Maven 3.x之后必须指定phase,默认为process-resources -->
  79.                <phase>process-resources</phase>
  80.                <goals>
  81.                  <goal>compress</goal>
  82.                </goals>
  83.                <configuration>
  84.                  <aggregations>
  85.                    <aggregation>
  86.                      <!-- the ultimate css file that is served up to the browser -->
  87.                      <output>${webapp.src.dir}/styles/all.css</output>
  88.                      <!--
  89.                        files to include, path relative to output's directory or
  90.                        absolute path
  91.                      -->
  92.                      <includes>
  93.                        <include>styles/a.css</include>
  94.                        <include>styles/b.css</include>
  95.                      </includes>
  96.                    </aggregation>
  97.                  </aggregations>
  98.                </configuration>
  99.              </execution>
  100.            </executions>
  101.          </plugin>
  102.         
  103.        <!-- war -->
  104.        <plugin>
  105.          <groupId>org.apache.maven.plugins</groupId>
  106.          <artifactId>maven-war-plugin</artifactId>
  107.          <version>2.6</version>
  108.          <configuration>
  109.            <warSourceDirectory>${webapp.src.dir}</warSourceDirectory>
  110.            <encoding>${build.source.encoding}</encoding>
  111.          </configuration>
  112.        </plugin>
  113.  
  114.      </plugins>
  115.    </build>
  116.  </project>

这样执行 mvn install 命令之后发现虽然执行了压缩任务,但是在目标目录下和war包中的js和css文件都是未经过压缩的文件。经过分析应该是执行顺序的问题,即yuicompress是在process-resources阶段首先执行的,然后是war命令拷贝原始的资源文件到目标目录,这个时候就将前面压缩过的文件覆盖了。

于是将yuicompressor-maven-plugin中execution的phase修改为install,再执行 mvn install 命令之后发现最终目标目录中的js和css文件已经是压缩过的了,但是war包里面的还是未压缩的。顺便说一句,在maven 2.x不需要显示地指定phase的,3.x之后必须指定。将phase修改为install是将压缩的执行顺序放到最后阶段,也就是说复制资源文件在前,压缩在后,但在war包生成之后,因此war包中的js和css文件是未经过压缩的。

最终将yuicompressor-maven-plugin中execution的phase依然设置为process-resources,然后在maven-war-plugin中将js和css文件排除掉即可,即maven-war-plugin的配置修如下:

  1.  <!-- war -->
  2.  <plugin>
  3.  <groupId>org.apache.maven.plugins</groupId>
  4.  <artifactId>maven-war-plugin</artifactId>
  5.  <version>2.6</version>
  6.  <configuration>
  7.    <warSourceDirectory>${webapp.src.dir}</warSourceDirectory>
  8.    <encoding>${build.source.encoding}</encoding>
  9.    <warSourceExcludes>**/*.js,**/*.css</warSourceExcludes>
  10.  </configuration>
  11.  </plugin>

参考资料:
yuicompressor maven plugin and maven-war-plugin
How to exclude files from being copied, by maven, into the exploded war

使用Maven Release插件发布项目

上一篇文章《使用Maven插件修改工程版本号》中提到修改maven工程的版本号,最终没有一个完美的解决方案,其实最终修改好版本号之后还是需要使用deploy命令发布到私有Maven服务器上面的,Maven Release插件可以自动执行整个的发布过程,主要包括:将当前的SNAPSHOT版本号修改为正式版,然后在SCM服务器(SVN或git)上打一个该版本的tag,编译程序并自动生成jar包、source源码包和javadoc文档包然后发布到指定的maven服务器上,最后将当前的版本号增加为新的版本号并修改为SNAPSHOT,这样就相当于一个自动构建的流程了。

不过为了能够正常实现上面的流程,还需要注意几个地方:
1. 需要在本地安装一个scm客户端,这里使用了svn客户端,windows下推荐下载Subversion for Windows,需要跟项目存放的svn server的版本一致,在http://sourceforge.net/projects/win32svn/files/中列出了各种版本的客户端。安装之后需要确定在命令行下面可以正常执行svn命令。

2. 确保可以执行mvn deploy命令,需要在maven的配置文件中设置发布服务器端的用户名和密码,并且其id需要与项目pom文件中定义的distributionManagement下面的repository(正式版)和snapshotRepository(测试版)中的id一致。

3. 在父工程的pom.xml文件中定义scm信息及配置release plugin插件:
./pom.xml中的关键信息

  1.  <project>
  2.      <!-- SCM信息 -->
  3.      <scm>
  4.          <!-- 只读的scm连接 -->
  5.          <connection>scm:svn:http://192.168.1.200/svn/framework/trunk</connection>
  6.          <!-- 可写的scm连接 -->
  7.          <developerConnection>scm:svn:http://192.168.1.99/200/framework/trunk</developerConnection>
  8.          <url>http://192.168.1.99/svn/framework/trunk</url>
  9.      </scm>
  10.  
  11.      <!-- 设定团队Maven服务器  -->
  12.      <distributionManagement>
  13.          <repository>
  14.              <id>nexus-releases</id>
  15.              <name>Local Nexus Repository</name>
  16.              <url>http://192.168.1.200:8080/content/repositories/releases</url>
  17.          </repository>
  18.          <snapshotRepository>
  19.              <id>nexus-snapshots</id>
  20.              <name>Local Nexus Repository</name>
  21.              <url>http://192.168.1.200:8080/content/repositories/snapshots</url>
  22.          </snapshotRepository>
  23.      </distributionManagement>
  24.  
  25.      <!-- 编译配置 -->
  26.      <build>
  27.          <!-- 插件 -->
  28.          <plugins>
  29.              <!-- 发布插件 -->
  30.              <plugin>
  31.                  <groupId>org.apache.maven.plugins</groupId>
  32.                  <artifactId>maven-release-plugin</artifactId>
  33.                  <version>2.5.3</version>
  34.                  <configuration>
  35.                      <preparationGoals>package deploy</preparationGoals>
  36.                      <autoVersionSubmodules>true</autoVersionSubmodules>
  37.                      <!--  项目打 tag 的 scm 路径配置信息 -->
  38.                      <tagBase>http://192.168.1.99/svn/lingsee.framework/tags</tagBase>
  39.                      <!-- 不使用默认的profile -->
  40.                      <useReleaseProfile>false</useReleaseProfile>
  41.                      <!--  在发布时不检查是否提交 svn 的文件过滤配置 -->
  42.                      <checkModificationExcludes>
  43.                          <checkModificationExclude>.project</checkModificationExclude>
  44.                          <checkModificationExclude>.settings</checkModificationExclude>
  45.                          <checkModificationExclude>.classpath</checkModificationExclude>
  46.                          <checkModificationExclude>**\.project</checkModificationExclude>
  47.                          <checkModificationExclude>**\.settings</checkModificationExclude>
  48.                          <checkModificationExclude>**\.classpath</checkModificationExclude>
  49.                      </checkModificationExcludes>
  50.                  </configuration>
  51.              </plugin>
  52.          </plugins>
  53.      </build>
  54.  </project>

设置好之后可以执行mvn命令,命令列表如下:

release:clean 清除一些插件生成的相关文件
release:prepare 准备发布,相当于发布前的准备。此命令会首先去去掉版本号中的SNAPSHOT标志符,在svn服务器生成一个指定版本的tag,编译并打包项目
release:perform 正式发布提交
release:rollback 回滚,如果prepare的过程中出现了错误可以执行此命令回滚prepare的操作。有两点需要注意:一是在svn服务器上创建的tag无法删除;二是如果执行了release:clean命令的话,无法进行回滚

默认情况下Release插件会将源码和javadoc进行打包,如果想自己控制的话需要在插件的configuration中设置useReleaseProfile为false,这样跟直接使用mvn deploy进行项目发布的情况一致了。

参考资料:
Maven_Release_Plugin配置
Maven多模块项目每日构建
Maven Release Plugin deployment of sources.jar and javadoc.jar

使用Maven插件修改工程版本号

当Maven下面的子模块比较多的时候,每次修改工程的版本号都是一件非常的痛苦的事情,因为子模块都引用了顶级父模块的pom,所以虽然在父模块中定义了工程的版本号,但每个子模块中要显示地指定父模块的版本号,否则无法找到父模块的pom。举例如下:
./pom.xml

  1.  <project>
  2.      <groupId>com.example.framework</groupId>
  3.      <artifactId>lingseeframework</artifactId>
  4.      <packaging>pom</packaging>
  5.      <version>1.0.2-SNAPSHOT</version>
  6.  
  7.      <properties>
  8.          <framework.version>3.0.2-SNAPSHOT</framework.version>
  9.      </properties>
  10.  
  11.      <modules>
  12.          <module>common</module>
  13.      </modules>
  14.  
  15.      <dependencyManagement>
  16.          <dependencies>
  17.              <dependency>
  18.                  <groupId>com.example.framework</groupId>
  19.                  <artifactId>common</artifactId>
  20.                  <version>${framework.version}</version>
  21.              </dependency>
  22.  
  23.          </dependencies>
  24.      </dependencyManagement>
  25.  </project>

./common/pom.xml

  1.  <project>
  2.      <parent>
  3.          <groupId>com.example.framework</groupId>
  4.          <artifactId>framework</artifactId>
  5.          <version>1.0.2-SNAPSHOT</version>
  6.          <relativePath>..</relativePath>
  7.      </parent>
  8.      <artifactId>common</artifactId>
  9.  </project>

Maven Release Plugin中有一个release:update-versions 指令可以自动更新maven工程及子工程的版本号,并且也会自动更新framework.version属性的值,但唯一有问题的是其版本号只能以-SNAPSHOT结尾,即使使用developmentVersion参数指定了不带SNAPSHOT结尾的版本号也不行,如使用

mvn release:update-versions -DautoVersionSubmodules=false -DdevelopmentVersion=1.0.3

命令执行之后,所有的版本号都变成了1.0.3-SNAPSHOT,而不是1.0.3。不过有人hack了代码,在release:update-versions中增加了一个releaseVersion参数,可以设定正式版的版本号,项目地址为:https://github.com/ge0ffrey/maven-release,可以下载下来进行编译使用。

另外还有一个专门用来修改项目版本号的Maven插件:Versions Maven Plugin,使用起来也非常方便,只要在父工程目录下执行下面的命令即可:

mvn versions:set -DnewVersion=1.0.3

后面的版本名称不限于SNAPSHOT类型,可以任意设置,但这个插件最大的问题就是无法自动修改framework.version的属性的值。

另外,在查阅资料的过程中在statck overflow上发现了有人利用Maven AntRun Plugin设置了一个可以交互输入版本号的方式,即创建了一个profile,代码如下:

  1.  <project>
  2.          <profile>
  3.              <id>change-version</id>
  4.              <build>
  5.                  <defaultGoal>validate</defaultGoal>
  6.                  <plugins>
  7.                      <plugin>
  8.                          <groupId>org.apache.maven.plugins</groupId>
  9.                          <artifactId>maven-antrun-plugin</artifactId>
  10.                          <version>1.7</version>
  11.                          <dependencies>
  12.                          <!-- Using org.apache.ant:ant:1.8.4 to avoid https://issues.apache.org/bugzilla/show_bug.cgi?id=51161 -->
  13.                              <dependency>
  14.                                  <groupId>org.apache.ant</groupId>
  15.                                  <artifactId>ant</artifactId>
  16.                                  <version>1.8.4</version>
  17.                              </dependency>
  18.                              <dependency>
  19.                                  <groupId>org.codehaus.plexus</groupId>
  20.                                  <artifactId>plexus-utils</artifactId>
  21.                                  <version>1.5.15</version>
  22.                              </dependency>
  23.                          </dependencies>
  24.                          <executions>
  25.                              <execution>
  26.                                  <id>catch-new-version</id>
  27.                                  <goals>
  28.                                      <goal>run</goal>
  29.                                  </goals>
  30.                                  <phase>validate</phase>
  31.                                  <configuration>
  32.                                      <target>
  33.                                          <!-- http://ant.apache.org/manual/Tasks/input.html -->
  34.                                          <input message="请输入新的版本(当前版本:${project.version})" addproperty="new-user-version" defaultvalue="${project.version}" />
  35.                                      </target>
  36.                                      <!-- 将ant中的属性应用到maven中 -->
  37.                                      <exportAntProperties>true</exportAntProperties>
  38.                                  </configuration>
  39.                              </execution>
  40.                          </executions>
  41.                      </plugin>
  42.                      <plugin>
  43.                          <groupId>org.codehaus.mojo</groupId>
  44.                          <artifactId>versions-maven-plugin</artifactId>
  45.                          <version>2.2</version>
  46.                          <executions>
  47.                              <execution>
  48.                                  <id>set-new-version</id>
  49.                                  <goals>
  50.                                      <goal>set</goal>
  51.                                  </goals>
  52.                                  <phase>validate</phase>
  53.                                  <configuration>
  54.                                      <generateBackupPoms>false</generateBackupPoms>
  55.                                      <newVersion>${new-user-version}</newVersion>
  56.                                  </configuration>
  57.                              </execution>
  58.                          </executions>
  59.                      </plugin>
  60.                  </plugins>
  61.              </build>
  62.          </profile>
  63.      </profiles>
  64.  </project>

使用的时候指定profile为change-version即可,为了不使每个子模块都出现input对话框,在执行mvn命令的时候加上 -N 参数。但这种方式依然无法修改framework.version属性的值,如果在antrun中将new-user-version修改为framework.version或者给其赋值的话都无法成功,也就是无法覆盖已经存在的maven的属性值。

综合来说,两种方式都无法完美地实现三个要求,即自动修改父工程及所有子工程的的版本号、版本号不限于SNAPSHOT和自动修改framework.version的属性,现在看来只有重新编译ge0ffrey修改的maven release插件才能满足要求。

参考资料:
MRELEASE-699 release:update-versions should support -DreleaseVersion too
Is there a way to capture user input in maven and assign it to a maven propertty
Input Task

php-fpm+nginx环境下只显示The page you are looking for is temporarily unavailable无PHP错误信息的解决办法

今天在升级wordpress的时候,过程中突然出现“The page you are looking for is temporarily unavailable”错误,但在php、php-fpm和nginx的日志中均没有发现php的错误信息,将php.ini中的display_errors打开也没有php错误出现,在网站根目录下面写了个phpinfo可以正常运行,重启php依然如故,但重启了nginx之后网站访问就正常了。

原来wordpress升级的过程中因为比较耗时,造成php-cgi的进程过多,使用

  1. netstat -anpo | grep "php-cgi" | wc -l

命令查看结果输出0,表示PHP FastCGI的进程数太大了,修改nginx conf目录下的scgi_params文件,将默认的 scgi_param SCGI 1; 修改为 scgi_param SCGI 5;。也可以在nginx.conf中将fastcgi的超时时间设置长一些,比如:

http {
  fastcgi_connect_timeout 300;
  fastcgi_send_timeout 300;
  fastcgi_read_timeout 300;
  fastcgi_buffer_size 64k;
  fastcgi_buffers 4 64k;
}

最后一定要重启php和nginx。

参考资料:
解决nginx的The page you are looking for is temporarily unavailable错误办法