Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
泽鹿 committed Mar 9, 2018
1 parent 57ea753 commit 3de3a2a
Show file tree
Hide file tree
Showing 10 changed files with 2,583 additions and 5 deletions.
528 changes: 528 additions & 0 deletions 201709/2.md

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions 201709/3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# 跨页面通信的各种姿势

>作者简介:nekron 蚂蚁金服·数据体验技术团队
将跨页面通讯类比计算机进程间的通讯,其实方法无外乎那么几种,而web领域可以实现的技术方案主要是类似于以下两种原理:

* 获取句柄,定向通讯
* 共享内存,结合轮询或者事件通知来完成业务逻辑

由于第二种原理更利于解耦业务逻辑,具体的实现方案比较多样。以下是具体的实现方案,简单介绍下,权当科普:

## 一、获取句柄
### 具体方案

父页面通过`window.open(url, name)`方式打开的子页面可以获取句柄,然后通过postMessage完成通讯需求。

```js
// parent.html
const childPage = window.open('child.html', 'child')

childPage.onload = () => {
childPage.postMessage('hello', location.origin)
}

// child.html
window.onmessage = evt => {
// evt.data
}
```
### tips
1. 当指定`window.open`的第二个name参数时,再次调用`window.open('****', 'child')`会使之前已经打开的同name子页面刷新
2. 由于安全策略,异步请求之后再调用`window.open`会被浏览器阻止,不过可以通过句柄设置子页面的url即可实现类似效果

```
// 首先先开一个空白页
const tab = window.open('about:blank')
// 请求完成之后设置空白页的url
fetch(/* ajax */).then(() => {
tab.location.href = '****'
})
```


### 优劣
缺点是只能与自己打开的页面完成通讯,应用面相对较窄;但优点是在跨域场景中依然可以使用该方案。



## 二、localStorage
### 具体方案
设置共享区域的storage,storage会触发storage事件

```js
// A.html
localStorage.setItem('message', 'hello')

// B.html
window.onstorage = evt => {
// evt.key, evt.oldValue, evt.newValue
}
```

### tips
1. 触发写入操作的页面下的**storage listener**不会被触发
2. storage事件只有在发生改变的时候才会触发,即重复设置相同值不会触发listener
3. safari隐身模式下无法设置localStorage值

### 优劣
API简单直观,兼容性好,除了跨域场景下需要配合其他方案,无其他缺点

## 三、BroadcastChannel
### 具体方案
`localStorage`方案基本一致,额外需要初始化

```js
// A.html
const channel = new BroadcastChannel('tabs')
channel.onmessage = evt => {
// evt.data
}

// B.html
const channel = new BroadcastChannel('tabs')
channel.postMessage('hello')
```

### 优劣
`localStorage`方案没特别区别,都是同域、API简单,`BroadcastChannel`方案兼容性差些(chrome > 58),但比`localStorage`方案生命周期短(不会持久化),相对干净些。

## 四、SharedWorker
### 具体方案
`SharedWorker`本身并不是为了解决通讯需求的,它的设计初衷应该是类似总控,将一些通用逻辑放在SharedWorker中处理。不过因为也能实现通讯,所以一并写下:

```js
// A.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.onmessage = evt => {
// evt.data
}

// B.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.postMessage('hello')

// worker.js
const ports = []
onconnect = e => {
const port = e.ports[0]
ports.push(port)
port.onmessage = evt => {
ports.filter(v => v!== port) // 此处为了贴近其他方案的实现,剔除自己
.forEach(p => p.postMessage(evt.data))
}
}

```

### 优劣
相较于其他方案没有优势,此外,API复杂而且调试不方便。

## 五、Cookie
### 具体方案
一个古老的方案,有点`localStorage`的降级兼容版,我也是整理本文的时候才发现的,思路就是往`document.cookie`写入值,由于cookie的改变没有事件通知,所以只能采取轮询脏检查来实现业务逻辑。

方案比较丑陋,势必被淘汰的方案,贴一下原版思路地址,我就不写demo了。

[communication between browser windows (and tabs too) using cookies](https://stackoverflow.com/questions/4079280/javascript-communication-between-browser-tabs-windows/4079423)

### 优劣
相较于其他方案没有存在优势的地方,只能同域使用,而且污染cookie以后还额外增加AJAX的请求头内容。

## 六、Server
之前的方案都是前端自行实现,势必受到浏览器限制,比如无法做到跨浏览器的消息通讯,比如大部分方案都无法实现跨域通讯(需要增加额外的postMessage逻辑才能实现)。通过借助服务端,还有很多增强方案,也一并说下。

### 乞丐版
后端无开发量,前端定期保存,在tab被激活时重新获取保存的数据,可以通过校验hash之类的标记位来提升检查性能。

```js
window.onvisibilitychange = () => {
if (document.visibilityState === 'visible') {
// AJAX
}
}
```

### Server-sent Events / Websocket
项目规模小型的时候可以采取这类方案,后端自行维护连接,以及后续的推送行为。

#### SSE

```js
// 前端
const es = new EventSource('/notification')

es.onmessage = evt => {
// evt.data
}
es.addEventListener('close', () => {
es.close()
}, false)


// 后端,express为例
const clients = []

app.get('/notification', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
clients.push(res)
req.on('aborted', () => {
// 清理clients
})
})
app.get('/update', (req, res) => {
// 广播客户端新的数据
clients.forEach(client => {
client.write('data:hello\n\n')
setTimeout(() => {
client.write('event:close\ndata:close\n\n')
}, 500)
})
res.status(200).end()
})
```

#### Websocket
`socket.io``sockjs`例子比较多,略

### 消息队列
项目规模大型时,需要消息队列集群长时间维护长链接,在需要的时候进行广播。

提供该类服务的云服务商很多,或者寻找一些开源方案自建。

例如MQTT协议方案(阿里云就有提供),web客户端本质上也是websocket,需要集群同时支持ws和mqtt协议,示例如下:

```js
// 前端
// 客户端使用开源的Paho
// port会和mqtt协议通道不同
const client = new Paho.MQTT.Client(host, port, 'clientId')

client.onMessageArrived = message => {
// message. payloadString
}
client.connect({
onSuccess: () => {
client.subscribe('notification')
}
})
// 抑或,借助flash(虽然快要被淘汰了)进行mqtt协议连接并订阅相应的频道,flash再通过回调抛出消息

// 后端
// 根据服务商提供的Api接口调用频道广播接口
```
80 changes: 80 additions & 0 deletions 201710/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 5分钟前端国际化

> 作者简介 Kid 蚂蚁金服·数据体验技术团队
## 背景
需要国际化的react项目已经迭代了1年多,文件众多,包含了jsx和普通的js对象文件。粗略估计有几千个中文词条。本文先介绍了采用的国际化方案,然后给出了国际化的过程和一个自己开发的脚本[i18n-pick](https://github.com/ProtoTeam/i18n-pick),按照教程,可以帮助前端jsx项目5分钟快速国际化。

## 方案选择
先大体上介绍下我选择的国际化方案。国际化方案很多,我这里列举主要的几种:

* 编译期间转化:例如wepack的i18n-webpack-plugin,打包的时候对_('key')进行转义
* 运行期间转化:react-intl等,把中文词条写成intl.get()的方式,在运行时获取中文文案
* wordpress的getText方案:gettext是一个filter 钩子, 用来替换和本地化翻译文本, 替换 __()、_e()、_x()、_ex() 和 _n() 函数包含的文本

由于项目中我选择了antd作为视觉组件库。所以想和antd提供的官方的国际化方式保持统一。antd推荐的是react-intl,不过另一款类似的react-intl-universal也有不少人推荐,两者都比较成熟。所以我对两种进行了比较:

| | react-intl | react-intl-universal |
| :--- | :--- | :--- |
| 切换不刷新页面 |||
| js文件支持(重要) |||
| 名词单双数,默认值,html |||
| 无破坏性 | 劣(装饰器的代码实现会改变ref) ||

名词单双数,默认值,html这种功能两者都有。我这里就不多说了,具体的功能感兴趣的可以去看下API。比较关注的其实是js文件支持那块。react-intl只支持在jsx文件的内容中使用,但是由于项目配置化编程的缘故,很多中文是写在js对象中的。react-intl不支持在普通js对象中使用,很不方便。而且他的装饰器实现会改变组件的ref。他唯一的好处是他的切换不需要刷新页面,不过这种低频的操作刷新页面倒也无妨。

针对以上的原因,最终选择了react-intl-universal作为国际化方案。不过后来真实使用的时候,发现他提供的支持js对象的方式不是很好,于是还是直接采取了react-intl-universal的思想。简单的包装了下他们的依赖intl-messageformat~这里不详细描述了,他的api官网文档可以查到。

国际化方案选择完了之后,开始执行阶段。以上无论是选择哪种方案,编码时基本都要求一种特殊的形式。要么intl.get(),要么是文案前加上_#这种。对于已经迭代了很久的项目,这就涉及到了一项力气活。对中文文案进行提取以及替换。在这里就直接分享脚本[i18n-pick](https://github.com/ProtoTeam/i18n-pick),描述下整个的国际化过程了。

# 使用教程
主要分为3步,安装,扫描和提取,然后使用翻译工具来进行词条的翻译,具体步骤如下:
### 安装
`cnpm i i18n-pick`
cnpm用的淘宝镜像,会快一些。
### 扫描
`./node_modules/i18n-pick/bin/i18n-pick.js scan [path]`
命令最后的path选择你的代码目录,运行完成后会在项目根目录生成i18n-messages文件夹,包含jsx.text,text.text和zh-CH.json三个文件。具体实现是调用了babel的transformFileSync方法,在编译成语法树的时候,解析下面几种[babel-type](https://babeljs.io/docs/core-packages/babel-types/)

- JSXAttribute
- JSXText
- AssignmentExpression
- ObjectProperty
- ArrayExpression

这里的基本含括了所有的情况,如果有遗漏的,欢迎联系我。将解析的这几种的value与/[\u4e00-\u9fa5]/进行比对。将包含中文文案的文件名,行数,文案内容记录下来。JSX内的中文文案存到jsx.text,一般JS内的中文文案存到text.text。

分开存的原因是因为替换的时候,JSX内的文案需要加上大括号才行。

同时我会把提取出来的文案内容存到了zh-CH.json中。这里为了配合翻译工具atool-i10n的使用,json中的存储格式也是按照他的要求提供的。这里有个小tip,参见附录。

### 提取
`./node_modules/i18n-pick/bin/i18n-pick.js pick`
然后执行pick操作,就是将jsx.text,text.text文件的内容按行分析,对文件进行内容替换。这里最开始我将key值定为了自增长的数字。为了保证源码一定的阅读性,我同时将原文案以`/**/`注释的形式标在文末。后来,吸取了评论区[lany9527](https://github.com/lany9527)同学的建议。将中文作为了key值~~然后我会在文件头部import一下依赖。效果如下:

![](https://user-gold-cdn.xitu.io/2017/10/27/a442a5311f83be336a88c02b4d1dabc4)

`base/reactIntlUnicersal`这个文件需要自己放到自己的项目中,代码可以参考[链接](https://github.com/ProtoTeam/i18n-pick/blob/master/base/reactIntlUniversal.jsx)

### 翻译
然后建议安装atool-l18n这种翻译工具,直接翻译成英文文案。就可以编译运行了~当然后续还得有一些css的调整工作。
`cnpm i atool-l10n`

`node_modules/.bin/atool-l10n`

## 总结
本文主要是分享了一个文案提取的脚本,来让前端jsx项目快速国际化。如有使用上的问题,欢迎在评论区询问~

## tip
多谢评论区[lany9527](https://github.com/lany9527)同学的建议,脚本已经更新。不再以自增长的数字作为key值了。换成以中文名作为key进行提取,已经更新脚本~

## 附录:
1.目前脚本不支持中文中有换行的情况,所以得修正下scan之后的三个文件的内容。并且这部分内容得手动去替换。不过这种情况很少,我的项目扫出2000个词条只有两条有这个问题。

![](https://user-gold-cdn.xitu.io/2017/10/27/24840bc4e334fc9fdfa28159d1afa67b)

2.第二种是pick操作执行之后可能会编译出错,那是因为你的项目中可能手写了`\n`这样的文案,得手动处理下这种情况。

![](https://user-gold-cdn.xitu.io/2017/10/27/3ea6528f127425b8f205dc8fa5add73a)

3.第三种是不支持中文中含有`\"`的情况,这部分也得自己处理,原因是我以中文作为key,为了提取后的值过eslint,得用单引号引起来。就得对双引号单引号进行转义。无法处理已经转义过的内容。在完成了文案的转化之后可以再用scan命令扫描一遍,看下哪些没有处理好的,再手动处理下~
Loading

0 comments on commit 3de3a2a

Please sign in to comment.