Vue 里 $mount 方法的作用

关于 Vue 的 $mount 方法,一问一答。

今天由同学问道:“vue文档里说如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。这个方法在项目中有什么用呢,是一个组件可以挂载到多个dom上就这一个作用吗?”

我的答案如下:

有些时候你创建 vm 的时候 DOM 可能还没准备好,或者写 vm 的时候不确定它要挂在哪个 DOM 上,这个时候就有用了。

当项目大的时候,很多情况是开发的时候估计不到的。比如 A 类里面想使用一个 B 类,但是 B 类有 10 个子类,对应不同场景,你写 A 类的时候根本不知道产品中会使用哪个 B 的子类,所以就要想办法把控制权交出去,这就叫“控制反转”——即控制权从开发者的手上,交到使用者的手上。

我觉得 $mount 这里也是类似的,就是你写了一个 Vue 类,但是不能控制使用场景。所以就要有一个 $mount 方法,由使用者控制,绑到目标 DOM 上。

他又问:“懂了,还有一个问题是一个组件能不能利用$mount属性挂载到多个dom上,我刚刚试了一下是可以渲染,这种用法常见吗??”

我答:

我没这么做过,不过我认为是合理的。

我觉得这个跟 Vue 的定位有关。Vue 和 React、Angular 不一样,后者是大公司企业级产品。Vue 其实很想取代 jQuery,所以这方面其实跟 jq 的插件是很相似的。

Webpack 笔记

记录一些 webpack 的使用经验。我是渐进式使用 webpack 的,一开始只用它打包 js。但是现在开始会把越来越多的依赖交给它管理,所以记一篇笔记。这里不会教怎么使用,而是结合开发经验,总结一些特定需求的解决方案。

我使用的版本:3.5.1

我是渐进式使用 webpack 的,一开始只用它打包 js。但是现在开始会把越来越多的依赖交给它管理,所以记一篇笔记。这篇不是教程,而是结合开发经验,总结一些特定需求的解决方案。

继续阅读“Webpack 笔记”

Vue 笔记:事件

记一些 Vue 的笔记。本篇均与事件处理相关。

onload

需要侦听 load

<template>
  <img src="/path/to/img" @load="onLoad">
</template>

<script>
export default {
  methods: {
    onLoad(event) {
      // do something
    }
  }
}
</script>

drop

需要阻止 dragover 的默认行为,不然不触发。

<template>
  <div class="drop-area" @dragover.prevent="onDragOver" @drop="onDrop">
</template>

<script>
export default {
  methods: {
    onDragOver(event) {
      // 此时仍然可以处理
    },
    onDrop(event) {
      console.log(event.dataTransfer);
    }
  }
}
</script>

JavaScript 异步开发全攻略

把之前写的《JavaScript 异步开发全攻略》更新了内容,然后放在 Gitbook 上,https://meathill.gitbooks.io/javascript-async-tutorial/content/ 欢迎阅读分享,因为我会不时维护,请关注,star。

之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

JavaScript 异步开发全攻略

Node.js 8 中的 util.promisify

Node.js 8 内建了“Promise 化”功能,可以把之前回调模式的方法,很简单的封装成支持 Promise 的方法,只需要调用 util.promisify(原函数) 即可。对于非 Node.js 标准风格的函数,也提供自定义转换函数的功能。

Node.js 8 于上个月月底正式发布,带来了很多新特性。其中比较值得注意的,便有 util.promisify() 这个方法。

如果你已经很熟悉 Promise,请继续往下看。如果你还不熟悉 Promise,可以先跳过去看下下章:Promise 介绍

util.promisify()

虽然 Promise 已经普及,但是 Node.js 里仍然有大量依赖回调的异步函数,如果我们把每个函数都封装一遍,那真是齁麻烦齁麻烦的,比齁还麻烦。

所以 Node.js 8 就提供了 util.promisify() 这个方法,方便我们把原来的异步回调方法改成支持 Promise 的方法,接下来,想继续 .then().then().then() 搞队列,还是 await 就看实际需要了。

我们看下范例,让读取目录文件状态的 fs.stat 支持 Promise:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
stat('.')
  .then((stats) => {
    // Do something with `stats`
  })
  .catch((error) => {
    // Handle the error.
  });

怎么样,很简单吧?按照文档的说法,只要符合 Node.js 的回调风格,所有函数都可以这样转换。也就是说,只要满足下面两个条件,无论是不是原生方法,都可以:

  1. 最后一个参数是回调函数
  2. 回调函数的参数为 (err, result),前面是可能的错误,后面是正常的结果

结合 Await/Async 使用

同样是上面的例子,如果想要结合 Await/Async,可以这样使用:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
async function readStats(dir) {
  try {
    let stats = await stat(dir);
    // Do something with `stats`
  } catch (err) { // Handle the error.
    console.log(err);
  }
}
readStats('.');

自定义 Promise 化处理函数

那如果现有的使用回调的函数不符合这个风格,还能用 util.promisify() 么?答案也是肯定的。我们只要给函数增加一个属性 util.promisify.custom,指定一个函数作为 Promise 化处理函数,即可。请看下面的代码:

const util = require('util');

// 这就是要处理的使用回调的函数
function doSomething(foo, callback) { 
  // ...
}

// 给它增加一个方法,用来在 Promise 化时调用
doSomething[util.promisify.custom] = function(foo) { 
  // 自定义生成 Promise 的逻辑
  return getPromiseSomehow(); 
};

const promisified = util.promisify(doSomething);
console.log(promisified === doSomething[util.promisify.custom]);
// prints 'true'

如此一来,任何时候我们对目标函数 doSomething 进行 Promise 化处理,都会得到之前定义的函数。运行它,就会按照我们设计的特定逻辑返回 Promise 实例。

我们就可以升级以前所有的异步回调函数了。

继续阅读“Node.js 8 中的 util.promisify”

小试 Element UI

经过一个小项目的试用,我觉得 Element UI 1.2.5 设计上过分激进,质量上存在一些影响开发进度的 bug,所以不推荐使用。

不确定写多长,写先结论吧:暂时不推荐使用。原因如下:

  1. 影响使用的小 Bug 有点多
  2. 需要重新学习一门语言


接下来详述。

从前司离职之后,我开始更新技术栈。离开惯用的 Backbone,考虑再三,投入 Vue 怀抱。选择 Vue,而不是竞品 Angular、React 有三个理由:

  1. 文档友好,社区活跃。
  2. 模块拆分的很好,学习曲线平缓。
  3. 基于标准化技术,可以最大限度的避免浪费。

不过实操之后发现,Vue 与我惯用的 Bootstrap 有些冲突,主要在于:

  1. Bootstrap 对过渡效果和切换的操作依赖于样式,比如 .active.in。Vue 在处理模板时会把当前样式先缓存起来,然后根据数据增删绑定的样式。此时就可能出问题,tab 页切不动或者动画突然打断之类的。
  2. Bootstrap 会广播特定的事件,这些事件无法被 Vue 捕获,只能在 mounted() 的钩子里手工绑定。

于是我觉得,既然根基(jQuery)变了,最好把整条线都更新了吧。左右看了看,准备先试下 Element UI。这是饿了么推出的基于 Vue2.0 的组件库,目测组件齐全,文档详细,而且直接以 2.0 为基础,符合我追新的想法。

实际用了之后……唉……有点……遗憾。项目地址

首先,Element UI 把所有组件都封装了,包括布局,比如 <el-row><el-col>,我觉得这样太过了。从现实经验来看,布局元素几乎不可能够用,别人总要补充一些。封装的元素我不太知道最终生成的代码是什么样的,也就不好操作,总不能审查元素一个一个看吧?——对了,Element 的文档里缺少样式列表,也是个问题。

封装的另一个问题,所有元素都要通过后期渲染,总让我感觉不舒服。以及,我几乎无论干什么都要查文档,几乎没法直接动手,这和我选择 Vue 的初衷是相违背的。

接下来,小 Bug,有点多。除去布局和提示之类,我只用到3个组件:<el-button><el-table><el-pagination>,结果就遇到4个 bug,浪费很多时间去调试,有两个我给他们开了 issue,还有两个懒得弄了。这里列一下吧:

  1. <el-button :loading="scope.row.fetching"> 无法把 loading 绑定到数据的 .fetching 属性上
  2. <el-pagination> 设置 total 不更新视图
  3. <el-pagination> 更新 total 之后再次广播 current-change 事件,导致重复刷新
  4. <el-table> 里每行的 ref 属性没法正确生成数组

2017-04-04 更新:

经过 issue 沟通,我的理解的确有些问题,

  1. Vue 绑定属性的时候,如果该属性层次较深,比如 a.b.c,就不会修改它的 getter/setter,于是会失去响应式更新的特性;同时也不会报错。
  2. <el-table> 不是通过 v-for 渲染的,自然 ref 的表现也就正常了。

可能别的组件很健壮吧,我运气不好。

总之,我觉得就目前这个版本,1.2.5,来看,Element UI 还没到让人放心用并且用得好的程度。

下一次我可能会选别家的再试下,或者继续用 Bootstrap 然后自己拼些小组件出来——我这次就是想找个有 loading 的 button 才找 UI 库的。


啊,最后,还是感谢 Element UI 团队,感谢饿了么。希望你们再接再厉,相信将来这套库会更好。

使用 Webpack 时需避免循环引用

使用 Webpack 等打包工具时,需要注意尽量避免循环引用,即 A 引用 B,B 也引用 A,不然可能出现难以预料的问题。

开发日历控件的时候,对方变更了一下需求,基本上将最终产品分成两个:

  1. 选择连续时间段
  2. 选择多个不连续时间

那么我们知道,对于这种大部分功能一致,只有若干函数逻辑不同的产品,最合适的就是状态模式。于是很自然的,我就拿“2”作为标准模式,“1”作为新模式,将其重构成父类和子类,大概关系如下:

// 父类
// DatePicker.js

import RangeDatePicker from './RangeDatePicker';

class DatePicker {
  ....
  static getInstance(el, options) {
    if (options.scattered) {
      return new DatePicker(el, options);
    } else {
      return new RangeDatePicker(el, options);
    }
}


// 子类
// RangeDatePicker.js

import DatePicker from './DatePicker';

class RangeDatePicker extends DatePicker {

}

因为这个类只有两个成员,所以我把工厂方法 .getInstance() 放到了父类里面,通过判断参数确定应该返回哪一类实例。代码写完,测试的时候却报错:

Super expression must either be null or a function, not undefined

这个意思很明显,被继承的父类不能未定义。然则 DatePicker 明明是定义了的,只是验证两个类文件的话,均未出现任何语法错误。

遇事不决先 Google,还真找到很多结果,不过大多数都和 React.Component 有关,翻了半天一无所获,只好自力更生。打开 Chrome 开发者工具,勾上“Pause on Exceptions”,观察发生异常时的状况,一遍又一遍,我渐渐意识到,发生这个错误的时候,DatePicker 还未能在 webpack 的环境中完成注册。问题找到了!

与其它编译类语言不同,JS 是动态语言,所有 JS 代码都是放到统一的环境里跑的,类的代码如此,import 也是如此。所以对于其他语言,比如 ActionScript、Java,循环引用,即 A 引用 B,B 也引用 A,是没问题的,因为类的代码都会编译到执行文件,执行的时候,都已经在环境中;而 JS 是边执行边置入环境,具体到我这里,在将父类 DatePicker 放入环境时,会先 import 子类 RangeDatePicker 的代码,而子类又会要求 import 父类的代码,父类的代码正在引入中,于是便产生了问题。

想明白这点,后面就好办了,直接创建一个工厂类,把工厂方法放到里面执行,问题便解决了:

import DatePicker from './DatePicker';
import RangeDatePicker from './RangeDatePicker';

export default {
  createDatePicker(el, options) {
    if (options.scattered) {
      return new DatePicker(el, options);
    } else {
      return new RangeDatePicker(el, options);
    }
  }
}

PS:当年写依赖注入和包管理工具的时候,就卡在这个地方,怎么都想不通,于是一直也没写完。没想到这些个浓眉大眼有头发的,也都这么不负责任,这种问题都不解决就搞出来让全世界人用了。

七牛 Node SDK 会导致 Electron 启动新实例

在 Electron 的 main process 里调用七牛云 SDK,会启动一个新实例,原本的上传会暂停。这个时候关掉新实例,上传会继续。当前文件上传完成后,下个文件又会启动一个新实例。如此反复。

如题,暂时不确定是哪里导致的。

总之,在 Electron 的 main process 里调用七牛云 SDK qiniu.io.putFile(),会启动一个新实例,原本的上传会暂停。这个时候关掉新实例,上传会继续。当前文件上传完成后,下个文件又会启动一个新实例。如此反复。

文档中的代码如下:

qiniu.io.putFile(uptoken, key, localFile, extra, function(err, ret) {
      if(!err) {
        // 上传成功, 处理返回值
        console.log(ret.hash, ret.key, ret.persistentId);       
      } else {
        // 上传失败, 处理返回代码
        console.log(err);
      }
});

已开 issue

估计要等春节后修复了。

暂时可以用社区版 SDK 先顶上。

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