fox的博客

github地址 -> https://github.com/FoxDaxian


  • Home

  • Archives

tapable简析

Posted on 2021-05-09 In 原理

由于webpack底层使用Tapable创建和执行钩子,所以也大致了解下tapable的原理

Tapable使用es6语法翻新了一遍,目前都是基于类的方式编写的。我们先看下目录结构:

alt
我们其实主要看Hook.js和HookCodeFactory.js就够了,其他的基本都是大同小异的,都是依赖于这两个基类,然后扩展Hook的compile方法和HookCodeFactory的content方法,下面直接上代码。
因为几乎是大同小异,所以我们这里以最简单的SyncHook为实例:

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
"use strict";
// 基类
const Hook = require("./Hook");
const HookCodeFactory = require("./HookCodeFactory");

class SyncHookCodeFactory extends HookCodeFactory {
content({ onError, onResult, onDone, rethrowIfPossible }) {
return this.callTapsSeries({
onError: (i, err) => onError(err),
onDone,
rethrowIfPossible
});
}
}

const factory = new SyncHookCodeFactory();

class SyncHook extends Hook {
// 未提供的方法直接抛出异常提示
tapAsync() {
throw new Error("tapAsync is not supported on a SyncHook");
}

tapPromise() {
throw new Error("tapPromise is not supported on a SyncHook");
}
// 为调用方法call、callAsync、promise提供依赖
compile(options) {
factory.setup(this, options);
return factory.create(options);
}
}

module.exports = SyncHook;

正如上面的代码所以,直观来看就是集成了两个基类,然后导出钩子函数,先从Hook说起:

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
"use strict";

const util = require("util");
// 抛出警告
const deprecateContext = util.deprecate(() => {},
"Hook.context is deprecated and will be removed");

class Hook {
constructor(args = []) {
// 参数们
this._args = args;
// 类似监听模式下的队列
this.taps = [];
// 拦截器
this.interceptors = [];
// 初始化相关调用方法
this.call = this._call;
this.promise = this._promise;
this.callAsync = this._callAsync;
// 自定义的方法队列
this._x = undefined;
}

compile(options) {
// 必须重写compile方法
throw new Error("Abstract: should be overriden");
}
// 用于创建相关调用方法的私有方法
_createCall(type) {
// 被重写的compile方法
return this.compile({
taps: this.taps,
interceptors: this.interceptors,
args: this._args,
type: type
});
}

_tap(type, options, fn) {
// 参数处理
if (typeof options === "string") {
options = {
name: options
};
} else if (typeof options !== "object" || options === null) {
throw new Error("Invalid tap options");
}
if (typeof options.name !== "string" || options.name === "") {
throw new Error("Missing name for tap");
}
if (typeof options.context !== "undefined") {
deprecateContext();
}
options = Object.assign({ type, fn }, options);
// 拦截器
options = this._runRegisterInterceptors(options);
this._insert(options);
}

tap(options, fn) {
this._tap("sync", options, fn);
}

tapAsync(options, fn) {
this._tap("async", options, fn);
}

tapPromise(options, fn) {
this._tap("promise", options, fn);
}
// 拦截器
_runRegisterInterceptors(options) {
for (const interceptor of this.interceptors) {
if (interceptor.register) {
const newOptions = interceptor.register(options);
if (newOptions !== undefined) {
options = newOptions;
}
}
}
return options;
}

withOptions(options) {
const mergeOptions = opt =>
Object.assign({}, options, typeof opt === "string" ? { name: opt } : opt);

// Prevent creating endless prototype chains
options = Object.assign({}, options, this._withOptions);
const base = this._withOptionsBase || this;
const newHook = Object.create(base);

newHook.tap = (opt, fn) => base.tap(mergeOptions(opt), fn);
newHook.tapAsync = (opt, fn) => base.tapAsync(mergeOptions(opt), fn);
newHook.tapPromise = (opt, fn) => base.tapPromise(mergeOptions(opt), fn);
newHook._withOptions = options;
newHook._withOptionsBase = base;
return newHook;
}

isUsed() {
return this.taps.length > 0 || this.interceptors.length > 0;
}

intercept(interceptor) {
this._resetCompilation();
this.interceptors.push(Object.assign({}, interceptor));
if (interceptor.register) {
for (let i = 0; i < this.taps.length; i++) {
this.taps[i] = interceptor.register(this.taps[i]);
}
}
}

_resetCompilation() {
this.call = this._call;
this.callAsync = this._callAsync;
this.promise = this._promise;
}

_insert(item) {
this._resetCompilation();
let before;
if (typeof item.before === "string") {
before = new Set([item.before]);
} else if (Array.isArray(item.before)) {
before = new Set(item.before);
}
let stage = 0;
if (typeof item.stage === "number") {
stage = item.stage;
}
let i = this.taps.length;
while (i > 0) {
i--;
const x = this.taps[i];
this.taps[i + 1] = x;
const xStage = x.stage || 0;
if (before) {
if (before.has(x.name)) {
before.delete(x.name);
continue;
}
if (before.size > 0) {
continue;
}
}
if (xStage > stage) {
continue;
}
i++;
break;
}
this.taps[i] = item;
}
}

function createCompileDelegate(name, type) {
return function lazyCompileHook(...args) {
this[name] = this._createCall(type);
return this[name](...args);
};
}

Object.defineProperties(Hook.prototype, {
_call: {
value: createCompileDelegate("call", "sync"),
configurable: true,
writable: true
},
_callAsync: {
value: createCompileDelegate("callAsync", "async"),
configurable: true,
writable: true
},
_promise: {
value: createCompileDelegate("promise", "promise"),
configurable: true,
writable: true
}
});

module.exports = Hook;

我们需要用tap、tapAsync、tapPromise往队列中添加函数,然后使用call来调用,不过call会根据不同类型的钩子产出不同的函数,这也是tapable做的优化,而这些函数的产出这来自HookCodeFactory:

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
"use strict";

class HookCodeFactory {
constructor(config) {
this.config = config;
this.options = undefined;
this._args = undefined;
}
// 创建相关的方法体,对不同的type进行判断
// Fcuntion接受任意个参数,除最后一个,之前的都是函数的形参
create(options) {
this.init(options);
let fn;
switch (this.options.type) {
case "sync":
fn = new Function(
this.args(),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `throw ${err};\n`,
onResult: result => `return ${result};\n`,
onDone: () => {
console.log('完成函数');
},
rethrowIfPossible: true
})
);
break;
case "async":
fn = new Function(
this.args({
after: "_callback"
}),
'"use strict";\n' +
this.header() +
this.content({
onError: err => `_callback(${err});\n`,
onResult: result => `_callback(null, ${result});\n`,
onDone: () => "_callback();\n"
})
);
break;
case "promise":
let code = "";
code += '"use strict";\n';
code += "return new Promise((_resolve, _reject) => {\n";
code += "var _sync = true;\n";
code += this.header();
code += this.content({
onError: err => {
let code = "";
code += "if(_sync)\n";
code += `_resolve(Promise.resolve().then(() => { throw ${err}; }));\n`;
code += "else\n";
code += `_reject(${err});\n`;
return code;
},
onResult: result => `_resolve(${result});\n`,
onDone: () => "_resolve();\n"
});
code += "_sync = false;\n";
code += "});\n";
fn = new Function(this.args(), code);
break;
}
this.deinit();
return fn;
}
// 设置Hook中的_x变量
setup(instance, options) {
instance._x = options.taps.map(t => t.fn);
}

/**
* @param {{ type: "sync" | "promise" | "async", taps: Array<Tap>, interceptors: Array<Interceptor> }} options
*/
init(options) {
this.options = options;
this._args = options.args.slice();
}

deinit() {
this.options = undefined;
this._args = undefined;
}
// 创建函数顶部内容
header() {
let code = "";
if (this.needContext()) {
code += "var _context = {};\n";
} else {
code += "var _context;\n";
}
code += "var _x = this._x;\n";
if (this.options.interceptors.length > 0) {
code += "var _taps = this.taps;\n";
code += "var _interceptors = this.interceptors;\n";
}
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.call) {
code += `${this.getInterceptor(i)}.call(${this.args({
before: interceptor.context ? "_context" : undefined
})});\n`;
}
}
return code;
}

needContext() {
for (const tap of this.options.taps) if (tap.context) return true;
return false;
}

callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) {
let code = "";
let hasTapCached = false;
for (let i = 0; i < this.options.interceptors.length; i++) {
const interceptor = this.options.interceptors[i];
if (interceptor.tap) {
if (!hasTapCached) {
code += `var _tap${tapIndex} = ${this.getTap(tapIndex)};\n`;
hasTapCached = true;
}
code += `${this.getInterceptor(i)}.tap(${
interceptor.context ? "_context, " : ""
}_tap${tapIndex});\n`;
}
}
code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n`;
const tap = this.options.taps[tapIndex];
switch (tap.type) {
case "sync":
if (!rethrowIfPossible) {
code += `var _hasError${tapIndex} = false;\n`;
code += "try {\n";
}
if (onResult) {
code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
} else {
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
}
if (!rethrowIfPossible) {
code += "} catch(_err) {\n";
code += `_hasError${tapIndex} = true;\n`;
code += onError("_err");
code += "}\n";
code += `if(!_hasError${tapIndex}) {\n`;
}
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone && onDone()) {
code += onDone();
}
if (!rethrowIfPossible) {
code += "}\n";
}
break;
case "async":
let cbCode = "";
if (onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => {\n`;
else cbCode += `_err${tapIndex} => {\n`;
cbCode += `if(_err${tapIndex}) {\n`;
cbCode += onError(`_err${tapIndex}`);
cbCode += "} else {\n";
if (onResult) {
cbCode += onResult(`_result${tapIndex}`);
}
if (onDone) {
cbCode += onDone();
}
cbCode += "}\n";
cbCode += "}";
code += `_fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined,
after: cbCode
})});\n`;
break;
case "promise":
code += `var _hasResult${tapIndex} = false;\n`;
code += `var _promise${tapIndex} = _fn${tapIndex}(${this.args({
before: tap.context ? "_context" : undefined
})});\n`;
code += `if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n`;
code += ` throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n`;
code += `_promise${tapIndex}.then(_result${tapIndex} => {\n`;
code += `_hasResult${tapIndex} = true;\n`;
if (onResult) {
code += onResult(`_result${tapIndex}`);
}
if (onDone) {
code += onDone();
}
code += `}, _err${tapIndex} => {\n`;
code += `if(_hasResult${tapIndex}) throw _err${tapIndex};\n`;
code += onError(`_err${tapIndex}`);
code += "});\n";
break;
}
return code;
}
// 创建call函数内容
callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) {
if (this.options.taps.length === 0) return onDone();
const firstAsync = this.options.taps.findIndex(t => t.type !== "sync");
const next = i => {
if (i >= this.options.taps.length) {
return onDone();
}
const done = () => next(i + 1);
const doneBreak = skipDone => {
if (skipDone) return "";
return onDone();
};
return this.callTap(i, {
onError: error => onError(i, error, done, doneBreak),
onResult:
onResult &&
(result => {
return onResult(i, result, done, doneBreak);
}),
onDone:
!onResult &&
(() => {
return done();
}),
rethrowIfPossible:
rethrowIfPossible && (firstAsync < 0 || i < firstAsync)
});
};
return next(0);
}


args({ before, after } = {}) {
let allArgs = this._args;
if (before) allArgs = [before].concat(allArgs);
if (after) allArgs = allArgs.concat(after);
if (allArgs.length === 0) {
return "";
} else {
return allArgs.join(", ");
}
}

getTapFn(idx) {
return `_x[${idx}]`;
}

getTap(idx) {
return `_taps[${idx}]`;
}

getInterceptor(idx) {
return `_interceptors[${idx}]`;
}
}

module.exports = HookCodeFactory;

核心内容都在这里,剩下的方法基本都大同小异,具体的代码细节并没有仔细去看,毕竟是别人写的,就好像你通过答案接触题目一样,感觉时间成本太高,所以仅仅是简单了解一下。
有一点很重要,比如SyncHook和SyncLoopHook这两种方法,他们的区别仅仅就是产出的函数内容不一样,前者是直接获取钩子函数然后调用,后者是一个do-while循环调用钩子函数,可想而知剩余的方法的区别也是方法内体,也就是说你需要更多的关注content方法的onResult这个参数

什么是tree shaking,它是怎样工作的?

Posted on 2021-05-09 In 原理简析(翻译)

当javascript应用体积越来越大时,一个有利于减少体积的办法是拆分为不同的模块,伴随着模块化的产生,我们也可以进一步的移除多余的代码,比如那些虽然被应用,但是没有被实际用到的代码。tree shaking就是上述说法的一种实现,它通过去除所有引入但是并没有实际用到的代码来优化我们的最终打包结果的体积。

比如说,我们有一个工具文件,其中包含一些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// math.js
export function add(a, b) {
console.log("add");
return a + b;
}

export function minus(a, b) {
console.log("minus");
return a - b;
}

export function multiply(a, b) {
console.log("multiply");
return a * b;
}

export function divide(a, b) {
console.log("divide");
return a / b;
}

在我们的应用入口文件中,我们仅仅引入其中某一个方法,比如 add

1
2
3
4
// index.js
import { add } from "./math";

add(1, 2);

假设我们使用webpack进行打包,下面是结果,我们仍然可以看到所有的方法,虽然我们仅仅想引入add方法

不过,一旦我们开启 tree shaking,就只有我们引入的add方法会出现在bundle中


tree shaking的原理

尽管90年代就有了tree shaking的概念,但是对于前段来说,tree shaking真正可以使用是在引入 es6-module 后。因为tree shaking 仅仅能分析静态语法。
在 es6 modules 之前,我们有commonjs规范,可以通过require()语法引入,但是,require是动态的,意味着我们可以在if - else 中使用它。

1
2
3
4
5
6
7
var myDynamicModule;

if (condition) {
myDynamicModule = require("foo");
} else {
myDynamicModule = require("bar");
}

上面的是commonjs模块的语法,不过tree shaking无法使用,因为在程序运行之前无法判断哪个模块会被实际应用。也就是说语法分析不能识别。

es6引入了新的完全静态的语法,使用import发育,不在支持动态引入。(后来引入了的import(),返回promise,还是支持动态引入的.)

1
2
3
4
5
6
// not work
if (condition) {
import foo from "foo";
} else {
import bar from "bar";
}

相反,我们只能定义所有的引入在if - else之外的全局环境内,

1
2
3
4
5
6
7
8
import foo from "foo";
import bar from "bar";

if (condition) {
// do stuff with foo
} else {
// do stuff with bar
}

除了其他的改进好处,新的语法有效的支持了tree shaking,任何代码不需要运行就可以通过语法解析判断出是否真正呗用到,以便进一步减少打包后的体积

tree shaking 甩掉了什么

webpack中的tree shaking,尽可能的甩掉了所有未使用到的代码,例如,引入了但是没有实际应用的代码会被消除。

1
2
3
import { add, multiply } from "./mathUtils";

add(1, 2);

上面的代码,multiply因为被实际应用,所以会被消除

即使对象上有定义的属性,但是如果没有被访问,也会被移除

1
2
3
4
5
// myInfo.js
export const myInfo = {
name: "Ire Aderinokun",
birthday: "2 March"
}
1
2
3
import { myInfo } from "./myInfo.js";

console.log(myInfo.name);

before:

after:

不过 tree shaking 并不能消除所有的代码,因为 tree shaking 只会处理方法和变量,看下面的例子

1
2
3
4
5
6
7
8
9
// myClass.js
class MyClass {}

MyClass.prototype.saySome = function () {} // 副作用

// 扩展数据的方法
Array.prototype.unique = function () {}

export default MyClass
1
2
3
import MyClass from './myClass';

console.log('index');

这个时候就不会消除类文件myClass,因为会触发getter、setter,而getter、setter是不透明的,可能会有副作用
详情参考

rollup在线转换

也许你会说我们能判断是否是原生方法,进而进行排除,其实不然,我们可以这样

1
2
3
let a = 'a';
a += 'rr';
a += 'ay';

一句话就是,因为js本身是动态语言,所以情况太多,处理起来会有风险,tree shaking 的本意是优化,而不是影响,所以不是所有的代码都可以tree shaking

关于 ‘side effects’

中文直译:副作用
什么是副作用:对当前包意外产生任意影响就是副作用

一个典型的具有副作用的形式是 pollfills,因为它在window上增加了各种宿主环境不支持的最新方法,所以不能添加side effects

如何tree shake

webpack目前支持通过设置mode来开启,比如

1
2
3
4
5
module.exports = {
...,
mode: "production",
...,
};

webpack老版本依赖uglifyjs

variable fonts - 更小更灵活的字体

Posted on 2021-05-09 In 原理

原文链接

variable fonts(下文中vf为缩写)是数字时代制作的字体技术,用更小的文件大小在web上提供更丰富的排版,但是一项新的技术往往伴随着新的挑战和复杂未知的情况。不过,我们要拥抱技术,那么怎么才能使用它呢?

让我们从以下几个问题去学习一下variable fonts。

  • 什么是variable fonts?
  • variable fonts能做什么
  • 拉伸或者扭曲字体会不会有不好的效果和影响?
  • variable fonts有哪些优点?
  • 怎么在web上使用variable fonts?
  • 有哪些潜在的缺陷需要注意?
  • variable fonts何时才会相对成熟?

什么是variable fonts?

有人解释它为一个存在多种字体格式单字体文件。一般来说,字体的不同格式,比如斜体、粗细、拉伸存储在分开的单个文件内,而现在,你可以存储多种字体格式在一个openType可变字体文件内,正因为如此,这个vf文件相对来说体积会更小。

资源

多个静态字体文件可以被存储到一个vf文件

因为只有一种排版的轮廓点,所以文件体积很小。这些点决定了文字的直接表现。修改轮廓点的位置意味着能够动态的在浏览器里改变文字的样子。这使得在半粗体和粗体之间的转换成为可能。向下面这样:

资源

修改vf字体的例子,这些`轮廓点`的数量没有变化,仅仅是位置发生了改变

在各种轴(将一个可修改范围抽象化为一条(x)轴,或者说横坐标)上可以以非常小的数值进行改变,比如粗细轴,一点点的修改就会发生很大的风格变化,像regular和font-weight: 700之间有其他的值可以进行指定。

资源

一个vf字体可以有很多类似的轴,比如增加一个身高轴,就能延伸出更多风格的字体。也可以想想成为一个有x和y的坐标轴,当x轴的可修改范围为50,y轴的可修改范围为500的时候,你就会得到25000中不同风格的字体,并且文件大小也很可观。

资源

各种各样的字体

variable fonts能做什么?

这个得根据字体的设计来决定,字体的设计提供了各种各种可以被修改的轴,比如粗细,长短以及任何合理范畴之内的。下面提供五个常用的保留轴:

  • wdth: 用于修改字的宽窄
  • wght: 用于修改字的粗细
  • ital: 是否倾斜,0为非倾斜,1为斜体
  • slnt: 用于修改字的倾斜程度
  • opsz: 对于字形的修改(待确认)

尽管宽窄、粗细是更为常见的供修改轴,但是也有一些自定义轴,比如衬线(衬线是字的笔画开始和结束部分的额外装饰)轴等。

资源

通过改变`轴`生成的动画,有没有很酷炫?

拉伸或者扭曲字体会不会有不好的效果和影响?

当vf字体改变宽窄、粗细或者其他维度的时候,不会造成不好的影响。但是如果换做transform: scaleX(0.5),就会发生不好的影响,因为它直接修改了字体的设计,设计师看了会打人。

为什么拉伸或者扭曲字体是一个很严重的问题?因为字体设计师在每个字符的协调上下了很多心血,所以这样的字体符合正常的审美。而草率的拉伸或者扭曲字体会导致设计师的心血功亏一篑。即使修改之后的不同是很微小的,但是也会影响字体整体的外观和感觉。

资源

仔细看上面这张图中的字母O,下面的O已经超出蓝色范围,而上面的依旧保持的很好。吐槽,本人没觉得有啥美感的丢失

variable fonts有哪些优点?

最明显的优点,或许你已经想到了,就是提供丰富的自定义web字体

网站开发者可以利用不同风格的字体去突出某些部分的趣味性和重要性,网站可以以编辑设计的方式处理更多的排版,提供更丰富的视觉展示和个性化方案。我创建了一个测试网站,在这个网站上,我限制颜色风格,换句话说,我仅仅用了4中左右的颜色来表现网站的层次感,然后用了多大18中的字体去丰富网站。在我看来,这样比减少样式风格更加简介和独特。你可以点击右下角按钮来切换不同的字体主题,获得不同的体验。

资源

一个使用字体变换改变网站风格的测试网站

更小的文件体积

vf字体用更小的文件带来更多的可选风格。比如你想使用三四种不同粗细的字体,你可以用vf字体来获得更小文件体积的收益。举个例子:Süddeutsche Zeitung Magazin
该网站的字体加起来一共有236kb大小,其中四中不同粗细的字体加起来共166kb,如果换用vf字体,可以较少到80kb,足足减少了50%!

资源

如果使用vf字体,至少可以节约一半的带宽

细颗粒度的控制

vf字体在如何渲染字体上提供细颗粒度的控制,你可以设置font-weight:430来提供更好地效果。因为这是一个可选的,所以像font-weight:bold这样的方式,仍然是奏效的

更好地文字适配

如果vf字体提供宽窄轴操作能力,你可以让文字在移动设备上有更好的可读性。在宽一点的屏幕上,也能更好地利用空间。这个例子可以很清晰的展示这种效果: browser example

与动画结合

所有轴都可以通过css来产生一个过渡的动画效果。这能让你的网站带来很酷和充满活力的效果。在微软示例页面中,你可以通过滚动来查看令人印象深刻的动画效果。

一种更重视视觉美的文字

这个概念来自印刷技术,通常指在小字号的时候更加可读,大字号的时候更加富有个性。在金属活字时期(使用金属作为载体的活字印刷术),只能通过修改的文字尺寸来进行优化。后来,通过数字化技术,你可以设计一个适配所有尺寸的字体。现在相同的情况随着vf字体的出现得以解决。例如,小字号的时候笔画可以更粗一点,这意味着更低的对比度使可读性更高。另一方面,当大字号的情况下,空间更多,所以有更多的操纵性,和对比度。类似的变化在vf字体中可以在单一文件内逐渐产生。
资源

怎么在web上使用variable fonts?

  1. 找到可用的vf字体
    这个技术还是非常积极地,所以,如果你想使用它,你首先要找到相关资源。这有一个资源可以供你使用,在这个网站里你能尝试很多vf字体,很多都是在github开源,并且可以直接下载的。这也有一些很不错的资源

  2. 整合到你的网站中的样式表内
    2018上半年,超过一般的浏览器已经支持的很好了。

    通过@font-face引入到页面内:

    1
    2
    3
    4
    5
    @font-face {
    font-family: 'VennVF';
    src: url('VennVF_W.woff2') format('woff2-variations'),
    url('VennVF_W.woff2') format('woff2');
    }

    找到字体可变轴和可变范围,根据设计的不同的vf都有不同的轴和不同的范围,如果你不知道vf字体能做什么改变,你可以使用在线工具,他也可以帮你生成现成的css。
    然后我们进行css的开发,不过在这之前,先说一下即将在css4字体模块中增加的可以设置vf字体的高级属性:

    • font-weight:可以设置1-999的任意数值

    • font-stretch:是一个百分比的值,100%是正常的,50%是紧缩的,200%是拉伸的,其对应的关键字应该可以使用,这对印刷来说是可怕的,因为它不能拉伸字体,拉伸字体会导致不好的结果,但是vf的改变是在涉及范围内的拉伸,是可以接受的。

    • font-style:一个倾斜的属性,从-90deg到90deg,当然关键字也是可以使用。90deg看起来是奇怪的,8deg是大部分字体中采用的最大值。

    • font-optical-sizing:这是一个新的属性,有两个可选属性auto和none。一般来说,浏览器会设置为auto,但你也可以设置为none

      不是所有vf字体都能控制上面的属性,这得根据字体的设计和可用范围来决定。我做了一些测试,safari支持font-weight和font-stretch,并且,如果optical可用,它会自动打开optical sizing。但是使用font-style: italic的结果是,没有更新vf字体的italic轴范围。

      只有在sarari上,这些高级属性兼容的还可以。所以,如果想保证稳定性,你需要使用一个低级的属性:font-variation-settings,你可以设置四部分,其实和上面的差不多。

      1
      2
      3
      4
      p {
      font-family: "VennVF";
      font-variation-settings: "wght" 550, "wdth" 125;
      }

      这段代码改变字体粗细为550,还有宽窄为125。在不远的将来,你或许可以使用高级属性来得到同样的效果:

      1
      2
      3
      4
      5
      p {
      font-family: "VennVF";
      font-weight: 550;
      font-stretch: 125%;
      }

      当然,vf字体其实还有更多的自定义轴可以使用,都可以使用font-variation-setting属性来设置:

      1
      2
      3
      4
      h1 {
      font-family: 'VennVF', sans-serif;
      font-variation-settings: "TRMC" 0, "SKLA" 0, "SKLB" 0, "TRME" 0;
      }

      效果看起来像这样:

      资源

  3. 兼容不支持vf字体的浏览器

    如果你现在就想使用vf字体的话,在不支持的版本上,网站风格会和你想象中的完全不一样,所以我们需要一个回退方案,这个利用的css的特性查询功能:

    1
    2
    3
    @supports (font-variation-settings: normar){
    /* set some property */
    }

    点击查看@supports的各浏览器兼容,个人认为兼容还是可以的。
    然后,像下面这样设置vf,就可以适配大部分浏览器了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    body {
    font-family: 'Venn', sans-serif;
    }

    @supports (font-variation-settings: normal) {
    body {
    font-family: 'VennVF', sans-serif;
    }
    }

    解释一下:首先上面的body为正常的字体,下面为积极地做法,如果支持font-variation-settings,那么就采取vf字体,然后可以设置一些具体的字体细节。否则会静默失败。
    可能有人会用:not来配合@supports,有时候匹配成功不是因为not,而是因为@supports不支持,所以尽量避免。

有哪些潜在的缺陷需要注意?

vf字体为web字体带来了新的活力和发灰控件,但是,一项新的技术往往会伴随着很多我们需要注意的问题。

  • 太多的选项
  • 更多的与web无关的字体只是需要学习
  • vf字体不一定总会对性能有所提高
  • 你也许仍然需要多个字体文件以适配某些字体,比如罗马字体和斜体
  • 可能会因为著作权、许可证而造成其他问题

variable fonts何时才会相对成熟?

2018年大部分浏览器都已经支持了,很快移动设备也会支持,因为vf会节约很大的带宽。我期待2019年vf字体能够替换静态字体被用在各个web站点中。adobe和谷歌会在推动这项技术中一定会占主要部分,因为他们同样需要减少字体文件大小,虽然不知道这件事什么时候会发生。但是一定会很快。
我对文件大小没有太大的兴趣,我更多的兴趣实在使用更少的样色主题和更多的字体去设置网站的风格,像这个网站。

参考链接
  • 更好地方案去使用vf字体
  • vf字体详细说明
  • 可变字体
  • 字体历史

vue简析

Posted on 2021-05-09 In 原理

流程分析

响应式分析

web component

Posted on 2021-05-09 In 原理
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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>

<my-btn click-fnname="whenClick">
<div slot="text" class="text">123</div>
</my-btn>
<template class="template">
<style>
/* 指代当前宿主 */
:host {
display: block;
width: 100px;
height: 40px;
border: 1px solid rgba(0, 0, 0, .4);
border-radius: 10px;
text-align: center;
line-height: 40px;
cursor: pointer;
}

:host(:hover) {
border-color: blue;
}

.slotBox {
color: red;
}
</style>
<div class="slotBox">
<slot name="text" class=".slotText">默认的slot展示</slot>
</div>
</template>
<script>
const config = {
// ...
funcs: {
whenClick() {
console.log('我被点击了');
}
}
// ...
};
const customTemplate = (function (config) {
return class extends HTMLElement {
// 监听可以更新的属性
static get observedAttributes() {
return ['click-fnname'];
}
constructor() {
super();
this._clickFnname = () => {
console.log('默认点击事件');
}
// this 就是当前这个自定义的标签
this.template = document.querySelector('.template');
this.shadow = this.attachShadow({
mode: 'open'
})
// 添加template到shadow dom
this.shadow.appendChild(document.importNode(this.template.content, true))
}
// 当被添加的时候会被触发
connectedCallback() {
console.log('调用了');
this.addEventListener('click', this._clickFnname)
}
// 接绑的时候调用
disconnectedCallback() {
console.log('disconnectedCallback');

}
// 自定义标签属性改变时触发
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'click-fnname':
// 获取全局下的事件
this._clickFnname = config.funcs[newValue];
break;
default:
break;
}
}
}
})(config);
customElements.define('my-btn', customTemplate);

</script>
</body>

</html>

参考链接: 英文 | 中文 | 一些例子

webp

Posted on 2021-05-09 In 原理

2010年,谷歌发布了一种新的图片格式:WebP,它是可以当初png和jpg的一种替代格式,在不损失图片质量的前提下,会尽可能的减少文件大小.

WebP有哪些优点呢?

由于WebP提供的性能和优点,它简直完美。不像其他屙屎,WebP有无损和有损两个压缩方式,还支持动画和透明度

WebP png jpg gif
Lossy compression √ √ √ x
Lossless compression √ √ √ √
Transparency √ √ x √
Animation √ x x √

即使有这么多的优点,webp依然提供比其他格式更小的文件大小,在这个测试中,web有损图片比jpg格式要小30%,无损图片要比png格式小25。

怎么转换到webp格式呢

  1. 在线工具

    1. squoosh
    2. online-convert.com
  2. 命令行工具
    cwebp是一个不错的工具,可以转换图片到webp格式

    1
    2
    // cwebp -q [图片大小] [输入] -o [输出]
    cwebp -q 75 image.png -o image.webp
  3. node工具
    imagemin,还有它的插件imagemin-webp,是一个转换图片到webp格式的工具
    下面这个例子将所有的png和jpg图片转成webp

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /* convert-to-webp.js */

    const imagemin = require("imagemin");
    const webp = require("imagemin-webp");

    imagemin(["*.png", "*.jpg"], "images", {
    use: [
    webp({ quality: 75})
    ]
    });
  4. sketch
    可以使用sketch导出webp格式的图片

当下环境如何在开发中使用webp格式呢

可以先查看一下webp的兼容性
可以看到,当今各端支持率已高达70%多,虽然webp有这么多的有点,但是也不能直接使用,而不提供一种向后兼容的方式,否则在不支持的浏览器中,用户体验会很差。
我们可以使用HTML5中的元素,该标签允许为单张图片提供多个源。想下面这样

1
2
3
4
5
<picture>
<source type="image/webp" srcset="image.webp">
<source type="image/jpeg" srcset="image.jpg">
<img src="image.jpg" alt="My Image">
</picture>

source标签作为不同的源,img标签作为当浏览器不支持时的一种回退方案,当前支持率高达85%以上

除了html的办法,当然还有其他方案,比如:

1
2
3
var isSupportWebp = !![].map && document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') == 0;

console.log(isSupportWebp);

或者

1
2
3
4
5
6
7
8
9
10
11
window.isSupportWebp = false;//是否支持
(function() {
var img = new Image();
function getResult(event) {
//如果进入加载且图片宽度为1(通过图片宽度值判断图片是否可以显示)
window.isSupportWebp = event && event.type === 'load' ? img.width == 1 : false;
}
img.onerror = getResult;
img.onload = getResult;
// 如果可以在那么则支持
img.src = 'data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=';

webp官网

微信小程序简析

Posted on 2021-05-09 In 原理

流程图

demo

优化点:

最直接的方式的控制小程序的大小,比如制定代码规范和通用组件的控制
分包: 包括分包、分包预加载、独立分包
某些请求进行前置,比如某些量级很小的数据、用户必定会点击的页面的数据预请求
利用微信提供的缓存 storage
避免白屏,将一些静态或极少更新的状态传递给下一页,并结合骨架图进行页面的框架展示
控制setdata的使用次数和传达数据大小,建议一定少于64kb
减少WXML中非必须节点的使用
合理使用onpagescroll

分包介绍

本质上是按需加载,分为主包和分包,主包会运行app等公共方法和加载一些公共资源。当进入某个分包页面的时候,才会按需下载并执行展示出来。

目前小程序所有分包大小不超过8M

单个分包/主包不能超过2M

主要应用

小程序首次启动的下载和加载时间大大缩短

demo

demo

参数解释:

pages:主包

subpackages:分包

root:分包根目录 → 分包所在的目录
name:分包别名 → 预下载的时候可以引用该别名,方便
pages:相对于root的分包路径,可包含多个
preloadRule:开启分包预下载,key - value形式,key代表某个页面,比如上图中的index主页面,当进入该页面的时候,需要预下载哪些分包

packages:需要下载的分包,数组形式
network:可以指定只有在某些网络下才会下载,默认是wifi

关于独立分包:独立分支只需要在subpackages中的添加 independent: true 即可,注意不要依赖主包和其他分包的内容,可能无法获取全局数据,因为主包可能未加载,不过可以在独立主包中定义一个全局数据,等加载主要的时候会与主包和全局数据进行合并,

分析babel的输出

Posted on 2019-08-09 Edited on 2021-05-09 In 解析
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
  (function(modules) {
// 缓存对象
var installedModules = {};

// require方法
function __webpack_require__(moduleId) {
// 是否命中缓存
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 新建 + 缓存
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});

// 执行module方法
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);

// 标志是否已加载模块,之后缓存里会走
module.l = true;

return module.exports;
}

// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;

// expose the module cache
__webpack_require__.c = installedModules;


// 设置commonjs导出的对象上的a的值
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__._hasOwnProperty(exports, name)) {
// 利用 getter 可以通过a获取到module的值
Object.defineProperty(exports, name, {
enumerable: true,
get: getter
});
}
};

// 定义 __esModule 标志
__webpack_require__.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
Object.defineProperty(exports, '__esModule', {value: true});
};

// 适配commonjs规范
// 导出的为 require.a => 执行 getter函数 获得导出的内容
__webpack_require__.n = function(module) {
var getter =
module && module.__esModule
? function getDefault() {
return module['default'];
}
: function getModuleExports() {
return module;
};
__webpack_require__.d(getter, 'a', getter);
return getter;
};

// Object.prototype.hasOwnProperty.call
__webpack_require__._hasOwnProperty = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
})({})

概述下上面打包后的代码,是一个立即执行函数,接受的参数是一个对象,对象的key为引入的模块路径,对应的value为导出的内容,不过babel会根据ejs or cjs来进行不同的适配导出。
iife函数内为:

  1. installedModules 闭包环境缓存模块对象
  2. webpack_require 变种的require方法
  3. webpack_require.d 适配commonjs的转换方法
  4. webpack_require.r 给babel转换的es6模块增加标志,也就是通过该方法来设置区分ejs 和 cjs的标志
  5. _webpack_require.n 根据 __esModule 导出

举例说明:

我们有以下几个文件,内容都很简单。

1
2
3
4
// es6.js
export default {
type: 'esjs'
}
1
2
3
4
// commonjs.js
module.exports = {
type: 'commonjs'
}
1
2
3
4
5
6
7
8
9
// index.js
import es6 from './es6';
import conmon from './commonjs';

console.log(require('./es6'));
console.log(es6, 'import来的');

console.log(require('./commonjs'));
console.log(conmon, 'import来的');

通过webpack打包后的输出内容我们只取上面iife函数的参数部分,并去掉eval来提升可读性。

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
// bundle.js

(function(modules){
// ******
// 巴拉巴拉
// ******
// Load entry module and return exports
return __webpack_require__((__webpack_require__.s = './index.js'));
})({
'./commonjs.js': function(module, exports) {
module.exports = {type: 'commonjs'};
},
'./es6.js': function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);

// 相当于 module.exports.default,这就是为什么我们require的时候,需要加上 .default
__webpack_exports__['default'] = {
type: 'esjs'
};
},
'./index.js': function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__);
var es6FromImport = __webpack_require__('./es6.js');
var commonjsFromImport = __webpack_require__(
'./commonjs.js'
);
var commonjsFromImport_default = __webpack_require__.n(
commonjsFromImport
);
console.log(__webpack_require__('./es6.js'));
console.log(es6FromImport['default'], 'import来的');

console.log(__webpack_require__('./commonjs.js'));
console.log(
commonjsFromImport_default.a,
'import来的'
);
}
})

一点点分析,从入口开始 => webpack_require(‘./index.js’)
首先会查看是否命中缓存,如果命中,那理所当然直接返回,否则进行新建 + 缓存的操作,边边角角直接略过,咱们看下面这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// 新建
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
});

// 赋值
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);

// 导出
return module.exports;

首先新建,并创建默认的导出对象,这也就说明了为什么文件没有导出,默认是{}的问题,然后,利用call传递函数执行上下文环境,并传入module, module.exports, webpack_require 参数,最后return了module中的exports的值。

来看关键的赋值这一步。
针对es6js,因为你是export default,所以babel会增加一个__esModule变量,进行ejs的标识,因为我们导出的时候,会根据ejs规范,导出得对象赋值到default上,如下

1
2
3
4
5
// 赋值 __esModule
__webpack_require__.r(__webpack_exports__);
__webpack_exports__['default'] = {
type: 'esjs'
};

当我们使用ejs规范import的时候,babel会进行default的读取,所以我们可以直接获取到我们想要的值,然后如果你使用require的话,babel会按照commonjs直接进行读取,所以会导致我们需要 .default 才能拿到我们真正想要的值,结果如下

针对cjs,由于使用cjs规范,所以我们的导出是不涉及default的,即

1
module.exports = { type: 'commonjs' }

其实我们直接导出即可,因为 webpack_require 的返回值就是 module.exports。
不过打包后的代码用 webpack_require.n 对commonjs的导出做了处理,判断是否为 es6 规范的导出,如果是那么导出default,不是直接导出module.exports,然后使用 getter 设置返回函数的a属性,获取a属性即返回cjs module的导出,猜测这是对es6 import的统一处理。

1
2
3
4
5
6
// require
var commonjsFromImport = __webpack_require__('./commonjs.js');
// 设置getter
var commonjsFromImport_default = __webpack_require__.n(commonjsFromImport);

console.log(commonjsFromImport_default.a, 'import来的');

结果如下:

简单总结

require 对 ejs 规范的导出不是很友好,换句话说,考虑的很单一,所以会有default的问题
import 适配的 ejs 和 cjs,会根据 __esModule 进行导出的判断,返回使用者真正想要的

不过进步一认证,发现如果是ejs的导出,会直接导出webpack_export[‘default’],也就是 module.export.defualt,看起来不需要处理 __esModule 的请求,暂时还不清楚到底是怎么回事。有缘窃听下回分解吧。

具体babel是怎么解析的,暂时不涉及,只分析结果,得出一点点结论。

小提示

类似这种的代码 (0, foo.bar)() 相当于 foo.bar.call(winodw|global|this) 改变执行时的上下文环境

string.prototype.replace 特殊符号

Posted on 2019-08-05 Edited on 2021-05-09 In 介绍
  1. $$ => 代表 $

  1. $& => 代表匹配的内容

    $n => 代表正则匹配的第n个括号内的内容,意味着replace第一个参数为必须为正则

  1. $` => 代表匹配之前的所有内容

  1. $’ => 代表匹配之后的所有内容

cookieless记录用户信息?

Posted on 2019-07-29 Edited on 2021-05-09 In 畅想

cookie是什么:

cookie是由web服务器保存在用户浏览器(客户端)上的小文件,它可以包含用户信息,用户操作信息等等,无论何时访问服务器,只要同源,就能携带到服务端

常见方式

  1. 一般:请求一个接口,返回是否登录,如果登录成功,服务器(set-cookie)设置cookie到浏览器,以后请求api会继续请求
  2. jwt:将用户id.payload.签证进行加密,并且注入到客户端cookie,之后每次请求会在服务端解析该cookie,并获取对应的用户数据,由于存在客户端,所以解放了服务端,减少服务端压力。也可以将该cookie放到根域名下,这样就可以登录一次,遍地开花。

可以看到,常见的方式都是利用cookie(或者浏览器storage),这样你的信息还是会被看到,如果别人获取到你的cookie也有办法进行破解甚至直接复制登录。那么有没有办法不借用cookie来记录用户信息的?


利用缓存存储用户信息

  1. 优点:安全可靠
  2. 缺点:依赖服务端

原理概述:

请求一个资源,如果设置cache-control、lastmodify、etag等,会进行缓存相关的判定:

  1. cache-control:是否强缓存,如果命中直接读取浏览器缓存的上次返回内容
  2. last-modify:如果未命中强缓存,进行时间的判断,如果有if-modified-since并且和last-modify那么读取缓存,否则重新拉取资源
  3. etag:如果未命中强缓存,通过etag唯一标志福来判断是否需要拉取最新资源,etag一般用文件内容的hash加密后内容,如果是大文件,个人建议使用文件大小+最后修改时间作为唯一标志
    综上所述,如果我们请求一个很小的资源文件,例如1字节的图片,服务端设置cache-control: max-age=0,跳过强缓存,服务端设置etag,保证每次都做协商缓存,然后根据etag的变化来决定是否需要拉新(记录用户信息),如果etag没有变化,那么就读取缓存(缓存记录用户信息)

代码示例

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
const Koa = require('koa');
const KoaBody = require('koa-body');
const fs = require('fs');
const path = require('path');
const glob = require('glob');

const baseDate = {
visitCount: 1,
date: undefined,
info: ''
};

const app = new Koa();

app.use(KoaBody());

app.use(async ctx => {
// 更新资源
if (ctx.req.url === '/updateInfo' && ctx.req.method === 'POST') {
const session = require('./session.json');
session.info = ctx.request.body.info;
await writeFile('session.json', JSON.stringify(session));
ctx.body = {
code: 1
};
}

const imgType = glob
.sync('*.+(jpg|png)')
.map(file => path.resolve('/', file));

if ((fileIndex = imgType.indexOf(ctx.req.url)) !== -1) {
const res = ctx.response;
const req = ctx.req;
res.type = path.extname(imgType[fileIndex]).slice(1);
const filePath = path.join('./', imgType[fileIndex]);
res.set('cache-control', 'public, max-age=0');

let session;
try {
session = require('./session.json');
} catch (e) {
session = {...baseDate};
session.date = new Date().toLocaleString();
await writeFile('session.json', JSON.stringify(session));
} finally {
// use force refresh to clear etag, because if serve set etag,
// browser will carry if-none-match field in request header. and we can use if-none-match to judge somethine
// etag 大文件一般用文件大小 + 修改时间 来生成,而不是读取文件内容
md5 = convertMd5(JSON.stringify(session));
res.etag = md5;
if (req.headers['if-none-match']) {
// console.log('缓存');
session.visitCount = +session.visitCount + 1;
session.date = new Date().toLocaleString();
await writeFile('./session.json', JSON.stringify(session));
res.status = 304;
} else {
// console.log('清除缓存');
session.visitCount = 1;
session.date = new Date().toLocaleString();
session.info = '';
await writeFile('./session.json', JSON.stringify(session));
ctx.body = fs.createReadStream(filePath);
}
}
}
});

app.listen(3000);

运行demo

有问题欢迎交流

1…34

FoxDaxian

遇到好玩的,稀奇的,新鲜的,记录下来,分享出去。
40 posts
11 categories
4 tags
© 2021 FoxDaxian
Powered by Hexo v3.9.0
|
Theme – NexT.Muse v7.2.0