使用Promise实现简单的Ajax缓存

业务场景

在不少业务场景下,我们需要实现简单的请求缓存(即某个请求只发起一次请求),例如上传 Token 的获取、获取配置的接口等。

这些接口可以通过 Promise 实现简单的缓存并能够控制更新,而不需要另外引入缓存层。

示范代码

用七牛上传作例子,一般我们会把七牛上传封装为一个单独的 Upload 组件,外部只需要调用组件,而 token 的获取封装到组件内部实现。

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
//Upload.vue
let fetchToken = null;
export default {
data() {
return {
token: ''
};
},
methods: {
async upload() {
try {
// ...
}
catch(err) {
alert(err.message);
this.refreshToken();
}
},
refreshToken() {
fetchToken = null;
this.fetchToken();
},
fetchToken() {
if (!fetchToken) {
fetchToken = request.get('/api/qiniu/token');
}
try {
this.token = await fetchToken;
}
catch(err) {
console.error(err);
}
}
},
created() {
this.fetchToken();
}
};

上面是一个简单的缓存上传 token 的例子,并且会在上传失败时刷新 token。

与直接缓存 Token 的值比较,缓存请求有什么好处?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 缓存值的代码
export default {
methods: {
fetchToken() {
if (!fetchToken) {
fetchToken = await request.get('/api/qiniu/token');
}
try {
this.token = fetchToken;
}
catch(err) {
console.error(err);
}
}
}
}

一个比较常见的 Upload 组件 的应用场景,在一个页面里同时使用多次该组件。

1
2
3
4
<template>
<div class="upload1"><upload /></div>
<div class="upload2"><upload /></div>
</template>

就上面的代码例子,如果使用缓存值的方法,那么页面一打开就会请求两次获取 Token 接口。

继续完善 Upload 组件

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
//Upload.vue
let fetchToken = null;
export default {
methods: {
async upload() {
try {
this.fetchToken();
const token = await fetchToken;
// ...
} catch (err) {
alert(err.message);
this.refreshToken();
}
},
refreshToken() {
fetchToken = null;
this.fetchToken();
},
fetchToken() {
if (!fetchToken) {
fetchToken = request.get('/api/qiniu/token');
}
}
},
created() {
this.fetchToken();
}
};

为了防止多个 Upload 组件 token 不同步问题,不再通过this.token保存 token,而是每次都等待 fetchToken resolved,保证获取到的 token 一定是最新的。

当然,这里还有很多需要优化,例如失败后的重试、判断是 401 失败才刷新 token、设置错误时间、定时刷新等等,但总体思路就是上面代码所展示的内容。

另外再介绍一个经典应用场景

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
const fetchConfig = (() => {
let configRequest = null;
return () => {
if (!configRequest) {
configRequest = Promise.all([services.customer.config1, services.customer.config2])
.then(([data1, data2]) => {
return { data1, data2 };
})
.catch(err => {
configRequest = null;
return Promise.reject(err);
});
}
return configRequest;
};
})();

export default {
async beforeRouteEnter(to, from, next) {
try {
// 配置信息仅需要成功请求一次
const [data, config] = await Promise.all([services.customer.getInfo(), fetchConfig()]);
next(vm => {
vm.data = data;
vm.config = config;
vm.init();
};
} catch (err) {
next(err);
}
}
};
分享到评论

好的业务组件设计

一个好的业务组件必然是逻辑清晰以及方便修改维护。

下面以 Vue 为例子进行进行概念上的简单说明。

一些涉及到的概念

  1. 数据驱动 UI
  2. 单向数据流
  3. 有限状态机

模板

模板应保证逻辑清晰,业务复杂的部分可拆分成独立的业务组件又或者通过 computed 组装数据关系。

不应该在模板写逻辑语句,仅使用简单的条件判断以及方法调用或表达式。

状态的设计(data、computed)

状态分全局状态以及本地状态,全局状态就是 sotre(Vuex 或者自己另外定义的 Vue 对象),本地状态包括数据(data)以及根据数据响应的状态(computed)。

需要根据 data、store 或者 其他 computed 响应的同步状态都属于 computed,computed 必须是同步数据,computed 内部禁止任何异步操作。

所有需要异步获取以及无法由其他数据响应变化的数据都是 data。

有时候可以把 computed 作为一个不可写属性使用,返回一个常量或者其他想输入到模板的值。

原则是 data 的结构应清晰简单,数据之间的关系放在 computed。

数据的处理

服务器获取的数据不要在业务组件直接写请求,应通过 services 封装。

数据单位应保存一致(例如时间使用 13 位,金钱使用分),需要进行转换的数据按就近原则进行处理:如果是服务器数据在 services 进行转换,如果是用户输入的数据,通过 computed 进行 get/set 处理。

发送到服务器的数据也是一样,应在 services 里面处理数据的单位以及一些简单的判断。

保证代码的可维护性

  1. 代码、数据逻辑之间的关系应保持简单与一致,不应存在多种不同的业务关系模型。
  2. 单向数据流。
  3. 数据处理以及操作分开。
  4. 交互状态复杂的业务组件应用有限状态机以及 computed。
分享到评论

几个CSS技巧的分享

创建剪切动画

对于剪切动画,使用clip-path代替width/height,避免DOM重排导致性能过低。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.animate {
width: 200px;
height: 200px;
background: #000;
animation: 1s clip;
}
@keyframes clip {
0% {
clip-path: inset(0 0 0 0);
}
100% {
clip-path: inset(0 100% 100% 0);
}
}

clip-path也能用来进行其他规则/不规则图形的剪切

1
2
3
4
5
.clip {
clip-path: polygon(0 100%, 50% 0, 100% 100%, 0 30%, 100% 30%); /* 多边形 */
clip-path: circle(30px at 35px 35px); /* 圆形 */
clip-path: ellipse(30px 25px at 35px 35px); /* 椭圆 */
}

优化动画性能

除了使用transform3d开启gpu加速,还可以使用will-change强制gpu加速优化动画性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.animate {
width: 200px;
height: 200px;
background: #000;
animation: 1s clip;
will-change: clip-path;
}
@keyframes clip {
0% {
clip-path: inset(0 0 0 0);
}
100% {
clip-path: inset(0 100% 100% 0);
}
}

实现长宽比

使用padding模拟,然后子元素使用绝对定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 1:1 */
.container {
width: 200px;
}
.container:after {
display: block;
content: ' ';
padding-top: 100%;
}

/* 16:9 */
.container {
width: 200px;
}
.container:after {
display: block;
content: ' ';
padding-top: calc(100% * 9 / 16);
}

垂直居中

我们常用的方式:

  • dislay: inline-block
  • top: 50% + transform: tranlsateY(-50%)
  • display: flex

其余还有padding上下撑高display: tableposition + margin: auto绝对定位 + margin等等,这些属于不常用、特殊场景才能用、CSS3之前的hack方式,CSS3之后就不必使用这些来实现垂直居中,就不多说了。

其中display: flex属于万金油,大多数场景可以直接用它,但还是有些特殊的场景不能用:

  1. 子元素需要文字截断,为了兼容4.X的Android浏览器,必须使用其他方式(一般是transform)
  2. 子元素需要多行布局,4.x的Android不支持flex-wrap,不能多行布局
分享到评论

使用node.js构建命令行工具

工具说明

  • inquirer.js:一个封装了常用命令行交互的node.js模块,通过该模块可以很方便地构建一个新的命令行应用。

  • shell.js:跨平台的unix shell命令模块。

  • Node版本:由于inquirer.js的异步方法默认返回Promise,建议使用node.js>=8。

目标

工作中有大量项目上线前最后一步需要执行测试、编译、更新版本号、提交,甚至执行的命令都是一样,在这里我们通过命令行工具将这些步骤一键自动化,同时进行预检查,防止错漏。

准备

  1. 创建一个新的Node.js项目。
  2. 创建文件bin/my-cli.js,node.js项目通常会把cli入口放在bin目录下,其他模块放在lib目录下。
  3. 在bin/my-cli.js文件头部添加#!/usr/bin/env node
  4. 添加"bin": {"my-cli": "./bin/my-cli.js"},到package.json,声明我们要使用的命令。
  5. 项目根目录下执行npm link,创建一个全局命令my-cli

稍微修改下my-cli.js,添加代码console.log("I am a cli tool!"),然后打开控制台运行my-cli命令,如果看到控制台输出I am a cli tool!就表示成功。

安装依赖

首先安装主要依赖的两个模块(关于这两个模块的使用请参考官方文档)

npm install inquirer shelljs

构建发布流程自动化

接下来首先实现测试、更新版本号、构建、自动提交发布的自动化

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
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));

const { version } = await inquirer.prompt([
{
type: 'list',
name: 'version',
message: '版本号更新方式:',
choices: [
{
name: `v${semver.inc(pkg.version, 'patch')}: Fix Bugs / Patch`,
value: 'patch'
},
{
name: `v${semver.inc(pkg.version, 'minor')}: Release New Version`,
value: 'minor'
},
]
}
]);
// 拉取最新版本
shelljs.exec('git pull');
// 运行测试
shelljs.exec('npm test');
//通过npm version更新版本号,但不自动添加git tag,而是在构建完成后由cli工具添加
shelljs.exec(`npm version ${version} --no-git-tag-version`);
// 构建
shelljs.exec('npm run build');
// 提交发布代码
const nextVersion = semver.inc(pkg.version, version);
shelljs.exec('git add . -A');
shelljs.exec(`git commit -m "build: v${nextVersion}"`)
shelljs.exec(`git tag -a v${nextVersion} -m "build: ${nextVersion}"`);
shelljs.exec("git push")
shelljs.exec("git push --tags");

添加新功能:配置检查

接下来给my-cli添加一个功能:

当检查到package.json的my-cli对象的check-baidu-id属性为true时,检查项目的config.json是否存在baidu-id属性

1
2
3
4
5
6
7
8
9
10
11
12
if (pkg['my-cli'] && pkg['my-cli']['check-baidu-id']) {
const configPath = path.join(process.cwd(), 'config.json');
if (!fs.existsSync(configPath)) {
shelljs.echo('找不到config.json');
shelljs.exit(1);
}
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (!config['baidu-id']) {
shelljs.echo('config.json缺少属性[baidu-id]');
shelljs.exit(1);
}
}

最后一步

这样一个简单的cli程序就实现完毕了,它自动化了构建发布流程,构建发布之前还进行了配置检查。

在实际项目中,为了提高程序的稳定性,还需要添加检查当前项目是否存在package.json,防止json解析出错、执行前确认等功能,具体见示例代码。

示例代码

地址:https://github.com/Aturan/node-cli-example

结语

虽然上述功能使用shell也可以实现,但代码编写就没那么方便快速,而且一旦碰到更复杂的问题,用shell实现就很麻烦,维护也是一个问题。

PS. 其实也可以用python,对于Ubuntu,系统自带Python是一个优势,在服务器不需要安装环境就可以直接使用,再加上Python也有Inquirer模块。

分享到评论