react-native 项目 run-android 报无法安装应用

1
2
3
4
5
6
7
8
9
z at ZsMBP in ~/Projects/ReactXP/reactxp/samples/TodoList (master●)
$ react-native run-android
Scanning 740 folders for symlinks in /Users/z/Projects/ReactXP/reactxp/samples/TodoList/node_modules (7ms)
JS server already running.
Building and installing the app on the device (cd android && ./gradlew installDebug)...
Could not install the app on the device, read the error above for details.
Make sure you have an Android emulator running or a device connected and have
set up your Android development environment:
https://facebook.github.io/react-native/docs/android-setup.html

这个错误的解释废话太多,还是错的, 其实手动运行下命令cd android && ./gradlew installDebug就会发现是./android/.gradlew 这个文件没有执行权限导致的,运行一下

1
chmod +x ./android/.gradlew

就可以了

webpack2 -从0搭建一个完整的前端开发技术栈(6)

  1. 1. ¶react-hot-loader
  2. 2. ¶react 测试

之前的框架是 full-stack 的,最近需要帮以前工作室的学妹弄一个简单的 h5 页面时才发现,自己居然手头没有一套靠谱的纯粹配置,如果从其他项目中剥离出来又实在提不起兴趣,于是下了官方的 create-react-app, 发现各种问题,比如

  1. 不支持 react 热更新,只支持热刷新
  2. 与 sass 整合很不方便,sass 直接编译到源码目录,干扰视线。。
  3. babel,webpack 也不支持扩展;

无法适应大型项目,和我理解中的只是用来写 demo, 或者简单页面的脚手架差不多。临时看了一下 styled-components, 发现这货比我想象的要高级,支持嵌套语法,auto-prefixer, 既然是 css-in-js,那么动态复用就完全不用担心了,更让我惊艳的是工具链的支持,在 atom 中,基本上必装的 language-babel 插件已经默认支持了,在 js 中也可以享受到强大的 css 提示。

于是在 create-react-app + styled-components 的组合下写起展示类页面来体验也挺不错的。

不过,没有一套自己常用的纯前端的脚手架总感觉心里不太安稳,而且转眼过去大半年了,webpack2, react-hot-loader v3 已经是正式版了,个人也积累了一些不错的库,于是便打算再折腾一个最新的脚手架。

作为一名合格的「前端配置工程师」,每半年~一年升级一次配置才能紧跟前端的潮流。。

先放代码:https://github.com/zaaack/react-starter.git

这里主要记录一下猜到的坑。。

react-hot-loader

毫无悬念,虽然之前有过成功配置 react-hot-loader 的经历,而且那时也是 v3,只不是是 v3-beta, 现在是 v3 正式版,但是还是在这里卡住了大部分时间…

其实官方的两个 demo 已经是非常友好的了,

webpack2 官方也有一个教程,不过需要注意的是 babel-preset-es2015 的参数配置有点老了,具体可以参考上面的 react-hot-loader-minimal-boilerplate

不过 react-hot-loader 还是太 hack了点,很多地方都需要加 patch,不仅仅是在 entry 中,babel presets 中也要加。开始时就是忘了加 babel presets 中的 react-hot-loader/babel, 导致虽然 log 正常,但是却完全没反应。没有报错提示无意是最难调试的了,只能去翻源码以及在源码中加 log。

发现之后加上去了,但是还是老样子,到底是哪里有问题呢?经过反复耐心测试才发现居然是少了 babel-polyfill, 而且还不能使用 babel-transform-runtime, 虽然之前的配置中 可以通过对 transform-runtime 加 {polyfill:false} 参数使之共存,但是这个 trick 貌似在最新版中不能用了,不过没关系,可以通过 NODE_ENV 区分啊,大不了 开发环境就不加嘛。

可是这样还是不行,不过还好在反复实践中发现
虽然 webpack 中配置了 babel, 但是还是会去读取 .bablerc 文件,在翻 babel-loader 的源码时在文档中成功找到了相关依据,改了一个配置就搞定了。。此时 react-hot-loader 所有的坑都已填满,可以放心食用了。。

react 测试

其实之前一直都没写过 react 测试,老实说除了一些基础 node 库之外,我还没在业务代码中写过测试呢,更不用说 UI 测试了。。所以这次就干脆把测试也加上了。React 自带了 react-dom/test-utils,不过 API 太过冗长,Facebook 自己都不用, 参考了下阮老师的这篇, 发现很多 demo 都跑不起来了,倒不仅仅是因为接口的变动,更坑的是 bug…bug.

比如 renderIntoDocument 这个方法,最新的 react 已经去掉了返回值,就算看源码自己用 callback 实现也不行,因为我就尝试了,callback 也没有返回值了!连之前 callback 可以用的 this也变成 null 了。。所以要么直接用 document,要么用 airbnb 的 enzyme, 考虑到写测试是一件很痛苦的事情, 而且
enzyme 的接口其实和 jquery 很类似,目测挺方便的,本着不给自己添麻烦原则,就只能牺牲一下硬盘空间了。。我的 node_modules 目录已经快撑爆了吧。。

然而,随便给 demo 代码写了点 测试,想点击一个 react-router 的 Link 实现跳转,死活没反应。在 js-dom 的官方 readme 上看见了巨大的 “不支持跳转” 的说明,好吧,这很好解决,加个 NODE_ENV=test 换成 createMemoryHistory 不就可以了,issue 中也有类似的解决方案。可以还不行!!!一样的没有任何报错,任何反应(可见良好的错误提示是多么重要)。无聊之下又开始翻源码,打 log, 可以确定事件是触发了的,于是又去看 react-router 的 Link 组件,打 log, 终于发现是 因为 Link 中为了确保是鼠标左键点击做了 event.button === 0 的判断,而 jsdom 中的 MouseEvent.button === undefined, 并且是个老 issue 了,果断再来一发

至于 Facebook 官方的测试框架 jest 强大的 snapshot 功能,可喜的发现 AVA 已经 内置了,使用非常方便,就不细说了,直接看代码和官方文档吧。

恩,至此所有坑已填完,可以上传 repo 了。是的,这篇文章其实不是什么分享,只是单纯的吐槽,解决这些坑其实几乎毫无价值,半年之后,可能有些修了,可能又会有新的 bug。。人生苦短,且行且珍惜,所以半年更新一下配置,又可以保持一下自己的折腾能力了。。

最后,再来安利一发自己的 react 脚手架 react-starter

以后有时间再整个 koa 后台的 starter,graphql 的,ssr 的。。。

mac 下代理开发环境总结

  1. 1. ¶1. 代理服务器
  2. 2. ¶2. 手机代理软件

在工作中开发,代理技术对于开发环境是不可避免的,小到入职时配置 VPN,再到开发环境中通过代理使用线上 API,通过代理拦截 App 请求,合理的代理应用对于开发来说经常会有事半功倍的作用。这里记录一下我在工作中代理的运用。

1. 代理服务器

我这里用的使用的是由阿里巴巴开源的基于 nodejs 的代理工具— anyproxy,优点是支持 https,使用 nodejs 作为配置文件,学习成本低,缺点是配置 API 看起来太老,连 promise 都不支持,大公司的开源项目不知道什么时候会挂刚上了官网,4.0 beta 已经开始支持 promise 了。

2. 手机代理软件

Android 5.0 以下可以使用 DroidProxy 直接修改 wifi 代理配置,但是 5.0 以上更新了安全策略,只能用开 VPN 的了。我这里用的是虽然界面很丑,但是好得能用的 Drony. (以上 APK 可以去 https://apkpure.com搜索下载)。虽然占用了 VPN,不过一般电脑上各种代理和 VPN 都是开启状态,因此手机上还是能走电脑上的代理和 VPN 的。

Drony 和 connectbot 的 ssh 端口转发冲突,导致无法代理 dns 访问电脑 vpn 的内网,localproxy chains 相关配置未测试…

rem 布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* /js/lib/flexrem.js
*
*/
$inital-rem-size: 100px !default;
$design-width: 375px !default;
html {
font-size: $inital-rem-size / $design-width * 320px;
}
@each $width in (320px, 360px, 400px, 414px, 480px, 500px, 650px) {
@media only screen and (min-width: #{$width}) {
html {
font-size: $inital-rem-size / $design-width * $width;
}
}
}
@function px2rem($px) {
@if unitless($px) { // 不管带不带单位都可以
$px: $px * 1px;
}
@return $px / $inital-rem-size * 1rem;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/**
*
* 根据屏幕宽度自动设置html的fontSize, 使rem的大小自适应
* usage:
* dui.flexrem({
* //设计稿宽度
* designWidth: <number>, initial: 375
* //rem 初始像素值,initial: 100
* //html的fontSize = (opts.initRem/opts.designWidth*winWidth)+'px'
* initRem: <number>,
* //用来计算的最大的 winWidth, initial: 640
* maxWinWidth: <number>
* //用来计算的最小的 winWidth, initial: 320
* minWinWidth: <number>,
* })
*
* 由于android webview 偶尔会在 document.load 之后识别 viewport 导致 window.innerWidth 发生变化, 这段 js 需要在 window.onload 后运行,因此还需要配合 css 使加载前页面也能正确显示
*
*
* $inital-rem-size: 100px; // 默认值
* $design-width: 375px;// 默认值
* @import '/css/card/widgets/_flexrem.scss';
*
* width: @px2rem(12)rem; => 12/100 = 0.12rem
*
*/
!(function(win, doc) {
var dui = window.dui || (window.dui = {}),
timer = null,
firstCalled = true,
defaults = {
designWidth: 375,
initRem: 100,
maxWinWidth: 640,
minWinWidth: 320,
},
evts = 'onorientationchange' in win ? ['orientationchange', 'resize'] : ['resize'];
if (dui.flexrem !== undefined) {
return console.error('flexrem.js shouldn\'t be called twice!!')
}
/**
* auto set html's fontSize to make rem flexable
* @param {object} opts
* {
* //设计稿宽度
* designWidth: <number>, initial: 375
* //rem 初始像素值,initial: 100
* //html的fontSize = (opts.initRem/opts.designWidth*winWidth)+'px'
* initRem: <number>,
* //用来计算的最大的 winWidth, initial: 960
* maxWinWidth: <number>
* }
* @return
*/
dui.flexrem = function(opts) {
if(!firstCalled) console.error('flexrem() can only be called once!');
firstCalled =false
var options = Object.assign({}, defaults, opts)
for (var i = 0; i < evts.length; i++) {
win.addEventListener(evts[i], function() {
clearTimeout(timer);
timer = setTimeout(setFontSize, 300);
}, false);
}
win.addEventListener("pageshow", function(e) {
if (e.persisted) {
clearTimeout(timer);
timer = setTimeout(setFontSize, 300);
}
}, false);
// 初始化
setFontSize();
function setFontSize() {
var winWidth = window.innerWidth || window.clientWidth;
winWidth = Math.min(winWidth, options.maxWinWidth)
winWidth = Math.max(winWidth, options.minWinWidth)
var size = (winWidth / options.designWidth) * options.initRem;
doc.documentElement.style.fontSize = size + 'px';
}
}
function toType(obj) {
return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
if (typeof Object.assign != 'function') {
Object.assign = function (target, varArgs) { // .length of function is 2
'use strict';
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
}(window, document));

graphql学习笔记-从0搭建一个完整的前端开发技术栈(5)

  1. 1. GraphQL 简介
    1. 1.1. ¶查询和修改
      1. 1.1.1. ¶查询字段(Fields)
      2. 1.1.2. ¶参数(Arguments)
      3. 1.1.3. ¶别名(Alias)
      4. 1.1.4. ¶片段(Fragments)
      5. 1.1.5. ¶变量(Variables)
      6. 1.1.6. ¶操作名(Operation name)
      7. 1.1.7. ¶指令(Directives)
      8. 1.1.8. ¶行内片段
    2. 1.2. ¶schema和type 的定义方式
    3. 1.3. ¶更多参考

本来打算放弃这个系列了,但是前端发展太快,这个系列估计可以“永无止境”了。。

最近看了下 graphql, 了解了下当前的相关技术栈的整合情况,发现 relay 已经超出了我对查询一个 api 的耐心,而开源组织的 apollo client 以及一些简化的第三方 graphql 客户端都非常好用,大部分应用其实没有必要那么动辄 70多kb 的高性能客户端数据框架,平时 restful 的 api 没有字段按需返回, 缓存只用 http 协议自带的,性能也是完全满足需求的,而 relay 个人感觉为了优化而牺牲开发体验是得不偿失的,一个查询就需要新建一个文件,就算是服务端 mvc 一个文件也能写好几个查询啊,过去一行代码搞定的事情现在搞成一个文件,大大增加了前端项目的复杂度,简直不能忍。

还好 graphql 是一种协议,这意味着只要有足够多的人关注就会有层出不穷的社区项目,比如 apollo 就是其中优秀的一种,支持 redux, ssr(server-side-rendering), 可以搭配 react/angular/vanilla 使用。

不过既然是写博客记录,还是从头开始写起吧,先来看看 GraphQL。

GraphQL 简介

简单的说,GraphQL 是一种传输协议,最大的价值在于可以通过查询语言按需返回数据,避免了 restful 传输过多不必要的字段的问题,但是由于每次请求返回的数据不一样,因此复用接口时无法使用 http 自带的缓存协议,如etag,因此需要客户端框架来处理缓存,如 relay, apollo。 这篇文章主要内容其实就是官网的 learn 部分中第一篇的中文化,毕竟官网真心写的太好了。。主要的目的是为了后面服务端和客户端配置 GraphQL 提供一些基础。

使用 GraphQL 时,大致流程是先需要服务端定义好各种类型,然后通过 “root” 对象类型来进行查询,官网上称作entry point(入口点),然后客户端通过发送查询语句来得到相应的数据。这里先对查询语句有大致的了解,后续可以在实践中逐步学习服务端类型的定义。

查询和修改

直接看 Graphql 可能会感到难以接受,因为不知道背后的实现原理,对实际使用时并没有明确的概念。实际上GraphQL 只是一种协议而已,等到实现的时候其实并没有想像的那么神奇,服务度的每一个可以查询的接口都需要通过 schema 去定义出来。。

查询字段(Fields)

查询是和 json 非常类似的,只不过“只有key”部分而已,因为 value 部分等着由服务端提供啊。。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var query = `{
hero {
name
}
}`
// ==>
var response = {
"data": {
"hero": {
"name": "R2-D2"
}
}
}

可以看到服务端可以返回的数据结构和查询语句非常类似。

参数(Arguments)

查询语句中可以传递参数,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var query = `{
human(id: "1000") {
name
height
unitHeight: height(unit: FOOT)
}
}`
// =>
var response = {
"data": {
"human": {
"name": "Luke Skywalker",
"height": 1.72,
"unitHeight": 5.6430448
}
}
}

在查询语句里参数可以通过argName: value or argName: $variableName的方式传递值或者变量;
在服务端的 schema 中略有差异,通过argName: typeName = defaultValue的方式设置参数类型和默认值。

在这个例子中还使用了下面介绍的别名来得到同一字段的不同形式的值。

通过参数可以很容易的实现分页,客户端框架一般会提供包装的分页方法来进行优化。

别名(Alias)

别名用于在同一层级获取有着相同结构的不同数据,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var query = `{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}`
// =>
var response = {
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}

片段(Fragments)

片段就是一个类型的一部分字段的集合,可以通过es6展开运算符来展开所有字段到当前的查询结构中,极大的方便了复用(感觉和类型放在一起更好理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var query = `{
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}`
// =>
var response = {
"data": {
"empireHero": {
"name": "Luke Skywalker"
},
"jediHero": {
"name": "R2-D2"
}
}
}

变量(Variables)

虽然有了参数,但是查询语句只是一个字符串而已,还是需要有能传入动态参数的地方(虽然es6模版字符串也能做到),因此引入了变量的概念。
引入变量需要三个步骤

  1. 替换查询语句中的静态值为$variableName
  2. 声明$variableName为查询语句中可接受的变量
  3. variableName: value 通过另一个分开的变量键值传递给服务端,通常是 json 形式的 get 参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var query = `query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}`
var variables = {
"episode": "JEDI"
}
// =>
var response = {
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker"
},
{
"name": "Han Solo"
},
{
"name": "Leia Organa"
}
]
}
}
}

操作名(Operation name)

上面例子中的查询语句很多是一种省略形式,我们来看一个完整的:

1
2
3
4
5
6
7
8
var query = `query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}`

其中query是操作类型,是可以自定义的,比如定义成mutation, subscription,这里只是约定俗成的名字. HeroNameAndFriends是查询名字,服务端需要有对应的配置才能查询.

在 graphql 中,服务端的定义大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var schema = `
type Query {
hero(episode: $episode) {
name
friends {
name
}
# ...
}
}
type Mutation {
addHero(hero: $hero)
}
schema {
query: Query
mutation: Mutation
}
`

通过 schema 定义的字段在 GraphQL 中叫做 entry point (入口点), 其实和其他的对象类型差不多,唯一的区别在于可以通过query xxx { ... }mutation xxx { ... }的方式用来得到内部的查询定义。

指令(Directives)

通过指令可以通过变量修改查询的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var query = `
query Hero($episode: Episode, $withFriends: Boolean!) {
hero(episode: $episode) {
name
friends @include(if: $withFriends) {
name
}
}
}
`
var variables = {
"episode": "JEDI",
"withFriends": false
}
// =>
var response = {
"data": {
"hero": {
"name": "R2-D2"
}
}
}

自带有两种指令,服务端也可以自定义更多:

@include(if: Boolean) 参数为true则包含该字段
@skip(if: Boolean) 参数为true则忽略该字段

行内片段

片段的简写形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var query = `
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
`

schema和type 的定义方式

参考:http://graphql.org/learn/schema

更多参考

http://graphql.org/learn

web-selection

  1. 1. ¶js操作光标
    1. 1.1. ¶textarea 或者 input的情况
    2. 1.2. ¶div+contenteditable 的情况(其实就是和不能输入的普通节点的情况是一样的)

js操作光标

textarea 或者 input的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function getCursorPosition (ctrl) {//获取光标位置函数
var pos = 0; // IE Support
if (document.selection) {
ctrl.focus ();
var Sel = document.selection.createRange ();
Sel.moveStart ('character', -ctrl.value.length);
pos = Sel.text.length;
}
// Firefox support
else if (selectionStart in ctrl)
pos = ctrl.selectionStart;
return (pos);
}
function setCursorPosition(ctrl, pos){//设置光标位置函数
if (ctrl.setSelectionRange) {
ctrl.focus();
ctrl.setSelectionRange(pos,pos);
}
else if (ctrl.createTextRange) {
var range = ctrl.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
}

div+contenteditable 的情况(其实就是和不能输入的普通节点的情况是一样的)

1
2
3
4
5
6
7
8
9
10
11
var sel = window.getSelection()
// 获取全部range
var ranges = []
for(var i = 0; i < sel.rangeCount; i++) {
ranges[i] = sel.getRangeAt(i);
}
// 获取第一个range,一般也只有一个
var range = sel.getRangeAt(0)

range的属性:

1
2
3
4
startContainer: Node # 开始位置所在节点
startOffset: number # 开始位置在所在节点的偏移
endContainer: Node # 结束位置在所在节点的偏移
endOffset: number # 结束位置在所在节点的偏移

需要注意的是这里的节点是Node而不是Element, 区别请看:
http://www.cnblogs.com/jscode/archive/2012/09/04/2670819.html
https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType?redirectlocale=en-US&redirectslug=nodeType

简单讲Element就是我们平时所看到的标签节点,它是Node的子集,Node还包括

Constant Value Description
Node.ELEMENT_NODE 1 一个dom节点如 c or <div>.
Node.TEXT_NODE 3 文本节点
Node.PROCESSING_INSTRUCTION_NODE 7 Axml文档解析指示节点 如 <?xml-stylesheet … ?> .
Node.COMMENT_NODE 8 注释节点,如<!-- 我是注释~~ -->.
Node.DOCUMENT_NODE 9 document节点, <html>.
Node.DOCUMENT_TYPE_NODE 10 DocumentType 节点,如 HTML5 文档的 <!DOCTYPE html>.
Node.DOCUMENT_FRAGMENT_NODE 11 DocumentFragment 节点.

参考:
https://developer.mozilla.org/en-US/docs/Web/API/Range

react笔记

  1. 1. ¶react笔记
    1. 1.1. ¶focus是使用属性还是方法
    2. 1.2. ¶react实现模态窗

react笔记

最近项目中用到了react,记录些自己的思考。。

focus是使用属性还是方法

原先使用属性,但是问题是focus、value等都是可能由用户改变的,而属性是不可变的,父级组件不知道组件的状态发生了改变,则此时的focus还是原来的,下次更新属性时可能会“误”改变focus状态。解决办法主要是通过onBlur/onFocus实现双向绑定,保证父级组件的状态中的focus和子级组件的实际状态保持一致。。这样一不小心就多了很多方法。。

事实上我们看dom的设计,虽然dom的api大部分都是由属性构成的,但是focus恰好就是一个函数。。于是最终我的解决办法是给组件增加一个focus函数,调用时通过设置ref标签,通过this.refs.myInput.focus()来改变组件的focus状态…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Input = React.createClass({
render: function () {
return <input type="text" ref="input" />
},
focus: function () {
this.refs.input.focus()
}
})
var Parent = React.createClass({
render: function () {
return (
<div>
<button onClick={ this.onClick }>聚焦</button>
<Input ref="myInput" />
</div>
)
},
onClick: function () {
this.refs.myInput.focus()
}
})

react实现模态窗

实现模态窗有个坑就是挂载点一般需要在body最后append一个节点,这样可以实现全局位置。但是react的组件是分层封装的,如何实现一个子组件挂载在别的挂载点呢?看下已有的优秀实践,发现一般都是用 unstable_renderSubtreeIntoContainer 这个不稳定的接口实现的(如:https://github.com/reactjs/react-modal) 这个方法和render的区别是增加了一个组件实例的参数用来指定父级组件。借此原理写了一个工具函数,用来对组件进行一个包装,返回一个包装组件,不仅可以将属性传递给真实组件,还可以调用真实组件的成员方法。开始时由于项目太老还通过render+setProps模拟,不过现在升级了还是用新方法吧。。写这个函数的个中曲折(踩坑)一言难述,看注释吧。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 在body最后面单独append一个挂载点来挂载组件,用来制作弹窗等。
* @param {React.Component} Component
* @param {object} config
* @return {React.Component}
*/
function makeSingletonDom(Component, config) {
config = {
containerId: config && config.containerId || null
}
var SingletonDomWrapper
var Cls = SingletonDomWrapper = React.createClass({
componentWillMount: function () {
if (!Cls.isRealMount) {
Cls.container = document.createElement('div')
if (config.containerId) Cls.container.id = config.containerId
document.body.appendChild(Cls.container)
Cls.isRealMount = true
}
Cls.mountNum = Cls.mountNum || 0
Cls.mountNum++
this.renderSubtree()
},
componentDidMount: function() {
this.copyMethods()
},
renderSubtree: function (props, callback) {
var that = this
ReactDOM.unstable_renderSubtreeIntoContainer(
this, <Component {...this.props}/>, Cls.container, function () {
Cls.instance = this
callback && callback.call(this)
})
},
copyMethods: function () {
if (this.real) return
var instance = Cls.instance
var that = this
this.real = instance
for (var key in instance) {
if (instance.hasOwnProperty(key)
&& (instance[key] instanceof Function)
&& ! (key in this)) {
this[key] = (function (instanceMethod) {
return function() {
var args = arguments
// 避免调用组件方法和设置组件属性同时进行时发生冲突导致组件设置属性失败
// 记得文档上说render函数可能以后会做成异步的,因此需要通过callback
// 得到返回的实例,因此试着加上setTimeout就成功了。后来想了想,可能是因为
// dom api毕竟都是同步的,所谓的异步实现可能也是用setTimeout,因此这里用
// setTimeout实际上是在上次setTimeout之后顺序执行的。。做了下测试也符合猜想,见下图
setTimeout(function () {
that.renderSubtree(that.props, function () {
instanceMethod.apply(instance, [].slice.call(args))
})
})
}
}(instance[key]))
}
}
},
componentWillReceiveProps: function(nextProps) {
this.renderSubtree(nextProps)
},
render: function () {
return false
},
componentWillUnmount: function() {
Cls.mountNum--
if (Cls.mountNum === 0) {
this.clearContainer()
}
},
clearContainer: function () {
ReactDOM.unmountComponentAtNode(Cls.container)
document.body.removeChild(Cls.container)
Cls.instance = null
Cls.container = null
}
})
return SingletonDomWrapper
}

setTimeout测试:

setTimeout测试

electron笔记

  1. 1. ¶electron 使用笔记总结
    1. 1.1. ¶渲染进程和后台进程
    2. 1.2. ¶踩坑记录
      1. 1.2.1. ¶编译native 包
      2. 1.2.2. ¶mac 下隐藏窗口不会自动将焦点交还给原先窗口
      3. 1.2.3. ¶mac下不能使用复制黏贴快捷键

electron 使用笔记总结

渲染进程和后台进程

渲染进程就是浏览器进程,后台进程就是命令行调用时的没有界面的进程。

后台进程通过 new BrowserWindow 可以创建一个渲染进程,由于两者是不同进程,因此不能共享内存,只能通过ipc传输序列化数据。

踩坑记录

前段时间在做 ELaunch, 踩了不少坑,记录一下。

编译native 包

npm install 时会对包含native代码的包进行编译,但是是根据本地安装的node版本编译的,而不是electron中包含的node, 因此electron需要重新编译一次:

官方文档: http://electron.atom.io/docs/tutorial/using-native-node-modules/

1
2
3
4
5
6
7
npm install --save-dev electron-rebuild
# Every time you run "npm install", run this:
./node_modules/.bin/electron-rebuild
# On Windows if you have trouble, try:
.\node_modules\.bin\electron-rebuild.cmd

mac 下隐藏窗口不会自动将焦点交还给原先窗口

类似iterm2/alfred快捷键呼出的效果 这是mac下独有的bug,windows和linux下默认就是需要的。

在mac下其实已经有解决办法了, 就是

1
2
3
4
5
6
7
8
## 隐藏
mainWin.hide()
app.hide && app.hide()
## 显示
mainWin.show()
app.show && app.show()

issue: https://github.com/electron/electron/issues/6669

mac下不能使用复制黏贴快捷键

需要手动创建菜单,添加快捷键

issue: https://github.com/moose-team/friends/issues/123#issuecomment-106843964

ios上safari div滚动时默认未开启平滑滚动

在ios上将一个div设置成可以滚动,它的效果和安卓以及电脑上都是不一样的,感觉没有那种弹性,原因是ios safari在非window的滚动都默认没有开启平滑滚动,需要给滚动的div设置一个属性就可以了:

1
2
3
.scroll-div{
-webkit-overflow-scrolling: touch;
}