赵小金

嗨~


  • 首页

  • 归档

二进制的魅力

发表于 2021-11-16 | 分类于 js , 学习日记

什么是二进制

世界上有10种人:一种是懂得二进制的,另一种是不懂二进制的

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”,由18世纪德国数理哲学大师莱布尼兹发现。当前的计算机系统使用的基本上是二进制系统。

十进制0至9的二进制表示:

十进制 二进制
0 0
1 1
2 10
3 11
4 100
5 101
6 110
7 111
8 1000
9 1001

1
2
3
加法: 0+0=0;0+1=1;1+0=1;1+1=10

求 1101 + 1011 的和

img

8位二进制数

256 128 64 32 16 8 4 2 1
___ ___ __ __ __ _ _ _ _

如何得到二进制

e.g. 将125转化成二进制 1111101
可以使用短除法得到125的二进制值
img

1. 按位或 |

|与||操作符的道理也是一样的,只要两个数中有一个数为1,结果就为1,其他则为0

1. 按位与 &

&运算符表示只有两个数的值为1时,才返回1

使用场景:我们管理系统做表单checkbox多选是,加入我们有a、b、c、d四个chebox的选项,1,2,4,8分别代表checkbox的value值,当我们全选a + b + c + d的时候,可以将1 | 2 | 4 | 8 = 15传给java后台,如果只选择a + c的时候,则传递给java后台的值为1 | 4 = 5。
那么我们做编辑功能的时候,如何判断checkbox是否被选中?很简单,用&符号就可以,比如用户选择了a+c,后台给我们返回5,c的值为1,那么 4 & 5 = 4, b的选项我们是没有选择的,则 2 & 5 = 0,也就是说,选中的通过&的运算值为>0的值,没有选中的将会为0。

1
2
3
4
1 | 2 | 4 | 8 = 15
1 | 4 = 5
4 & 5 = 4
2 & 5 = 0
按位异或 ^

两个操作数相应的比特位有且只有一个1时,结果为1,否则为0

使用场景:
(1)假如我们通过某个条件来切换一个值为0或者1

1
2
3
4
5
6
7
8
//普通的写法
function update(toggle) {
var num = toggle ? 1 : 0;
console.log(num)
}
update(true);
// 通过异或我们可以这么写
num = num ^ 1; //num为true的返回 0, 为 false返回1

(2)可以在不使用第三变量的值的情况下交换两个变量的值

1
2
3
4
5
6
let a = 5,
b = 6;

a = a ^ b;
b = a ^ b;
a = a ^ b;

vue-多模块打包

发表于 2021-08-04
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
/* eslint-disable no-useless-escape */
const path = require('path')
const glob = require('glob')
const projectname = process.argv[3]
console.log(projectname)
// 导入compression-webpack-plugin
const CompressionWebpackPlugin = require('compression-webpack-plugin')
// 定义压缩文件类型
const productionGzipExtensions = ['js', 'css']
process.env.VUE_APP_VERSION = require('./package.json').version
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

// 配置pages多页面获取当前文件夹下的html和js
function getEntry (globPath) {
let entries = {}
let basename
let tmp
let pathname
// 多项目打包
if (process.env.NODE_ENV === 'production') {
entries = {
index: {
// page的入口
entry: `src/pages/${projectname}/${projectname}.js`,
// 模板来源
template: `src/pages/${projectname}/${projectname}.html`,
// 在 dist/index.html 的输出
filename: 'index.html',
title: projectname
// chunks: ['chunk-vendors', 'chunk-common', 'index']
}
}
console.log(entries)
} else {
glob.sync(globPath).forEach(function (entry) {
basename = path.basename(entry, path.extname(entry))
tmp = entry.split('/').splice(-3)
pathname = basename // 正确输出js和html的路径

entries[pathname] = {
entry: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[1] + '.js',
template: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[2],
filename: tmp[2]
}
})
}

return entries
}
let htmls = getEntry('./src/pages/**/*.html')

// 配置end
module.exports = {
productionSourceMap: false,
publicPath: '/',
outputDir: 'dist/' + projectname,
pages: htmls,
// 统一配置打包插件
configureWebpack: config => {
const options = {
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
},
resolve: {
alias: {
'@user': path.join(__dirname, './src/pages/user'),
'@admin': path.join(__dirname, './src/pages/admin')
}
}
}
if (process.env.NODE_ENV === 'production') {
return {
...options,
plugins: [
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), // 匹配文件名
threshold: 10240, // 对10K以上的数据进行压缩
minRatio: 0.8,
// 删除源文件, 配合webpack-bundle-analyzer时不能删除源文件,且需要配置静态服务器
deleteOriginalAssets: true // 是否删除源文件
})
// new BundleAnalyzerPlugin()
]
}
} else {
return options
}
},
chainWebpack: config => {
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => {
// 修改它的选项...
options.limit = 10000
return options
})
if (process.env.NODE_ENV === 'production') {
const fonts = config.module.rule('fonts')
fonts
.use('url-loader')
.loader('url-loader')
.tap(options => {
options = {
limit: 10000,
name: '[name].[ext]',
// publicPath: `http:${cdnPath}/fonts`,
publicPath: '/fonts',
outputPath: 'font/'
}
return options
})
}
},
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [
// 配置全局less变量自动引入,不用每次再手动引入
path.join(__dirname, 'src/iview-theme/index.less')
]
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
minChunkSize: 10000
}
}
}
}
}
}

vue多页面预渲染与静态资源cdn地址的处理

发表于 2020-09-23 | 分类于 js , 学习日记 , prerender-spa-plugin

目标

  • 需要预渲染的页面实现预渲染
  • 页面中静态资源地址使用cdn静态资源地址

主要问题

publicPath在打包使用prerender-spa-plugin预渲染插件时publicPath必须为’’,’/‘或者页面运行时服务端地址(127.0.0.1:port)

尝试解决

prerender-spa-plugin 插件使用时可以使用postProcess函数对预渲染生成的页面进行进一步处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cdnPath = '//cdn.com'
new PrerenderSpaPlugin({
staticDir: path.join(__dirname, '/dist'),
routes: ['/', '/aboutus'],
postProcess (renderedRoute) {
// 将地址替换
renderedRoute.html = renderedRoute.html.replace(
/(<script[^<>]*src=\")((?!http|https)[^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig,
`$1${cdnPath}$2$3`
).replace(
/(<link[^<>]*href=\")((?!http|https)[^<>\"]*)(\"[^<>]*>)/ig,
`$1${cdnPath}$2$3`
)
return renderedRoute
},
renderer: new Renderer({
injectProperty: '__PRERENDER_INJECTED__',
inject: 'prerender'
})
})

如果是单页面这样就OK了,但我的项目是多页面应用,这几个多页面分别是index.html,admin.html,other.html,只有index.html中的/ about这两个路由需要预渲染,我如果改了vue.config.js文件中的publicPath路径的话整个项目在打包时都换变成’/‘路径,所以需要将index.html页面与其他页面分开打包。

1
2
3
4
// package.json
"scripts": {
"build-index": "vue-cli-service build --mode index",
}

科普环境变量文件使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
为一个特定模式准备的环境文件 (例如 .env.production) 将会比一般的环境文件 (例如 .env) 拥有更高的优先级。


模式是 Vue CLI 项目中一个重要的概念。默认情况下,一个 Vue CLI 项目有三个模式:

development 模式用于 vue-cli-service serve
production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
test 模式用于 vue-cli-service test:unit

# 你可以替换你的项目根目录中的下列文件来指定环境变量:
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略

# 一个环境文件只包含环境变量的“键=值”对:
FOO=bar
VUE_APP_SECRET=secret

# 被载入的变量将会对 vue-cli-service 的所有命令、插件和依赖可用。

所以上面--mode index相当于指定模式为index,这里我们还需要一个index模式的环境变量文件.env.index

1
2
3
4
5
6
// .env.index文件

// 指定其他模式都会默认是development模式,所以这里需要显式指定为production模式
NODE_ENV = 'production'
// 这里变量是自己定义的(这里用来区别打包时的页面)
VUE_APP_PAGE = 'index'

下面这些代码的作用:

  1. 将index.html页面和其他页面单独输出打包
  2. index.html页面打包时资源路径是’/‘,执行预渲染之后再将链接替换成cdn地址
  3. 其他页面打包时资源路径是’//cdn.com’

副作用:index页面无法使用路由懒加载(静态资源文件的引用路径无法修改)

1
2
3
4
5
6
7
8
9
10
// package.json

// --no-clean表示打包时不删除dist文件
/**
1. vue-cli-service build 打包其他页面
2. vue-cli-service build --mode index --no-clean 打包index页面不删除dist文件直接覆盖
*/
"scripts": {
"build": "vue-cli-service build && vue-cli-service build --mode index --no-clean "
},
1
2
3
4
// vue.config.js

// publicPath配置
publicPath: (process.env.VUE_APP_PAGE === 'index' || process.env.NODE_ENV === 'development') ? '/' : '//cdn.com'
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
// vue.config.js

// 配置pages多页面获取当前文件夹下的html和js
function getEntry (globPath) {
let entries = {}
let basename
let tmp
let pathname
let result

glob.sync(globPath).forEach(function (entry) {
basename = path.basename(entry, path.extname(entry))
tmp = entry.split('/').splice(-3)
pathname = basename // 正确输出js和html的路径

// 打包环境
if (process.env.NODE_ENV === 'production') {
if (basename === 'index') {
result = {
index: {
entry: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[1] + '.js',
template: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[2],
filename: tmp[2]
}
}
return true
}
entries[pathname] = {
entry: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[1] + '.js',
template: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[2],
filename: tmp[2]
}
} else {
// 运行环境
entries[pathname] = {
entry: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[1] + '.js',
template: 'src/' + tmp[0] + '/' + tmp[1] + '/' + tmp[2],
filename: tmp[2]
}
}
})
// 打包时index模式
if (process.env.VUE_APP_PAGE === 'index') {
return result
}

return entries
}
let htmls = getEntry('./src/pages/**/*.html')
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
// vue.config.js

// 统一配置打包插件
configureWebpack: config => {
// index.html打包
if (process.env.NODE_ENV === 'production' && process.env.VUE_APP_PAGE === 'index') {
return {
...options,
plugins: [
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), // 匹配文件名
threshold: 10240, // 对10K以上的数据进行压缩
minRatio: 0.8,
// 删除源文件, 配合webpack-bundle-analyzer时不能删除源文件,且需要配置静态服务器
deleteOriginalAssets: false // 是否删除源文件
}),
new PrerenderSpaPlugin({
staticDir: path.join(__dirname, '/dist'),
routes: ['/', '/aboutus'],
postProcess (renderedRoute) {
// add CDN
renderedRoute.html = renderedRoute.html.replace(
/(<script[^<>]*src=\")((?!http|https)[^<>\"]*)(\"[^<>]*>[^<>]*<\/script>)/ig,
`$1${cdnPath}$2$3`
).replace(
/(<link[^<>]*href=\")((?!http|https)[^<>\"]*)(\"[^<>]*>)/ig,
`$1${cdnPath}$2$3`
)
return renderedRoute
},
renderer: new Renderer({
injectProperty: '__PRERENDER_INJECTED__',
inject: 'prerender'
})
})
]
}
} else if (process.env.NODE_ENV === 'production') {
// 其他页面打包
return {
...options,
plugins: [
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), // 匹配文件名
threshold: 10240, // 对10K以上的数据进行压缩
minRatio: 0.8,
// 删除源文件, 配合webpack-bundle-analyzer时不能删除源文件,且需要配置静态服务器
deleteOriginalAssets: false // 是否删除源文件
})
]
}
} else {
// 运行环境
return options
}
},

学习日记vuecli-webpack配置-持续更新

发表于 2020-07-10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 修改字体路径
chainWebpack: config => {
const fonts = config.module.rule('fonts')
fonts
.use('url-loader')
.loader('url-loader')
.tap(options => {
options = {
limit: 10000,
name: '[name].[ext]',
publicPath: 'https://xxx',
outputPath: 'font/'
}
return options
})
}
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
// Vue单页面预渲染
// vue.config.js
1. 第一步,publicPath的值必须是''或'/'
publicPath: '/'

2. 第二步,引用插件
const PrerenderSpaPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSpaPlugin.PuppeteerRenderer

new PrerenderSpaPlugin({
staticDir: path.join(__dirname, '/dist'),
routes: ['/', '/aboutus'],
renderer: new Renderer({
injectProperty: '__PRERENDER_INJECTED__',
inject: 'prerender'
})
})

// main.js
3. 第三步,main.js中触发
new Vue({
router,
store,
i18n, // 多语言i18n
render: h => h(App),
/* 这句非常重要,否则预渲染将不会启动 */
mounted () {
document.dispatchEvent(new Event('render-event'))
}
}).$mount('#app')

4. 如果publicPath是cdn地址,需要做单独处理,下一篇博客我将专门记录prerender-spa-plugin与静态资源cdn地址的处理

学习Promise

发表于 2020-07-01

如果要弄懂promise,就必须弄懂什么是异步、什么是同步。

什么是同步呢

你可以理解为同一个时间,你只能干一件事

1
2
3
4
5
6
7
8
9
10
function second() {
console.log('second')
}
function first(){
console.log('first')
second()
console.log('Last')
}
first()
//first、second、last

调用栈
1
2
3
4
5
6
7
8
当执行此代码时,将创建一个全局执行上下文并将其推到调用堆栈的顶部;// 这个不太重要,下面是重点
first()函数先上,现在他在顶部;
然后打印‘first’,然后执行完了,这个时候这个console.log会自动弹走,就是这个console.log虽然是后进来的,但是他先走了;
现在first函数仍然在顶部,他下面还有second函数,所以不会弹走;
执行second()函数,这时候second函数在顶部;
打印‘second’,然后执行完了,弹走这个console.log,这时候second在顶部;
这个时候second函数的事儿都干完了,他也弹走了,这时候first函数在顶部;
浏览器会问,first你还有事吗,first说我还有一个,执行打印‘last’

什么是异步呢

1
2
3
4
5
6
7
8
9
const getList = () => {
setTimeout(() => {
console.log('我执行了!');
}, 2000);
};
console.log('Hello World');
getList();
console.log('哈哈哈');
//Hello World、哈哈哈、我执行了!(两秒以后执行最后一个)
消息队列

刚才我们说了,同步的时候,浏览器会维护一个‘执行栈’,除了执行栈,在开启多线程的时候,浏览器还会维护一个消息列表,除了主线程,其余的都是副线程,这些副线程合起来就叫消息列表。

增加难度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log('我是定时器!');
})
new Promise(function(resolve) {
console.log('我是promise!');
resolve();
}).then(function() {
console.log('我是then!');
})
console.log('我是主线程!');

执行顺序:
我是promise!
我是主线程!
我是then!
我是定时器!
事件轮询

上面我们说了,浏览器为了提升效率,为js开启了一个不太一样的多线程,因为js不能同时执行嘛,那副线程(注意是副线程里面哈)里面谁执行,这个选择的过程,就可以理解为事件轮询。我们先用事件轮询的顺序分析一下上面的代码,再来上概念:

1
2
3
4
5
6
promise函数肯定首先执行,他是主线程嘛,打印‘我是promise’;
然后继续走主线程,打印‘我是主线程’;
然后主线程走完了,开始走消息列表;
(宏任务和微任务一会再讲)
这个时候会先执行promise.then,因为他是微任务,里面的‘我是then!’
消息列表里面在上面的是定时器,但是定时器是宏任务,优先级比较低,所以会往后排;

什么是宏任务?微任务?
1
2
3
4
5
6
7
8
9
10
11
12
13
 宏任务(Macrotasks):js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等。

微任务(Microtasks):promise、process.nextTick(node环境)、Object.observe, MutationObserver等。

微任务比宏任务要牛逼一点

浏览器执行的顺序:
(1)执行主代码块,这个主代码块也是宏任务
(2)若遇到Promise,把then之后的内容放进微任务队列
(3)遇到setTimeout,把他放到宏任务里面
(4)一次宏任务执行完成,检查微任务队列有无任务
(5)有的话执行所有微任务
(6)执行完毕后,开始下一次宏任务。

下面用代码来深入理解上面的机制:

1
2
3
4
5
6
7
8
9
10
11
setTimeout(function() {
console.log('4')
})

new Promise(function(resolve) {
console.log('1') // 同步任务
resolve()
}).then(function() {
console.log('3')
})
console.log('2')

这段代码作为宏任务,进入主线程。
先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。
接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
遇到console.log(),立即执行。
整体代码script作为第一个宏任务执行结束。查看当前有没有可执行的微任务,执行then的回调。 (第一轮事件循环结束了,我们开始第二轮循环。)
从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。 执行结果:1 - 2 - 3 - 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
27
28
29
30
31
32
33
34
35
36
37
console.log('1')
setTimeout(function() {
console.log('2')
process.nextTick(function() {
console.log('3')
})
new Promise(function(resolve) {
console.log('4')
resolve()
}).then(function() {
console.log('5')
})
})

process.nextTick(function() {
console.log('6')
})

new Promise(function(resolve) {
console.log('7')
resolve()
}).then(function() {
console.log('8')
})

setTimeout(function() {
console.log('9')
process.nextTick(function() {
console.log('10')
})
new Promise(function(resolve) {
console.log('11')
resolve()
}).then(function() {
console.log('12')
})
})

整体script作为第一个宏任务进入主线程,遇到console.log(1)输出1。
遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。
遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。
遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。
又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。
现在开始执行微任务,我们发现了process1和then1两个微任务,执行process1,输出6。执行then1,输出8。 第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮事件循环从setTimeout1宏任务开始:
首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。
new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。
现在开始执行微任务,我们发现有process2和then2两个微任务可以执行输出3,5。 第二轮事件循环结束,第二轮输出2,4,3,5。第三轮事件循环从setTimeout2宏任务开始:
直接输出9,将process.nextTick()分发到微任务Event Queue中。记为process3。
直接执行new Promise,输出11。将then分发到微任务Event Queue中,记为then3。
执行两个微任务process3和then3。输出10。输出12。 第三轮事件循环结束,第三轮输出9,11,10,12。 整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

学习Promise

发表于 2020-07-01

promise是什么?

ES6提供Promise构造函数,我们创造一个Promise实例,Promise构造函数接收一个函数作为参数,这个传入的函数有两个参数,分别是两个函数 resolve和reject作用是,resolve将Promise的状态由未成功变为成功,将异步操作的结果作为参数传递过去;相似的是reject则将状态由未失败转变为失败,在异步操作失败时调用,将异步操作报出的错误作为参数传递过去。
实例创建完成后,可以使用then方法分别指定成功或失败的回调函数,比起f1(f2(f3))的层层嵌套的回调函数写法,链式调用的写法更为美观易读

Promise的特点

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

  • 对象不受外界影响,初始状态为pending(等待中),结果的状态为resolve和reject,只有异步操作的结果决定这一状态
  • 状态只能由pending变为另外两种的其中一种,且改变后不可逆也不可再度修改,
    即pending -> resolved 或 pending -> reject
1
2
3
4
5
6
7
8
9
10
11
let promise = new Promise((resolve, reject)=>{
reject("拒绝了");
resolve("又通过了");
});
promise.then((data)=>{
console.log('success' + data);
},(error)=>{
console.log(error)
});

执行结果: "拒绝了"

then

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

catch

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

1
2
3
4
5
6
7
getJSON('/post/1.json').then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});

上面代码中,一共有三个 Promise 对象:一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});

// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

1
2
3
4
5
6
7
const promise = new Promise(function (resolve, reject) {
resolve('ok');
setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test

上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。

catch()方法之中,还能再抛出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};

someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为 y 没有声明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

上面代码中,catch()方法抛出一个错误,因为后面没有别的catch()方法了,导致这个错误不会被捕获,也不会传递到外层。如果改写一下,结果就不一样了。

1
2
3
4
5
6
7
8
9
10
11
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错,因为y没有声明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

上面代码中,第二个catch()方法用来捕获前一个catch()方法抛出的错误。

finally

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject。

1
2
3
4
5
6
7
8
9
10
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);

p
.then(console.log)
.catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

canvas-学习日记之弧线

发表于 2020-06-03

arcTo

用法:

ctx.arcTo(x1,y1,x2,y2,radius)

参数解析:

(1)(x1,y1):必需,规定第一个控制点的坐标。
(2)(x2,y2):必需,规定第二个控制点的坐标。
(3)radius:必需,规定圆弧所在圆的半径尺寸。

详细分析如下:

(1)起点与第一个控制点连接成一条直线。
(2)第一个控制点与第二个控制点连接成一条直线。
(3)那么通过这两条直线与圆的半径可以绘制一个与直线相切的圆弧。
(4)起点与圆弧连接起来,就是最终绘制的图案。

绘制示意图:
1
2
3
4
5
6
7
8
9
var x0=100,
y0=400,
x1 = 500,
y1 = 400,
x2 = 450,
y2 = 450;
radius = 20;
ctx.moveTo(x0,y0);
ctx.arcTo(x1,y1,x2,y2,radius);

(1)radius=20
image
(2)radius=50
image
(3)var x0=400; radius=100
image

画一个月亮
1
2
3
4
5
6
7
8
9
10
11
ctx.save()
ctx.strokeStyle = '078'
ctx.beginPath();
ctx.arc(50,50,50,0.5*Math.PI,1.5*Math.PI,true);
ctx.arcTo(200, 50, 50, 100, 50*dis(50,0,200,50)/150);
ctx.stroke();
ctx.restore()

function dis(x1,y1,x2,y2){
return Math.sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1))
}

image

解析:

image

canvas学习日记-文字

发表于 2020-05-27

font

font 属性设置或返回画布上文本内容的当前字体属性,语法与 CSS font 属性相同。

font

  • 默认值:”20px sans-serif”
  • ctx.font = font-style font-variant font-weight font-zise font-family

























属性 值
font-style
normal (Default)

italic (斜体字:通常字体会设计一个专门的斜体字)

oblique (倾斜字体:简单的将字体倾斜)
font-variant
normal (Default)

small-caps (将英文中的小写字母变成小型的大写字母)

font-weight
lighter

normal (Default)

bold

bolder

W3C将font-weight分为不同的等级:

100,200,300,400(normal),

500,600,700(bold),

800,900
font-size
20px (Default)

2em

150%

font-size可以分为以下几种几号:

xx-small

x-small

medium

large

x-large

xx-large

font-family
web安全字体

fillText
| 参数 | 描述 |
|–|–|–|
| text | 规定在画布上输出的文本。 |
| x | 开始绘制文本的 x 坐标位置(相对于画布)。 |
| y | 开始绘制文本的 y 坐标位置(相对于画布)。 |
| maxWidth | 可选。允许的最大文本宽度,以像素计。 |

基本使用:

1
2
3
ctx.font = 'bold 40px Arial'
ctx.fillText('Canvas', 0, 200)
ctx.strokeText('Canvas', 0, 300)

image

1
2
3
4
5
6
7
8
9
// 渐变
const linearGrad = ctx.createLinearGradient(0,0,800,0)
linearGrad.addColorStop(0.0,'red')
linearGrad.addColorStop(0.25,'orange')
linearGrad.addColorStop(0.5,'yellow')
linearGrad.addColorStop(0.75,'green')
linearGrad.addColorStop(1.0,'purple')
ctx.fillStyle = linearGrad
ctx.fillText('canvascanvascanvascanvascanvascanvascanvascanvascanvas', 0, 400)

image

1
2
3
4
5
6
7
8
9
// 纹理
const backgroundImg = new Image()
backgroundImg.src = 'https://www.cnblogs.com/skins/coffee/images/bg_body.gif'
backgroundImg.onload = () => {
const pattern = ctx.createPattern(backgroundImg, 'repeat')
ctx.fillStyle = pattern
ctx.font = 'bold 100px Arial'
ctx.fillText('canvas', 0, 500)
}

image

textAlign属性:调整文本的水平对齐方式

值 描述
left 起始点为左边界
center 起始点为中间位置
center 起始点为右边界
1
2
3
4
5
6
7
8
9
10
11
12
13
// textAlign
ctx.font = '40px quick'
ctx.textAlign = 'left'
ctx.fillText('left', 400, 1000)
ctx.textAlign = 'center'
ctx.fillText('center', 400, 1100)
ctx.textAlign = 'right'
ctx.fillText('right', 400, 1200)

ctx.strokeStyle = 'rgba(0,0,0,.4)'
ctx.moveTo(400,900)
ctx.lineTo(400,1200)
ctx.stroke()

image

textBaseline属性:调整文本的垂直对齐方式

值 描述
top 起始点为上边界
middle 起始点为中间位置
bottom 起始点为下边界
alphabetic(Default) (拉丁文)
ideographic (汉字,日文等方块文字)
hanging (印度语)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// textBaseline
ctx.textAlign = 'center'
ctx.font = '40px quick'

ctx.textBaseline = 'top'
ctx.fillText('top-中英文-abcdefg', 400, 1300)

ctx.textBaseline = 'middle'
ctx.fillText('middle-中英文-abcdefg', 400, 1400)

ctx.textBaseline = 'bottom'
ctx.fillText('bottom-中英文-abcdefg', 400, 1500)

ctx.textBaseline = 'alphabetic'
ctx.fillText('alphabetic-中英文-abcdefg', 400, 1600)

ctx.textBaseline = 'ideographic'
ctx.fillText('ideographic-中英文-abcdefg', 400, 1700)

ctx.textBaseline = 'hanging'
ctx.fillText('hanging-中英文-abcdefg', 400, 1800)

image

canvas学习日记

发表于 2020-05-17

canvas是基于状态绘制的,首先我们来理解一下什么是基于状态,这个词单看比较难理解,这样,我们从另一个角度理解一下,那就是它不是基于对象绘制:就是说我们不会创建一个线条的对象,然后基于这个对象去定义它的各项属性;而是我们对canvas这个画布的整体设置了一些属性,最后来进行绘制

路径

1、beginPath() 新建一条路径,路径一旦创建成功,图形绘制命令被指向到路径上生成路径
2、moveTo(x, y) 把画笔移动到指定的坐标(x, y)。相当于设置路径的起始点坐标。
3、closePath() 闭合路径之后,图形绘制命令又重新指向到上下文中
4、stroke() 通过线条来绘制图形轮廓
5、fill() 通过填充路径的内容区域生成实心的图形

关于状态绘制意想不到的坑

绘制三条线段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ctx.lineWidth= 10
ctx.beginPath();
ctx.moveTo(100,100);
ctx.lineTo(200,100);
ctx.strokeStyle = "red";
ctx.stroke();

// ctx.beginPath();
ctx.moveTo(100,200);
ctx.lineTo(200,200);
ctx.strokeStyle = "blue";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(100,300);
ctx.lineTo(200,300);
ctx.strokeStyle = "yellow";
ctx.stroke();

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ctx.beginPath();
ctx.moveTo(100,100);
ctx.lineTo(200,100);
ctx.strokeStyle = "red";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(100,200);
ctx.lineTo(200,200);
ctx.strokeStyle = "blue";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(100,300);
ctx.lineTo(200,300);
ctx.strokeStyle = "yellow";
ctx.stroke();

image

1、第一个beginPath()可以不使用,但是为了代码更规整,加上更好一些
2、beginPath 和 lineTo 一起相当于是moveTo
3、即使写了stroke(),接下来的线条没有写beginPath(),下面的状态也会覆盖上面的状态

绘制一个箭头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ctx.beginPath()
ctx.lineTo(100,350)
ctx.lineTo(500,350)
ctx.lineTo(500,200)
ctx.lineTo(700,400)
ctx.lineTo(500,600)
ctx.lineTo(500,450)
ctx.lineTo(100,450)
ctx.lineTo(100,350)

ctx.fillStyle = 'red'
ctx.lineWidth = 10

ctx.fill()
ctx.stroke()

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ctx.beginPath()
ctx.lineTo(100,350)
ctx.lineTo(500,350)
ctx.lineTo(500,200)
ctx.lineTo(700,400)
ctx.lineTo(500,600)
ctx.lineTo(500,450)
ctx.lineTo(100,450)
ctx.closePath()

ctx.fillStyle = 'red'
ctx.lineWidth = 10

ctx.stroke()
ctx.fill()

image

1、如果直接通过lineTo()闭合曲线时会有一个缺角,但是通过closePath()闭合可以避免这个问题
2、填充方法fill()写在描边方法stroke()下面时,会覆盖掉一部分描边

lineCap

lineCap 属性设置或返回线条末端线帽的样式。

属性 描述
butt 默认。向线条的每个末端添加平直的边缘。
round 向线条的每个末端添加圆形线帽。
square 向线条的每个末端添加正方形线帽。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var lineCap = ['butt','round','square'];
cxt.strokeStyle="red";
cxt.beginPath();
cxt.moveTo(10,10);
cxt.lineTo(140,10);
cxt.moveTo(10,140);
cxt.lineTo(140,140);
cxt.stroke();

cxt.strokeStyle="green";
for (i = 0; i < lineCap.length; i++){
cxt.lineWidth=15;
cxt.lineCap=lineCap[i];
cxt.beginPath();
cxt.moveTo(25+i*50,10);
cxt.lineTo(25+i*50,140);
cxt.stroke();
}

image

??未解之谜

当我不设置lineWidth或者设置为1的时候,后面的状态没有完全覆盖前面的状态,而是叠加了前面的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ctx.beginPath();
ctx.moveTo(100,100);
ctx.lineTo(200,100);
ctx.strokeStyle = "red";
ctx.stroke();

// ctx.beginPath();
ctx.moveTo(100,200);
ctx.lineTo(200,200);
ctx.strokeStyle = "blue";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(100,300);
ctx.lineTo(200,300);
ctx.strokeStyle = "yellow";
ctx.stroke();

image

矩形

1、rect(x, y, width, height):绘制一个矩形。
2、fillRect(x, y, width, height):绘制一个填充的矩形。
3、strokeRect(x, y, width, height):绘制一个矩形的边框。
4、clearRect(x, y, widh, height):清除指定的矩形区域,然后这块区域会变的完全透明。

1
2
ctx.fillRect(10, 10, 100, 50);     // 绘制矩形,填充的默认颜色为黑色
ctx.strokeRect(10, 70, 100, 50); // 绘制矩形边框

image

1
2
3
ctx.fillRect(10, 10, 100, 50);     // 绘制矩形,填充的默认颜色为黑色
ctx.strokeRect(10, 70, 100, 50); // 绘制矩形边框
ctx.clearRect(15, 15, 50, 25);

image

闭包与模块

发表于 2020-05-03 | 分类于 js

一、前言

闭包是基于词法作用域( 和动态作用域对应,词法作用域是由你写代码时,将变量写在哪里来决定的,因此当词法分析器处理代码时,会保持作用)书写代码时所产生的自然结果,甚至不需要为了利用闭包而有意地创建闭包。闭包的创建和使用在动态语言的代码中随处可见。你缺少的只是识别,拥抱和使用闭包的思维。

当函数可以记住并访问所在的词法作用域,即使函数在当前词法作用域之外执行。就产生了闭包。

一般情况下,当函数执行完毕,垃圾回收机制会期待函数的整个内部作用域被销毁,但当闭包存在时,会阻止这件事情的发生,事实上内部作用域依旧存在,此时内部函数依旧持有对外部函数作用域的引用,这个引用就叫做闭包。无论通过何种方式将内部函数传递到所在的词法作用域之外,他都会持有对 原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

所以说,在javascript,python这种动态语言中,因为函数是一级对象,无论何时何地,只要将函数当做第一级的值类型并到处传递,都会看到闭包的运用,可以说,闭包无处不在。

二、循环与闭包

要说明闭包,for循环是最常见的例子。

1
2
3
4
5
6
for(var i = 1; i < 5; i++) {
let j = i
setTimeout(() => {
console.log(j)
}, j * 1000)
}

正常情况下,我们对这段代码行为的预期分别是输出数字1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次5。
这里引申出一个更深入的问题,代码中到底有什么缺陷导致他的行为同语义所暗示的不一致呢?
缺陷是我们试图假设循环中的每个迭代在运行时,都会给自己“捕获一个i的副本”。但是根据作用域的工作原理,实际情况是尽管循环中的5个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
下面回到正题,缺陷是什么?我们需要更多的闭包作用于,特别是在循环的过程中每个迭代都需要一个闭包作用域。

IIFE会通过声明并立即执行一个函数来创建作用域。

1
2
3
4
5
6
7
for(var i = 1; i < 5; i++) {
(function(){
setTimeout(() => {
console.log(i)
}, i * 1000)
})()
}

这样可以吗?

不行。
如果作用域是空的,那么仅仅将他们封闭起来是不够的。仔细看一下,我们的IIFE只是一个什么都没有的空作用域,他需要包含一点实质内容才能为我们所用。

他需要有自己的变量,用来在每个迭代中存储i的值:

1
2
3
4
5
6
7
for(var i = 1; i < 5; i++) {
(function(i){
setTimeout(() => {
console.log(i)
}, i * 1000)
})(i)
}

ES6的let声明

let可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
本质上这是一个将一个块转换成一个可以被关闭的作用域。然后下面这些看起来很酷的代码就可以正常运行了:

1
2
3
4
5
6
7
8
9
10
11
12
for(var i = 1; i < 5; i++) {
let j = i
setTimeout(() => {
console.log(j)
}, j * 1000)
}

for(let i = 1; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}

三、模块

模块是一个利用闭包的典型案例:

模块模式至少具备两个条件:

1)必须有外部的封闭包装函数来创建内部作用域,该函数至少被调用一次(每次调用都会创建一个新的模块作用域),如果是ITFE调用就只产生一个实例(单例模式);

2)封闭函数返回至少一个内部函数的引用(可以直接返回该内部函数,如jQuery;也可以返回一个对象,该对象至少包含一个属性,指向内部函数的引用),这样内部函数才能在私有作用域形成闭包,而且可以访问或者修改私有的状态;

比如模块的一个很常见的应用就是返回作为公共API返回的对象:

1
2
3
4
5
6
7
8
9
10
11
12
var foo = (function () {
function change () {
console.log('change')
}
function identify () {
console.log('identify')
}
return {
change: change,
identify: identify,
}
})()

模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var foo = function () {
function change (){
// 修改公共API
publicAPI.identify = identify2
}
function identify1 (){
console.log('identify1');
}
function identify2 (){
console.log('identify2');
}
var publicAPI = {
change:change,
identify:identify,
}
return publicAPI
}

foo.identify() // identify1
foo.change()
foo.identify() // identify2

通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加删除方法和属性,以及修改他们的值。

四、模块加载器/管理器

模块管理器本质上并没有任何的“魔力”,本质上就是讲模块定义封装进一个友好的API。下面是一个简单的模块加载器的实现:

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
var MyModules = (function () {
var modules = []
//参数name:模块名称
//参数deps:依赖的模块名称
//impl:名为name的模块实现
var define = function define (name, deps, impl) {
var i = 0;
for(i; i<deps.length; i++){
deps[i]=modules[deps[i]];
}
modules[name] = impl.apply(impl,deps);
};
var get = function (name) {
return modules[name];
};
return {
define:define,
get:get
}
})();

//调用
MyModules.define('bar',[],function(){
function hello(){
return 'hello';
}
return {
hello:hello,
};
});
MyModules.define('foo',['bar'],function(bar){
function awesome(){
console.log('foo ' + bar.hello());
}
return{
awesome:awesome,
}
})
var bar=MyModules.get('bar');
var foo=MyModules.get('foo');
foo.awesome();

foo和bar模块东营市通过一个返回公共API的函数来定义的,foo甚至接受bar的实例作为以来参数,并能相应的使用它。

ES6的模块机制

ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员。

ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”可以在导入模块是同步的加载模块文件。

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

function hello (who) {
return 'let me introduce:' + who
}

export hello

foo.js

import hello from 'bar'

var hungry = 'hippo'

function awesome () {
console.log( hello( hungry ) )
}

export awesome

import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上。expor会将模块的一个标识符(变量,函数)导出为公共API。

模块文件中的内容会被当做好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。

123

赵小金

赵小金的博客

28 日志
14 分类
9 标签
© 2022 赵小金
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4