Electron + Vuex 导致视图无法自动刷新的问题

Electron 中,使用 .getGlobal(‘data’) 取出来的对象,Vue 没法直接加上 getter/setter,会导致响应式失效。这个时候可以通过深度 Object.assign({}, source) 来解决。

前几天开发 Meart 的过程中,碰到一个棘手的问题:

  1. 修改数据,视图不变
  2. 切换视图,再回到原来的视图,数据刷新
  3. 通过 Vue devtool 查看 Vuex,有数据提交
  4. 点击最新提交点,或者 “commit all”,之后所有操作正常

因为我对 Electron 和 Vue 都不是很了解,所以这个问题困扰我很长时间。我尝试了各种办法,包括 .splice()let data = this.data; this.data = data;,但是都没有效果。后来实在没办法了跑到 Vue 论坛里发帖求助

没想到很快就得到回复,对方虽然没用过 Electron,但答案看起来方向是对的:remote.getGlobal('var') 得到的对象不是一般意义上的对象,Vue 没有办法给它加上 getter/setter,所以无法实现响应式。

我尝试了一下果然如此,用 require('data.json') 替换从主线程中取值,就一切问题都解决了。或者把 .getGlobal('data') 取出来的值,深度 Object.assign({}, source) 一下也可以。

看来有必要补一下 ES5 里面 Object 新增函数的知识了。

node.js 复制文件最快的方法

偶尔需要复制个文件,不用太复杂的库,简单写个函数即可。

Subway

最快的方法

var fs = require('fs');

fs.createReadStream('test.log').pipe(fs.createWriteStream('newLog.log'));

改进使其可以接受回调

function copy(source, to, callback) {
  var read = fs.createReadStream(source);
  source.on('error', function (err) {
    done(err);
  }

  var write = fs.createWriteStream(to);
  write.on('error', function (err) {
    done(err);
  }
  write.on('finish', function () {
    done();
  }
  read.pipe(write);

  function done(err) {
    if (err) {
      throw err;
    }
    callback();
  }
}

继续添加 Promise,并且用 ES2015 的写法

function copy(source, to) {
  return new Promise( resolve => {
    let read = fs.createReadStream(source);
    source.on('error', err => {
      throw err;
    });

    let write = fs.createWriteStream(to);
    write.on('error', err => {
      throw err;
    });
    write.on('finish', ()=> {
      resolve();
    });

    read.pipe(write);
  });
}

// example
copy('a.txt', 'b.txt')
  .then( () => {
    console.log('copy success');
  })
  .catch( err => {
    console.log('copy error: ', err);
  });

来源:StackOverflow

Fastest way to copy file in node.js

bower.json 指定版本

bower 支持 dependencies 里用 “#+版本号” 指定特定版本的依赖。

为了使用部署脚本部署代码而不是登录到服务器上用命令行,我需要尽量简化部署步骤。

相对来说,composer 会简单一些,因为只要提交 composer.lock 文件然后 composer install --no-dev 即可。经测试 bower install --production 它是不会自己去更新了安装新依赖的,只会从缓存里安装。所以需要想个办法。

Value must be a valid semver range, a Git URL, or a URL (inc. tarball and zipball).

按照官网上的描述,只允许版本号、Git URL 或者安装包 URL。然则我试了一下,写 “#+版本号” 也是可以的,所以我们现在是这样:

{
  "dependencies": {
    "tiger-prawn": "#68fa36ceb88c1b0e9b9472b6901d957f424e50b7"
  }
}

Bootstrap 4 alpha 4 发布,v3 停止维护

业内广泛使用的前端框架发布了最新的 “v4 alpha 4” 版本,同时宣布关闭 v3 所有的问题和 pull request,以后将更专注于推进 v4 的发布。

业内广泛使用的前端框架 Bootstrap 发布了最新的 “v4 alpha 4” 版本,同时宣布关闭 v3 所有的问题和 pull request,以后将更专注于推进 v4 的发布。

作者是这么说的:

Bootstrap 3 都发布3年多了;4 也发布一年多了。老子还有全职工作要干,真心没工夫维护两个版本。

感谢开源作者为我们贡献了如此优秀的框架,帮助我们节省大量的时间。v4 我也试用过,的确很好,有很多好用的新组件,整个 CSS 都用 Sass 重写了,结构也简洁很多。

奉上 v4 文档地址:

Bootstrap 4 alpha 4 文档


来源

Bootstrap 4 Alpha 4

用 SVG 画五角星

使用 SVG 绘制一个五角星。

先上代码:

See the Pen draw a star by Meathill (@meathill) on CodePen.


话说前两日想起来填坑,需要用五角星做示例。正好前几天买了本 SVG 的书,就想试一下。这篇文章记录其中几个要点。

多边形 <polygon>

在 SVG 中,画多边形要用到 <polygon>,它有一个属性 points 可以用来定义各个点的座标。有了它我们就可以连点成线做出想要的多边形了。

创建 SVG 元素

以前直接使用 document.createElement() 就能创建出想要的元素,然而 SVG 并非普通的文档,所以要小小的变动一下,使用命名空间来创建合适的元素:

var svg = document.getElementById('svg')
  , ns = svg.namespaceURI
  , star = document.createElementNS(ns, 'polygon');

先取到页面中写好的 SVG 节点,然后取得它的命名空间,最后借由它创建需要的元素。

使用 JS 操作元素

SVG 元素都是 SVGElement,对他们进行操作是需要一点技巧的。

比如,向对变形增加顶点就需要

var point = svg.createSVGPoint();
point.x = 10;
point.y = 10;
star.points.appendItem(point);

总结

整体说来 SVG 操作还是蛮简单的,将来可能简单的绘图都直接用代码写了。

使用 Phantomjs 导出 PDF

使用 Phantomjs 可以很方便的导出 PDF,并且几乎可以在导出前进行各种编辑调整,使其满足要求。

接到一个新需求,给用户导出电子协议,也就是 PDF 文档。因为我们后台使用 PHP,所以自然就去寻找 PHP 的解决方案。看了几个库,包括 Packagist 几万十几万下载的库,唉,不得不说,虽然 PHP 是世界上最好的语言,但是 PHP 开发者,审美水平真的,说好听点就是,没法看……

第二个问题是,PHP 很多都要“拼” PDF,一行一行,一个元素一个元素,组装起一个完整的文档。相当费力,而且样式不好控制。从 HTML 转换也有类似的问题。研究了一会儿觉得蛋越来越疼,唉,算了,还是换 Phantomjs 吧。

Phantomjs 是一个命令行 Webkit 工具,我们可以把它理解成不输出页面的浏览器,但它支持浏览器的各种功能,因为有 Webkit 嘛。所以渲染网页然后抓图就是小菜一碟了。

用 Phantomjs 输出 PDF 非常简单:

  • 首先,约定好宽高(为方便打印,我们的电子协议要分页,而且有页头页脚),完成页面模板。
  • 完成 Phantomjs 脚本。因为只用它生成文档,所以不需要 Web 服务。
  • 用 PHP 调用脚本,生成 PDF 文档,然后 readfile 给用户下载。

这样做的好处是我们随时可以预览效果,HTML 好读好改,PHP 替换其中的内容也很方便。而且代码非常简单,结合官方示例,很快就写出来了:

'use strict';

var page = require('webpage').create()
  , system = require('system')
  , args = system.args
  , url = args.length > 1 ? args[1] : 'http://www.dianjoy.com/'
  , filename = args.length > 2 ? args[2] : 'tmp';

page.viewportSize = {
  width: 800,
  height: 1100
};
url = decodeURIComponent(url);
page.open(url, function (status) {
  console.log(status);
  if (status === 'success') {
    page.render('/tmp/pdf/' + filename + '.pdf');
  }
  phantom.exit();
});

部署这段代码最大的问题反而是 GFW 导致 npm install phantomjs -g 失败,直接下载 zip 也不行(因为放在 Amazon S3 上)。于是继续给病魔加油,早日弄死方校长,及其它筑墙士。

第二个问题则是 PHP 执行脚本老不成功。只看文档很简单:

exec('/usr/local/phantomjs/bin/phantomjs pdf.js http://meathill.com/ meathill');

但实际上既没有生成文档,也没有任何返回,调试半天,我突然想起来前阵子用 Apktool 解析安装包的时候也遭遇过类似的问题,于是在末尾加上 2>&1 问题竟然就解决了。

Google 之,也不是很懂。回头再说吧。


其它参考:

shell_exec


图文无关。其实是一位大学友人今日喜得贵子,放张她的奇怪照片祝贺她。

Template was precompiled with an older version of Handlebars

使用本地高版本 Handlebars 替换 gulp-handlebars 里的底版本依赖是个好主意。

gulp-handlebars 对 Handlebars 的要求是 ^3.0.0,预编译器的版本是 6。Handlebars 4 之后升级预编译器到 7,所以如果使用最新版本 Handlebars,就会报 “Template was precompiled with an older version of Handlebars” 错误。

这个时候有几个解决方案,比如 这个页面 中提到的先删再装。不过经我实测,npm update 可能导致失效,还要重弄,太麻烦。所以最简单的,就是直接用本地的高版本覆盖依赖中的版本:

npm install handlebars --save-dev
rm -rf ./node_modules/gulp-handlebars/node_modules/handlebars
cp -r ./node_modules/handlebars ./node_modules/gulp-handlebars/node_modules

Gulp 中顺序执行任务

Gulp 顺序执行支持3中模式:callback、返回 stream、返回 Promise 对象。

书接上文,Chrome 插件中无法直接使用 Handlebars 处理模板。两种方案,一是利用沙箱,将 .eval() 放在独立的环境中执行,好处是其它跨域的操作也能这样处理,坏处是写起来麻烦。另一种则是利用 Handlebars 的“预编译”功能,将模板提前编译好,直接在代码中引用。好处是写起来更顺畅,并且从发布插件的角度来看,早晚都要这样做。

我决定选用后者,于是我需要把模板提取出来进行预编译,然后我准备写个 Gulp 任务来搞定这个。

从页面中提取模板不算太难,写个正则就好,这里提前约定,模板使用 <script type="x-handlebars-template"> 标签包裹。

gulp.task('template', function () {
  let promise = new Promise((resolve, reject) => {
    fs.readFile('popup.html', (err, content) => {
      if (err) reject(err);
      resolve(content);
    }
  });
  promise.then((content) => {
    content.replace(/<script([^>]*)>([\S\s]+?)<\/script>/g, (match, attr, template) {
      // 具体处理模板,略掉了
    });
  })
});

模板编译之后,还需要用 Webpack 打包才能使用,于是要增加 Webpack 的任务。这个比较简单,参考它的 文档 即可。不过很明显,必须等全部模板预编译完成才能打包。之前我已经试过用 run sequence 顺序执行任务,不过这里我用到 Promise,情况似乎有变。

稍微 Google 一下,得知要能顺序执行任务有三种选择:

使用 callback

gulp.task('task', function (callback) {
  setTimeout(function () {
    // 任务执行完成后,调用 callback
    callback();
  }, 5000);
});

返回 stream

这个好像是标准做法,也是我之前做的。

gulp.task('task', function (callback) {
   returen gulp.src('*.js')
     .pipe(uglify())
     .pipe(gulp.desc('dist/');
});

返回 Promise 对象

gulp.task('task', function () {
  return new Promise(resolve, reject) => {
    resolve();
  });
});

既然支持 Promise 那就好办了,在 .then() 前面加一个 return 就好。


完整代码

参考文章

Handling Sync Tasks with Gulp JS

在Chrome 扩展中使用 Handlebars

Chrome 扩展中无法直接使用 Handlebars,最好使用预编译功能在开发环境中将模板处理好然后直接使用。

Chrome 扩展可以访问到各种用户敏感数据,比如 cookie 之类,所以 Chrome 团队对它的限制非常严格(见 Using eval in Chrome Extensions. Safely.),比如常规环境下完全不能使用 evalnew Function()。这给我们使用 Handlebars 之类的模板工具带来不小的麻烦。

一个解决方案就是使用上文中介绍的 sandbox。将可能使用到 eval 的代码放到一个独立沙盒中,不让它访问到那些敏感信息;然后通过 postMessage API 与之进行数据通讯,完成模板生成工作。

我觉得这个方案不太理想。首先我不喜欢 <iframe>;其次每次渲染模板还要 post 来 post 去,渲染结果也是异步传递(这点暂时存疑)。

另一套方案,则是利用 Handlebars 的预编译功能,将模板在开发环境中编译成函数,在扩展中直接使用。这样做的好处也很明显,因为发布插件时也会先处理代码,这个时候将模板处理完,工作效率更高。

等写完再补充具体代码吧。

Backbone + ES2015 Class

在 ES2015 下使用 Backbone,需要一点点小技巧。

小型项目还是 Backbone 用起来比较舒心,所以写着写着又开始用它了。这次还用到 ES2015 和 Babel,于是疑问就来了:怎么在 ES2015 下使用 Backbone 呢?

普通用法

Backbone 自带 .extend() 方法,在早期的蛮荒岁月可以帮我们方便的创建各种子类。

var MyView = Backbone.View.extend({
  events: {

  },
  tagName: 'div',
  initialize: function () {

  }
});

以前这样确实很方便,代码也很好读。但是 ES2015 引入了新的 Class 规范,写出来是这样婶儿的:

class MyView extends Backbone.View {
  // 其实不能这样写
  tagName: 'div'
  events: {

  }
  // 上面这样写是不对的
  constructor() {

  }
}

这个时候问题来了。ES2015 不支持直接声明类属性,也就是上面代码中的 tagNameevents,是不能这样写的。因为它实际上只是重新包装了原型继承那一套东西,所以上面的代码实际上等效于:

MyView = function () {

}
MyView.prototype = new Backbone.View();
MyView.prototype.tagName = 'div';
MyView.prototype.events = {

};

这样的结果,不同实例间实际在共享方法和属性,包括 tagNameevents。如果是简单对象,比如字符串和数字,用 = 赋值还好;如果是复杂对象,比如数组,就很容易出问题。

Backbone 1.0 之前也会有这个问题,但是忘记从哪个版本开始就修复了。

新的用法

想要在 ES2015 中继续 Backbone 当然也是可以的。目前看来有三种方式:

  1. 把属性转化为方法。Backbone 对属性的处理都是委托给 _.result(),所以属性和方法的效果是一样的。
  2. 继续使用 MyClass.prototype.someProp = 'value';
  3. 将初始化对象放在构造函数中。

我个人比较喜欢第三种方法,因为它更接近之前的写法,而且结合 Babel、Webpack 等编译打包工具,也可以做到私有。

class MyView extends Backbone.View {
  constructor(init) {
    super(_.extend(init, {
      events: {

      },
      tageName: 'div'
    });
  }
}

未来,ES2016

ES2016 中引入了很重要的 Decorators 概念,顾名思义,它会显式的告诉运行时下面是些什么东西,那么运行时自然也就可以按照对应的规则去处理。

@props({
  tagName: 'div',
  events: {

  }
})
class MyView extends Backbone.View {
  constructor(init) {
    super(init);
  }
}

参考文章

本篇内容主要参考自以下两篇: