优雅的使用 SVG ICON

创建 Icon 组件

index.jsx

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
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import cs from 'classnames'

import styles from './style.module.css'
const requireAll = requireContext => requireContext.keys().map(requireContext)
const req = require.context('RESOURCES/icons/svg', false, /\.svg$/)
requireAll(req)

class Icon extends PureComponent {
render () {
const { name, className } = this.props
return (
<svg
className={cs(
className,
styles['svg-icon']
)}
aria-hiidden="true"
>
<use xlinkHref={`#icon-${name}`} />
</svg>
)
}
}

Icon.propTypes = {
name: PropTypes.string.isRequired,
className: PropTypes.object
}

export default Icon

style.module.css

1
2
3
4
5
6
7
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}

使用 svg-sprite

安装 svg-sprite-loader,这是一个 webpack loader ,可以将多个 svg 打包成 svg-sprite

1
npm install svg-sprite-loader --save-dev

配置 webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [svgPath],
options: {
symbolId: 'icon-[name]'
}
},
{
test: /\.(png|jpe?g|gif|svg)$/,
loader: 'url-loader',
exclude: [svgPath],
options: {
...
}
}
...

更进一步优化 SVG

安装 svgo

1
npm install svgo --save-dev

修改 package.json

1
2
3
4
{
"svgo": "node ./node_modules/svgo/bin/svgo src/**/*.svg"

}

评论和共享

RxJS 基础概念

RxJS 基础概念

简介

  • Reactive
  • Lodash for events
  • Observable
  • Stream-based

Reactive

abc三个变量之间存在加法关系:

1
a = b + c

在传统方式下,这是一种一次性的赋值过程,调用一次就结束了,后面b和c再改变,a也不会变了。

而在Reactive的理念中,我们定义的不是一次性赋值过程,而是可重复的赋值过程,或者说是变量之间的关系:

1
a: = b + c

定义出这种关系之后,每次b或者c产生改变,这个表达式都会被重新计算。不同的库或者语言的实现机制可能不同,写法也不完全一样,但理念是相通的,都是描述出数据之间的联动关系。

RxJS提供了各种API来创建数据流:

  • 单值:of, empty, never
  • 多值:from
  • 定时:interval, timer
  • 从事件创建:fromEvent
  • 从Promise创建:fromPromise
  • 自定义创建:create

创建出来的数据流是一种可观察的序列,可以被订阅,也可以被用来做一些转换操作,比如:

  • 改变数据形态:map, mapTo, pluck
  • 过滤一些值:filter, skip, first, last, take
  • 时间轴上的操作:delay, timeout, throttle, debounce, audit, bufferTime
  • 累加:reduce, scan
  • 异常处理:throw, catch, retry, finally
  • 条件执行:takeUntil, delayWhen, retryWhen, subscribeOn, ObserveOn
  • 转接:switch

也可以对若干个数据流进行组合:

  • concat,保持原来的序列顺序连接两个数据流
  • merge,合并序列
  • race,预设条件为其中一个数据流完成
  • forkJoin,预设条件为所有数据流都完成
  • zip,取各来源数据流最后一个值合并为对象
  • combineLatest,取各来源数据流最后一个值合并为数组

在RxJS中,存在这么几种东西:

  • Observable (可观察对象): 表示一个概念,这个概念是一个可调用的未来值或事件的集合。
  • Observer (观察者): 一个回调函数的集合,它知道如何去监听由 Observable 提供的值。
  • Subscription (订阅): 表示 Observable 的执行,主要用于取消 Observable 的执行。
  • Operators (操作符): 采用函数式编程风格的纯函数 (pure function),使用像 mapfilterconcatflatMap 等这样的操作符来处理集合。
  • Subject (主体): 相当于 EventEmitter,并且是将值或事件多路推送给多个 Observer 的唯一方式。
    • BehaviorSubject: 保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。
    • ReplaySubject: 类似于 BehaviorSubject,它可以发送一定时间内多个旧值给新的订阅者,可以传入两个参数,数量和过期时间。
    • AsyncSubject: 只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。
  • Schedulers (调度器): 用来控制并发并且是中央集权的调度员,允许我们在发生计算时进行协调,例如 setTimeoutrequestAnimationFrame 或其他。

弹珠图

弹珠图

例子:赚钱是为了买房,买房是为了赚钱。

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
const Rx = require('rxjs/Rx')
const { Observable, Subject } = Rx

// 挣钱是为了买房,买房是为了赚钱
const house$ = new Subject()
const houseCount$ = house$.scan((acc, num) => acc + num, 0).startWith(0)

// 工资始终不涨
const salary$ = Observable.interval(100).mapTo(2)
const rent$ = Observable.interval(3000)
.withLatestFrom(houseCount$)
.map(arr => arr[1] * 5)

// 一买了房,就没现金了……
const income$ = Observable.merge(salary$, rent$)
const cash$ = income$
.scan((acc, num) => {
const newSum = acc + num

const newHouse = Math.floor(newSum / 100)
if (newHouse > 0) {
house$.next(newHouse)
}

return newSum % 100
}, 0)

houseCount$.subscribe(num => console.log(`houseCount: ${num}`))
cash$.subscribe(num => console.log(`cash: ${num}`))

RxJS 中文文档:http://cn.rx.js.org/manual/overview.htm

RxJS 函数可视化:http://rxmarbles.com/

评论和共享

JavaScript 运行机制

事件循环

事件循环

  • 同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop (事件循环)。

宏任务与微任务

  • macro-task (宏任务):包括整体代码 scriptsetTimeoutsetInterval
  • micro-task(微任务):Promiseprocess.nextTick

不同类型的任务会进入对应的 Event Queue,比如 setTimeoutsetInterval 会进入相同的 Event Queue

宏任务与微任务

评论和共享

MacOS使用分享

使用 MacOS 的正确姿势

从 2017/11/022017/11/11 经过好几天的各种摸索,终于基本习惯了 MacOS。并且也装了很多可以提高工作效率的软件和插件。下面按重要程度来排序。

Homebrew

Homebrew是一款Mac OS平台下的软件包管理工具,拥有安装、卸载、更新、查看、搜索等很多实用的功能。简单的一条指令,就可以实现包管理,而不用你关心各种依赖和文件路径的情况,十分方便快捷。

具体的安装方法看这里

Homebrew Cask

装完 Homebrew 当然要装 Homebrew Cask 啦,如何安装和使用看这里

Homebrew 主要用来下载一些不带界面的命令行下的工具和第三方库来进行二次开发

Homebrew cask 主要用来下载一些带界面的应用软件,下载好后会自动安装,并能在mac中直接运行使用。一些免费好用的mac软件没有在苹果官方app store商店上架,我们就可以在brew cask中下载。

阅读全文

MongoDB 数据备份与还原

mongodump

mongodump 是针对数据库进行备份的官方工具。具体参数如下

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
general options:
--help print usage
--version print the tool version and exit

verbosity options:
-v, --verbose=<level> more detailed log output (include multiple times for more verbosity, e.g. -vvvvv, or specify a numeric value, e.g. --verbose=N)
--quiet hide all log output

connection options:
-h, --host=<hostname> mongodb host to connect to (setname/host1,host2 for replica sets)
--port=<port> server port (can also use --host hostname:port)

ssl options:
--ssl connect to a mongod or mongos that has ssl enabled
--sslCAFile=<filename> the .pem file containing the root certificate chain from the certificate authority
--sslPEMKeyFile=<filename> the .pem file containing the certificate and key
--sslPEMKeyPassword=<password> the password to decrypt the sslPEMKeyFile, if necessary
--sslCRLFile=<filename> the .pem file containing the certificate revocation list
--sslAllowInvalidCertificates bypass the validation for server certificates
--sslAllowInvalidHostnames bypass the validation for server name
--sslFIPSMode use FIPS mode of the installed openssl library

authentication options:
-u, --username=<username> username for authentication
-p, --password=<password> password for authentication
--authenticationDatabase=<database-name> database that holds the user's credentials
--authenticationMechanism=<mechanism> authentication mechanism to use

namespace options:
-d, --db=<database-name> database to use
-c, --collection=<collection-name> collection to use

query options:
-q, --query= query filter, as a JSON string, e.g., '{x:{$gt:1}}'
--queryFile= path to a file containing a query filter (JSON)
--readPreference=<string>|<json> specify either a preference name or a preference json object
--forceTableScan force a table scan

output options:
-o, --out=<directory-path> output directory, or '-' for stdout (defaults to 'dump')
--gzip compress archive our collection output with Gzip
--repair try to recover documents from damaged data files (not supported by all storage engines)
--oplog use oplog for taking a point-in-time snapshot
--archive=<file-path> dump as an archive to the specified path. If flag is specified without a value, archive is written to stdout
--dumpDbUsersAndRoles dump user and role definitions for the specified database
--excludeCollection=<collection-name> collection to exclude from the dump (may be specified multiple times to exclude additional collections)
--excludeCollectionsWithPrefix=<collection-prefix> exclude all collections from the dump that have the given prefix (may be specified multiple times to exclude additional prefixes)
-j, --numParallelCollections= number of collections to dump in parallel (4 by default) (default: 4)
--viewsAsCollections dump views as normal collections with their produced data, omitting standard collections

对整个数据库备份

1
mongodump -o <directory-path>

其中 <directory-path> 是路径,默认为 dump

对某个 db 或者 collections 备份

1
mongodump -d <database-name> -c <collection-name> -o <directory-path>

其中 <database-name> 是数据库名, <collection-name> 是集合名

备份数据格式

不同的 database 存放在以 <database-name> 命名的文件内。

不同的 collection 有两个文件

  • <database-name>.bson 存放数据
  • <database-name>.metadata.json 存放表结构
阅读全文

Bootstrap 五列布局

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
.col-xs-1-5,.col-sm-1-5,.col-md-1-5,.col-lg-1-5 {
min-height: 1px;
padding-left: 15px;
padding-right: 15px;
position: relative;
}

.col-xs-1-5 {
width: 20%;
float: left;
}

@media screen and (min-width: 768px) {
.col-sm-1-5 {
width: 20%;
float: left;
}
}

@media screen and (min-width: 992px) {
.col-md-1-5 {
width: 20%;
float: left;
}
}

@media screen and (min-width: 1200px) {
.col-lg-1-5 {
width: 20%;
float: left;
}
}

评论和共享

PWAs 技术初探

PWAs 是什么

PWAs (Progressive Web Apps),PWAs是指2017年2月4日,谷歌推出的“小程序”增强型网页应用,它无需下载安装,却可以和本地APP一样,放置在桌面上。

优点

  1. 你只需要基于开放的 W3C 标准的 web 开发技术来开发一个app。不需要多客户端开发。
  2. 用户可以在安装前就体验你的 app。
  3. 不需要通过 AppStore 下载 app。app 会自动升级不需要用户升级。
  4. 用户会受到‘安装’的提示,点击安装会增加一个图标到用户首屏。
  5. 被打开时,PWAs 会展示一个有吸引力的闪屏。
  6. chrome 提供了可选选项,可以使 PWAs 得到全屏体验。
  7. 必要的文件会被本地缓存,因此会比标准的web app 响应更快(也许也会比native app响应快)
  8. 安装及其轻量 – 或许会有几百 kb 的缓存数据。
  9. 网站的数据传输必须是 https 连接。
  10. PWAs 可以离线工作,并且在网络恢复时可以同步最新数据。

PWAs 技术目前被 Firefox,Chrome 和其他基于Blink内核的浏览器支持。微软正在努力在Edge浏览器上实现。Apple还没有动作。不过浏览器支持对 PWAs 不是很重要,app 任然可以运行在不支持 PWAs 技术的浏览器里,不能离线访问,和原来一样没有影响。

并不是只有单页应用可以 PWAs 化,大多数网站都可以 PWAs 化,包括 WordPress 站点或者静态站点。

示例

示例代码可以在 https://github.com/sitepoint-editors/pwa-retrofit 找到。

改造步骤

第一步:开启 HTTPS

因为,Service Worker 很复杂,你可以修改示例代码来达到自己的目的。这是一个标准的 web worker,浏览器用一个单独的线程来下载和执行它。它没有调用 DOM 和其他页面 api 的能力,但他可以拦截网络请求,包括页面切换,静态资源下载,ajax请求所引起的网络请求。如果让第三方代码可以拦截来自其他网站的 service worker,将是一个灾难。

所以 PWAs 需要 HTTPS 链接已保证网站的安全。

不过你可以在 HTTP 链接下测试你的 PWAs,不过只能在 localhost 或者任何 127.x.x.x 的地址上。

第二步:创建一个 Manifest 文件

manifest 文件提供了一些我们网站的信息,例如 namedescription 和需要在主屏使用的图标的图片,启动屏的图片等。

manifest 文件是一个 JSON 格式的文件,位于你项目的根目录。它必须用Content-Type: application/manifest+json 或者 Content-Type: application/json 这样的 HTTP 头来请求。这个文件可以被命名为任何名字,在示例代码中他被命名为 /manifest.json

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
{
"name" : "PWA Website",
"short_name" : "PWA",
"description" : "An example PWA website",
"start_url" : "/",
"display" : "standalone",
"orientation" : "any",
"background_color" : "#ACE",
"theme_color" : "#ACE",
"icons": [
{
"src" : "/images/logo/logo072.png",
"sizes" : "72x72",
"type" : "image/png"
},
{
"src" : "/images/logo/logo152.png",
"sizes" : "152x152",
"type" : "image/png"
},
{
"src" : "/images/logo/logo192.png",
"sizes" : "192x192",
"type" : "image/png"
},
{
"src" : "/images/logo/logo256.png",
"sizes" : "256x256",
"type" : "image/png"
},
{
"src" : "/images/logo/logo512.png",
"sizes" : "512x512",
"type" : "image/png"
}
]
}

在页面的 <head> 中引入:

1
<link rel="manifest" href="/manifest.json">

manifest 中主要属性有:

  1. name —— 网页显示给用户的完整名称
  2. short_name —— 当空间不足以显示全名时的网站缩写名称
  3. description —— 关于网站的详细描述
  4. start_url —— 网页的初始 相对 URL(比如 /)
  5. scope —— 导航范围。比如,/app/scope 就限制 app 在这个文件夹里。
  6. background-color —— 启动屏和浏览器的背景颜色
  7. theme_color —— 网站的主题颜色,一般都与背景颜色相同,它可以影响网站的显示
  8. orientation —— 首选的显示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary
  9. display —— 首选的显示方式:fullscreen, standalone(看起来像是native app),minimal-ui(有简化的浏览器控制选项) 和 browser(常规的浏览器 tab)
  10. icons —— 定义了 src URL, sizestype 的图片对象数组。

MDN 提供了完整的 manifest 属性列表: Web App Manifest properties

第三步:创建一个 Service Worker

Service Worker 是拦截和响应你的网络请求的编程接口。这是一个位于你根目录的一个单独的 javascript 文件。

你的 js 文件(在示例代码中是 /js/main.js)可以检查是否支持 Service Worker,并且注册:

1
2
3
4
if ('serviceWorker' in navigator) {
// register service worker
navigator.serviceWorker.register('/service-worker.js');
}

如果你不需要离线功能,可以简单的创建一个空的 /service-worker.js 文件 —— 用户会被提示安装你的 app。

service worker 主要有三个事件: installactivatefetch

Install 事件

这个事件在app被安装时触发。它经常用来缓存必要的文件。缓存通过 Cache API来实现。

首先,我们来构造几个变量:

  1. 缓存名称(CACHE)和版本号(version)。你的应用可以有多个缓存但是只能引用一个。我们设置了版本号,这样当我们有重大更新时,我们可以更新缓存,而忽略旧的缓存。
  2. 一个离线页面的URL(offlineURL)。当离线时用户试图访问之前未缓存的页面时,这个页面会呈现给用户。
  3. 一个拥有离线功能的页面必要文件的数组(installFilesEssential)。这个数组应该包含静态资源,比如 CSS 和 JavaScript 文件,但我也把主页面(/)和图标文件写进去了。如果主页面可以多个URL访问,你应该把他们都写进去,比如/和/index.html。注意,offlineURL也要被写入这个数组。
  4. 可选的,描述文件数组(installFilesDesirable)。这些文件都很会被下载,但如果下载失败不会中止安装。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// configuration
const
version = '1.0.0',
CACHE = version + '::PWAsite',
offlineURL = '/offline/',
installFilesEssential = [
'/',
'/manifest.json',
'/css/styles.css',
'/js/main.js',
'/js/offlinepage.js',
'/images/logo/logo152.png'
].concat(offlineURL),
installFilesDesirable = [
'/favicon.ico',
'/images/logo/logo016.png',
'/images/hero/power-pv.jpg',
'/images/hero/power-lo.jpg',
'/images/hero/power-hi.jpg'
];

installStaticFiles() 方法添加文件到缓存,这个方法用到了基于 promiseCache API。当必要的文件都被缓存后才会生成返回值。

1
2
3
4
5
6
7
8
9
10
// install static assets
function installStaticFiles() {
return caches.open(CACHE)
.then(cache => {
// cache desirable files
cache.addAll(installFilesDesirable);
// cache essential files
return cache.addAll(installFilesEssential);
});
}

最后,我们添加 install 的事件监听函数。 waitUntil 方法确保所有代码执行完毕后,service worker 才会执行 install。执行 installStaticFiles()方法,然后执行 self.skipWaiting()方法使service worker进入 active 状态。

1
2
3
4
5
6
7
8
9
// application installation
self.addEventListener('install', event => {
console.log('service worker: install');
// cache core files
event.waitUntil(
installStaticFiles()
.then(() => self.skipWaiting())
);
});

Activate 事件

当 install完成后, service worker 进入active状态,这个事件立刻执行。你可能不需要实现这个事件监听,但是示例代码在这里删除老旧的无用缓存文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// clear old caches
function clearOldCaches() {
return caches.keys()
.then(keylist => {
return Promise.all(
keylist
.filter(key => key !== CACHE)
.map(key => caches.delete(key))
);
});
}

// application activated
self.addEventListener('activate', event => {
console.log('service worker: activate');
// delete old caches
event.waitUntil(
clearOldCaches()
.then(() => self.clients.claim())
);
});

注意,最后的 self.clients.claim() 方法设置本身为 activeservice worker

Fetch 事件

当有网络请求时这个事件被触发。它调用respondWith()方法来劫持 GET 请求并返回:

  1. 缓存中的一个静态资源。
  2. 如果 #1 失败了,就用 Fetch API(这与 service worker 的fetch 事件没关系)去网络请求这个资源。然后将这个资源加入缓存。
  3. 如果 #1 和 #2 都失败了,那就返回一个适当的值。
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
// application fetch network data
self.addEventListener('fetch', event => {
// abandon non-GET requests
if (event.request.method !== 'GET') return;
let url = event.request.url;
event.respondWith(
caches.open(CACHE)
.then(cache => {
return cache.match(event.request)
.then(response => {
if (response) {
// return cached file
console.log('cache fetch: ' + url);
return response;
}
// make network request
return fetch(event.request)
.then(newreq => {
console.log('network fetch: ' + url);
if (newreq.ok) cache.put(event.request, newreq.clone());
return newreq;
})
// app is offline
.catch(() => offlineAsset(url));
});
})
);
});

最后这个 offlineAsset(url) 方法通过几个辅助函数返回一个适当的值:

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
// is image URL?
let iExt = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'].map(f => '.' + f);
function isImage(url) {

return iExt.reduce((ret, ext) => ret || url.endsWith(ext), false);

}


// return offline asset
function offlineAsset(url) {
if (isImage(url)) {
// return image
return new Response(
'<svg role="img" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg"><title>offline</title><path d="M0 0h400v300H0z" fill="#eee" /><text x="200" y="150" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="50" fill="#ccc">offline</text></svg>',
{ headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-store'
}}
);
}
else {
// return page
return caches.match(offlineURL);
}
}

offlineAsset() 方法检查是否是一个图片请求,如果是,那么返回一个带有 “offline” 字样的 SVG。如果不是,返回 offlineURL 页面。

第四步:创建一个可用的离线页面

离线页面可以是一个静态页面,来说明当前用户请求不可用。然而,我们也可以在这个页面上列出可以访问的页面链接。

main.js 中我们可以使用 Cache API 。然而API 使用 promises,在不支持的浏览器中会引起所有 javascript 运行阻塞。为了避免这种情况,我们在加载另一个 /js/offlinepage.js 文件之前必须检查离线文件列表和是否支持 Cache API

1
2
3
4
5
6
7
// load script to populate offline page list
if (document.getElementById('cachedpagelist') && 'caches' in window) {
var scr = document.createElement('script');
scr.src = '/js/offlinepage.js';
scr.async = 1;
document.head.appendChild(scr);
}

/js/offlinepage.js locates the most recent cache by version name, 取到所有 URL的key的列表,移除所有无用 URL,排序所有的列表并且把他们加到 ID 为 cachedpagelist 的 DOM 节点中:

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
// cache name
const
CACHE = '::PWAsite',
offlineURL = '/offline/',
list = document.getElementById('cachedpagelist');
// fetch all caches
window.caches.keys()
.then(cacheList => {
// find caches by and order by most recent
cacheList = cacheList
.filter(cName => cName.includes(CACHE))
.sort((a, b) => a - b);
// open first cache
caches.open(cacheList[0])
.then(cache => {
// fetch cached pages
cache.keys()
.then(reqList => {
let frag = document.createDocumentFragment();
reqList
.map(req => req.url)
.filter(req => (req.endsWith('/') || req.endsWith('.html')) && !req.endsWith(offlineURL))
.sort()
.forEach(req => {
let
li = document.createElement('li'),
a = li.appendChild(document.createElement('a'));
a.setAttribute('href', req);
a.textContent = a.pathname;
frag.appendChild(li);
});
if (list) list.appendChild(frag);
});
})
});

PWA 陷阱

URL 隐藏

我们的示例代码隐藏了 URL 栏,我不推荐这种做法,除非你有一个单 url 应用,比如一个游戏。对于多数网站,manifest 选项 display: minimal-ui 或者 display: browser 是最好的选择。

缓存太多

你可以缓存你网站的所有页面和所有静态文件。这对于一个小网站是可行的,但这对于上千个页面的大型网站实际吗?没有人会对你网站的所有内容都感兴趣,而设备的内存容量将是一个限制。即使你像示例代码一样只缓存访问过的页面和文件,缓存大小也会增长的很快。

也许你需要注意:

  • 只缓存重要的页面,类似主页,和最近的文章。
  • 不要缓存图片,视频和其他大型文件
  • 经常删除旧的缓存文件
  • 提供一个缓存按钮给用户,让用户决定是否缓存

缓存刷新

在示例代码中,用户在请求网络前先检查该文件是否缓存。如果缓存,就使用缓存文件。这在离线情况下很棒,但也意味着在联网情况下,用户得到的可能不是最新数据。

静态文件,类似于图片和视频等,不会经常改变的资源,做长时间缓存没有很大的问题。你可以在HTTP 头里设置 Cache-Control 来缓存文件使其缓存时间为一年(31,536,000 seconds):

1
Cache-Control: max-age=31536000

页面,CSS和 script 文件会经常变化,所以你应该改设置一个很短的缓存时间比如 24 小时,并在联网时与服务端文件进行验证:

1
Cache-Control: must-revalidate, max-age=86400

评论和共享

React v16.0

新的渲染返回类型:碎片和字符串

现在可以从组件的渲染方法中返回一个包含元素的数组

1
2
3
4
5
6
7
8
9
render() {
// No need to wrap list items in an extra element!
return [
// Don't forget the keys :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}

添加了对返回字符串的支持

1
2
3
render() {
return 'Look ma, no spans!';
}

更好的服务端渲染

React 16 完全重写服务器渲染,支持流。同时编译不再进行 process.env 检查(Node 读取 process.env 非常慢)。并且比 React 15 快大概三倍

支持自定义 DOM 属性

React 现在会将自定义属性传递给 DOM,而不是忽略不认识的 HTMLSVG 属性。这使得我们能够不必在维护的 React 特性的白名单,并能够减少文件体积

1
2
// Your code:
<div mycustomattribute="something" />
1
2
// React 15 output:
<div />
1
2
// React 16 output:
<div mycustomattribute="something" />

减少文件体积

React 现在使用 Rollup 来进行扁平化的打包以处理不同目标格式,而这使得体积和性能都有了提高。相较于之前的版本体积减少了32%

MIT 协议

新核心架构

异步渲染 - 一种周期性地对浏览器执行调度渲染工作的策略。结果如下,通过异步渲染,应用能够更好的响应,因为 React 避免阻塞了主线程。

新的弃用

保留(Hydrating)服务端渲染的容器现在有了更清晰的 API 定义。若你想重用服务端渲染的 HTML,使用 ReactDOM.hydrate 而不是 ReactDOM.render。若你只是想做客户端渲染则继续使用 ReactDOM.render 即可。

更新

  • React 15 已对使用 unstable_handleError 进行了限制,不再为错误边界提供文档支持。该方法已重命名为 componentDidCatch。你可以使用 codemod 来自动地迁移代码到新的 API

  • 通过 null 调用 setState 不再触发更新。这允许你确定在更新函数里你是否想要重新渲染。

  • setState 回调函数(第二个参数)现在会在 componentDidMount / componentDidUpdate 之后立刻触发,而非等到所有组件都已渲染。

  • 当使用 <B /> 来替换 <A />B.componentWillMount 现在会在 A.componentWillUnmount 之前触发。之前,在某些情况下,A.componentWillUnmount 会立刻触发。

JavaScript环境要求

React 16 依赖于集合类型 MapSet。若你要支持老式的可能未提供原生支持的浏览器和设备(例如 IE < 11),考虑在你的应用库中包含一个全局的 polyfill,例如 core-jsbabel-polyfill

一个使用 core-js 支持老版浏览器的 React 16 polyfill 环境大致如下:

1
2
3
4
5
6
7
8
9
10
import 'core-js/es6/map';
import 'core-js/es6/set';

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);

评论和共享

深入了解闭包

深入了解闭包

定义

闭包的定义比较混乱,不同人和不同的书籍有不同的理解。严格来说,闭包需要满足三个条件:【1】访问所在作用域;【2】函数嵌套;【3】在所在作用域外被调用

经典定义: 函数对象可以通过作用域链相互关联起来,函数体内的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为“闭包”(函数变量可以被隐藏于作用域链之内,因此看起来是函数将变量“包裹”了起来)

从技术角度讲: 所有 JavaScript 函数都是闭包:它们都是对象,它们都关联到作用域链。

定义1: 闭包是指可以访问其所在作用域的函数

定义2: 闭包是指有权访问另一个函数作用域中的变量的函数

定义3: 闭包是指在函数声明时的作用域以外的地方被调用的函数

定义4: 闭包是保存定义时的变量作用域

个人理解

我个人认为,闭包应该是一种思想,不同的人有不同的理解,我们不应该纠结闭包的概念到底是什么,而是应该理解这种思想背后的原理。

原理

首先要理解闭包必须要理解 词法作用域作用域链

词法作用域: 函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数运行时决定的。

变量作用域: 是成语选代码中定义这个变量的区域。

作用域链: 每一段 JavaScript 代码都有一个与之关联的作用域链。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当 JavaScript 需要查找变量 x 值的时候(变量解析) 它会从链的第一个对象开始查找,如果这个对象有一个名为 x 的属性,则会直接使用这个属性的值,如果不存在,JavaScript 会继续查找链上的下一个对象。以此类推。如果作用域链上不存在 x ,就认为这段代码作用域链上不存在 x,并最终跑出一个引用错误(ReferenceError)的异常。

理解了这些相关概念后,理解闭包就容易多了。我们通过一个例子来说明闭包的原理。

img1

1
2
3
4
5
6
7
8
9
10
function foo(){
var a = 2;

function bar(){
console.log(a);
}
return bar;
}
var baz = foo();
baz();

【1】代码执行流进入全局执行环境,并对全局执行环境中的代码进行声明提升(hoisting)

【2】执行流执行第9行代码 var baz = foo();,调用 foo() 函数,此时执行流进入 foo() 函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在两个执行环境,foo() 函数为当前执行流所在执行环境

【4】执行流执行第2行代码 var a = 2; ,对a进行 LHS 查询,给 a 赋值2

【5】执行流执行第7行代码 return bar; ,将 bar() 函数作为返回值返回。按理说,这时 foo() 函数已经执行完毕,应该销毁其执行环境,等待垃圾加收。但因为其返回值是 bar 函数。bar 函数中存在自由变量 a,需要通过作用域链到 foo() 函数的执行环境中找到变量 a 的值,所以虽然 foo 函数的执行环境被销毁了,但其变量对象不能被销毁,只是从活动状态变成非活动状态;而全局执行环境的变量对象则变成活动状态;执行流继续执行第9行代码 var baz = foo(); ,把 foo() 函数的返回值 bar 函数赋值给 baz

【6】执行流执行第10行代码 baz(); ,通过在全局执行环境中查找 baz 的值,baz 保存着 foo() 函数的返回值 bar 。所以这时执行 baz(),会调用 bar() 函数,此时执行流进入 bar() 函数执行环境中,对该执行环境中的代码进行声明提升过程。此时执行环境栈中存在三个执行环境, bar() 函数为当前执行流所在执行环境

在声明提升的过程中,由于a是个自由变量,需要通过 bar() 函数的作用域链 bar() -> foo() -> 全局作用域 进行查找,最终在 foo() 函数中也就是代码第2行找到 var a = 2; ,然后在 foo() 函数的执行环境中找到a的值是2,所以给a赋值2

【7】执行流执行第5行代码 console.log(a); ,调用内部对象 console,并从 console 对象中 log 方法,将a作为参数传递进入。从 bar() 函数的执行环境中找到a的值是2,所以,最终在控制台显示2

【8】执行流执行第6行代码,bar() 的执行环境被弹出执行环境栈,并被销毁,等待垃圾回收,控制权交还给全局执行环境

【9】当页面关闭时,所有的执行环境都被销毁

总结

从上述说明的第5步可以看出,由于闭包 bar() 函数的原因,虽然 foo() 函数的执行环境销毁了,但其变量对象一直存在于内存中,就是为了能够使得调用 bar() 函数时,可以通过作用域链访问到父函数 foo() ,并得到其变量对象中储存的变量值。直到页面关闭,foo() 函数的变量对象才会和全局的变量对象一起被销毁,从而释放内存空间

由于闭包占用内存空间,所以要谨慎使用闭包。尽量在使用完闭包后,及时解除引用,以便更早释放内存

1
2
3
4
5
6
7
8
9
10
11
12
//通过将baz置为null,解除引用
function foo(){
var a = 2;
function bar(){
console.log(a);//2
}
return bar;
}
var baz = foo();
baz();
baz = null;
//后续代码

评论和共享

深入了解 Babel

深入了解 Babel

本文所研究的是 babel 6 版本。
babel 62015年10月30号 发布,主要做了以下更新:

  • 拆分成几个核心包,babel-core,babel-node,babel-cli…
  • 没有了默认的转换,现在你需要手动的添加 plugin。也就是插件化
  • 添加了 preset,也就是预置条件。
  • 增加了 .babelrc 文件,方便自定义的配置。

babel 中有很多包,必须弄明白这些是干嘛的,才能让我们更好的使用这个工具。

  • babel-core
  • babel-cli
  • babel-external-helpers
  • babel-node
  • babel-register
  • babel-runtime
  • babel-polyfill

babel-core

babel 的核心包,包括核心 api,比如 transform,主要是处理转码的。 它会把我们的 js 代码,抽象成 ast,即 abstract syntax tree 的缩写,是源代码的抽象语法结构的树状表现形式。

主要 API

1
2
var babel = require('babel-core');
var transform = babel.transform;
1
2
// babel.transform(code: string, options?: Object)
transform("code", options) // => { code, map, ast }
1
2
3
4
5
6
7
8
9
10
11
// babel.transformFile(filename: string, options?: Object, callback: Function)
var path = require('path');
var result = babel.transformFileSync(
path.resolve(__dirname, './test.js'),
{
presets: ['env'],
plugins: ['transform-runtime'],
},
function(err, result) {// { code, map, ast }
console.log(result);
});
1
2
3
4
5
6
7
8
// babel.transformFileSync(filename: string, options?: Object)
var result = babel.transformFileSync(
path.resolve(__dirname, './test.js'),
{
presets: ['env'],
plugins: ['transform-runtime'],
}
);
1
2
// babel.transformFromAst(ast: Object, code?: string, options?: Object)
// 把 ast 传入,解析为 code 代码

babel-cli

提供命令行运行 babel

babel-external-helpers

babel-cli 中的一个 command,用来生成 helper 函数。

babel 有很多辅助函数,例如 toArray 函数, jsx 转化函数。这些函数是 babel transform 的时候用的,都放在 babel-helpers 这个包中。如果 babel 编译的时候检测到某个文件需要这些 helpers,在编译成模块的时候,会放到模块的顶部。

但是如果多个文件都需要提供,会重复引用这些 helpers,会导致每一个模块都定义一份,代码冗余。所以 babel 提供了这个命令,用于生成一个包含了所有 helpers 的 js 文件,用于直接引用。然后再通过一个 plugin,去检测全局下是否存在这个模块,存在就不需要重新定义了。

使用:

  1. 执行 babel-external-helpers 生成 helpers.js 文件

    1
    node_modules/.bin/babel-external-helpers > helpers.js
  2. 安装 plugin

    1
    npm install --save-dev babel-plugin-external-helpers
  3. 然后在 babel 的配置文件加入

    1
    2
    3
    {
    "plugins": ["external-helpers"]
    }
  4. 入口文件引入 helpers.js

    1
    require('./helpers.js');

如果使用了 transform-runtime,就不需要生成 helpers.js 文件了,这个在后面的 babel-runtime 再说。

babel-node

也是 babel-cli 下面的一个 command,主要是实现了 node 执行脚本和命令行写代码的能力。

babel-register

通过改写 node 本身的 require,添加钩子,然后在 require 其他模块的时候,就会触发 babel 编译。也就是你引入 require('babel-register') 的文件代码,是不会被编译的。只有通过 require 引入的其他代码才会。

1
npm install babel-register --save-dev
1
2
require('babel-register')({ presets: ['react'] });
require('./test')

它的特点就是实时编译,不需要输出文件,执行的时候再去编译。所以它很适用于开发。总结一下就是,多用在 node 跑程序,做实时编译用的,通常会结合其他插件作编译器使用,比如 mocha 做测试的时候。

babel-runtime

1
npm install babel-runtime --save

Babel 转译后的代码要实现源代码同样的功能需要借助一些帮助函数。可能会重复出现在一些模块里,导致编译后的代码体积变大。Babel 为了解决这个问题,提供了单独的包 babel-runtime 供编译模块复用工具函数。(core-jsregenerator

启用插件 babel-plugin-transform-runtime 后,Babel 就会使用 babel-runtime 下的工具函数,转译代码如下:

1
2
3
4
5
6
'use strict';
// 之前的 _defineProperty 函数已经作为公共模块 `babel-runtime/helpers/defineProperty` 使用
var _defineProperty2 = require('babel-runtime/helpers/defineProperty');
var _defineProperty3 = _interopRequireDefault(_defineProperty2);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = (0, _defineProperty3.default)({}, 'name', 'JavaScript');

除此之外,babel 还为源代码的非实例方法(Object.assign,实例方法是类似这样的 “foobar”.includes(“foo”))和 babel-runtime/helps 下的工具函数自动引用了 polyfill。这样可以避免污染全局命名空间,非常适合于 JavaScript 库和工具包的实现。例如 const obj = {}, Object.assign(obj, { age: 30 }); 转译后的代码如下所示:

1
2
3
4
5
6
7
8
9
'use strict';
// 使用了 core-js 提供的 assign
var _assign = require('babel-runtime/core-js/object/assign');
var _assign2 = _interopRequireDefault(_assign);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var obj = {};
(0, _assign2.default)(obj, {
age: 30
});

babel-runtime 适合 JavaScript 库和工具包实现

  • 避免 babel 编译的工具函数在每个模块里重复出现,减小库和工具包的体积;
  • 在没有使用 babel-runtime 之前,库和工具包一般不会直接引入 polyfill。否则像 Promise 这样的全局对象会污染全局命名空间,这就要求库的使用者自己提供 polyfill。这些 polyfill 一般在库和工具的使用说明中会提到,比如很多库都会有要求提供 es5 的 polyfill。在使用 babel-runtime 后,库和工具只要在 package.json 中增加依赖 babel-runtime,交给 babel-runtime 去引入 polyfill 就行了;

core-js

core-js 是用于 JavaScript 的组合式标准化库,它包含 ES5 (e.g: object.freeze), ES6PromiseSymbols, Collections, Iterators, Typed arrayses7+提案等等的 polyfills 实现。

regenerator

它是来自于 facebook 的一个库,链接。主要就是实现了 generator/yeild, async/await。

所以 babel-runtime 是单纯的实现了 core-js 和 regenerator 引入和导出,比如这里是 filter 函数的定义,做了一个中转并处理了 esModule 的兼容。

babel-polyfill

Babel 默认只转换新的 JavaScript 语法,而不转换新的 API。例如,IteratorGeneratorSetMapsProxyReflectSymbolPromise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转译。如果想使用这些新的对象和方法,必须使用 babel-polyfill,为当前环境提供一个垫片。

不同于 babel-runtime 的是,babel-polyfill 是一次性引入你的项目中的,就像是 React 包一样,同项目代码一起编译到生产环境。

注意: babel

transform-runtime 和 babel-polyfile 对比

  • babel-polyfill 是当前环境注入这些 es6+ 标准的垫片,好处是引用一次,不再担心兼容,而且它就是全局下的包,代码的任何地方都可以使用。缺点也很明显,它可能会污染原生的一些方法而把原生的方法重写。如果当前项目已经有一个 polyfill 的包了,那你只能保留其一。而且一次性引入这么一个包,会大大增加体积。如果你只是用几个特性,就没必要了,如果你是开发较大的应用,而且会频繁使用新特性并考虑兼容,那就直接引入吧。

  • transform-runtime 是利用 plugin 自动识别并替换代码中的新特性,你不需要再引入,只需要装好 babel-runtime 和 配好 plugin 就可以了。好处是按需替换,检测到你需要哪个,就引入哪个 polyfill,如果只用了一部分,打包完的文件体积对比 babel-polyfill 会小很多。而且 transform-runtime 不会污染原生的对象,方法,也不会对其他 polyfill 产生影响。所以 transform-runtime 的方式更适合开发工具包,库,一方面是体积够小,另一方面是用户(开发者)不会因为引用了我们的工具,包而污染了全局的原生方法,产生副作用,还是应该留给用户自己去选择。缺点是随着应用的增大,相同的 polyfill 每个模块都要做重复的工作(检测,替换),虽然 polyfill 只是引用,编译效率不够高效。

plugin

babel-plugin-transform-runtime

transform-runtime 是为了方便使用 babel-runtime 的,它会分析我们的 ast 中,是否有引用 babel-rumtime 中的垫片(通过映射关系),如果有,就会在当前模块顶部插入我们需要的垫片。

另外,它还有几个配置

1
2
3
4
5
6
7
8
9
10
11
// 默认值
{
"plugins": [
["transform-runtime", {
"helpers": true,
"polyfill": true,
"regenerator": true,
"moduleName": "babel-runtime"
}]
]
}

如果你只需要用 regenerator,不需要 core-js 里面的 polyfill 那你就可以在 options 中把 polyfill 设为 falsehelpers 设为 false,就相当于没有启用 babel-plugin-external-helpers 的效果,比如翻译 async 的时候,用到了 asyncToGenerator 函数,每个文件还会重新定义一下。moduleName 的话,就是用到的库,你可以把 babel-runtime 换成其他类似的。

presets

presets 就是 plugins 的组合,你也可以理解为是套餐

babel-preset-lastet(包括 es2105,es2016,es2017)跟默认情况下的 env 是一样的,也就是说包括 lastest 在内,这四个 presets 都要被 babel-preset-env 代替。即:

babel-preset-env

它能根据当前的运行环境,自动确定你需要的 pluginspolyfills。通过各个 es 标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 featureplugins 之间的映射关系,最终确定需要的 plugins

注意: babel-preset-env 并不是包括所有的 babel-preset-esbabel-preset-stag,而是所有的 babel-preset-esbabel-preset-stag-4
详情请看这里

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
//.babelrc

{
"presets": [
[
"env",
{
"targets": { // 配支持的环境
"browsers": [ // 浏览器
"last 2 versions",
"safari >= 7"
],
"node": "current"
},
"modules": true, //设置ES6 模块转译的模块格式 默认是 commonjs
"debug": true, // debug,编译的时候 console
"useBuiltIns": false, // 是否开启自动支持 polyfill
"include": [], // 总是启用哪些 plugins
"exclude": [] // 强制不启用哪些 plugins,用来防止某些插件被启用
}
]
],
plugins: [
"transform-react-jsx" //如果是需要支持 jsx 这个东西要单独装一下。
]
}

useBuiltIns

env 会自动根据我们的运行环境,去判断需要什么样的 polyfill,而且,打包后的代码体积也会大大减小,但是这一切都在使用 useBuiltIns,而且需要你安装 babel-polyfill,并 import。它会启用一个插件,替换你的import 'babel-polyfill',不是整个引入了,而是根据你配置的环境和个人需要单独的引入 polyfill

总结

  • 具体项目还是需要使用 babel-polyfill 配合 useBuiltIns,只使用 babel-runtime 的话,实例方法不能正常工作(例如 "foobar".includes("foo"));
  • JavaScript 库和工具可以使用 babel-runtime 配合 babel-plugin-transform-runtime,在实际项目中使用这些库和工具,需要该项目本身提供 polyfill

参考文献

评论和共享

作者的图片

Archie Shi

Nothing to say


Front-End Development Engineer