-
Notifications
You must be signed in to change notification settings - Fork 328
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
泽鹿
committed
Mar 9, 2018
1 parent
57ea753
commit 3de3a2a
Showing
10 changed files
with
2,583 additions
and
5 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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接口调用频道广播接口 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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命令扫描一遍,看下哪些没有处理好的,再手动处理下~ |
Oops, something went wrong.