挖坑&测试
update test 更新文章 1. 测试新主题 挖坑 挖坑 update config 更新文章 1. 简易FaaS平台 更新文章 1. 修复错误 更新文章:Promise信任问题 update theme 1. 合并js post: update notedly fix: update faas feature: change theme * fix: comment * feature: pgp * fix: delete local file post: update darkmode update: update dependencies fix: navbar in post incorrect height * pre code adapt to dark mode update new post useCallback update dependencies new post tiny router * add static files update vue tiny router 添加备案 * 更新依赖 add post Add ignore file
6
.dockerignore
Normal file
@ -0,0 +1,6 @@
|
||||
git
|
||||
dist
|
||||
build
|
||||
db.json
|
||||
public
|
||||
node_modules
|
2
.gitignore
vendored
@ -7,3 +7,5 @@ public/
|
||||
.deploy*/
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
.next/
|
||||
out/
|
||||
|
3
.vscode/extensions.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"compile-hero.disable-compile-files-on-did-save-code": false
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
## 0.0.2-0 (2021-04-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 更换分支 ([1ce1a25](https://e.coding.net/Defectink/xfy/blog/commits/1ce1a25f953294daf2e2baaf9d20a947268f31b2))
|
||||
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
FROM node:15.0.1-alpine as builder
|
||||
FROM node:current-alpine as builder
|
||||
WORKDIR /root
|
||||
COPY ./ ./
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
|
||||
&& apk update \
|
||||
&& apk upgrade \
|
||||
&& apk add --no-cache yarn \
|
||||
&& yarn install \
|
||||
&& yarn config set registry https://registry.npm.taobao.org \
|
||||
&& yarn \
|
||||
&& yarn dev
|
||||
|
||||
FROM nginx:alpine
|
||||
VOLUME ["/etc/localtime","/etc/localtime"]
|
||||
COPY --from=builder /root/public/ /usr/share/nginx/html
|
||||
|
36
Gulpfile.js
@ -2,32 +2,30 @@ const { series, parallel } = require('gulp');
|
||||
const { src, dest } = require('gulp');
|
||||
const cleanCSS = require('gulp-clean-css');
|
||||
const htmlmin = require('gulp-htmlmin');
|
||||
const uglify = require('gulp-uglify-es').default;
|
||||
const terser = require('gulp-terser');
|
||||
const htmlclean = require('gulp-htmlclean');
|
||||
|
||||
function html() {
|
||||
return src('./public/**/*.html')
|
||||
.pipe(htmlclean())
|
||||
.pipe(htmlmin({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true
|
||||
}))
|
||||
.pipe(dest('./public'))
|
||||
};
|
||||
return src('./public/**/*.html')
|
||||
.pipe(htmlclean())
|
||||
.pipe(
|
||||
htmlmin({
|
||||
collapseWhitespace: true,
|
||||
removeComments: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
})
|
||||
)
|
||||
.pipe(dest('./public'));
|
||||
}
|
||||
|
||||
function css() {
|
||||
return src('./public/**/*.css')
|
||||
.pipe(cleanCSS())
|
||||
.pipe(dest('./public'))
|
||||
};
|
||||
return src('./public/**/*.css').pipe(cleanCSS()).pipe(dest('./public'));
|
||||
}
|
||||
|
||||
function js() {
|
||||
return src('public/**/*.js')
|
||||
.pipe(uglify())
|
||||
.pipe(dest('./public'))
|
||||
return src('public/**/*.js').pipe(terser()).pipe(dest('./public'));
|
||||
}
|
||||
|
||||
exports.default = parallel(html, css, js);
|
@ -1,5 +1,3 @@
|
||||
## 小破站
|
||||
|
||||
[](https://app.netlify.com/sites/brave-nobel-bc5790/deploys)
|
||||
|
||||
[xfy.plus](https://xfy.plus/)
|
||||
[xfy](https://www.defectink.com/)
|
1008
_config.fluid.yml
941
_config.volantis.yml
Normal file
@ -0,0 +1,941 @@
|
||||
############################### Volantis ###############################
|
||||
# use_cdn: /source/js/* 中的JS文件(JS Only)使用jsdelivr的min版本加速
|
||||
# 默认使用 https://cdn.jsdelivr.net/npm/hexo-theme-volantis@<%- theme.info.theme_version %>/source/js/*.min.js 的CDN压缩版本(min.js),注意版本号对应关系!!可以通过修改以下配置项覆盖
|
||||
# 开发者注意 use_cdn 设置为 false
|
||||
use_cdn: false
|
||||
info:
|
||||
theme_name: Volantis # This is theme's name.
|
||||
theme_version: '4.3.1' # This is theme's version.
|
||||
theme_docs: https://volantis.js.org/ # This is theme's URL.
|
||||
theme_repo: https://github.com/volantis-x/hexo-theme-volantis
|
||||
cdn:
|
||||
js: # https://cdn.jsdelivr.net/npm/hexo-theme-volantis@<%- theme.info.theme_version %>/source/js/app.min.js # 注意版本!!!
|
||||
css:
|
||||
first: #/css/first.css (需自行替换CDN 首屏样式 cover navbar search )
|
||||
style: # /css/style.css (需自行替换CDN 异步加载 Others... )
|
||||
########################################################################
|
||||
|
||||
############################### Navigation Bar ############################### > start
|
||||
# 注意事项:建议规范全站路径 URL 最后带一个 "/" 例如 "about/"
|
||||
navbar:
|
||||
visiable: auto # always, auto
|
||||
logo: # choose [img] or [icon + title]
|
||||
img: /images/img/favicon.webp
|
||||
icon:
|
||||
title:
|
||||
menu:
|
||||
- name: 首页
|
||||
icon: fas fa-home
|
||||
url: /
|
||||
- name: 分类
|
||||
icon: fas fa-folder-open
|
||||
url: categories/
|
||||
- name: 标签
|
||||
icon: fas fa-tags
|
||||
url: tags/
|
||||
- name: 归档
|
||||
icon: fas fa-archive
|
||||
url: archives/
|
||||
- name: 友链
|
||||
icon: fas fa-link
|
||||
url: friends/
|
||||
- name: PGP
|
||||
icon: fas fa-lock
|
||||
url: pgp/
|
||||
- name: 关于
|
||||
icon: fas fa-info-circle
|
||||
url: about/
|
||||
- name: # 可自定义
|
||||
icon: fas fa-moon # 可自定义
|
||||
toggle: darkmode
|
||||
search: Search... # Search bar placeholder
|
||||
############################### Navigation Bar ############################### > end
|
||||
|
||||
############################### Cover ############################### > start
|
||||
cover:
|
||||
height_scheme: half # full, half
|
||||
layout_scheme: blank # blank (留白), search (搜索), dock (坞), featured (精选), focus (焦点)
|
||||
display:
|
||||
home: false
|
||||
archive: false
|
||||
others: false # can be written in front-matter 'cover: true'
|
||||
background: https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture
|
||||
# background: https://bing.ioliu.cn/v1/rand?w=1920&h=1200
|
||||
logo: # https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/Logo-Cover@3x.png
|
||||
title: 'Defectink'
|
||||
subtitle: ''
|
||||
search: '' # search bar placeholder
|
||||
features:
|
||||
- name: 首页
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.2/assets/svg/1f3e0.svg
|
||||
url: /
|
||||
- name: 示例
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f396.svg
|
||||
url: /examples/
|
||||
- name: 社区
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f389.svg
|
||||
url: /contributors/
|
||||
- name: 博客
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f4f0.svg
|
||||
url: /archives/
|
||||
- name: 源码
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f9ec.svg
|
||||
url: https://github.com/volantis-x/hexo-theme-volantis/
|
||||
- name: Github
|
||||
icon: #
|
||||
img: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0.2/assets/svg/1f525.svg
|
||||
url: https://github.com/DefectingCat/
|
||||
############################### Cover ############################### > end
|
||||
|
||||
pages:
|
||||
# 友链页面配置
|
||||
friends:
|
||||
layout_scheme: traditional # simple: 简单布局, traditional: 传统布局, sites: 网站卡片布局
|
||||
|
||||
############################### Article Layout ############################### > start
|
||||
# 文章布局
|
||||
article:
|
||||
# 文章列表页面的文章卡片布局方案
|
||||
preview:
|
||||
scheme: landscape # landscape
|
||||
# pin icon for post
|
||||
pin_icon: https://cdn.jsdelivr.net/gh/twitter/twemoji@13.0/assets/svg/1f4cc.svg
|
||||
# auto generate title if not exist
|
||||
auto_title: true # false, true
|
||||
# auto generate excerpt if not exist
|
||||
auto_excerpt: true # false, true
|
||||
# show split line or not
|
||||
line_style: solid # hidden, solid, dashed, dotted
|
||||
# show author
|
||||
author: false # true, false
|
||||
# show readmore button
|
||||
readmore: auto # auto, always
|
||||
# 文章详情页面的文章卡片本体布局方案
|
||||
body:
|
||||
# 文章顶部信息
|
||||
# 从 meta_library 中取
|
||||
top_meta: [date, updated] #启用评论数量需在此添加
|
||||
# ----------------
|
||||
# 文章页脚组件
|
||||
footer_widget:
|
||||
# ----------------
|
||||
# 参考资料、相关资料等 (for layout: post/docs)
|
||||
references:
|
||||
title: 参考资料
|
||||
icon: fas fa-quote-left
|
||||
# 在 front-matter 中:
|
||||
# references:
|
||||
# - title: 某篇文章
|
||||
# url: https://
|
||||
# 即可显示此组件。
|
||||
# ----------------
|
||||
# 相关文章,需要安装插件 (for layout: post)
|
||||
# npm i hexo-related-popular-posts
|
||||
related_posts:
|
||||
enable: false
|
||||
title: 相关文章
|
||||
icon: fas fa-bookmark
|
||||
max_count: 5
|
||||
# 设为空则不使用文章头图
|
||||
placeholder_img: https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture
|
||||
# ----------------
|
||||
# 版权声明组件 (for layout: post)
|
||||
copyright:
|
||||
enable: false
|
||||
permalink: '本文永久链接是:'
|
||||
content:
|
||||
- '博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议'
|
||||
- permalink
|
||||
# ----------------
|
||||
# 打赏组件 (for layout: post)
|
||||
donate:
|
||||
enable: false
|
||||
images:
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/qrcode/github@volantis.png
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/qrcode/github@volantis.png
|
||||
# 文章底部信息
|
||||
# 从 meta_library 中取
|
||||
bottom_meta: [tags]
|
||||
# meta library
|
||||
meta_library:
|
||||
# 默认文章作者(可在 _data/author.yaml 中增加其他作者,并在 front-matter 中设置)
|
||||
# https://volantis.js.org/advanced-settings/#多人协同
|
||||
author:
|
||||
avatar:
|
||||
name: Arthur
|
||||
url: /
|
||||
# 文章创建日期
|
||||
date:
|
||||
icon: fas fa-calendar-alt
|
||||
title: '水于:'
|
||||
format: 'll' # 日期格式 http://momentjs.com/docs/
|
||||
# 文章更新日期
|
||||
updated:
|
||||
icon: fas fa-edit
|
||||
title: '最后水于:'
|
||||
format: 'll' # 日期格式 http://momentjs.com/docs/
|
||||
# 文章分类
|
||||
category:
|
||||
icon: fas fa-folder-open
|
||||
# 文章浏览计数
|
||||
counter:
|
||||
icon: fas fa-eye
|
||||
unit: '次浏览'
|
||||
# 文章评论数量:支持 valine和waline
|
||||
valinecount:
|
||||
icon: fas fa-comment-dots
|
||||
desc: '' # 条评论
|
||||
walinecount:
|
||||
icon: fas fa-comment-dots
|
||||
desc: '' # 条评论
|
||||
# 文章字数和阅读时长
|
||||
wordcount:
|
||||
icon_wordcount: fas fa-keyboard
|
||||
icon_duration: fas fa-hourglass-half
|
||||
# 文章标签
|
||||
tags:
|
||||
icon: fas fa-hashtag
|
||||
# 分享
|
||||
share:
|
||||
- id: #qq
|
||||
img: #https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qq.png
|
||||
- id: #qzone
|
||||
img: #https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/qzone.png
|
||||
- id: #weibo
|
||||
img: #https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/weibo.png
|
||||
- id: # qrcode # 当id为qrcode时需要安装插件 npm i hexo-helper-qrcode
|
||||
img: # https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/wechat.png
|
||||
- id: # telegram
|
||||
img: # https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/128/telegram.png
|
||||
############################### Article Layout ############################### > end
|
||||
|
||||
############################### Comments ############################### > start
|
||||
comments:
|
||||
title: <i class='fas fa-comments'></i> 评论
|
||||
subtitle:
|
||||
service: gitalk #vssue # valine, twikoo, waline, minivaline, disqus, disqusjs, gitalk, vssue, livere, isso, hashover
|
||||
# Valine
|
||||
# https://valine.js.org/
|
||||
valine:
|
||||
# js: https://cdn.jsdelivr.net/npm/valine@1.4/dist/Valine.min.js
|
||||
path: # 全局评论地址 目前设置全局评论地址后visitor失效,这是valine的问题
|
||||
placeholder: 快来评论吧~ # 评论占位提示
|
||||
# 其他配置项按照yml格式继续填写即可 除了 [el path placeholder emojiCDN emojiMaps] 选项
|
||||
appId: # your appId
|
||||
appKey: # your appKey
|
||||
meta: [nick, mail, link] # valine comment header info
|
||||
requiredFields: [nick, mail]
|
||||
enableQQ: true # Unstable avatar link
|
||||
recordIP: false # Record commenter IP
|
||||
avatar: robohash # gravatar style https://valine.js.org/avatar
|
||||
pageSize: 10 # comment list page size
|
||||
lang: zh-cn
|
||||
highlight: true
|
||||
mathJax: false
|
||||
# MiniValine
|
||||
# https://github.com/MiniValine/MiniValine
|
||||
minivaline:
|
||||
js: https://cdn.jsdelivr.net/npm/minivaline@latest
|
||||
path: # 全局评论地址
|
||||
placeholder: 快来评论吧~ # 全局评论占位提示
|
||||
# 更多选项 https://minivaline.js.org/docs/cn/#/Options 按照yml格式继续填写即可 (除了 [el path placeholder] 选项)
|
||||
# emoticonUrl 等列表选项 可参考 https://github.com/MiniValine/hexo-next-minivaline
|
||||
# 下面是一个例子:
|
||||
backend: waline
|
||||
serverURL: https://waline.vercel.app
|
||||
# Disqus
|
||||
# https://disqus.com
|
||||
disqus:
|
||||
shortname:
|
||||
# optional
|
||||
autoload: false
|
||||
path: # 全局评论地址
|
||||
# DisqusJS
|
||||
# https://github.com/SukkaW/DisqusJS
|
||||
disqusjs:
|
||||
path: # 全局评论地址
|
||||
# 配置项按照yml格式继续填写即可 除了 [siteName url identifier] 选项
|
||||
#shortname:
|
||||
#api:
|
||||
#apikey:
|
||||
#admin:
|
||||
#nesting:
|
||||
# Gitalk
|
||||
# https://gitalk.github.io/
|
||||
gitalk:
|
||||
# 配置项按照yml格式继续填写即可 除了 [id distractionFreeMode] 选项
|
||||
clientID: 3d2dbbd47aefe936f859
|
||||
clientSecret: 1b33ce8e8b599a8317c89e3cfa83a5d8cc5656ac
|
||||
repo: DefectingCat.github.io
|
||||
owner: DefectingCat
|
||||
admin: [DefectingCat]
|
||||
path: # 全局评论地址
|
||||
# Vssue 暂不支持Pjax
|
||||
# https://vssue.js.org/zh/
|
||||
vssue:
|
||||
owner: DefectingCat
|
||||
repo: DefectingCat.github.io
|
||||
clientId:
|
||||
clientSecret:
|
||||
# LiveRe 暂不支持Pjax
|
||||
# https://www.livere.com
|
||||
livere:
|
||||
uid:
|
||||
# Isso 暂不支持Pjax
|
||||
# https://posativ.org/isso/
|
||||
isso:
|
||||
url: https://example.com/(path/)
|
||||
src: https://example.com/(path/)js/embed.min.js
|
||||
# HashOver 暂不支持Pjax
|
||||
# https://www.barkdull.org/software/hashover
|
||||
hashover:
|
||||
src: https://example.com/(path/)comments.php
|
||||
# Twikoo
|
||||
# https://twikoo.js.org/
|
||||
twikoo:
|
||||
js: https://cdn.jsdelivr.net/npm/twikoo@latest # 建议锁定版本
|
||||
path: # 全局评论地址
|
||||
# 其他配置项按照yml格式继续填写即可 除了 [el path] 选项
|
||||
envId: xxxxxxxxxxxxxxx # 腾讯云环境id
|
||||
# Waline
|
||||
# https://waline.js.org/
|
||||
waline:
|
||||
js: https://cdn.jsdelivr.net/npm/@waline/client/dist/Waline.min.js
|
||||
path: # 全局评论地址 目前设置全局评论地址后visitor失效,这是waline的问题
|
||||
placeholder: 快来评论吧~ # 评论占位提示
|
||||
imageHosting: https://7bu.top/api/upload # 图床api(默认使用去不图床)
|
||||
# 其他配置项按照yml格式继续填写即可 除了 [el path placeholder uploadImage] 选项
|
||||
meta: [nick, mail, link] # waline comment header info
|
||||
requiredFields: [nick, mail]
|
||||
serverURL: xxxxxxxxxxxxxxx # Waline 的服务端地址(必填) 测试用地址: https://waline-ruddy.vercel.app
|
||||
avatar: robohash # gravatar style https://waline.js.org/client/basic.html#avatar
|
||||
pageSize: 10 # 评论每页显示数量
|
||||
lang: zh-CN
|
||||
|
||||
############################### Comments ############################### > end
|
||||
|
||||
############################### Sidebar ############################### > start
|
||||
sidebar:
|
||||
# 主页、分类、归档等独立页面
|
||||
for_page: [blogger]
|
||||
# layout: docs/post 这类文章页面
|
||||
for_post: [toc]
|
||||
# 侧边栏组件库
|
||||
widget_library:
|
||||
# ---------------------------------------
|
||||
# blogger info widget
|
||||
blogger:
|
||||
class: blogger
|
||||
display: [desktop, mobile] # [desktop, mobile]
|
||||
avatar: /images/img/mona.webp
|
||||
shape: rectangle # circle, rectangle
|
||||
url: /about/
|
||||
title:
|
||||
subtitle:
|
||||
jinrishici: true # Poetry Today. You can set a string, and it will be displayed when loading fails.
|
||||
social:
|
||||
- icon: fas fa-rss
|
||||
url: /atom.xml
|
||||
- icon: fas fa-envelope
|
||||
url: mailto:me@xxx.com
|
||||
- icon: fab fa-github
|
||||
url: https://github.com/DefectingCat
|
||||
# ---------------------------------------
|
||||
# toc widget (valid only in articles)
|
||||
toc:
|
||||
class: toc
|
||||
display: [desktop, mobile] # [desktop, mobile]
|
||||
header:
|
||||
icon: fas fa-list
|
||||
title: 本文目录
|
||||
list_number: false
|
||||
min_depth: 2
|
||||
max_depth: 5
|
||||
# ---------------------------------------
|
||||
# category widget
|
||||
category:
|
||||
class: category
|
||||
display: [desktop] # [desktop, mobile]
|
||||
header:
|
||||
icon: fas fa-folder-open
|
||||
title: 文章分类
|
||||
url: /blog/categories/
|
||||
# ---------------------------------------
|
||||
# tagcloud widget
|
||||
tagcloud:
|
||||
class: tagcloud
|
||||
display: [desktop, mobile] # [desktop, mobile]
|
||||
header:
|
||||
icon: fas fa-tags
|
||||
title: 热门标签
|
||||
url: /blog/tags/
|
||||
min_font: 14
|
||||
max_font: 24
|
||||
color: true
|
||||
start_color: '#999'
|
||||
end_color: '#555'
|
||||
|
||||
# ---------------------------------------
|
||||
# qrcode widget
|
||||
donate:
|
||||
class: qrcode
|
||||
display: [desktop, mobile] # [desktop, mobile]
|
||||
height: 64px # Automatic height if not set
|
||||
images:
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/qrcode/github@volantis.png
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/blog/qrcode/github@volantis.png
|
||||
# ---------------------------------------
|
||||
# webinfo widget
|
||||
webinfo:
|
||||
class: webinfo
|
||||
display: [desktop]
|
||||
header:
|
||||
icon: fas fa-award
|
||||
title: 站点信息
|
||||
type:
|
||||
article:
|
||||
enable: true
|
||||
text: '文章数目:'
|
||||
unit: '篇'
|
||||
runtime:
|
||||
enable: true
|
||||
data: '2020/01/01' # 填写建站日期
|
||||
text: '已运行时间:'
|
||||
unit: '天'
|
||||
wordcount:
|
||||
enable: true
|
||||
text: '本站总字数:' # 需要启用 wordcount
|
||||
unit: '字'
|
||||
visitcounter:
|
||||
service: leancloud # busuanzi, leancloud
|
||||
siteuv:
|
||||
enable: true
|
||||
text: '本站访客数:'
|
||||
unit: '人'
|
||||
sitepv:
|
||||
enable: true
|
||||
text: '本站总访问量:'
|
||||
unit: '次'
|
||||
lastupd:
|
||||
enable: true
|
||||
friendlyShow: true # 更友好的时间显示
|
||||
text: '最后活动时间:'
|
||||
unit: '日'
|
||||
############################### Sidebar ############################### > end
|
||||
|
||||
############################### Tag Plugins ############################### > start
|
||||
# 内置标签插件的配置
|
||||
tag_plugins:
|
||||
# {% note text %}
|
||||
note: # style for default note:
|
||||
icon: '\f054'
|
||||
color: ''
|
||||
iconfont: 'Font Awesome 5 Free'
|
||||
# {% checkbox %}
|
||||
checkbox:
|
||||
interactive: false # enable interactive for user
|
||||
color: '' # color for default checkbox
|
||||
# {% link title, url, img %}
|
||||
link:
|
||||
placeholder: https://cdn.jsdelivr.net/gh/volantis-x/cdn-org/logo/256/safari.png
|
||||
############################### Tag Plugins ############################### > end
|
||||
|
||||
############################### Site Footer ############################### > start
|
||||
site_footer:
|
||||
# layout of footer: [aplayer, social, license, info, copyright]
|
||||
layout: [license, beian, copyright]
|
||||
social:
|
||||
- icon: #fas fa-rss
|
||||
url:
|
||||
# or
|
||||
- img:
|
||||
url:
|
||||
# or
|
||||
- avatar:
|
||||
url:
|
||||
# site source
|
||||
source: https://github.com/volantis-x/volantis-docs/
|
||||
# analytics using leancloud
|
||||
analytics:
|
||||
<span id="lc-sv">本站总访问量为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 次</span>
|
||||
<span id="lc-uv">访客数为 <span id='number'><i class="fas fa-circle-notch fa-spin fa-fw" aria-hidden="true"></i></span> 人</span>
|
||||
# site copyright
|
||||
copyright: '[Copyright © 2021 xfy](/)'
|
||||
# You can add your own property here. (Support markdown, for example: br: '<br>')
|
||||
br: '<br>'
|
||||
beian: '[<a target="_blank" href="https://beian.miit.gov.cn/">皖ICP备17017808号-1</a>](/)'
|
||||
############################### Site Footer ############################### > end
|
||||
|
||||
############################### Plugins ############################### > start
|
||||
plugins:
|
||||
################ required plugins ################
|
||||
# jquery
|
||||
jquery: https://cdn.jsdelivr.net/npm/jquery@3.5/dist/jquery.min.js
|
||||
# fontawesome
|
||||
fontawesome: https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.14/css/all.min.css
|
||||
################ optional plugins ################
|
||||
|
||||
######## Plugins to improve loading speed:
|
||||
|
||||
# 预加载
|
||||
preload:
|
||||
enable: true
|
||||
service: flying_pages # instant_page, flying_pages
|
||||
instant_page: https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@2/js/instant_page.js
|
||||
flying_pages: https://cdn.jsdelivr.net/gh/gijo-varghese/flying-pages@2.1.2/flying-pages.min.js
|
||||
|
||||
# 图片懒加载
|
||||
# https://www.npmjs.com/package/vanilla-lazyload
|
||||
lazyload:
|
||||
enable: true
|
||||
js: https://cdn.jsdelivr.net/npm/vanilla-lazyload@17.1.0/dist/lazyload.min.js
|
||||
onlypost: false
|
||||
loadingImg: # https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@3/img/placeholder/c617bfd2497fcea598e621413e315c368f8d8e.svg
|
||||
blurIn: true # 模糊加载效果 (loadingImg为空时有效)
|
||||
|
||||
######## Plugins to optimize the experience:
|
||||
|
||||
# highlight.js
|
||||
highlightjs:
|
||||
enable: true # Please set hexo.config.highlight.enable = false !!!
|
||||
js: https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@10/build/highlight.min.js
|
||||
css: https://cdn.jsdelivr.net/npm/highlight.js@11.2.0/styles/atom-one-light.css
|
||||
# more: https://www.jsdelivr.com/package/npm/highlight.js?path=styles
|
||||
|
||||
# https://scrollrevealjs.org/api/reveal.html
|
||||
scrollreveal:
|
||||
enable: #true
|
||||
js: https://cdn.jsdelivr.net/npm/scrollreveal@4.0.6/dist/scrollreveal.min.js
|
||||
distance: 32px
|
||||
duration: 800 # ms
|
||||
interval: 20 # ms
|
||||
scale: 1 # 0.1~1
|
||||
|
||||
# Codeblock Copy Button
|
||||
clipboard:
|
||||
enable: true
|
||||
js: https://cdn.jsdelivr.net/npm/clipboard@2/dist/clipboard.min.js
|
||||
|
||||
######## Plugins for SEO:
|
||||
|
||||
# npm i hexo-wordcount
|
||||
wordcount:
|
||||
enable: #true
|
||||
|
||||
######## Plugins for ...
|
||||
# Button Ripple Effect
|
||||
nodewaves:
|
||||
enable: #true
|
||||
css: https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.css
|
||||
js: https://cdn.jsdelivr.net/npm/node-waves@0.7.6/dist/waves.min.js
|
||||
|
||||
# fontawesome animation
|
||||
fontawesome_animation:
|
||||
enable: #true
|
||||
css: https://cdn.jsdelivr.net/gh/l-lin/font-awesome-animation/dist/font-awesome-animation.min.css
|
||||
|
||||
# Typing Effects
|
||||
comment_typing:
|
||||
enable: #true
|
||||
js: https://cdn.jsdelivr.net/gh/volantis-x/cdn-volantis@2/js/comment_typing.js
|
||||
|
||||
# Slide Background
|
||||
backstretch:
|
||||
enable: #true
|
||||
js: https://cdn.jsdelivr.net/npm/jquery-backstretch@2.1.18/jquery.backstretch.min.js
|
||||
position: cover # cover: sticky on the cover. fixed: Fixed as background for the site.
|
||||
shuffle: true # shuffle playlist
|
||||
duration: 10000 # Duration (ms)
|
||||
fade: 1500 # fade duration (ms) (Not more than 1500)
|
||||
images: # For personal use only. At your own risk if used for commercial purposes !!!
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/001.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/002.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/003.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/004.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/005.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/006.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/012.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/016.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/019.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/025.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/033.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/034.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/035.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/038.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/039.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/042.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/046.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/051.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/052.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/054.jpg
|
||||
- https://cdn.jsdelivr.net/gh/volantis-x/cdn-wallpaper-minimalist/2020/056.jpg
|
||||
|
||||
# APlayer is only available in mainland China.
|
||||
# APlayer config: https://github.com/metowolf/MetingJS
|
||||
aplayer:
|
||||
enable: #true
|
||||
js:
|
||||
aplayer: https://cdn.jsdelivr.net/npm/aplayer@1.10/dist/APlayer.min.js
|
||||
meting: https://cdn.jsdelivr.net/npm/meting@2.0/dist/Meting.min.js
|
||||
# Required
|
||||
server: netease # netease, tencent, kugou, xiami, baidu
|
||||
type: playlist # song, playlist, album, search, artist
|
||||
id: 3175833810 # song id / playlist id / album id / search keyword
|
||||
# Optional
|
||||
fixed: false # enable fixed mode
|
||||
theme: '#1BCDFC' # main color
|
||||
autoplay: false # audio autoplay
|
||||
order: list # player play order, values: 'list', 'random'
|
||||
loop: all # player loop play, values: 'all', 'one', 'none'
|
||||
volume: 0.7 # default volume, notice that player will remember user setting, default volume will not work after user set volume themselves
|
||||
list_max_height: 320px # list max height
|
||||
list_folded: true
|
||||
pjax:
|
||||
enable: true
|
||||
cover: true # 封面是否pjax处理 false:每次切换页面封面都重载,适合封面较少的情况 true:封面经过Pjax处理,适合封面较多的情况
|
||||
timeout: 5000 # The timeout in milliseconds for the XHR requests. Set to 0 to disable the timeout.
|
||||
cacheBust: false # When set to true, Pjax appends a timestamp to skip the browser cache.
|
||||
animation: false # false, nprogress, circle
|
||||
banUrl:
|
||||
# 被屏蔽的 url 地址将不启用 pjax 跳转,可以在控制台下使用 window.location.pathname 获取
|
||||
# - '/artitalk/' # artitalk 不支持 pjax
|
||||
# - '/bb/' # bbtalk 不支持 pjax
|
||||
|
||||
# 从 issues 加载动态数据
|
||||
# {% issues sites/timeline/friends | api=xxx | group=key:a,b,c %}
|
||||
# 例如:
|
||||
# {% issues sites | api=https://api.github.com/repos/volantis-x/examples/issues?sort=updated&state=open&page=1&per_page=100 | group=version:latest,v6,v5,v4,v3,v2,v1,v0 %}
|
||||
|
||||
# 暗黑模式 darkmode
|
||||
# 样式:source/css/_plugins/dark.styl
|
||||
# 开关按钮:在 navbar.menu 中添加:
|
||||
# - name: 暗黑模式 # 可自定义
|
||||
# icon: fas fa-moon # 可自定义
|
||||
# toggle: darkmode
|
||||
darkmodejs:
|
||||
enable: true
|
||||
|
||||
# 旧版 Internet Explorer 淘汰行动
|
||||
# https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support
|
||||
# 本主题不支持Internet Explorer的任何版本!!!
|
||||
killOldVersionsOfIE:
|
||||
enable: true
|
||||
|
||||
# 禁用JavaScript提示
|
||||
# 本页面需要浏览器支持(启用)JavaScript
|
||||
# 主题中的某些插件必须启用JavaScript才能正常工作,例如开启scrollreveal如果禁用JavaScript会导致卡片消失
|
||||
killNoScript:
|
||||
enable: true
|
||||
|
||||
# Artitalk https://artitalk.js.org
|
||||
# 配置过程请参考:https://artitalk.js.org/doc.html
|
||||
# 使用过旧版本的请修改Leancloud shuoshuo class部分列名:https://artitalk.js.org/release.html
|
||||
# 除appID和appKEY外均为选填项
|
||||
artitalk:
|
||||
# Set `layout: artitalk` to enable in page
|
||||
# 配置项按照yml格式继续填写即可
|
||||
appId: ogP8qj3veMh0LFpFWMPOyF0X-MdYXbMMI # your appID
|
||||
appKey: nHXLd3N3Jgh460t2iRQKWAtr # your appKEY
|
||||
# serverURL: #leancloud绑定的安全域名,使用国际版的话不需要填写
|
||||
# lang: # 语言设置,zh为汉语,en为英语,es为西班牙语。默认为汉语
|
||||
# pageSize: #每页说说的显示数量
|
||||
# shuoPla: #在编辑说说的输入框中的占位符
|
||||
# avatarPla: #自定义头像url的输入框的占位符
|
||||
# motion: #加载动画的开关,1为开,0为关,默认为开
|
||||
# bgImg: #说说输入框背景图片url
|
||||
# color1: #说说背景颜色1&按钮颜色1
|
||||
# color2: #说说背景颜色2&按钮颜色2
|
||||
# color3: #说说字体颜色
|
||||
# cssUrl: #自定义css接口
|
||||
|
||||
# BBtalk https://bb.js.org
|
||||
bbtalk:
|
||||
js: https://cdn.jsdelivr.net/npm/bbtalk@0.1.5/dist/bbtalk.min.js # BBtalk.js
|
||||
appId: 0KzOX4vC7Jsk6vzUGNeEiUaI-gzGzoHsz # your appID
|
||||
appKey: HwCiWuxfpvKiLm4teCUgTIba # your appKEY
|
||||
serverURLs: https://bbapi.heson10.com # Request Api 域名
|
||||
|
||||
# Tidio聊天功能
|
||||
# https://www.tidio.com/
|
||||
tidio:
|
||||
enable: #true
|
||||
id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
############################### Plugins ############################### > end
|
||||
|
||||
############################### Rightmenu ############################### > start
|
||||
# 自定义右键菜单
|
||||
rightmenu:
|
||||
enable: #true
|
||||
# hr: 分割线, music: 音乐控制器
|
||||
layout:
|
||||
[
|
||||
home,
|
||||
help,
|
||||
examples,
|
||||
contributors,
|
||||
hr,
|
||||
source_docs,
|
||||
source_theme,
|
||||
hr,
|
||||
print,
|
||||
hr,
|
||||
dark_mode,
|
||||
hr,
|
||||
music,
|
||||
]
|
||||
# 可选功能项
|
||||
print:
|
||||
name: 打印页面
|
||||
icon: fa fa-print
|
||||
onclick: document.execCommand('print')
|
||||
# 自定义菜单的格式如下
|
||||
help:
|
||||
name: 常见问题
|
||||
icon: fa fa-question
|
||||
url: https://volantis.js.org/faqs/
|
||||
examples:
|
||||
name: 示例博客
|
||||
icon: fa fa-rss
|
||||
url: https://volantis.js.org/examples/
|
||||
contributors:
|
||||
name: 加入社区
|
||||
icon: fa fa-fan fa-spin
|
||||
url: https://volantis.js.org/contributors/
|
||||
source_docs:
|
||||
name: 本站源码
|
||||
icon: fa fa-code-branch
|
||||
url: https://github.com/volantis-x/volantis-docs/
|
||||
source_theme:
|
||||
name: 主题源码
|
||||
icon: fa fa-code-branch
|
||||
url: https://github.com/volantis-x/hexo-theme-volantis/
|
||||
dark_mode:
|
||||
name: Dark mode
|
||||
icon: fas fa-moon
|
||||
toggle: darkmode
|
||||
############################### Rightmenu ############################### > end
|
||||
|
||||
############################### Search ############################### > start
|
||||
# To use hexo search, you need to install the following plugins:
|
||||
# npm i hexo-generator-search hexo-generator-json-content
|
||||
search:
|
||||
enable: true
|
||||
service: hexo # hexo, google, algolia, azure, baidu
|
||||
js:
|
||||
google:
|
||||
apiKey:
|
||||
engineId:
|
||||
algolia:
|
||||
applicationID:
|
||||
apiKey:
|
||||
indexName:
|
||||
azure:
|
||||
serviceName:
|
||||
indexName:
|
||||
queryKey:
|
||||
baidu:
|
||||
apiId:
|
||||
############################### Search ############################### > end
|
||||
|
||||
############################### Color Scheme ############################### > start
|
||||
color_scheme:
|
||||
# ------------
|
||||
# 通用颜色
|
||||
common:
|
||||
# 主题色
|
||||
theme: '#44D7B6'
|
||||
# 链接色
|
||||
link: '#2196f3'
|
||||
# 按钮色
|
||||
button: '#44D7B6'
|
||||
# 鼠标放到交互元素上时的色
|
||||
hover: '#ff5722'
|
||||
# 主题色块内部的文字颜色
|
||||
inner: '#fff'
|
||||
# 选中区域文字的背景颜色
|
||||
selection: 'alpha(#2196f3, 0.2)'
|
||||
# ------------
|
||||
# 亮色主题(默认)
|
||||
light:
|
||||
# 网站背景色
|
||||
site_bg: '#f4f4f4'
|
||||
# 网站背景上的文字
|
||||
site_inner: '#fff'
|
||||
# 网站页脚文字
|
||||
site_footer: '#666'
|
||||
|
||||
# 卡片背景色
|
||||
card: '#fff'
|
||||
# 卡片上的普通文字
|
||||
text: '#444'
|
||||
|
||||
# 区块和代码块背景色
|
||||
block: '#f6f6f6'
|
||||
# 代码块高亮时的背景色
|
||||
codeblock: '#FFF7EA'
|
||||
# 行内代码颜色
|
||||
inlinecode: '#c74f00'
|
||||
|
||||
# 文章部分
|
||||
h1: '#3a3a3a'
|
||||
h2: '#3a3a3a'
|
||||
h3: '#333'
|
||||
h4: '#444'
|
||||
h5: '#555'
|
||||
h6: '#666'
|
||||
p: '#444'
|
||||
|
||||
# 列表文字
|
||||
list: '#666'
|
||||
# 列表 hover 时的文字
|
||||
list_hl: 'mix($color-theme, #000, 80)'
|
||||
# 辅助性文字
|
||||
meta: '#888'
|
||||
# ------------
|
||||
# 暗色主题
|
||||
dark:
|
||||
# 网站背景色
|
||||
site_bg: '#222'
|
||||
# 网站背景上的文字
|
||||
site_inner: '#eee'
|
||||
# 网站页脚文字
|
||||
site_footer: '#aaa'
|
||||
# 卡片背景色
|
||||
card: '#333'
|
||||
# 卡片上的普通文字
|
||||
text: '#eee'
|
||||
|
||||
# 区块和代码块背景色
|
||||
block: '#3a3a3a'
|
||||
# 代码块高亮时的背景色
|
||||
codeblock: '#343a3c'
|
||||
# 行内代码颜色
|
||||
inlinecode: '#D56D28'
|
||||
|
||||
# 文章部分
|
||||
h1: '#eee'
|
||||
h2: '#eee'
|
||||
h3: '#ddd'
|
||||
h4: '#ddd'
|
||||
h5: '#ddd'
|
||||
h6: '#ddd'
|
||||
p: '#bbb'
|
||||
|
||||
# 列表文字
|
||||
list: '#aaa'
|
||||
# 列表 hover 时的文字
|
||||
list_hl: 'mix($color-theme, #fff, 80)'
|
||||
# 辅助性文字
|
||||
meta: '#888'
|
||||
# 夜间图片亮度
|
||||
brightness: 70%
|
||||
############################### Color Scheme ############################### > end
|
||||
|
||||
############################### Custom css ############################### > start
|
||||
custom_css:
|
||||
toc_smooth: true # TOC 目录平滑滚动效果
|
||||
cursor:
|
||||
enable: #true
|
||||
text: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/text.png
|
||||
pointer: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/pointer.png
|
||||
default: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/left_ptr.png
|
||||
not-allowed: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/circle.png
|
||||
zoom-out: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/zoom-out.png
|
||||
zoom-in: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/zoom-in.png
|
||||
grab: https://cdn.jsdelivr.net/gh/inkss/common@master/cursor/openhand.png
|
||||
font_smoothing: true # font-smoothing for webkit
|
||||
max_width: 960px # Sum of body width and sidebar width (This limit will be exceeded when the device width is greater than 2000px, reaching 75% of the total width)
|
||||
scrollbar:
|
||||
size: 4px
|
||||
border: 2px
|
||||
navbar:
|
||||
height: 64px
|
||||
width: auto # auto, max
|
||||
effect: [shadow, blur] # [shadow, floatable, blur]
|
||||
sidebar:
|
||||
effect: [shadow] # [shadow, floatable, blur]
|
||||
body:
|
||||
effect: [shadow] # [shadow, floatable, blur]
|
||||
highlight:
|
||||
language: true # show language of codeblock
|
||||
copy_btn: true
|
||||
grayscale: false # Enable grayscale effect
|
||||
text_align: # left, right, justify, center
|
||||
h1: left
|
||||
h2: left
|
||||
h3: left
|
||||
h4: left
|
||||
p: justify
|
||||
gap:
|
||||
h2: 48px # Spacing above H2 (only px unit)
|
||||
h3: 24px # Spacing above H3 (only px unit)
|
||||
h4: 16px # Spacing above H4 (only px unit)
|
||||
p: 1em # Paragraph spacing between paragraphs
|
||||
line_height: 1.6 # normal, 1.5, 1.75, 2 ...
|
||||
border_radius:
|
||||
card: 8px
|
||||
codeblock: 4px
|
||||
searchbar: 8px
|
||||
button: 4px
|
||||
fontsize:
|
||||
root: 16px
|
||||
h1: 1.5rem # 不推荐用在文章中
|
||||
h2: 1.5rem
|
||||
h3: 1.25rem
|
||||
h4: 1.125rem
|
||||
h5: 1rem
|
||||
h6: 1rem
|
||||
list: .9375rem
|
||||
meta: .875rem
|
||||
code: .8125rem
|
||||
footnote: .78125rem
|
||||
fontfamily:
|
||||
logofont:
|
||||
fontfamily: '"Varela Round", "PingFang SC", "Microsoft YaHei", Helvetica, Arial'
|
||||
name: 'Varela Round'
|
||||
url: https://cdn.jsdelivr.net/gh/volantis-x/cdn-fonts/VarelaRound/VarelaRound-Regular.ttf
|
||||
weight: normal
|
||||
style: normal
|
||||
bodyfont:
|
||||
fontfamily: 'UbuntuMono, "Varela Round", "PingFang SC", "Microsoft YaHei", Helvetica, Arial'
|
||||
name: 'UbuntuMono'
|
||||
url: https://cdn.jsdelivr.net/gh/volantis-x/cdn-fonts/UbuntuMono/UbuntuMono-Regular.ttf
|
||||
weight: normal
|
||||
style: normal
|
||||
codefont:
|
||||
fontfamily: 'Menlo, UbuntuMono, Monaco'
|
||||
# name: 'Monaco'
|
||||
# url: https://cdn.jsdelivr.net/gh/volantis-x/cdn-fonts/Monaco/Monaco.ttf
|
||||
# weight: normal
|
||||
# style: normal
|
||||
############################### Custom css ############################### > end
|
||||
|
||||
############################### Analytics ############################### > start
|
||||
analytics:
|
||||
busuanzi: #https://cdn.jsdelivr.net/gh/volantis-x/cdn-busuanzi@2.3/js/busuanzi.pure.mini.js
|
||||
leancloud: # 请使用自己的 id & key 以防止数据丢失
|
||||
app_id: u9j57bwJod4EDmXWdxrwuqQT-MdYXbMMI
|
||||
app_key: jfHtEKVE24j0IVCGHbvuFClp
|
||||
custom_api_server: # 国际版一般不需要写,除非自定义了 API Server
|
||||
############################### Analytics ############################### > end
|
||||
|
||||
############################### SEO ############################### > start
|
||||
seo:
|
||||
# When there are no keywords in the article's front-matter, use tags as keywords.
|
||||
use_tags_as_keywords: true
|
||||
# When there is no description in the article's front-matter, use excerpt as the description.
|
||||
use_excerpt_as_description: true
|
||||
robots:
|
||||
home_first_page: index,follow
|
||||
home_other_pages: noindex,follow
|
||||
archive: noindex,follow
|
||||
category: noindex,follow
|
||||
tag: noindex,follow
|
||||
# robots can be written in front-matter
|
||||
############################### SEO ############################### > end
|
59
_config.yml
@ -2,6 +2,14 @@
|
||||
## Docs: https://hexo.io/docs/configuration.html
|
||||
## Source: https://github.com/hexojs/hexo/
|
||||
|
||||
import:
|
||||
meta:
|
||||
link:
|
||||
- <link rel="icon" type="image/png" sizes="32x32" href="/images/img/favicon.webp">
|
||||
- <link rel="stylesheet" href="/css/xfy.css">
|
||||
script:
|
||||
- <script></script>
|
||||
|
||||
# Site
|
||||
title: 🍭Defectink
|
||||
subtitle: '只要心还在跳'
|
||||
@ -49,8 +57,13 @@ highlight:
|
||||
line_number: false
|
||||
auto_detect: false
|
||||
tab_replace: ''
|
||||
wrap: true
|
||||
hljs: false
|
||||
wrap: false
|
||||
hljs: true
|
||||
prismjs:
|
||||
enable: false
|
||||
preprocess: true
|
||||
line_number: false
|
||||
tab_replace: ''
|
||||
|
||||
# Home page setting
|
||||
# path: Root path for your blogs index page. (default = '')
|
||||
@ -79,7 +92,8 @@ meta_generator: true
|
||||
date_format: YYYY-MM-DD
|
||||
time_format: HH:mm:ss
|
||||
## Use post's date for updated date unless set in front-matter
|
||||
use_date_for_updated: false
|
||||
# use_date_for_updated: false
|
||||
updated_option: 'date'
|
||||
|
||||
# Pagination
|
||||
## Set per_page to 0 to disable pagination
|
||||
@ -93,9 +107,8 @@ exclude:
|
||||
ignore:
|
||||
|
||||
# Extensions
|
||||
## Plugins: https://hexo.io/plugins/
|
||||
## Themes: https://hexo.io/themes/
|
||||
theme: fluid
|
||||
theme: volantis
|
||||
|
||||
# Deployment
|
||||
## Docs: https://hexo.io/docs/deployment.html
|
||||
@ -104,28 +117,24 @@ deploy:
|
||||
repo: git@github.com:DefectingCat/DefectingCat.github.io.git
|
||||
branch: gh-pages
|
||||
message: ❤
|
||||
- type: 'git'
|
||||
repo: git@e.coding.net:Defectink/xfy/blog.git
|
||||
branch: gh-pages
|
||||
message: ❤
|
||||
|
||||
# feed
|
||||
feed:
|
||||
type:
|
||||
- atom
|
||||
- rss2
|
||||
path:
|
||||
- /atom.xml
|
||||
- /rss.xml
|
||||
limit: 20
|
||||
hub:
|
||||
content:
|
||||
content_limit: 140
|
||||
content_limit_delim: ' '
|
||||
order_by: -date
|
||||
icon: icon.png
|
||||
autodiscovery: true
|
||||
template:
|
||||
# feed:
|
||||
# type:
|
||||
# - atom
|
||||
# - rss2
|
||||
# path:
|
||||
# - /atom.xml
|
||||
# - /rss.xml
|
||||
# limit: 20
|
||||
# hub:
|
||||
# content:
|
||||
# content_limit: 140
|
||||
# content_limit_delim: ' '
|
||||
# order_by: -date
|
||||
# icon: icon.png
|
||||
# autodiscovery: true
|
||||
# template:
|
||||
|
||||
# sitemap
|
||||
sitemap:
|
||||
|
15
package.json
@ -24,10 +24,9 @@
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-htmlclean": "^2.7.22",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"gulp-uglify-es": "^2.0.0",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"hexo": "^5.4.0",
|
||||
"hexo-cli": "^4.2.0",
|
||||
"hexo-cli": "^4.3.0",
|
||||
"hexo-deployer-git": "^3.0.0",
|
||||
"hexo-generator-archive": "^1.0.0",
|
||||
"hexo-generator-category": "^1.0.0",
|
||||
@ -36,15 +35,15 @@
|
||||
"hexo-generator-sitemap": "^2.1.0",
|
||||
"hexo-generator-tag": "^1.0.0",
|
||||
"hexo-renderer-ejs": "^1.0.0",
|
||||
"hexo-renderer-marked": "^4.0.0",
|
||||
"hexo-renderer-marked": "^4.1.0",
|
||||
"hexo-renderer-stylus": "^2.0.1",
|
||||
"hexo-theme-fluid": "^1.8.11",
|
||||
"hexo-theme-volantis": "^4.3.1",
|
||||
"nunjucks": "^3.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.2",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"hexo-server": "^2.0.0",
|
||||
"npm-check-updates": "^11.5.11"
|
||||
"npm-check-updates": "^11.8.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
let d = new Date();
|
||||
|
||||
let result = d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
|
||||
|
||||
console.log(result);
|
@ -1,6 +0,0 @@
|
||||
git checkout backup
|
||||
git merge master
|
||||
git push origin
|
||||
git push github
|
||||
git push xfy
|
||||
git checkout master
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><XnView_script version="1.0" name="添加水印">
|
||||
<Watermark filename="C:/Users/xfy/OneDrive/Pictures/Logo/文字logo标题.png" opacity="85" no_alpha="false" position="8" delta_x="-25" delta_y="-25" perc="15" size="3"/>
|
||||
<Watermark filename="D:/OneDrive/Pictures/Logo/文字logo标题.png" opacity="85" no_alpha="false" position="8" delta_x="-25" delta_y="-25" perc="15" size="3"/>
|
||||
<Output folder="" filename="{Filename}" case="0" startIndex="1" format="WEBP">
|
||||
<Options overwrite="1" orgDate="false" keepMeta="false" keepICC="false" keepFolder="false" keepParentFolder="false" keepExtension="true" delOrg="true" multipage="false" allPages="false" openExplorer="false" openBrowser="false" clearItems="false"/>
|
||||
<WEBP method="0" quality="75" filesize="128" compress="4" strength="60" sharpness="0" preset="0"/>
|
||||
|
@ -114,3 +114,4 @@ char words[50];
|
||||
`gets()`函数不会检查数组的长度,也就是它不知道数组何时会结束。如果输入的字符过长,就会导致缓冲区溢出(buffer overflow)。
|
||||
|
||||
在类 UNIX 系统中,会提示`Segmentation fault`。
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
在权威指南以及高级程序设计都出了新版之后,在其中遇到的一些新的 ECMAScript 的新特性也该记录下来了。
|
||||
|
||||
## 函数
|
||||
|
||||
### 条件式调用
|
||||
|
||||
在 ES2020 中,可以使用`?.()`来条件式的调用一个函数。这和对象的条件式访问类似,在正常情况下如果直接访问一个不存在(nulll 或 undefined)的表达式,会抛出 TypeError。而使用条件式调用,在这种情况下,则整个表达式的值为 undefined,不会抛出异常。
|
||||
|
||||
```js
|
||||
const test = (num, fn) => {
|
||||
fn?.(num);
|
||||
};
|
||||
```
|
||||
|
||||
当然他们也有一些些小小的区别:
|
||||
|
||||
```js
|
||||
test.fn(); // 常规属性访问,常规调用
|
||||
test?.fn(); // 条件式属性访问,常规调用
|
||||
test.fn?.(); // 常规属性访问,条件式调用
|
||||
```
|
||||
|
31
source/_md/Golang Web.md
Normal file
@ -0,0 +1,31 @@
|
||||
## 处理请求
|
||||
|
||||
每进来一个 HTTP 请求,Handler 都会为其创建一个 goroutine。默认 Handler `http.DefaulServeMux`。
|
||||
|
||||
`http.ListenAndServe(":4000", nil)` 第二个参数就是 Handler,为 nil 时,就是 `DefaulServeMux`。
|
||||
|
||||
`DefaulServeMux` 是一个 multiplexer。
|
||||
|
||||
### 编写 Handler
|
||||
|
||||
Handler 是一个 struct,它需要实现一个 `ServeHTTP()` 方法。
|
||||
|
||||
```go
|
||||
type myHandler struct {
|
||||
}
|
||||
|
||||
func (m *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := w.Write([]byte("Hello world"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注册一个 Handler 到 multiplexer 需要使用 `http.Handle`
|
||||
|
||||
```go
|
||||
http.Handle("/hello", &mh)
|
||||
http.Handle("/about", &a)
|
||||
```
|
||||
|
55
source/_md/Golang.md
Normal file
@ -0,0 +1,55 @@
|
||||
## 修改字符串
|
||||
|
||||
字符串底层是由字节组成的切片,但字符串本身是不可变的。修改字符串有两种方法:
|
||||
|
||||
1. 将其转换为 byte 切片
|
||||
|
||||
```go
|
||||
str := "xfy"
|
||||
s := []byte(str)
|
||||
s[0] = 'd'
|
||||
str = string(s)
|
||||
|
||||
fmt.Println(str)
|
||||
```
|
||||
|
||||
但这种方法不能处理中文,因为一个中文字符占 3 个 byte。不能只对某一个 byte 进行赋值。
|
||||
|
||||
2. 将其转换为 rune 切片
|
||||
|
||||
```go
|
||||
str := "小肥羊"
|
||||
s := []rune(str)
|
||||
s[0] = '大'
|
||||
str = string(s)
|
||||
|
||||
fmt.Println(str)
|
||||
```
|
||||
|
||||
rune 表示单个 Unicode 字符串,是按字符进行处理的,可以修改单个字符。
|
||||
|
||||
## 结构体赋值
|
||||
|
||||
给指针字段赋值时,可以省略解引用的星号:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
type Cat struct {
|
||||
name string
|
||||
age int
|
||||
color string
|
||||
}
|
||||
|
||||
myCat := Cat{
|
||||
"xfy",
|
||||
1,
|
||||
"cows",
|
||||
}
|
||||
|
||||
fmt.Println(myCat)
|
||||
|
||||
var c3 *Cat = new(Cat)
|
||||
c3.name = "test" // Equal to (*c3).name = "test"
|
||||
}
|
||||
```
|
||||
|
@ -493,7 +493,3 @@ for (let i = 0; i < 5; i++) {
|
||||
frag.append(ul);
|
||||
```
|
||||
|
||||
### Attr 类型
|
||||
|
||||
## 操作 DOM
|
||||
|
@ -1,3 +1,5 @@
|
||||
> 某咸鱼第一次尝试 JavaScript 的笔记,混乱不堪,并伴有多处错误。
|
||||
|
||||
## 全局对象
|
||||
|
||||
Global对象是一个特别的对象,在一般情况下无法访问。它不属于任何其他对象的属性和方法,最终都是它的属性和方法。所有在全局作用域中声明的函数和对象都是Global对象的属性。
|
||||
@ -376,11 +378,13 @@ for (let i of iterable) {
|
||||
|
||||
## 轮转时间片
|
||||
|
||||
* 解释性语言
|
||||
* ~~解释性语言~~
|
||||
* 单线程
|
||||
|
||||
js是单线程的,模拟多线程的操作。当有多个任务需要同时执行时,js会将多个任务分成极小时间单位的执行段,轮询在多个任务之间执行。
|
||||
|
||||
> JavaScript 并不是解释型语言 2021-06-27
|
||||
|
||||
<hr>
|
||||
|
||||
## 主流浏览器
|
||||
@ -660,7 +664,7 @@ a();
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
在a函数执行时,`a.[[scope]]`内第0位添加为a函数的AO。由此形成一个链式结构,在指定函数内查找变量时,从指定函数的作用域链顶端,从上(第0位)到下寻找变量。
|
||||
|
||||
@ -671,15 +675,15 @@ a();
|
||||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
由于b函数在a的内部,所以b函数在被定义时[[scope]]内就有了A的AO。
|
||||
|
||||

|
||||

|
||||
|
||||
而此时b的[[scope]]还不是完整的作用域链,当b函数执行时,将自己的AO添加到[[scope]]顶端。这时才形成了一个完整的作用域链。
|
||||
|
||||

|
||||

|
||||
|
||||
在b函数种的AO是a函数的引用。因此在b函数中修改a函数的AO的值,也会将a函数内的AO修改。
|
||||
|
||||
@ -737,7 +741,7 @@ a()(); // demo = a(); demo();
|
||||
|
||||
> 内部函数被返回到了外部,便形成了闭包。
|
||||
|
||||

|
||||

|
||||
|
||||
不适用return返回函数,直接操作外部变量来保存内部的函数。
|
||||
|
||||
@ -1331,7 +1335,7 @@ let obj = Object.create(null);
|
||||
|
||||
原型是一个隐式的内部属性,给没有原型的对象手动添加是不起作用的。可以添加上,但是并不能访问。(原始值不能作为参数)
|
||||
|
||||

|
||||

|
||||
|
||||
> null和undefined是原始值,不能被包装,没有原型。
|
||||
|
||||
@ -2229,7 +2233,7 @@ catch会将try内遇到的错误信息捕捉到
|
||||
|
||||
Catch捕捉到的Error对象:
|
||||
|
||||

|
||||

|
||||
|
||||
`error.name`对应的六种值信息:
|
||||
|
||||
@ -2252,8 +2256,6 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 严格模式
|
||||
|
||||
现在的浏览器是基于es3.0的方法 + es5.0的新增方法来执行js的。当遇到es3.0与5.0冲突的时候,使用严格模式`use strict`来解决冲突。
|
||||
|
62
source/_md/MutationObserver.md
Normal file
@ -0,0 +1,62 @@
|
||||
不久前添加到 DOM 规范中的 MutationObserver 接口,可以在 DOM 被修改时异步执行回调。MutationObserver 可以观察整个文档、DOM 树的一部分,或某个元素。此外还可以观察元素属性、子节点、文本,或者前三者任意组合的变化。
|
||||
|
||||
> MutationObserver 接口是为了取代废弃的 MutationEvent。
|
||||
|
||||
## 基本用法
|
||||
|
||||
MutationObserver 的实例需要通过调用 MutationObserver 构造函数,并传入一个回调函数来创建:
|
||||
|
||||
```js
|
||||
const observer = new MutationObserver(() => console.log('<body> attributes changed'));
|
||||
```
|
||||
|
||||
### observe 方法
|
||||
|
||||
创建后的实例通过`observe()`方法来监听指定的 DOM,它接收两个必要的参数:观察的 DOM 节点,以及 MutationObserverInit 对象。
|
||||
|
||||
观察 body 元素的 attributes 变化:
|
||||
|
||||
```js
|
||||
const observer = new MutationObserver(() => console.log('<body> attributes changed'));
|
||||
observer.observe(document.body, { attributes: true });
|
||||
```
|
||||
|
||||
### 回调与 MutationRecord
|
||||
|
||||
每个监听的回调都会收到一个 MutationRecord 实例的数组,它包含了发送的变化,以及 DOM 的哪部分收到了影响。
|
||||
|
||||
```js
|
||||
const observer = new MutationObserver((recoeds) => console.log(recoeds));
|
||||
observer.observe(document.body, { attributes: true });
|
||||
```
|
||||
|
||||
属性变化的 MutationRecord 实例数组:
|
||||
|
||||
```js
|
||||
document.body.className = '123';
|
||||
// [MutationRecord]
|
||||
// addedNodes: NodeList []
|
||||
// attributeName: "class"
|
||||
// attributeNamespace: null
|
||||
// nextSibling: null
|
||||
// oldValue: null
|
||||
// previousSibling: null
|
||||
// removedNodes: NodeList []
|
||||
// target: body.123
|
||||
// type: "attributes"
|
||||
```
|
||||
|
||||
连续修改属性会生成多个 MutationRecord 实例。
|
||||
|
||||
### disconnect 方法
|
||||
|
||||
默认情况下,只要观察的 DOM 不被垃圾回收,MutationObserver 的回调就会一直响应 DOM 变化事件。可以使用 `disconnect()`方法来提前终止回调。终止后,不仅停止此后变化事件的回调,也会抛弃已经加入任务队列要异步执行的回调。
|
||||
|
||||
```js
|
||||
const observer = new MutationObserver((recoeds) => console.log(recoeds));
|
||||
observer.observe(document.body, { attributes: true });
|
||||
document.body.className = '123';
|
||||
observer.disconnect();
|
||||
document.body.className = '456';
|
||||
// 没有输出
|
||||
```
|
@ -1,38 +0,0 @@
|
||||
## 登录概述
|
||||
|
||||
登录业务流程
|
||||
|
||||
1. 在登陆页面输入用户名和密码
|
||||
2. 调用后台接口进行验证
|
||||
3. 通过验证后,根据后台的相应状态跳转到项目主页
|
||||
|
||||
登录业务相关技术点
|
||||
|
||||
* HTTP 是无状态的
|
||||
* 通过 cookie 在客户端记录状态
|
||||
* 通过 session 在服务端记录状态
|
||||
* 通过 token 方式维持状态
|
||||
|
||||
如果前端与服务器不存在跨域,则使用 cookie + session 方式维持状态,如果存在跨域,则使用 token 的方式。
|
||||
|
||||
### token 原理分析
|
||||
|
||||
当客户端尝试登陆后,会触发如下阶段:
|
||||
|
||||
1. 登录页面输入用户名和密码进行登录;
|
||||
2. 服务器验证通过之后生产该用户的 token 并返回;
|
||||
3. 客户端接收到后存储该 token;
|
||||
4. 后续客户端所有请求都携带该 token;
|
||||
5. 服务器收到 token 后验证是否通过;
|
||||
|
||||

|
||||
|
||||
## 登录页面
|
||||
|
||||
使用 element-plus 来进行布局,使用到了如下的组件:
|
||||
|
||||
* `el-form`
|
||||
* `el-form-item`
|
||||
* `el-input`
|
||||
* `el-button`
|
||||
* 字体图标
|
@ -1,113 +0,0 @@
|
||||
## 值与引用
|
||||
|
||||
在许多编程语言中,赋值和参数传递可以通过值赋值(value-copy)或者引用复制(reference-copy)来完成。
|
||||
|
||||
例如在 C 中,传递一个引用值可以通过声明类似于这样的`int* num`参数来按引用传递,如果传递一个变量 x,那么`num`就是指向 x 的引用。引用就是指向变量的指针,如果不声明为引用的话,参数值总是通过值来传递的。即便是复杂的对象值也是如此(C++)。
|
||||
|
||||
与 C/C++ 不同的是,JavaScript 没有指针这一概念,值的传递方式完全由值来决定。JavaScript 中变量不可能成为指向另一个变量的指针。
|
||||
|
||||
基本类型(简单类型)的值总是通过以值复制的方式来赋值/传递,这些类型包括:`null`、`undefined`、字符串、数字、布尔和`symbol`。
|
||||
|
||||
而复合值,也就是对象(以及对象的子类型,数组、包装对象等)和函数,则总是以引用复制的方式来赋值/传递。
|
||||
|
||||
在了解了基本类型和引用类型的值之后,先来看下他们传递有什么不同:
|
||||
|
||||
基本类型:
|
||||
|
||||
由于基本类型是按值传递的,所以 a 与 b 是分别在内存中两处保存了自己的值。a 有在内存中有自己的空间,b 也有自己单独的空间,他们互不影响。
|
||||
|
||||
```js
|
||||
let a = 123;
|
||||
let b = a; // 按值进行传递
|
||||
|
||||
a += 1; // 修改 a
|
||||
console.log(a); // 124
|
||||
console.log(b); // 123 b 不受影响
|
||||
```
|
||||
|
||||
引用类型:
|
||||
|
||||
引用值的情况正好相反,所谓按引用传递,就是`arr1`与`arr2`指向的是内存中的同一块地址,**修改**任何一个变量的值,都会立即反应到另一个变量上。因为他们对应的是同一块内存。
|
||||
|
||||
```js
|
||||
let arr1 = [1, 2, 3];
|
||||
let arr2 = arr1;
|
||||
|
||||
arr1.push(99); // 修改 aar1
|
||||
console.log(arr1); // [ 1, 2, 3, 99 ]
|
||||
console.log(arr2); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
但是引用值还有个特性容易犯错,那就是修改:
|
||||
|
||||
这里咋一看是修改了`arr1`的值,但为什么没有反应到`arr2`身上呢?说好的一起变呢?
|
||||
|
||||
仔细回想一下引用值的定义,他们是因为指向同一块内存地址,所以修改这段地址中的值时,就会同时反应在两个变量上。但是这里的`arr1 = { name: 'xfy' }`并不是修改内存中的值,而是修改了`arr1`的指向,使其指向一块新的内存地址。而`arr2`还是指向以前的地址,所以`arr2`没有改变。
|
||||
|
||||
```js
|
||||
let arr1 = [1, 2, 3];
|
||||
let arr2 = arr1;
|
||||
|
||||
arr1 = { name: 'xfy' };
|
||||
console.log(arr1); // { name: 'xfy' }
|
||||
console.log(arr2); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
### 使用函数修改值
|
||||
|
||||
由于上述值的传递特性,这也会导致在传递给函数参数时发生个中问题。
|
||||
|
||||
修改引用值:
|
||||
|
||||
```js
|
||||
function changeValue(value) {
|
||||
// 按引用传递,可以直接修改
|
||||
value.push(99);
|
||||
// 重新赋值,并没有修改内存中的值
|
||||
value = { name: 'xfy' };
|
||||
}
|
||||
|
||||
let arr = [1, 2, 3];
|
||||
changeValue(arr);
|
||||
console.log(arr); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
修改基本值:
|
||||
|
||||
```js
|
||||
function changeValue(value) {
|
||||
// 按值传递,value 获取到 num 的值
|
||||
// 但是他们分别保存在两个内存中
|
||||
value++;
|
||||
}
|
||||
|
||||
let num = 123;
|
||||
changeValue(num);
|
||||
console.log(num); // 123
|
||||
```
|
||||
|
||||
这也就是为什么在 Vue3 的 Composition API 中使用 ref 封装的响应式变量必须要有`.value`属性。
|
||||
|
||||

|
||||
|
||||
## 强制类型转换
|
||||
|
||||
将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。
|
||||
|
||||
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。
|
||||
|
||||
### 抽象值操作
|
||||
|
||||
在了解强制类型之前,我们需要先掌握类型之间转换的基本规则。ES5 规范第 9 节定义了一些“抽象操作”和转换规则。
|
||||
|
||||
#### ToString
|
||||
|
||||
ToString 负责非字符串到字符串的强制类型转换操作。
|
||||
|
||||
基本值转换为字符串的规则为直接添加双引号:null 转换为`"null"`,true 转换为`"true"`等。数字也遵循这种规则,不过极大或极小的数字使用指数形式。
|
||||
|
||||
```js
|
||||
(1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000).toString()
|
||||
// "1.07e+21"
|
||||
```
|
||||
|
73
source/_md/使用Canvas实现手写签名.md
Normal file
@ -0,0 +1,73 @@
|
||||
Canvas 是一个强大的 HTML 特性,它使我们能够在 HTML 中绘制出色的图形。在那个没有 Canvas 的年代,我们还需要借助 Flash 来在网页上进行绘图。而现在的 Canvas 能为我们带来更高的性能,以及更方便的 API。
|
||||
|
||||
最近的一个项目里需要实现客户在屏幕上手写签名,和第三方对接无果,所以只好打算自己实现一个较为简单的版本。原本都是对 Canvas 往而远之的,最终上手试一下了,API 的易用性还是比想象中的要好用的。
|
||||
|
||||
## 创建一个 Canvas
|
||||
|
||||
Canvas 本身在 DOM 中的表现是一个 HTML 标签,所以第一步是我们的 HTML 中创建一个 `<canvas></canvas>` 标签:
|
||||
|
||||
```html
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script src="src/index.ts"></script>
|
||||
</body>
|
||||
```
|
||||
|
||||
剩下的活就交给 JavaScript 来做了。首先需要获取到 Canvas 的上下文,并设置画板为 2d:
|
||||
|
||||
```ts
|
||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
}
|
||||
```
|
||||
|
||||
除此之外,本次的目的是需要一个类似全屏的画板,所以我们的 canvas 需要占满整个浏览器窗口。这里使用的是 JavaScript 来为 canvas 设置宽和高,主要目的是为了后续的动态修改。使用 CSS 也是同样的效果:
|
||||
|
||||
```ts
|
||||
// 添加
|
||||
if (canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const pageWidth = document.documentElement.clientWidth;
|
||||
const pageHeight = document.documentElement.clientHeight;
|
||||
|
||||
canvas.width = pageWidth;
|
||||
canvas.height = pageHeight;
|
||||
}
|
||||
```
|
||||
|
||||
> 使用 CSS 为 canvas 设置宽高需要注意的是:canvas 本身是以图片的方式存在的,它并不是类似 SVG 的矢量图,如果设置相对值,可能会直接拉伸图片。
|
||||
|
||||
## 开始绘画
|
||||
|
||||
为了实现鼠标绘画,首先的思路当然是监听鼠标事件。鼠标事件大致分为三个部分:鼠标按下(mousedown)、鼠标移动(mousemove)和鼠标松开(mouseup)。我们的简易画板就由这三个事件组成。
|
||||
|
||||
### 第一步
|
||||
|
||||
canvas 的绘图离不开坐标,即使是 2d 绘图,也需要横向(x)与纵向(y)两个方向。所以我们事先准备一个对象用于保存每次开始的位置,也就是鼠标按下时的位置。
|
||||
|
||||
同时用一个布尔值判断绘画是否开始:
|
||||
|
||||
```ts
|
||||
/** @var painting Whether use is drawing */
|
||||
let painting = false;
|
||||
/** @var lastPoint Recored user mouse position */
|
||||
let lastPoint = {
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 点与线
|
||||
|
||||
canvas 有许多强大的 API,而实现利用鼠标绘画只需要用到两个最基本的 API:创建圆与直线。
|
||||
|
||||
我们主要使用的是鼠标的三个事件,而在鼠标按下时,即表明绘画已经开始,所以我们需要从创建一个点开始。也就是说,当鼠标按下时,在 canvas 中绘制一个圆点。绘制一个圆主要使用 `arc()` 这个方法:
|
||||
|
||||
```TS
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
```
|
||||
|
442
source/_md/全栈之路-Notedly后端开发.md
Normal file
@ -0,0 +1,442 @@
|
||||
主要依赖:
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"apollo-server-koa": "^3.0.1",
|
||||
"graphql": "^15.5.1",
|
||||
"koa": "^2.13.1"
|
||||
}
|
||||
```
|
||||
|
||||
## GraphQL
|
||||
|
||||
GraphQL API 主要有两个组成部分,即模式和解析器。
|
||||
|
||||
### 模式
|
||||
|
||||
GraphQL 模式的基本构件是对象模型,GraphQL 原生支持五种数据类型:
|
||||
|
||||
* String
|
||||
* Boolean
|
||||
* Int
|
||||
* Float
|
||||
* ID
|
||||
|
||||
简单的模式通常像这样以对象的形式创建:
|
||||
|
||||
```js
|
||||
const typeDefs = gql`
|
||||
type Note {
|
||||
id: ID!
|
||||
content: String!
|
||||
author: String!
|
||||
}
|
||||
type Query {
|
||||
hello: String
|
||||
notes: [Note!]!
|
||||
note(id: ID!): Note!
|
||||
}
|
||||
type Mutation {
|
||||
newNote(content: String!): Note!
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
### 解析器
|
||||
|
||||
GraphQL 的第二个部分就是解析器,它的作用就是负责解析 API 用户请求的数据。这里用到了两种解析器:查询(Query)和变更(Mutation)。
|
||||
|
||||
```ts
|
||||
const resolvers = {
|
||||
Query: {
|
||||
hello: () => 'Hello world!',
|
||||
notes: () => notes,
|
||||
note: (parent: unknown, args: { id: string }) => {
|
||||
return notes.find((item) => item.id === args.id);
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
newNote: (parent: unknown, args: { content: string }) => {
|
||||
const newValue = {
|
||||
id: String(notes.length),
|
||||
content: args.content,
|
||||
author: 'xfy',
|
||||
};
|
||||
notes.push(newValue);
|
||||
return newValue;
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
解析器还接收对应的参数:
|
||||
|
||||
* parent:父查询的结果;
|
||||
* args:用户在查询时传入的参数;
|
||||
* context:服务器应用传给解析器函数的信息,可能包含当前用户或数据库信息;
|
||||
* info:关于自身查询的信息;
|
||||
|
||||
## ApolloServer
|
||||
|
||||
Apollo Server 是一个开源的 GraphQL 服务器,支持很多的 Node.js 服务器框架。这里使用的是 Koa 配上 apollo-server-koa 来搭建后端 API 服务器。
|
||||
|
||||
### 定义模式
|
||||
|
||||
这里直接在 src 目录下创建了`schema.ts`,并将 GraphQL 的模式定义在这里:
|
||||
|
||||
```js
|
||||
import { gql } from 'apollo-server-koa';
|
||||
|
||||
export default gql`
|
||||
type Note {
|
||||
id: ID!
|
||||
content: String!
|
||||
author: String!
|
||||
}
|
||||
type Query {
|
||||
notes: [Note!]!
|
||||
note(id: ID!): Note!
|
||||
}
|
||||
type Mutation {
|
||||
newNote(content: String!): Note!
|
||||
updateNote(id: ID!, content: String!): Note!
|
||||
deleteNote(id: ID!): Boolean!
|
||||
}
|
||||
`;
|
||||
```
|
||||
|
||||
并在主应用`app.ts`中以这样进行引入`import typeDefs from './schema';`。
|
||||
|
||||
在定义的模式中,Query 是对应查询的模式,Mutation 是对应 CRUD 的模式。而 Note 是自定义的笔记存放格式,后续将继续对应数据库模型。
|
||||
|
||||
在模式中定义的参数,如`deleteNote(id: ID!)`会对应传递到其解析器的第二个形参。
|
||||
|
||||
### 定义解析器
|
||||
|
||||
解析器对应查询模式,目前分别需要两个解析器,分别是 Query 和 Mutation。这里的目录解构划分为:`src/resolvers`。
|
||||
|
||||
Query 比较简单,直接操作数据库模型进行查询即可:
|
||||
|
||||
```ts
|
||||
// Query.ts
|
||||
import models from '../models';
|
||||
|
||||
export default {
|
||||
notes: async (): Promise<void> => await models.Note.find(),
|
||||
note: async (parent: unknown, args: { id: string }): Promise<void> =>
|
||||
await models.Note.findById(args.id),
|
||||
};
|
||||
```
|
||||
|
||||
而 Mutation 则相对复杂一点,它分别对应了 CUD 的操作。
|
||||
|
||||
```ts
|
||||
// Mutation.ts
|
||||
import models from '../models';
|
||||
|
||||
export default {
|
||||
newNote: async (
|
||||
parent: unknown,
|
||||
args: { content: string }
|
||||
): Promise<void> => {
|
||||
return await models.Note.create({
|
||||
content: args.content,
|
||||
author: 'xfy',
|
||||
});
|
||||
},
|
||||
updateNote: async (
|
||||
parent: unknown,
|
||||
args: { id: string; content: string }
|
||||
): Promise<void> => {
|
||||
return await models.Note.findOneAndUpdate(
|
||||
{
|
||||
_id: args.id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
content: args.content,
|
||||
},
|
||||
},
|
||||
{
|
||||
new: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
deleteNote: async (
|
||||
parent: unknown,
|
||||
args: { id: string }
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await models.Note.findOneAndDelete({
|
||||
_id: args.id,
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
> 这里的 models 就是接下来将要定义的数据库模型。
|
||||
|
||||
为了统一在主应用`app.ts`中使用,这里还需要将多个分离的解析器统一导出:
|
||||
|
||||
```ts
|
||||
// src/resolves/index.ts
|
||||
import Query from './query';
|
||||
import Mutation from './mutation';
|
||||
|
||||
export default {
|
||||
Query,
|
||||
Mutation,
|
||||
};
|
||||
```
|
||||
|
||||
### 创建 Apollo Server
|
||||
|
||||
将模式与解析器定义完成之后,就应该交由 Apollo 来创建 Server 了。在主应用中分别将模式与解析器导入,并运行服务器。
|
||||
|
||||
```ts
|
||||
// app.ts
|
||||
import typeDefs from './schema';
|
||||
import resolvers from './resolvers';
|
||||
|
||||
// ...
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
context: () => {
|
||||
return { models };
|
||||
},
|
||||
});
|
||||
await server.start();
|
||||
// ...
|
||||
```
|
||||
|
||||
## 数据库
|
||||
|
||||
数据库采用的是 MongoDB,ODM(Object Document Mapper)使用的是 Mongoose。
|
||||
|
||||
### 连接数据库
|
||||
|
||||
Mongoose 也是连接 MongoDB 的客户端,简单封装一下就可直接连接数据库,后续 CRUD 根据模型来操作。
|
||||
|
||||
```ts
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 这个方法用于设置和连接数据库
|
||||
* @param DB_HOST 数据库地址
|
||||
*/
|
||||
connect: (DB_HOST: string): void => {
|
||||
// 使用 Mongo 驱动新的 URL 字符串解析器
|
||||
mongoose.set('useNewUrlParser', true);
|
||||
// 使用 findOneAndUpdate() 代替 useFindAndModify()
|
||||
mongoose.set('useFindAndModify', false);
|
||||
// 使用 createIndex() 代替 ensureIndex()
|
||||
mongoose.set('useCreateIndex', true);
|
||||
// 使用新的服务器发现和监控引擎
|
||||
mongoose.set('useUnifiedTopology', true);
|
||||
mongoose.connect(DB_HOST);
|
||||
mongoose.connection.on('error', (err) => {
|
||||
console.log(err);
|
||||
console.log(
|
||||
'MongoDB connection error. Please make sure MongoDB is running.'
|
||||
);
|
||||
process.exit();
|
||||
});
|
||||
},
|
||||
close: (): void => {
|
||||
mongoose.connection.close();
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 数据库模型
|
||||
|
||||
新建一个存放数据库模型的文件夹`src/models`。目前只用到了两个集合,分别是 ntoe 和 user。
|
||||
|
||||
文档的交叉引用需要将 type 设置为`mongoose.Schema.Types.ObjectId`,并配置 ref 引用到对应的模型。
|
||||
|
||||
```ts
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
// 定义笔记的数据库模式
|
||||
const NoteSchema = new mongoose.Schema(
|
||||
{
|
||||
content: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
author: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
require: true,
|
||||
},
|
||||
favoriteCount: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
favoritedBy: [
|
||||
{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// 添加 Date 类型的 createAt 和 updateAt 字段
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
// 通过模式定义 Note 模型
|
||||
const Note = mongoose.model('Note', NoteSchema);
|
||||
|
||||
export default Note;
|
||||
```
|
||||
|
||||
### CRUD 解析器
|
||||
|
||||
编写对应的 CRUD 模式与之前相同,写好对应数据类型以及格式即可。真正对应的操作在对应的解析器上,对应的解析器分别对应着数据的 CRUD。
|
||||
|
||||
Mongoose 的操作分别使用对应的模型来操作对应的集合,互相引用的 ID 需要引入`mongoose.Types.ObjectId()`来创建:
|
||||
|
||||
```ts
|
||||
newNote: async (
|
||||
parent: unknown,
|
||||
args: { content: string },
|
||||
ctx: { user: { id: string } }
|
||||
): Promise<void> => {
|
||||
if (!ctx.user)
|
||||
throw new AuthenticationError('You must be signed in to create a note');
|
||||
|
||||
return await models.Note.create({
|
||||
content: args.content,
|
||||
author: mongoose.Types.ObjectId(ctx.user.id),
|
||||
});
|
||||
},
|
||||
```
|
||||
|
||||
### 日期与时间
|
||||
|
||||
再编写数据库模型时,添加了 timestamps 来创建对应的创建时间和更新时间,均为 UTC 格式的 Date 类型。
|
||||
|
||||
```ts
|
||||
// 添加 Date 类型的 createAt 和 updateAt 字段
|
||||
timestamps: true,
|
||||
```
|
||||
|
||||
但原生的 GrapQL 是不支持 Date 类型的,可以变通一下使用 String 代替,但是这样就无法借助 GrapQL 提供的类型验证功能,以确保时间的正确性。
|
||||
|
||||
可以利用 GrapQL 来定义一个新的类型,并添加第三方包来创建 Date 类型。
|
||||
|
||||
创建新的类型需要在模式中声明一个类型:
|
||||
|
||||
```ts
|
||||
// src/schema.ts
|
||||
export default gql`
|
||||
scalar DateTime
|
||||
`
|
||||
```
|
||||
|
||||
之后在导出 resolvers 的时候,添加引入的第三方包类型为我们声明的新类型:
|
||||
|
||||
```ts
|
||||
// src/resolvers/index.ts
|
||||
export default {
|
||||
Query,
|
||||
Mutation,
|
||||
Note,
|
||||
User,
|
||||
DateTime: GraphQLDateTime,
|
||||
};
|
||||
```
|
||||
|
||||
之后在模式中就可以使用该类型:
|
||||
|
||||
```ts
|
||||
// src/schema.ts
|
||||
type Note {
|
||||
id: ID!
|
||||
content: String!
|
||||
author: User
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
favoriteCount: Int!
|
||||
favoritedBy: [User!]
|
||||
}
|
||||
```
|
||||
|
||||
甚至在自动生成的文档中还能引入第三方包所创建的说明:
|
||||
|
||||

|
||||
|
||||
### 嵌套查询
|
||||
|
||||
GraphQL 还有个引人注目的功能便是嵌套查询了,直接看 schema:
|
||||
|
||||
```gql
|
||||
type Query {
|
||||
notes: [Note!]!
|
||||
note(id: ID!): Note!
|
||||
}
|
||||
```
|
||||
|
||||
这段查询中,两个`note`都会返回对应的 Note 类型。在 Note 类型中,还会返回 User 类型。Note 类型本身代表一次数据库的查询,而对应的`author`字段并不在数据库模型中,反而是在对应的 User 库中。所以这里就需要再根据对应的索引来查询一次 User 库。
|
||||
|
||||
```gql
|
||||
type Note {
|
||||
id: ID!
|
||||
content: String!
|
||||
author: User
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
favoriteCount: Int!
|
||||
favoritedBy: [User!]
|
||||
commentNum: String
|
||||
}
|
||||
```
|
||||
|
||||
在 Query 中,对应的 Note 类型只需要定义一个查询 Note 库的方法即可:
|
||||
|
||||
```ts
|
||||
// resolves/query.ts
|
||||
notes: async (): Promise<void> => await models.Note.find(),
|
||||
note: async (parent: unknown, args: { id: string }): Promise<void> =>
|
||||
await models.Note.findById(args.id),
|
||||
```
|
||||
|
||||
对应的嵌套查询 User 库,则需要单独定义一个解析器,并且要将解析器注册于查询同名。
|
||||
|
||||
```js
|
||||
// resolves/note.ts
|
||||
/**
|
||||
* 嵌套查询笔记作者信息
|
||||
* @param note
|
||||
* @returns
|
||||
*/
|
||||
author: async (note: { author: string }): Promise<unknown> => {
|
||||
return await models.User.findById(note.author);
|
||||
},
|
||||
```
|
||||
|
||||
在注册所有的解析中,嵌套查询的解析器也需要于其父查询的名称相同:
|
||||
|
||||
```ts
|
||||
// resolves/index.ts
|
||||
export default {
|
||||
Query,
|
||||
Mutation,
|
||||
Note,
|
||||
User,
|
||||
Comment,
|
||||
Reply,
|
||||
DateTime: GraphQLDateTime,
|
||||
};
|
||||
```
|
||||
|
||||
我们只需要定义好对应查询数据库的方法与返回的数据格式,剩下的嵌套关系 GraphQL 便会帮我们处理好。
|
@ -1,40 +1,5 @@
|
||||
最近想使用 koa 与 mongodb 做一个简单的后端。
|
||||
|
||||
## 监听文件更改
|
||||
|
||||
* [How to watch and reload ts-node when TypeScript files change](https://stackoverflow.com/questions/37979489/how-to-watch-and-reload-ts-node-when-typescript-files-change)
|
||||
|
||||
```bash
|
||||
yarn add eslint prettier eslint-plugin-prettier eslint-config-prettier -D
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
|
||||
```
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {},
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 参数
|
||||
|
||||
|
82
source/_md/函数式编程指北.md
Normal file
@ -0,0 +1,82 @@
|
||||
## 函数式思想
|
||||
|
||||
> 面向对象编程(OO)通过封装变化使得代码更易理解。
|
||||
>
|
||||
> 函数式编程(FP)通过最小化变化使得代码更易理解。
|
||||
|
||||
函数式编程的目标是使用函数来抽象作用在数据之上的控制流与操作,从而在系统中消除副作用并减少对状态的改变。
|
||||
|
||||
不同于我们熟悉的过程式编程,函数式编程是声明式编程。
|
||||
|
||||
```ts
|
||||
const arr = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
// 过程式/命令式
|
||||
const newArr: number[] = [];
|
||||
for (const i of arr) {
|
||||
if (i > 2) newArr.push(i);
|
||||
}
|
||||
|
||||
// 函数式
|
||||
const findNumberGreatThanTwo = (val: number) => val > 2;
|
||||
const newArr2 = arr.filter(findNumberGreatThanTwo);
|
||||
|
||||
console.log(newArr, newArr2);
|
||||
```
|
||||
|
||||
函数式编程基于一个前提,即使用纯函数构建具有不变性的程序。纯函数的性质:
|
||||
|
||||
* 仅取决于提供的输入,而不依赖于任何函数在求值期间获调用间隔时可能变化的隐藏状态和外部状态。
|
||||
* 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数。
|
||||
|
||||
```ts
|
||||
let counter = 0;
|
||||
|
||||
function increment() {
|
||||
return ++counter; // 不纯的函数
|
||||
}
|
||||
```
|
||||
|
||||
引用透明是定义一个纯函数较为正确的方式。纯度在这个意义上表明一个函数的参数和返回值之间映射的关系。如果一个函数对于相同的输入始终产生相同的结果,那么它就是引用透明的。
|
||||
|
||||
```ts
|
||||
let counter = 0;
|
||||
function increment() {
|
||||
return ++counter;
|
||||
}
|
||||
|
||||
console.log(increment()); // 1
|
||||
console.log(increment()); // 2
|
||||
|
||||
const newCounter = 0;
|
||||
const plus1 = (counter: number) => {
|
||||
return counter + 1;
|
||||
};
|
||||
|
||||
const myCounter1 = plus1(newCounter); // 1
|
||||
const myCounter2 = plus1(newCounter); // 1
|
||||
|
||||
console.log(myCounter1, myCounter2);
|
||||
```
|
||||
|
||||
**不可变数据**是指那些被创建后就不能更改的数据。JavaScript 和其他许多语言意义,它的基本类型(String、Number 等)从本质上来说是不可变的。但是其他对象(引用值)都是可变的。即使函数的参数是按值进行传递的,但是我们任然可以通过改变原有内容的方式产生副作用。
|
||||
|
||||
```ts
|
||||
const person = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
|
||||
const changeName = (person: { name: string; age: number }) => {
|
||||
person.name = 'dfy';
|
||||
person = {
|
||||
name: 'aha',
|
||||
age: 19,
|
||||
};
|
||||
};
|
||||
|
||||
changeName(person);
|
||||
console.log(person); // name: 'dfy', age: 18
|
||||
```
|
||||
|
||||
根据上述一些基本原则(声明式、纯的和不可变),可以更简洁的描述函数式编程:函数式编程是指创建不可变的程序,通过消除外部可见的副作用,来对函数的声明式的求值过程。
|
88
source/_md/咸鱼的项目配置.md
Normal file
@ -0,0 +1,88 @@
|
||||
## ESlint
|
||||
|
||||
```bash
|
||||
yarn add typescript nodemon eslint prettier eslint-plugin-prettier eslint-config-prettier -D
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn add @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
|
||||
```
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {},
|
||||
};
|
||||
```
|
||||
|
||||
## vite + vue3
|
||||
|
||||
```bash
|
||||
yarn add eslint prettier eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
|
||||
```
|
||||
|
||||
```js
|
||||
// .eslintrc.json
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"parser":"vue-eslint-parser",
|
||||
"extends": [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"vue",
|
||||
"@typescript-eslint",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"quotes": ["error", "single"]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
目前为了兼容 script setup 语法,将关掉 eslint 中的`no-unused-vars`,开启 ts 中的`noUnusedLocals`。
|
||||
|
||||
```json
|
||||
// https://github.com/johnsoncodehk/volar/issues/47
|
||||
"noUnusedLocals": true,
|
||||
```
|
||||
|
||||
> 现在已经支持 script setup 语法。
|
||||
|
||||
```js
|
||||
// .prettierrc.json
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true
|
||||
}
|
||||
```
|
||||
|
@ -1,205 +0,0 @@
|
||||
浏览器发展至今,各种主流浏览器都实现了各自的长处,随之而来的就是各种不一致性的问题。
|
||||
|
||||
## 能力检测
|
||||
|
||||
最常用的一种客户端检测形式就是**能力检测**(特征检测)。能力检测不是去识别特定的浏览器,而是去识别浏览器的能力。只要确定了浏览器支持的特定能力,就可以给出特定的解决方案。检测手段也很简单,只需要用到简单的类型转换:
|
||||
|
||||
```js
|
||||
if (Object.propertyInQuestion) {
|
||||
// 使用特定能力
|
||||
}
|
||||
```
|
||||
|
||||
来看一个简单的例子,在IE5.0之前不支持`document.getElementById()`这个DOM方法,但是可以使用非标准的`document.all`属性来实现相同的目的。所以:
|
||||
|
||||
```js
|
||||
function getElementId(id) {
|
||||
if (document.getElementById) {
|
||||
return document.getElementById(id);
|
||||
} else if (document.all) {
|
||||
return document.all(id);
|
||||
} else {
|
||||
throw new Error('No way to get element id.')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这里先判断标准方法是否存在,如果存在就直接使用。如果不存在,就使用IE5.0之前的非标准方法。如果二者都没有,则抛出一个错误。
|
||||
|
||||
### 更可靠的能力检测
|
||||
|
||||
仅仅靠简单的类型转换来检测是不够完善的,不仅仅要知道某个属性是否存在,还需要知道它是不是我们所需要的那个方法。如果仅使用类型转换来做判断,那么可能会遇到这样的问题:
|
||||
|
||||
```js
|
||||
function isSortable(obj) {
|
||||
return !!obj.sort;
|
||||
}
|
||||
|
||||
let someObj = {
|
||||
sort: 1
|
||||
}
|
||||
|
||||
isSortable(someObj); //true
|
||||
```
|
||||
|
||||
可以考虑善用`typeof`操作符,例如:
|
||||
|
||||
```js
|
||||
function isSortable(obj) {
|
||||
return typeof obj.sort == 'function';
|
||||
}
|
||||
```
|
||||
|
||||
不过`typeof`操作符也不是完美的解决方案,在早期的IE中,某些DOM方法返回的是`object`而不是`function`。例如`document.createElement()`方法。
|
||||
|
||||
除此之外,IE的ActiveX对象与其他对象的行为差异很大。例如:
|
||||
|
||||
```js
|
||||
let xhr = new ActiveXObject('Microsoft.XMLHttp');
|
||||
if (xhr.open) { //发生错误
|
||||
// do something...
|
||||
}
|
||||
|
||||
typeof (xhr.open); //unknow
|
||||
```
|
||||
|
||||
当然针对IE也是有解决办法的:
|
||||
|
||||
```js
|
||||
//来自 Peter Michaux
|
||||
function isHostMethod(object, property) {
|
||||
let t = object[property];
|
||||
return t == 'function' || (!!(t == 'object' && object[property])) || t == 'unknow';
|
||||
}
|
||||
```
|
||||
|
||||
## 怪癖检测
|
||||
|
||||
怪癖检测,也就是Bug检测。通过确定浏览器以有的Bug来确定某一个特性不能正常工作。
|
||||
|
||||
在IE8以及之前中有个Bug,将某个实例的属性设置为与标记了`[[Enumerbale]]`为`false`的某个原型属性同名,那么该属性就不会被枚举。可以这样来检测:
|
||||
|
||||
```js
|
||||
(function hasEnumerableQuirk() {
|
||||
let obj = {
|
||||
toString: function () {}
|
||||
}
|
||||
for (let i in obj) {
|
||||
if (i == 'toString') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
```
|
||||
|
||||
在Safari 3以前的版本中也有一个Bug,实例会枚举被隐藏的同名的原型属性。
|
||||
|
||||
```js
|
||||
(function hasEnumerShadowQuirk() {
|
||||
let obj = {
|
||||
toString: function () {}
|
||||
}
|
||||
let count = 0;
|
||||
for (i in obj) {
|
||||
if (i == 'toString') {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return (count > 1);
|
||||
})()
|
||||
```
|
||||
|
||||
## 用户代理字符串的历史
|
||||
|
||||
这是一段很有趣的浏览器历史。
|
||||
|
||||
// maybe later
|
||||
|
||||
## 用户代理字符串检测
|
||||
|
||||
用户代理字符串也就是常见的UA(UserAgent)。考虑到各个主流浏览器的发展历史,所以UA的判断也变的比较复杂。当然对于现代更加复杂的浏览器环境来说,识别出详细的浏览器还是需要更多的检测依据。
|
||||
|
||||
### 识别呈现引擎
|
||||
|
||||
呈现引擎,也就是浏览器的内核。每个引擎都有一些自己的特性,但是要正确的识别出引擎,关键还是识别顺序。
|
||||
|
||||
为了不污染全局变量,这里使用局部变量的命名来命名。这个方法最终返回一个对象,这个对象就是根据检测到的引擎版本的键值对。
|
||||
|
||||
```js
|
||||
let client = function () {
|
||||
let engine = {
|
||||
// 主流引擎
|
||||
trident: 0,
|
||||
gecko: 0,
|
||||
webkit: 0,
|
||||
khtml: 0,
|
||||
opera: 0,
|
||||
|
||||
// 具体版本号
|
||||
ver: null
|
||||
}
|
||||
|
||||
return {
|
||||
engine: engine
|
||||
}
|
||||
```
|
||||
|
||||
基本的变量命名都准备好了,接下来就是判断了。我们的第一步就是识别 opera,因为它的用户代理字符串有可能完全模仿其他浏览器。
|
||||
|
||||
判断 opera 很简单,不需要去检测 ua 中的字符串,它有个全局变量`window.opera`供我们检测:
|
||||
|
||||
```js
|
||||
if (window.opera) {
|
||||
engine.ver = window.opera.version();
|
||||
engine.opera = parseFloat(engine.ver);
|
||||
}
|
||||
```
|
||||
|
||||
第二步就是 WebKit 了,WebKit 需要我们通过判断 ua 字符串内的特定内容来识别它。在客户端获取 UA 最好的办法就是通过`navigator.userAgent`属性。
|
||||
|
||||
```js
|
||||
let ua = navigator.userAgent;
|
||||
let webkit = /AppleWebKit\/(\S+)/;
|
||||
|
||||
if (webkit.test(ua)) {
|
||||
engine.ver = ua.match(webkit)[1];
|
||||
engine.webkit = parseFloat(engine.ver);
|
||||
}
|
||||
```
|
||||
|
||||
KHTML 的用户代理字符串中也包含 Gecko,因此在排除 KHTML 之前,无法准确检测基于 Gecko 的浏览器。
|
||||
|
||||
```js
|
||||
let khtml = /KHTML\/(\S+)/;
|
||||
let khtml1 = /Konqueror\/([^;]+)/;
|
||||
|
||||
if (khtml.test(ua) || khtml1.test(ua)) {
|
||||
engine.ver = ua.match(khtml)[1];
|
||||
engine.khtml = parseFloat(engine.ver);
|
||||
}
|
||||
```
|
||||
|
||||
在排除了 KHTML 与 WebKit 之后,就可以去检测 Gecko 了,Gecko 的版本号不一定会出现在 Gecko 关键字后面,而是会出现在`rv:`的后面。
|
||||
|
||||
```js
|
||||
let gecko = /rv:([^\)]+)\) Gecko\/\d{8}/;
|
||||
if (gecko.test(ua)) {
|
||||
engine.ver = ua.match(gecko)[1];
|
||||
engine.gecko = engine.ver;
|
||||
}
|
||||
```
|
||||
|
||||
在所有的大哥都被排除了之后,最后剩下的就是 IE 了。在最新版本的 IE11 中已经没有关键字`MSIE`,取而代之的是`rv:`。并且有个独有的关键字`WOW64`。
|
||||
|
||||
```js
|
||||
let trident = {
|
||||
wow: /WOW64/,
|
||||
rv: /rv:([^\)]+)/
|
||||
};
|
||||
if (trident.wow.test(ua)) {
|
||||
engine.ver = ua.match(trident.rv)[1];
|
||||
engine.trident = engine.ver;
|
||||
}
|
||||
```
|
||||
|
@ -126,3 +126,8 @@ location / {
|
||||
### 自动续期
|
||||
|
||||
letsencrypt 获取免费的证书很是方便,但是必须要三个月续期一次。
|
||||
|
||||
```bash
|
||||
0 4 1 * * /bin/sh /data/docker/xfy/certbot/renew.sh
|
||||
```
|
||||
|
||||
|
7
source/_md/数据结构与算法.md
Normal file
@ -0,0 +1,7 @@
|
||||
算法常见定义:
|
||||
|
||||
* 一个有限指令集,每条指令的描述不依赖语言;
|
||||
* 可能会接收一些输入;
|
||||
* 产出输出;
|
||||
* 一定在有限步骤后终止;
|
||||
|
20
source/_md/更优雅的上传图片-原生拖放与粘贴.md
Normal file
@ -0,0 +1,20 @@
|
||||
最早期 IE4 为 JavaScript 引入了拖放功能。当时只有两样东西可以拖放:图片和文本。现代 HTML5 规范在后续 IE 的拖放实现基础上标准化了拖放功能,目前所有主流浏览器都支持拖放功能。
|
||||
|
||||
拖放图片到指定的容器中,实现上传图片的功能在现代互联网已经是非常常见的功能。其主要实现手段就是利用了 JavaScript 原生拖放 API 与 File API。
|
||||
|
||||
## 主要思路
|
||||
|
||||
主要的上传功能不仅仅只包含一种上传的方式,还需要涵盖下目前主流浏览器支持良好的几种方式。分别有:拖拽、粘贴和手动选择图片。
|
||||
|
||||
所以主要的思路就是将上传的区域分成两个部分,一个是监听拖拽和粘贴的盒子;另一个是手动选择文件的`input[file]`按钮。
|
||||
|
||||
盒子分别监听拖放事件 和 粘贴事件,input 就只需要监听一个 change 事件。
|
||||
|
||||
还有个值得注意下的 API 就是 Blob 和 `window.URL.createObjectURL`。成功获取到用户选择的图片时,得到的就是一个 Blob 对象。
|
||||
|
||||
以前常见的展示方式是利用 FileReader 实例来读取图片,将其转换为 base64 给 `img` 标签展示图片。这种方法对内存没有过多的占用,兼容性也很好,缺点就是大一点的图片计算 base64 需要点时间,可能会阻塞当前进程(很短)。
|
||||
|
||||
还有一种方式就是利用 `window.URL.createObjectURL` 来将 Blob 或 File 对象创建一个对象 URL。这个函数返回一个指向内存中地址的字符串,且这个字符串是 URL,所以可以直接给 `img` 使用,不需要任何计算。缺点就是只要这个对象 URL 在使用中,就不能释放内存。最好在使用完了就立即手动释放它。
|
||||
|
||||
## 基本结构
|
||||
|
489
source/_posts/JavaScript-this全面解析.md
Normal file
@ -0,0 +1,489 @@
|
||||
---
|
||||
title: JavaScript-this全面解析
|
||||
date: 2021-07-12 11:38:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-fxxk-this
|
||||
---
|
||||
|
||||
## this 全面解析
|
||||
|
||||
this 和动态作用域有些许类似,他们都是在执行时决定的。this 是在调用时被绑定的,完全取决于函数的调用位置。
|
||||
|
||||
### 确定调用位置
|
||||
|
||||
当一个函数被调用是,会创建一个活动记录(执行期上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this 就是这个记录里的一个属性。
|
||||
|
||||
调用位置就是函数在代码中被调用的位置,而不是声明的位置。可以类似于这样来这个记录并分析出函数的真正调用位置。
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
// 当前调用栈:foo
|
||||
|
||||
console.log('foo');
|
||||
bar();
|
||||
}
|
||||
function bar() {
|
||||
// 当前调用栈:foo --> bar
|
||||
|
||||
console.log('bar');
|
||||
baz();
|
||||
}
|
||||
function baz() {
|
||||
// 当前调用栈:foo --> bar --> baz
|
||||
|
||||
console.log('baz');
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
### 绑定规则
|
||||
|
||||
this 是在运行时动态绑定的,所以在不同的情况下,this 可能会发生各种意料之外的情况。
|
||||
|
||||
#### 默认绑定
|
||||
|
||||
当函数在全局环境下独立调用时,this 会指向为全局对象。
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
function foo() {
|
||||
console.log(this.a); // 123
|
||||
}
|
||||
```
|
||||
|
||||
而当函数处理严格模式下,则不能将全局对象用于默认绑定,因此 this 会绑定到`undefined`
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
function foo() {
|
||||
"use strict"
|
||||
console.log(this.a); // TypeError: this is undefined
|
||||
}
|
||||
```
|
||||
|
||||
还有一个微妙的细节,虽然 this 的绑定完全取决于调用的位置,但是只有`foo()`函数本身处于非严格模式才能绑定到全局对象。如果只是函数执行时所在严格模式下,而本身是非严格模式,则不影响默认绑定规则。
|
||||
|
||||
```ts
|
||||
var a = 123;
|
||||
|
||||
function foo() {
|
||||
console.log(this.a);
|
||||
}
|
||||
|
||||
(() => {
|
||||
'use strict';
|
||||
foo();
|
||||
})();
|
||||
```
|
||||
|
||||
> 通常来说不推荐在代码中混用严格模式与非严格模式。
|
||||
|
||||
#### 隐式绑定
|
||||
|
||||
另外一种规则是考虑调用位置是否有上下文对象,或者说某个对象是否包含这个函数。
|
||||
|
||||
```ts
|
||||
function foo(this: typeof obj) {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
foo: foo
|
||||
};
|
||||
obj.foo() // xfy
|
||||
```
|
||||
|
||||
这种方法可以理解为将`foo()`的函数体赋值给了对象 obj 的一个属性,而执行时是从 obj 作为上下文对象来执行的。所以 this 隐式的绑定到了 obj 对象。
|
||||
|
||||
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
|
||||
|
||||
```ts
|
||||
function foo(this: typeof obj) {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
foo: foo,
|
||||
};
|
||||
obj.foo(); // xfy
|
||||
|
||||
const alotherObj = {
|
||||
name: 'dfy',
|
||||
obj: obj,
|
||||
};
|
||||
alotherObj.obj.foo(); // xfy
|
||||
```
|
||||
|
||||
**隐式丢失**
|
||||
|
||||
既然会隐式的绑定,那也就会出现隐式的丢失问题。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
|
||||
const bar = obj.foo; // 函数别名
|
||||
bar();
|
||||
```
|
||||
|
||||
虽然 bar 是`obj.foo`的一个引用,但是它引用的是函数体本身。可以理解为将函数体传递给了 bar 这个变量,这是调用`bar()`是一个不带任何修饰的函数调用,因此使用了默认绑定。
|
||||
|
||||
另一种常见且出乎意料的情况就是在传递回调函数时:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
|
||||
function doFoo(fn) {
|
||||
fn();
|
||||
}
|
||||
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
|
||||
doFoo(obj.foo);
|
||||
```
|
||||
|
||||
参数传递其实就是一种隐式赋值,因此我们传入函数是也会被隐式赋值。只要函数体被传递后,且调用时脱离了原有的对象,就会导致 this 的隐式丢失。
|
||||
|
||||
包括`setTimeout()`方法丢失 this 也是同理。
|
||||
|
||||
#### 显式绑定
|
||||
|
||||
因为原型的特性,JavaScript 中函数也自己的属性。大多数宿主环境都会提供`call()`与`apply()`来给我们显式的绑定 this。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
foo.call(obj);
|
||||
```
|
||||
|
||||
> call 与 apply 只是传参不同。
|
||||
|
||||
使用显式的绑定可以很好的解决传递参数时隐式丢失 this 的问题
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
function bar() {
|
||||
foo.call(obj);
|
||||
}
|
||||
setTimeout(bar, 1000);
|
||||
// 同理
|
||||
// setTimeout(() => {
|
||||
// obj.foo();
|
||||
// }, 1000);
|
||||
```
|
||||
|
||||
这里在`bar()`的内部直接手动显式的把`foo()`绑定到了 obj,无论之后怎么调用,在何处调用。都会手动的将 obj 绑定在`foo()`上。这种绑定称之为**硬绑定**。
|
||||
|
||||
不过这种绑定是特意的例子,这里手动为`foo()`绑定到了 obj。在多数情况下,我们可能需要更灵活一点。
|
||||
|
||||
在 [JavaScript 装饰器模式🎊 - 🍭Defectink (xfy.plus)](https://xfy.plus/defect/javascript-decorator.html) 中介绍了这种工作模式。通过一个包装器配合显式绑定就能解决大部分情况下的问题。
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
console.log(this.name);
|
||||
console.log(msg);
|
||||
}
|
||||
function wrapper(fn, obj) {
|
||||
return (...rest) => {
|
||||
fn.apply(obj, rest);
|
||||
};
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
const bar = wrapper(foo, obj);
|
||||
bar('嘤嘤嘤');
|
||||
```
|
||||
|
||||
但包装器不仅仅只是用来解决 this 丢失的问题,但对 this 绑定的问题 ES5 提供了内置的方法`Function.prototype.bind`。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
const bar = foo.bind(obj);
|
||||
bar();
|
||||
```
|
||||
|
||||
#### new 绑定
|
||||
|
||||
在传统面向类的语言中,“构造函数”是类中的一些特殊的方法,使用类时会调用类中的构造函数。通常类似于这样:
|
||||
|
||||
```js
|
||||
myObj = new MyClass()
|
||||
```
|
||||
|
||||
在 JavaScript 中,所有函数都可以被 new 操作符所调用。这种调用称为构造函数调用,实质上并不存在所谓的“构造函函数”,只有对于函数的“构造调用”。
|
||||
|
||||
使用 new 来发生构造函数调用时,会执行:
|
||||
|
||||
1. 创建(构造)一个新对象。
|
||||
2. 对新对象执行`[[Prototype]]`连接。
|
||||
3. 对新对象绑定到函数调用的 this。
|
||||
4. 如果函数没有返回其他对象,那么在 new 调用后自动返回这个新对象。
|
||||
|
||||
```js
|
||||
function Foo(name) {
|
||||
this.name = name;
|
||||
}
|
||||
const bar = new Foo('xfy');
|
||||
console.log(bar.name);
|
||||
```
|
||||
|
||||
使用 new 操作符来调用`foo()`时,会构造一个新对象并把它绑定到`foo()`中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
|
||||
|
||||
> ES6 的 class 只是一个语法糖,但是它也解决了一些问题。
|
||||
|
||||
### 优先级
|
||||
|
||||
上述描述的四条规则中,如果某处位置可以应用多条规则时,就要考虑到其优先级的问题。
|
||||
|
||||
毫无疑问,默认绑定肯定是优先级最低的绑定。所以先来考虑隐式绑定与显式绑定之间的优先级,用一个简单的方法就能测试出:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.age);
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
foo,
|
||||
};
|
||||
const dfy = {
|
||||
name: 'dfy',
|
||||
age: 81,
|
||||
foo,
|
||||
};
|
||||
|
||||
xfy.foo(); // 18
|
||||
dfy.foo(); // 81
|
||||
|
||||
xfy.foo.call(dfy); // 81
|
||||
dfy.foo.call(xfy); // 18
|
||||
```
|
||||
|
||||
很明显,显式绑定的优先级更高,也就是说在判断时应当先考虑是否存在显式绑定。
|
||||
|
||||
那么 new 绑定与隐式绑定呢?
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
this.a = msg;
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
foo,
|
||||
};
|
||||
xfy.foo('test');
|
||||
console.log(xfy);
|
||||
|
||||
const obj = new xfy.foo('this is obj');
|
||||
console.log(obj);
|
||||
```
|
||||
|
||||
可以看到这里对对象 xfy 中隐式绑定的函数进行了 new 操作,而最后的 this 被绑定到了新对象 obj 上,并没有修改 xfy 本身的值。所以 new 绑定的优先级比隐式绑定更高。
|
||||
|
||||
那 new 绑定与显式绑定呢?由于`call/apply`无法与 new 一起使用,所以无法通过`new xfy.foo.call(obj)`来测试优先级,但是我们可以通过硬绑定`bind()`来测试。
|
||||
|
||||
```js
|
||||
function foo(msg) {
|
||||
this.a = msg;
|
||||
}
|
||||
|
||||
const xfy = {
|
||||
name: 'xfy',
|
||||
foo,
|
||||
};
|
||||
|
||||
let obj = {};
|
||||
|
||||
const bar = xfy.foo.bind(obj);
|
||||
bar('obj');
|
||||
console.log(obj);
|
||||
|
||||
// bar was bind to obj
|
||||
const baz = new bar('this is baz');
|
||||
console.log(obj);
|
||||
console.log(baz);
|
||||
```
|
||||
|
||||
可以看到,在硬绑定之后,使用 new 操作对象 obj 的值并没有被改变,反而对 new 的新对象进行了修改。
|
||||
|
||||
但这真的说明 new 绑定比硬绑定优先级更高吗?实则不然,上述结果是因为 ES5 中内置的`Function.prototype.bind()`方法比较复杂,他会对 new 绑定做判断,如果是的话就会使用新创建的 this 替换硬绑定的 this。
|
||||
|
||||
这是来自 [MDN]([Function.prototype.bind() - JavaScript | MDN (mozilla.org)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#polyfill))的 polyfill bind 的方法,
|
||||
|
||||
```js
|
||||
// Yes, it does work with `new (funcA.bind(thisArg, args))`
|
||||
if (!Function.prototype.bind)
|
||||
(function () {
|
||||
var ArrayPrototypeSlice = Array.prototype.slice;
|
||||
Function.prototype.bind = function (otherThis) {
|
||||
if (typeof this !== 'function') {
|
||||
// closest thing possible to the ECMAScript 5
|
||||
// internal IsCallable function
|
||||
throw new TypeError(
|
||||
'Function.prototype.bind - what is trying to be bound is not callable'
|
||||
);
|
||||
}
|
||||
|
||||
var baseArgs = ArrayPrototypeSlice.call(arguments, 1),
|
||||
baseArgsLength = baseArgs.length,
|
||||
fToBind = this,
|
||||
fNOP = function () {},
|
||||
fBound = function () {
|
||||
baseArgs.length = baseArgsLength; // reset to default base arguments
|
||||
baseArgs.push.apply(baseArgs, arguments);
|
||||
return fToBind.apply(
|
||||
fNOP.prototype.isPrototypeOf(this) ? this : otherThis,
|
||||
baseArgs
|
||||
);
|
||||
};
|
||||
|
||||
if (this.prototype) {
|
||||
// Function.prototype doesn't have a prototype property
|
||||
fNOP.prototype = this.prototype;
|
||||
}
|
||||
fBound.prototype = new fNOP();
|
||||
|
||||
return fBound;
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
在这几段中:
|
||||
|
||||
```js
|
||||
fNOP.prototype.isPrototypeOf(this) ? this : otherThis,
|
||||
// 以及
|
||||
if (this.prototype) {
|
||||
// Function.prototype doesn't have a prototype property
|
||||
fNOP.prototype = this.prototype;
|
||||
}
|
||||
fBound.prototype = new fNOP();
|
||||
```
|
||||
|
||||
该 polyfill 检测了是否是使用 new 绑定,并修改 this 为 new 绑定。
|
||||
|
||||
#### 判断 this
|
||||
|
||||
根据上述优先级,可以得出一些判断 this 的结论(优先级从高到低):
|
||||
|
||||
1. 函数是否在 new 中调用(new 绑定)?
|
||||
|
||||
如果是的话, this 绑定的是新创建的对象。`const bar = new Foo()`
|
||||
|
||||
2. 函数是否通过`call/apply`或者硬绑定调用(显式绑定)?
|
||||
|
||||
如果是的话,this 绑定的是指定的对象。`const bar = foo.call(baz)`
|
||||
|
||||
3. 函数是否在某个上下文对象中调用(隐式绑定)?
|
||||
|
||||
如果是的话,this 绑定在那个上下文对象上。`const bar = obj.foo()`
|
||||
|
||||
4. 上述都不满足,那么就会使用默认绑定。
|
||||
|
||||
### 绑定例外
|
||||
|
||||
凡事都有例外,this 绑定也是同样。在某些情况下看上去可能是绑定某个规则,但实际上应用的可能是默认规则。
|
||||
|
||||
#### 被忽略的 this
|
||||
|
||||
把 null 或者 undefined 作为 this 的绑定对象传入`call/apply`与 bind 方法时,这些值会被忽略,从而应用默认绑定规则。
|
||||
|
||||
也就是说`call/apply`传入 null 或者 undefined 时与之间执行函数本身没有区别。
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
foo.call(null);
|
||||
```
|
||||
|
||||
这样使用`call/apply`的作用是利用他们的一些特性来解决一些小问题。
|
||||
|
||||
例如:展开数组
|
||||
|
||||
```js
|
||||
function bar(a, b) {
|
||||
console.log(a, b);
|
||||
}
|
||||
bar.apply(null, [1, 2]);
|
||||
```
|
||||
|
||||
当然,这在 ES6 中可以使用展开运算符来传递参数:
|
||||
|
||||
```js
|
||||
bar(...[1,2])
|
||||
```
|
||||
|
||||
又或是利用 bind 实现柯里化
|
||||
|
||||
```js
|
||||
function bar(a, b) {
|
||||
console.log(a, b);
|
||||
}
|
||||
|
||||
const baz = bar.bind(null, 1);
|
||||
baz(2);
|
||||
```
|
||||
|
||||
这里都是利用忽略 this 产生的一些副作用,但在某些情况下可能不安全,例如函数可能真的使用到了 this ,这在非严格模式下可能会修改全局对象。
|
||||
|
||||
如果真的需要使用这种方法,可以创建一个 DMZ 对象来代替 null。
|
||||
|
||||
```js
|
||||
const ¤ = Object.create(null);
|
||||
foo.call(¤, arg)
|
||||
```
|
||||
|
||||
#### 间接引用
|
||||
|
||||
另外需要注意的是,在某些情况下我们可能会无意的创建一个函数的间接引用。间接引用最容易在赋值期间发生:
|
||||
|
||||
```js
|
||||
function foo() {
|
||||
console.log(this.name);
|
||||
}
|
||||
const o = {
|
||||
foo,
|
||||
};
|
||||
const p = {};
|
||||
(p.foo = o.foo)();
|
||||
```
|
||||
|
||||
赋值表达式`p.foo = o.foo`返回的是目标函数的引用,所以在这里调用实际上是在全局环境下直接调用`foo()`。根据之前的规则,这里会应用默认绑定。
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
title: JavaScript-事件
|
||||
date: 2021-07-12 11:40:03
|
||||
tags: JavaScript
|
||||
categories: 实践
|
||||
url: javascript-event
|
||||
---
|
||||
|
||||
JavaScript 和 HTML 直接的交互是通过事件实现的。当文档或者浏览器发生交互时,使用侦听器(处理程序)来预定事件,以便事件发生时执行相应的代码。在传统软件工程中被称之为观察员模式。
|
||||
|
||||
事件最早是在 IE3 和 Netscape Navigator 2 中出现的。
|
||||
@ -191,7 +199,7 @@ btn.onclick = function() {
|
||||
|
||||
### DOM2 级事件处理程序
|
||||
|
||||
DOM2 级不再是元素的方法,它通过定义的两个方法来为元素添加或删除事件处理程序。这也是目前现代笔记常用的方法。
|
||||
DOM2 级不再是元素的方法,它通过定义的两个方法来为元素添加或删除事件处理程序。这也是目前现代比较常用的方法。
|
||||
|
||||
DOM2 级定义了两个方法:
|
||||
|
||||
@ -206,6 +214,69 @@ element.addEventListener('事件名', '事件处理程序函数', '捕获/冒泡
|
||||
|
||||
为 true 时表示在捕获阶段调用函数,为 false 时表示在冒泡阶段调用函数。默认为冒泡流。
|
||||
|
||||
此外,当使用匿名函数时,`removeEventListener()`
|
||||
此外,因为对象的引用性,当使用匿名函数时,`removeEventListener()`无法移除一个匿名的处理程序。
|
||||
|
||||
## 事件对象
|
||||
`addEventListener()`还可以将第三个参数接收为一个配置对象:
|
||||
|
||||
```js
|
||||
document.addEventListener('click', handleClick, {
|
||||
capture: true,
|
||||
once: true,
|
||||
passive: true
|
||||
})
|
||||
```
|
||||
|
||||
* capture:在捕获阶段调用函数;
|
||||
* once:事件监听器在触发一次后自动移除;
|
||||
* passive:表示事件处理程序永远不会调用`preventDefault()`;
|
||||
|
||||
## 自定义事件
|
||||
|
||||
客户端 JavaScript 的事件 API 非常强大,使用它可以自定义和派发自己事件。也就是说除了已经定义好的事件类型之外,我们还可以定义自定义的事件类型。
|
||||
|
||||
如果某个 JavaScript 对象上有`addEventListener()`方法,那它就是一个事件目标。着意味着该对象也有一个`dispatchEvent()`方法。`dispatchEvent()`就是下发自定义事件的,它下发的事件可以被`addEventListener()`监听到。
|
||||
|
||||
而自定义的事件可以通过`new CustomEvent()`来创建。`CustomEvent()`的第一个参数是一个字符串,表示事件类型;第二个参数是一个对象,用于指定事件对象的属性。
|
||||
|
||||
来看一个刻意设计的例子:
|
||||
|
||||
```js
|
||||
const btn = document.querySelector('#btn');
|
||||
const p = document.querySelector('#state');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('busy', {
|
||||
detail: true,
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('busy', {
|
||||
detail: false,
|
||||
})
|
||||
);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
document.addEventListener('busy', (e) => {
|
||||
if (e.detail) {
|
||||
p.textContent = 'Now loading...';
|
||||
p.style.color = 'red';
|
||||
} else {
|
||||
p.textContent = 'Idle.';
|
||||
p.style.color = 'green';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
这里自定义了一个 busy 事件,用于表示正在请求某些内容。这里的例子在点击了按钮后模拟开始加载,并使用`document.dispatchEvent()`下发了一个新的事件。同样在加载完成后,再下发一个同样的事件,并在事件对象中携带不同的属性用于表示加载完成。
|
||||
|
||||
后面为`document.addEventListener('busy')`监听了下发的事件,并根据传递的事件对象中的属性值来修改状态。
|
||||
|
||||
<iframe src="https://codesandbox.io/embed/jovial-hooks-zmfce?fontsize=14&hidenavigation=1&theme=light&view=preview"
|
||||
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
|
||||
title="自定义事件"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
></iframe>
|
@ -1,3 +1,11 @@
|
||||
---
|
||||
title: JavaScript-代理与反射
|
||||
date: 2021-07-12 11:42:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-proxy-and-reflect
|
||||
---
|
||||
|
||||
ECMAScript 6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。
|
||||
|
||||
## 反射 API
|
491
source/_posts/JavaScript-值与类型.md
Normal file
@ -0,0 +1,491 @@
|
||||
---
|
||||
title: JavaScript-值与类型
|
||||
date: 2021-07-12 11:41:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-value-and-type
|
||||
---
|
||||
|
||||
## 值与引用
|
||||
|
||||
在许多编程语言中,赋值和参数传递可以通过值赋值(value-copy)或者引用复制(reference-copy)来完成。
|
||||
|
||||
例如在 C 中,传递一个引用值可以通过声明类似于这样的`int* num`参数来按引用传递,如果传递一个变量 x,那么`num`就是指向 x 的引用。引用就是指向变量的指针,如果不声明为引用的话,参数值总是通过值来传递的。即便是复杂的对象值也是如此(C++)。
|
||||
|
||||
与 C/C++ 不同的是,JavaScript 没有指针这一概念,值的传递方式完全由值来决定。JavaScript 中变量不可能成为指向另一个变量的指针。
|
||||
|
||||
基本类型(简单类型)的值总是通过以值复制的方式来赋值/传递,这些类型包括:`null`、`undefined`、字符串、数字、布尔和`symbol`。
|
||||
|
||||
而复合值,也就是对象(以及对象的子类型,数组、包装对象等)和函数,则总是以引用复制的方式来赋值/传递。
|
||||
|
||||
在了解了基本类型和引用类型的值之后,先来看下他们传递有什么不同:
|
||||
|
||||
基本类型:
|
||||
|
||||
由于基本类型是按值传递的,所以 a 与 b 是分别在内存中两处保存了自己的值。a 有在内存中有自己的空间,b 也有自己单独的空间,他们互不影响。
|
||||
|
||||
```js
|
||||
let a = 123;
|
||||
let b = a; // 按值进行传递
|
||||
|
||||
a += 1; // 修改 a
|
||||
console.log(a); // 124
|
||||
console.log(b); // 123 b 不受影响
|
||||
```
|
||||
|
||||
引用类型:
|
||||
|
||||
引用值的情况正好相反,所谓按引用传递,就是`arr1`与`arr2`指向的是内存中的同一块地址,**修改**任何一个变量的值,都会立即反应到另一个变量上。因为他们对应的是同一块内存。
|
||||
|
||||
```js
|
||||
let arr1 = [1, 2, 3];
|
||||
let arr2 = arr1;
|
||||
|
||||
arr1.push(99); // 修改 aar1
|
||||
console.log(arr1); // [ 1, 2, 3, 99 ]
|
||||
console.log(arr2); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
但是引用值还有个特性容易犯错,那就是修改:
|
||||
|
||||
这里咋一看是修改了`arr1`的值,但为什么没有反应到`arr2`身上呢?说好的一起变呢?
|
||||
|
||||
仔细回想一下引用值的定义,他们是因为指向同一块内存地址,所以修改这段地址中的值时,就会同时反应在两个变量上。但是这里的`arr1 = { name: 'xfy' }`并不是修改内存中的值,而是修改了`arr1`的指向,使其指向一块新的内存地址。而`arr2`还是指向以前的地址,所以`arr2`没有改变。
|
||||
|
||||
```js
|
||||
let arr1 = [1, 2, 3];
|
||||
let arr2 = arr1;
|
||||
|
||||
arr1 = { name: 'xfy' };
|
||||
console.log(arr1); // { name: 'xfy' }
|
||||
console.log(arr2); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
### 使用函数修改值
|
||||
|
||||
由于上述值的传递特性,这也会导致在传递给函数参数时发生个中问题。
|
||||
|
||||
修改引用值:
|
||||
|
||||
```js
|
||||
function changeValue(value) {
|
||||
// 按引用传递,可以直接修改
|
||||
value.push(99);
|
||||
// 重新赋值,并没有修改内存中的值
|
||||
value = { name: 'xfy' };
|
||||
}
|
||||
|
||||
let arr = [1, 2, 3];
|
||||
changeValue(arr);
|
||||
console.log(arr); // [ 1, 2, 3, 99 ]
|
||||
```
|
||||
|
||||
修改基本值:
|
||||
|
||||
```js
|
||||
function changeValue(value) {
|
||||
// 按值传递,value 获取到 num 的值
|
||||
// 但是他们分别保存在两个内存中
|
||||
value++;
|
||||
}
|
||||
|
||||
let num = 123;
|
||||
changeValue(num);
|
||||
console.log(num); // 123
|
||||
```
|
||||
|
||||
这也就是为什么在 Vue3 的 Composition API 中使用 ref 封装的响应式变量必须要有`.value`属性。
|
||||
|
||||

|
||||
|
||||
## 强制类型转换
|
||||
|
||||
将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显式的情况;隐式的情况称为强制类型转换(coercion)。
|
||||
|
||||
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时(runtime)。
|
||||
|
||||
### 抽象值操作
|
||||
|
||||
在了解强制类型之前,我们需要先掌握类型之间转换的基本规则。ES5 规范第 9 节定义了一些“抽象操作”和转换规则。
|
||||
|
||||
#### ToString
|
||||
|
||||
ES5 规范 9.8 节定义了抽象操作 ToString。ToString 负责非字符串到字符串的强制类型转换操作。
|
||||
|
||||
基本值转换为字符串的规则为直接添加双引号:null 转换为`"null"`,true 转换为`"true"`等。数字也遵循这种规则,不过极大或极小的数字使用指数形式。
|
||||
|
||||
```js
|
||||
(1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000).toString()
|
||||
// "1.07e+21"
|
||||
```
|
||||
|
||||
`JSON.stringify()`不是强制类型转换,它涉及到 ToString 的规则:
|
||||
|
||||
1. 字符串、数字、布尔值和 null 的`JSON.stringify()`规则与 ToString 基本相同。
|
||||
2. 如果传递给`JSON.stringify()`的对象中定义了`toJSON()`方法,那么该方法会在字符串化之前调用,以便将对象转换为安全的 JSON 值。
|
||||
|
||||
#### ToNumber
|
||||
|
||||
ES5 规范 9.3 节定义了抽象操作 ToNumber。
|
||||
|
||||
其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。
|
||||
|
||||
对对象或数组会先转换为相应的基本类型值,如果返回的是非数字类型值,则再遵循上述规则将其强制转换为数字。
|
||||
|
||||
为了转换为相应的基本值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有`valueOf()`方法。如果有并返回基本类型值,就使用该值进行强制类型转换。如果没有就使用`toString()`的返回值来进行强制类型转换。
|
||||
|
||||
如果这两个方法都没有,就会产生 TypeError 错误。
|
||||
|
||||
#### ToBoolean
|
||||
|
||||
ES5 规范 9.2 节定义了抽象操作 ToBoolean。
|
||||
|
||||
这些是假值:
|
||||
|
||||
* undefined
|
||||
* null
|
||||
* false
|
||||
* +0、-0 和 NaN
|
||||
* `""`
|
||||
|
||||
假值的布尔强制类型转换结果为 false。
|
||||
|
||||
**假值对象**
|
||||
|
||||
规范中定义的所有对象都是真值,包括封装了假值的对象:
|
||||
|
||||
```js
|
||||
const a = new Boolean(false);
|
||||
const b = new Number(0);
|
||||
const c = new String('');
|
||||
|
||||
console.log(Boolean(a && b && c)); // true
|
||||
```
|
||||
|
||||
浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些值就是“假值对象”。
|
||||
|
||||
例如 IE 已废弃的用法`document.all`。它是一个类数组对象,包含 DOM 列表,曾经是一个真正的对象,不过它现在是一个假值对象。
|
||||
|
||||
## 显式强制类型转换
|
||||
|
||||
强制显式类型转换是那些显而易见的类型转换,它类似于静态语言中的类型转换,已被广泛接受。
|
||||
|
||||
和那些静态语言类似,JavaScript 有可以直接调用构造函数来显式的转换类型:
|
||||
|
||||
```js
|
||||
String(123)
|
||||
Number('123')
|
||||
```
|
||||
|
||||
### 奇特的`~`运算符
|
||||
|
||||
`~`运算符是位操作符(按位非),它比较令人费解。
|
||||
|
||||
字位运算只适用于 32 位整数,运算符会将操作数强制转换位 32 位格式。这是通过 ES5 规范 9.5 节定义的 ToInt32 来实现的。
|
||||
|
||||
严格来说这不是强制类型转换,因为返回的值的类型并没有变化。但字位运算符和某些特殊数字在一起使用时会产生类似强制类型转换的效果,返回另外一个数字。
|
||||
|
||||
```js
|
||||
0 | -1 // 0
|
||||
0 | NaN // 0
|
||||
0 | Infinity // 0
|
||||
0 | -Infinity // 0
|
||||
```
|
||||
|
||||
上面这些数字不能被转换为 32 为数字,因此 ToInt32 返回 0。
|
||||
|
||||
`~`运算符和`!`很像,它首先将值转换为 32 位数字,然后执行字位操作“非”(反转每一位)。类似于:
|
||||
|
||||
```js
|
||||
~32 // -(32 + 1) = -33
|
||||
```
|
||||
|
||||
它可以被应用在`indexOf()`中,`indexOf()`如果没有找到则返回 -1。这种返回值很难直接被 if 语句隐式转换所利用,所以我们可以使用`~`运算符来强制转换类型为布尔值。
|
||||
|
||||
```js
|
||||
let str = 'xfy'
|
||||
if (str.indexOf('x') != -1) // 找到
|
||||
if (~str.indexOf('x')) // -1 真值 找到
|
||||
```
|
||||
|
||||
### 显式解析数字字符串
|
||||
|
||||
上述介绍过,JavaScript 可以和静态语言类似的转换类型,数字也不例外。
|
||||
|
||||
但使用`Number('123')`转换数字与解析字符串到数组`parseInt('123')`有一点不同。解析字符串允许带有非数字的字符串:
|
||||
|
||||
```js
|
||||
Number('123') // 123
|
||||
parseInt('123') // 123
|
||||
|
||||
Number('123xfy') // NaN
|
||||
parseInt('123xfy') // 123
|
||||
```
|
||||
|
||||
`parseInt()`会从左往右一直解析到第一个非数字的字符串为止,并返回所有的数字。而`Number()`不能包含任何非数字的字符串。
|
||||
|
||||
并且`parseInt()`会尽可能尝试隐式的转换操作数的类型,下面这个示例之所以等于 18 是因为`1 / 0`返回 Infinity,而`parseInt()`会将 Infinity 当作字符串处理。当 19 进制遇到 Infinity 时会解析到第二个字符 n 为止,而 I(区分大小写)正好等于 18。
|
||||
|
||||
```js
|
||||
parseInt(1/0, 19) // 18
|
||||
```
|
||||
|
||||
## 隐式强制类型转换
|
||||
|
||||
隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。隐式强制类型转换的作用是减少冗余,让代码更简洁。
|
||||
|
||||
### 字符串与数字之间
|
||||
|
||||
字符串和数字之间利用一些操作符会触发隐式的强制类型转换。通过重载,`+`运算符既能用于数字加法,也能用于字符串拼接。
|
||||
|
||||
在遇到字符串与数字相加时,它从左向右开始,如果遇到的都是数字,则进行数学相加;如果下一位遇到了字符串,则执行字符串拼接。遇到一次字符串后,后续再遇到数字也时执行拼接操作。
|
||||
|
||||
```js
|
||||
console.log('42' + 0); // 420
|
||||
console.log(20 + 1 + '0'); // 210
|
||||
console.log(20 + 1 + '0' + 2); // 2102
|
||||
```
|
||||
|
||||
另外,`+`运算符如果遇到了非字符串或数字,它会尝试进行隐式转换。
|
||||
|
||||
```js
|
||||
[1, 2] + [3, 4]; // "1,23,4"
|
||||
```
|
||||
|
||||
根据规范 11.6.1 节,如果某个操作数是字符串或者能转换为字符串的话,`+`进行拼接操作。如果其中一个是对象,则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用`[[DefaultValue]]`,以数字作为上下文。
|
||||
|
||||
简单来说,就是对数组进行`valueOf()`操作时无法得到简单的基本值,于是转而使用`toString()`操作。因此两个数组分别得到了`"1,2"`和`"3,4"`。最终拼接为`"1,23,4"`。这和 ToNumber 抽象操作处理对象的方式一样。
|
||||
|
||||
上述提到过,如果从左到右遇到了一个字符串,那么`+`运算符就会执行拼接操作。可以利用这个特性来将数字转换为字符串。
|
||||
|
||||
```js
|
||||
console.log(21 + '');
|
||||
```
|
||||
|
||||
这是一种隐式的转换,它和显式的`String(21)`很类似。但是他们之间有一个细微的差别,根据 ToPrimitive 抽象操作,`21 + ''`会对 21 调用`valueOf()`方法,然后通过 ToString 抽象操作转换为字符串。而`String(21)`则是直接调用`toString()`。
|
||||
|
||||
```js
|
||||
const a = {
|
||||
toString() {
|
||||
return 99;
|
||||
},
|
||||
valueOf() {
|
||||
return 210;
|
||||
},
|
||||
};
|
||||
console.log(a + ''); // 210
|
||||
console.log(String(a)); // 99
|
||||
```
|
||||
|
||||
通常情况下这两种操作各有各的有点。`a + ''`更常见一点,虽然饱受诟病,但隐式强制类型转换仍然有它的用处。
|
||||
|
||||
### 隐式强制转换为布尔值
|
||||
|
||||
一些语句会触发隐式强制转换为布尔值的操作:
|
||||
|
||||
1. `if (...)`语句中的条件判断表达式。
|
||||
2. `for (...;...;..)`语句中的条件判断表达式。
|
||||
3. `while (...)`和`do .. while (...)`循环中的条件判断表达式。
|
||||
4. `? : `三元运算符中的条件判断表达式。
|
||||
5. `||`和`&&`左边的操作数。
|
||||
|
||||
### || 和 &&
|
||||
|
||||
逻辑运算符`||`和`&&`再大多数语言中都有,但在 JavaScript 中的表现与其他语言略有不同。
|
||||
|
||||
ES 5 规范中 11.11 节:
|
||||
|
||||
> `||`和`&&`运算符的返回值不一定是布尔类型,而是两个操作数其中一个。
|
||||
|
||||
`||`和`&&`首先会对左边操作数进行条件判断,如果其值不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
|
||||
|
||||
`||`返回结果为 true 的那个,但它是从左到右进行运算的,如果左侧操作数为 true,则不会对右侧操作数进行运算。反之亦然。
|
||||
|
||||
`&&`则相反,只有左侧操作数为 true 时才会对右侧操作数进行运算。
|
||||
|
||||
换一个角度来看:
|
||||
|
||||
```js
|
||||
const a = 32;
|
||||
const b = 'xfy';
|
||||
|
||||
a || b;
|
||||
// 大致相当于
|
||||
a ? a : b;
|
||||
|
||||
a && b;
|
||||
// 大致相当于
|
||||
a ? b : a;
|
||||
```
|
||||
|
||||
这里的大致相当于是因为三元运算符在条件判断中,如果 a 为 true 的话,a 可能会被执行两次。
|
||||
|
||||
`&&`有个类似于 if 语句的用法,在代码压缩工具中比较常见:
|
||||
|
||||
```js
|
||||
if (a) { b() };
|
||||
a && b();
|
||||
```
|
||||
|
||||
这是利用了`&&`的短路机制。
|
||||
|
||||
### 符号的强制类型转换
|
||||
|
||||
ES6 中引入了符号类型,它允许显式的强制类型转换,而不允许隐式的类型转换。
|
||||
|
||||
```js
|
||||
let s1 = Symbol('xfy');
|
||||
console.log(String(s1)); // "Symbol(xfy)"
|
||||
|
||||
let s2 = Symbol('dfy');
|
||||
console.log(s2 + ''); // TypeError: Cannot convert a Symbol value to a string
|
||||
```
|
||||
|
||||
## 宽松相等和严格相等
|
||||
|
||||
宽松相等(loose equals)和严格相等(strict equals)都用来 判断两个值是否“相等”。
|
||||
|
||||
他们的区别是:`==`允许在相等比较中进行强制类型转换,而`===`不允许。
|
||||
|
||||
### 抽象相等
|
||||
|
||||
ES5 规范 11.9.3 节的“抽象相等比较算法”定义了`==`运算符的行为。该算法简单而又全面,涵盖了所有可能出现的类型组合,以及它们进行强制类型转换的方式。
|
||||
|
||||
有几个特殊的情况需要注意:
|
||||
|
||||
* NaN 不等于 NaN。
|
||||
* +0 等于 --0。
|
||||
|
||||
11.9.3.1 的最后定义了对象的宽松相等。两个对象指向同一个值时即视为相等,不发生强制类型转换。
|
||||
|
||||
此外,`==`在毕竟两个不同类型的值时会发送隐式强制类型转换,会将其中的一或两者都转换为相同的类型后再比较。
|
||||
|
||||
#### 字符串与数字
|
||||
|
||||
字符串与数字是两种不同的数据类型,在进行宽松相等比较时,会触发隐式类型转换。
|
||||
|
||||
```js
|
||||
42 == '42'
|
||||
```
|
||||
|
||||
根据 ES5 规范 11.9.3.4-5 定义:
|
||||
|
||||
1. 如果`x == y`中,x 是数字,y 是字符串,则返回`x == ToNumber(y)`的结果。
|
||||
2. 如果`x == y`中,x 是字符串,y 是数字,则返回`ToNumber(x) == y`的结果。
|
||||
|
||||
总的来说,就是会将非数字的类型优先转换为字符串来与数字进行比较。
|
||||
|
||||
#### 其他类型与布尔值
|
||||
|
||||
`==`操作最容易出错的地方就是与布尔值 true 和 false 相比较。
|
||||
|
||||
```js
|
||||
42 == true // false
|
||||
```
|
||||
|
||||
数字 42 肯定是一个真值,但它居然不等于 true?
|
||||
|
||||
根据规范 11.9.3.6-7 中:
|
||||
|
||||
1. 如果`x == y`中,x 是布尔类型,则返回`ToNumber(x) == y`的结果。
|
||||
2. 如果`x == y`中,y 是布尔类型,则返回`x ==ToNumber(y) `的结果.
|
||||
|
||||
也就是说,布尔值会先被转换为数字,然后再与其他类型相比较。上述例子中,true 会先被转换为 1,然后`42 == 1`得到的结果为 false。
|
||||
|
||||
根据规范的定义,这也就意味着`42 == false`返回的也是 false。这看上去非常奇怪,一个真值居然既不等于 true,也不等于 false。
|
||||
|
||||
但仅仅只是看上去,根据规范的定义,宽松相等与布尔值相比较时并不涉及 ToBoolean 的转换,所以 42 是真值还是假值也无从上述判断。
|
||||
|
||||
目前宽松相等都会优先将布尔值转换为数字,再将剩下的操作数也转换为数字进行比较。所以不要使用宽松相等用来和布尔值比较,从而判断一个值的真假。
|
||||
|
||||
如果需要,下面的用法会更好:
|
||||
|
||||
```js
|
||||
if (a) {...}
|
||||
if (!!a) {...}
|
||||
if (Boolean(a)) {...}
|
||||
```
|
||||
|
||||
#### null 与 undefined
|
||||
|
||||
null 和 undefined 之间的宽松相等也涉及隐式强制类型转换。ES5 规范 11.9.3.2-3 规定,他们之间宽松相等返回 true。
|
||||
|
||||
也就是说,再宽松相等中,`null == undefined`(它们与其自身也相等)。除此之外的值都不和他们两个相等。
|
||||
|
||||
所以 null 和 undefined 之间的类型转换是安全可靠的。如果需要判断一个值是 null 或 undefined,就可以使用宽松相等。
|
||||
|
||||
```js
|
||||
const a = doSomething();
|
||||
if (a == null) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
这样的条件判断仅在 a 返回 null 或者 undefined 时才成立,除此之外的其他值都不成立,包括0、false和`''`这样的假值。
|
||||
|
||||
#### 对象与非对象
|
||||
|
||||
对象与基本类型值之间,ES5 规范 11.9.3.8-9 规定:
|
||||
|
||||
1. 如果`x == y`中,x 是基本值,y 是对象,则返回`x == ToPrimitive(y)`的结果。
|
||||
2. 如果`x == y`中,x 是对象,y 是基本值,则返回`ToPrimitive(x) == y`的结果。
|
||||
|
||||
> 基本类型布尔值会优先被转换为数字。
|
||||
|
||||
```js
|
||||
const a = ['123'];
|
||||
console.log(a == 123); // true
|
||||
```
|
||||
|
||||
这种情况 a 会先被调用 ToPrimitive 操作,得到字符串`'42'`,然后`'42' == 42`则又会将字符串转换为数字 42,最后`42 == 42`得到 true。
|
||||
|
||||
另外,手动包装的对象在隐式强制类型转换中也会被拆封。
|
||||
|
||||
```js
|
||||
console.log('xfy' == new String('xfy')); // true
|
||||
```
|
||||
|
||||
### 其他情况
|
||||
|
||||
上述已经全面的介绍了`==`运算符中的隐式强制类型转换,接下来再来看下一些较为极端的情况。
|
||||
|
||||
先来看看修改内置原生原型会导致哪些奇怪的结果。
|
||||
|
||||
#### 返回其他数字
|
||||
|
||||
```js
|
||||
a == 2 && a == 3 && a == 4
|
||||
```
|
||||
|
||||
这个表达式看上去怎么也不能成立,但是了解了上述的隐式类型转换之后,仿佛有了一线生机。只要变量 a 不是基本值,那它就有可能成立。
|
||||
|
||||
根据上述的 ToPrimitive 操作,对象会先调用`valueOf()`来尝试获取一个基本值,所以这里需要将 a 手动包装为Number 的一个实例。并修改内置原生原型`valueOf()`方法,让它产生副作用,使得每次转换时,都会将`valueOf()`返回的值增加。这样在`==`触发类型转换时,就可以使上述表达式成立。
|
||||
|
||||
```js
|
||||
let x = 2;
|
||||
Number.prototype.valueOf = function () {
|
||||
return x++;
|
||||
};
|
||||
const a = new Number(2);
|
||||
```
|
||||
|
||||
#### 极端的情况
|
||||
|
||||
```js
|
||||
[] == ![] // true
|
||||
```
|
||||
|
||||
事情看起来变得疯狂了,这仿佛表明一个空数组即是真值也是假值。但我们不能被表明现象所欺骗了,抽象相等会优先将布尔值转换为数字。也就是说`![]`的值为 false 会被转换为 0;而空数组`[]`不会涉及到 ToBoolean 的转换,它最终会 ToNumber 进行转换。空数组在 ToNumber 后得到的也是 0。所以二者相等。
|
||||
|
||||
## 小结
|
||||
|
||||
使用抽象相等时,两边的值都需要认真的推敲。
|
||||
|
||||
* 如果两边的值有 true 或 false,坚决不要使用`==`;
|
||||
* 如果两边的值有`[]`、`''`或 0,尽量不要使用`==`;
|
||||
|
||||
显式强制类型转换会明确的告诉了我们哪些地方进行了类型转换,有助于提高代码的可读性和可维护性。
|
||||
|
||||
而隐式强制类型转换则没有那么明显,是其他操作的副作用。实际上隐式强制类型转也有助于提高代码的可读性。
|
||||
|
||||
> 知其然,知其所以然
|
400
source/_posts/JavaScript-类.md
Normal file
@ -0,0 +1,400 @@
|
||||
---
|
||||
title: JavaScript-类
|
||||
date: 2021-07-12 15:19:24
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-class
|
||||
---
|
||||
|
||||
## 对象
|
||||
|
||||
### 将数组当作对象
|
||||
|
||||
数组和对象的差距貌似并不大,好像完全可以将一个数组当作对象使用,但这并不是一个好主意。数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好只用对象来存储键/值对,只用数组来存储下标/值对。
|
||||
|
||||
如果试图给一个数组添加一个类似数字的属性,例如字符串`'3'`,那么在数组进行存储之后会将其作为下标,并且转为数字。
|
||||
|
||||
```js
|
||||
let arr = []
|
||||
arr.a = 123
|
||||
arr.length // 0
|
||||
arr['3'] = 333
|
||||
arr.length // 4
|
||||
arr // (4) [空 ×3, 333, a: 123]
|
||||
```
|
||||
|
||||
### [[Get]]
|
||||
|
||||
对象属性访问有些很微妙同时非常重要的细节,例如一次经典的属性访问:
|
||||
|
||||
```js
|
||||
const obj = {
|
||||
name: 'xfy',
|
||||
};
|
||||
obj.name;
|
||||
```
|
||||
|
||||
`obj.name`是一次属性访问,但这不是简单的在对象 obj 中查找这个属性。
|
||||
|
||||
在语言规范中,`obj.name`在 obj 上实现了`[[Get]]`操作(这有点类似于函数调用:`[[Get]]()`)。对象默认的`[[Get]]`操作会先在该对象中查找是否具有名称相同的属性,如果有就返回。
|
||||
|
||||
如果没有找到名称相同的属性,则会继续遍历原型链,直到找到为止。如果原型链上也没有找到同名的属性,那么`[[Get]]`操作就会返回 undefined。
|
||||
|
||||
```js
|
||||
obj.age; // undefined
|
||||
```
|
||||
|
||||
这和访问变量时行为略微不同,如果引用了一个当前词法作用域中不存在的变量,则会抛出 ReferenceError。
|
||||
|
||||
所以仅通过返回值无法判断一个属性是否存在:
|
||||
|
||||
```js
|
||||
const myObj = {
|
||||
a: undefined,
|
||||
};
|
||||
myObj.a; // undefined
|
||||
myObj.b; // undefined
|
||||
```
|
||||
|
||||
### [[Put]]
|
||||
|
||||
有`[[Get]]`操作就有对应的`[[Put]]`操作。`[[Put]]`被触发时,实际行为取决于很多因素,包括对象中是否已经存在这个属性。
|
||||
|
||||
如果已经存在这个属性,`[[Put]]`算法大概会检查这些内容:
|
||||
|
||||
1. 属性是否是访问描述符 Setter?如果是,则直接调用 Setter。
|
||||
2. 属性的数据描述符中 writable 是否是 false?如果是,非严格模式下静默失败,严格模式抛出 TypeError 异常。
|
||||
3. 如果都不是,将该值设置为属性的值。
|
||||
|
||||
### Getter 和 Setter
|
||||
|
||||
对象有默认的`[[Get]]`与`[[Put]]`操作可以分别控制属性值的设置与获取。在 ES5 中可以是使用 getter 和 setter 来改写单个属性的默认操作。
|
||||
|
||||
> 目前只能改写对象的某个属性,未来可能能够修改整个对象的默认行为。
|
||||
|
||||
当给一个属性定义了一个 getter 或者 setter 时,这个属性会被定义为“访问描述符”(和“数据描述符”相对)。这会使其忽略他们的 value 和 writable 属性,取而代之的时 set 与 get(还有 configurable 和 enumerable)特性。
|
||||
|
||||
两种定义方式相同,都会在对象中创建一个不包含值的属性。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
get a() {
|
||||
return 123;
|
||||
},
|
||||
};
|
||||
console.log(obj.a);
|
||||
|
||||
Object.defineProperty(obj, 'b', {
|
||||
get: function () {
|
||||
return this.a * 2;
|
||||
},
|
||||
});
|
||||
console.log(obj.b);
|
||||
```
|
||||
|
||||
getter 和 setter 通常成对出现
|
||||
|
||||
```js
|
||||
let myObj = {
|
||||
get a() {
|
||||
return this._a_;
|
||||
},
|
||||
set a(val) {
|
||||
this._a_ = val * 3.14;
|
||||
},
|
||||
};
|
||||
myObj.a = 15;
|
||||
console.log(myObj.a);
|
||||
```
|
||||
|
||||
### 存在性
|
||||
|
||||
前面介绍过一般情况下对象无法区分属性值不存在还是其值为 undefined。我们可以使用`in`操作符与`Object.hasOwnProperty()`方法来判断一个对象中是否有这个属性。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
a: 123,
|
||||
};
|
||||
console.log('a' in obj);
|
||||
console.log('b' in obj);
|
||||
|
||||
console.log(obj.hasOwnProperty('a'));
|
||||
console.log(obj.hasOwnProperty('b'));
|
||||
```
|
||||
|
||||
这两个方法不同的是:
|
||||
|
||||
* `in`会检查属性是否存在于对象以及原型链中;
|
||||
* `Object.hasOwnProperty()`只会检查属性是否在对象中。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
a: 123,
|
||||
};
|
||||
|
||||
let myObj = Object.create(obj);
|
||||
console.log('a' in myObj);
|
||||
```
|
||||
|
||||
另外,enumerable 属性描述符直接影响到`for/in`的遍历,不可枚举的属性`for/in`会直接忽略。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
a: 123,
|
||||
};
|
||||
|
||||
Object.defineProperty(obj, 'b', {
|
||||
enumerable: false,
|
||||
value: 456,
|
||||
});
|
||||
|
||||
for (const i in obj) {
|
||||
console.log(i);
|
||||
}
|
||||
```
|
||||
|
||||
还有另外几个方法也能区分属性是否可枚举:
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
a: 123,
|
||||
};
|
||||
|
||||
Object.defineProperty(obj, 'b', {
|
||||
enumerable: false,
|
||||
value: 456,
|
||||
});
|
||||
|
||||
console.log(obj.propertyIsEnumerable('a'));
|
||||
console.log(obj.propertyIsEnumerable('b'));
|
||||
|
||||
console.log(Object.keys(obj));
|
||||
|
||||
console.log(Object.getOwnPropertyNames(obj));
|
||||
```
|
||||
|
||||
他们的区别是:
|
||||
|
||||
* `propertyIsEnumerable()`当前对象中的属性是否可枚举,不检查原型链;
|
||||
* `Object.keys()`返回当前对象所有可枚举属性的数组;
|
||||
* `getOwnPropertyNames()`会返回对象的所有属性,无论是否可枚举;
|
||||
|
||||
### 遍历
|
||||
|
||||
这里记录下`[Symbol.iterator]()`方法的一些使用习惯。一个可迭代对象需要拥有`[Symbol.iterator]()`方法才是可迭代对象,而`[Symbol.iterator]()`方法是这样组成的:
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
start: 0,
|
||||
end: 10,
|
||||
[Symbol.iterator]() { // Symbol.iterator是对象中的一个方法
|
||||
return { // 它返回一个next方法
|
||||
next: () => {
|
||||
return { // next方法还需要返回一个包含 value 和 done 的对象
|
||||
value: ++this.start,
|
||||
done: this.start > this.end,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Symbol.iterator 是对象中的一个方法,它返回一个 next 方法(当然也可以不返回),而 next 方法还需要返回一个包含 value 和 done 的对象。这样才构成了一个可迭代的对象。
|
||||
|
||||
Symbol.iterator 方法可以直接通过 this 访问原对象,而 next 方法是 Symbol.iterator 方法返回的一个方法,它需要通过 this 来获取原本对象时就需要使用箭头函数,因为普通函数会有自己的 this 值。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
start: 0,
|
||||
end: 10,
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
next() {
|
||||
return {
|
||||
value: ++this.start, // undefined
|
||||
done: this.start > this.end,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
另外,Symbol.iterator 也可以不返回 next 方法,它通过返回 this,然后在原对象中定义一个 next 方法。这样迭代器就照样能找到 next 方法,且能够使用 this 了。
|
||||
|
||||
```js
|
||||
// 这样也是可以的
|
||||
let obj = {
|
||||
start: 0,
|
||||
end: 10,
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
next() {
|
||||
return {
|
||||
value: ++this.start,
|
||||
done: this.start > this.end,
|
||||
};
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 类
|
||||
|
||||
类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类似的语法,但是和其他语言的类完全不同。
|
||||
|
||||
类意味着复制。
|
||||
|
||||
传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
|
||||
|
||||
多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类中引用父类,但是本质上引用的其实是复制的结果。
|
||||
|
||||
JavaScript 并不会(像类那样)自动创建对象的副本。
|
||||
|
||||
## 原型
|
||||
|
||||
考虑原型链之前先来看下什么是原型。我在《JavaScript 权威指南》中读到的最容易理解的一句话就是:对象不仅仅是简单的字符串(和 Symbol)到值的映射。除了维持自己的属性之外,JavaScript 对象也可以从其他对象继承属性,这个其他对象称其为“原型”。
|
||||
|
||||
JavaScript 与传统的 OOP 语言不同的是,它没有通常的类的概念。对象的方法通常是继承来的属性,而这种“原型式继承”也是 JavaScript 的主要特性。
|
||||
|
||||
JavaScript 的对象有个特殊的`[[Prototype]]`内置属性,也就是原型,其实就是对于其他对象的引用。几乎所有对象在创建时`[[Prototype]]`属性都会被赋予一个非空的值。一个对象的原型是其他的对象的引用,而其他的对象也会拥有原型,这样就形成了所谓的“原型链”。看上去和作用域链有点类似,虽然他们是两种东西,但是某些查找值的行为确实很类似。
|
||||
|
||||
`[[Prototype]]`引用有什么用呢,上述介绍过`[[Get]]`操作会在当前对象内寻找指定的属性,如果找不到,它就会继续上`[[Prototype]]`上寻找。
|
||||
|
||||
> 这里对`[[Get]]`的讨论是不考虑 ES6 的 Proxy 的情况下的。
|
||||
|
||||
这样的代码在前面可能已经见过,`Object.create`应该是用来描述原型最方便的方法了。先不考虑它的实现,它在这里的作用就是将 obj 设置为 otherObj 的原型。这样在对`otherObj.name`实施`[[Get]]`操作时,otherObj 本身并 name 这个属性,但是它会随着原型查找到 obj 上,并成功的访问到了值。
|
||||
|
||||
```js
|
||||
let obj = {
|
||||
name: 'xfy',
|
||||
};
|
||||
const otherObj = Object.create(obj);
|
||||
console.log(otherObj.name);
|
||||
```
|
||||
|
||||
### 原型链的尽头
|
||||
|
||||
前面说过,一个对象的原型是其他的对象的引用,而其他的对象也会拥有原型,这样就形成了所谓的“原型链”。那么这样的原型链到哪里才是个头呢?
|
||||
|
||||
所有普通的对象原型最终都会指向`Object.prototype`。这也就是为什么几乎所有对象都有`toString()`和`valueOf()`之类的方法了,因为他们是通过原型链访问到 Object 对应的方法的。
|
||||
|
||||
### 属性设置和屏蔽
|
||||
|
||||
在我刚学 JavaScript 的时候,有人曾告诉过我,如果给一个对象设置一个和其原型链上同名的方法,那么就会屏蔽它的方法,访问到的是该对象自己定义的同名方法。这种情况称之为重写。
|
||||
|
||||
重写方法这里就不多说了,但重写远比我们想象中的更复杂。它只是多种情况下的一种。如果为一个对象 obj 赋值一个 foo 属性,假设它的原型链已经有这个同名的属性了,那么它会出现三种情况:
|
||||
|
||||
1. 如果在`[[Prototype]]`链上这个同名属性没有被设置为只读(`writable:false`)属性,那么就会直接在 obj 添加一个 foo 属性,它是屏蔽属性。
|
||||
2. 如果`[[Prototype]]`链上这个同名属性为只读,那么无法修改已有属性或者在 obj 上创建屏蔽属性。在严格模式下会抛出一个错误,非严格模式下会静默失败。
|
||||
3. 如果`[[Prototype]]`链上这个同名属性是一个 setter,那就一定会调用这个 setter。不会创建屏蔽属性,也会重新定义这个 setter。
|
||||
|
||||
前面两种情况都好理解,来看下 setter 的情况:
|
||||
|
||||
```js
|
||||
let myObj = {
|
||||
name: 'xfy',
|
||||
set foo(val) {
|
||||
this._a_ = val * 2;
|
||||
},
|
||||
get foo() {
|
||||
return this._a_;
|
||||
},
|
||||
};
|
||||
const obj = Object.create(myObj);
|
||||
obj.foo = '110';
|
||||
console.log(obj.foo); // 220
|
||||
```
|
||||
|
||||
也就说属性屏蔽只是三种情况种的一种而已。
|
||||
|
||||
> 第二种令人意外的情况主要是为了模拟类属性的继承。更令人意外的是它只会发生在使用`=`赋值中,使用`Object.defineProperty()`并不会受到影响。
|
||||
|
||||
### 原型式继承
|
||||
|
||||
本咸鱼曾经在读高程三的时候研究过 JavaScript 的原型式继承: [JavaScript 面向对象的程序设计 - 🍭Defectink](https://www.defectink.com/defect/javascript-object-oriented-programming.html) 。所以这里不再赘述关于构造函数还有模拟类的历史问题等,这里记录一些那篇文章没有提到的东西。
|
||||
|
||||
那篇文章里记录过 Douglas Crockford 所介绍的实现继承的方法。
|
||||
|
||||
```js
|
||||
function object(o) {
|
||||
function F() {};
|
||||
F.prototype = o;
|
||||
return new F();
|
||||
}
|
||||
```
|
||||
|
||||
也就是`Object.create()`。
|
||||
|
||||
但当时并没有讨论构造函数之间的继承为什么用这种方式。如果有两个构造函数 Foo 和 Bar,假设 Bar 需要继承自 Foo,那么这两种写法为何不行?
|
||||
|
||||
```js
|
||||
// 引用机制!
|
||||
Bar.prototype = Foo.prototype;
|
||||
// 可能会有副作用
|
||||
Bar.prototype = new Foo();
|
||||
```
|
||||
|
||||
第一种方法有很严重的问题,因为 JavaScript 中的对象并不是按值赋值的,而是而引用赋值的。直接将`Bar.prototype = Foo.prototype`,那么二者就会关联起来,任何在子类原型上的修改都会翻译到父类原型上,因为他们本质上是一个 prototype。
|
||||
|
||||
第二种方法确实可能正常关联,但是如果父类 Foo 有一些副作用操作(例如修改状态、注册到其他对象,给 this 添加属性,等等),这样就会影响到子类创建的实例。
|
||||
|
||||
所以合适的方法就是使用`Object.create()`。但这是在 ES6 之前的情况,ES6 添加了新的辅助函数`Object.setPrototypeOf()`,可以用标准且可靠的方法来修改关联。
|
||||
|
||||
```js
|
||||
// ES6 之前需要抛弃默认的 Bar.prototype
|
||||
Bar.prototype = Object.create(Foo.prototype);
|
||||
// ES6 之后可以直接修改现有的 Bar.prototype
|
||||
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
|
||||
```
|
||||
|
||||
### 检查“类”关联
|
||||
|
||||
在传统的面向类的环境中,检查一个实例(JavaScript 的对象)的继承祖先(JavaScript 的委托关联)通常被称之为内省(反射)。
|
||||
|
||||
在 JavaScript 中也有类似的操作:
|
||||
|
||||
```js
|
||||
function Foo() {}
|
||||
const obj = new Foo();
|
||||
|
||||
obj instanceof Foo; // true
|
||||
```
|
||||
|
||||
instanceof 操作符的左侧是一个对象,右侧是一个函数。它回答的问题是:在 obj 的整条原型链`[[Prototype]]`上是否有`Foo.prototype`指向的对象?
|
||||
|
||||
但这个方法只能用在对象和函数之间的关系,如果需要判断两个对象之间是否通过`[[Prototype]]`关联,只用 instanceof 无法实现。
|
||||
|
||||
可以使用`.isPrototypeOf()`来检查两个对象之间是否关联,当然,它也可以用来检查“实例”和“类”之间的关联。
|
||||
|
||||
因为 JavaScript 中的类本质上就是使用原型链模仿出的结果,所以判断 obj 与其构造函数 Foo 之间的关系也可以这样:
|
||||
|
||||
```js
|
||||
Foo.prototype.isPrototypeOf(obj);
|
||||
```
|
||||
|
||||
由于类是虚假的,实际上我们并不需要 Foo,只需要它的原型对象 prototype。`.isPrototypeOf()`回答的问题是:在 obj 的整个原型链中是否出现过`Foo.prototype`?
|
||||
|
||||
在 ES5 中我们可以直接获取一个对象的原型:
|
||||
|
||||
```js
|
||||
class Foo {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
sayName() {
|
||||
console.log(this.name);
|
||||
}
|
||||
}
|
||||
|
||||
const obj = new Foo('xfy');
|
||||
|
||||
Object.getPrototypeOf(obj)
|
||||
// {constructor: ƒ, sayName: ƒ}
|
||||
// constructor: class Foo
|
||||
// sayName: ƒ sayName()
|
||||
// __proto__: Object
|
||||
```
|
||||
|
||||
如果经常和 Chrome 之类的浏览器打交道的话,可能还在对象中见到过`.__proto__`属性。这个属性神奇的引用了`[[Prototype]]`对象,并且在 ES6 成为了标准。
|
187
source/_posts/JavaScript-编译与闭包.md
Normal file
@ -0,0 +1,187 @@
|
||||
---
|
||||
title: JavaScript-编译与闭包
|
||||
date: 2021-07-12 11:36:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-compile-and-closure
|
||||
---
|
||||
|
||||
## 编译原理
|
||||
|
||||
尽管 JavaScript 经常被归类为“动态”或“解释执行”的语言,但实际上它是一门编译语言。JavaScript 引擎进行的编译步骤和传统编译语言非常相似,但有些地方可能比预想的要复杂。
|
||||
|
||||
传统编译流程:
|
||||
|
||||
* 分词/此法分析(Tokenizing/Lexing)
|
||||
|
||||
这个过程会将有字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如:`var a = 2`;这段程序通常会被分解成词法单元:`var`、`a`、`=`、`2`;空格是否会被当成词法单元,取决于空格在这门语言种是否具有意义。
|
||||
|
||||
* 解析/语法分析(Parsing)
|
||||
|
||||
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
|
||||
|
||||
`var a = 2`的 AST 为:
|
||||
|
||||
```
|
||||
VariableDeclaration
|
||||
--Identifier = a
|
||||
--AssignmentExpression
|
||||
----NumericLiteral = 2
|
||||
```
|
||||
|
||||
* 代码生成
|
||||
|
||||
将 AST 转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。简单来说就是将 AST 转换为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存等),并将值 2 存储在 a 中。
|
||||
|
||||
## JavaScript 的编译
|
||||
|
||||
JavaScript 的编译由 JavaScript 引擎来负责(包括执行)。编译通常由三个部分组成:
|
||||
|
||||
* 引擎:从头到尾负责整个 JavaScript 的编译以及执行;
|
||||
* 编译器:负责语法分析以及代码生成;
|
||||
* 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
|
||||
|
||||
在我们看来`var a = 2`;这是一个普通的变量声明。而在 JavaScript 引擎看来这里有两个完全不同的声明:
|
||||
|
||||
1. `var a`,编译器会寻找当前作用域中是否有同样的声明。如果有,则忽略该声明,并继续编译;否则它会在当前作用域(全局/函数作用域)的集合中声明一个新的变量,并命名为 a。
|
||||
2. 接下来编译器会为引擎生成运行时所需的代码,这些代码用来处理赋值(`a = 2`)操作。引擎会在当前作用域中查找变量 a。如果能找到,则为其赋值;如果找不到,则继续向上查找(作用域链)。
|
||||
|
||||
由于编译的第一步操作会寻找所有的`var`关键词声明,无论它在代码的什么位置,都会声明好。在代码真正运行时,所有声明都已经声明好了,哪怕它是在其他操作的下面,都可以直接进行。这就是`var`关键词的声明提升。
|
||||
|
||||
```js
|
||||
a = 2;
|
||||
console.log(a);
|
||||
var a;
|
||||
```
|
||||
|
||||
### LHS 和 RHS
|
||||
|
||||
编译器在编译过程的第二步生成了代码,引擎执行它时,就会查找变量 a 来判断它是否已经声明过。但引擎如何进行查找,影响最终查找的结果。
|
||||
|
||||
LHS 和 RHS 分别对应的是左侧查找与右侧查找。左右两侧分别代表**一个赋值操作的左侧和右侧**。也就说,当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询。
|
||||
|
||||
例如:`a = 2`,这里进行的就是 LHS 查询。这里不关心 a 的当前值,只想找到 a 并为其赋一个值。
|
||||
|
||||
而:`console.log(a)`,这里进行的是 RHS 查询。因为这里需要取到 a 的值,而不是为其赋值。
|
||||
|
||||
“赋值操作的左侧和右侧”并不一定代表就是`=`的左右两侧,赋值操作还有其他多种形式。因此,可以在概念上理解为“查询被赋值的目标(LHS)”以及”查询目标的值(RHS)“。
|
||||
|
||||
小测验:
|
||||
|
||||
寻找 LHS 查询(3处)以及 RHS 查询(4处)。
|
||||
|
||||
```js
|
||||
function foo(a) {
|
||||
var b = a;
|
||||
return a + b;
|
||||
}
|
||||
var c = foo(2);
|
||||
```
|
||||
|
||||
LHS:
|
||||
|
||||
* `var c = foo(...)`:为变量 c 赋值
|
||||
* `foo(2)`:传递参数时,为形参 a 赋值 2
|
||||
* `var b = a`:为变量 b 赋值
|
||||
|
||||
RHS:
|
||||
|
||||
* `var c = foo(...)`:查询`foo()`
|
||||
* `var b = a`:(为变量 b 赋值时)取得 a 的值
|
||||
* `return a + b`:取得 a 与 b(两次)
|
||||
|
||||
## 异常
|
||||
|
||||
通过详细的了解异常可以准确的确定发生的问题所在。
|
||||
|
||||
在 LHS 查询时,如果到作用域顶部还没有查询到声明,则作用域会热心的帮我们(隐式)创建一个全局变量(非严格模式下)。
|
||||
|
||||
而在 RHS 查询时,如果在作用域顶部还没有查询到声明,就会抛出一个 ReferenceError 异常。
|
||||
|
||||
在严格模式下,LHS 如果没有找到声明,引擎会抛出一个和 RHS 类似的 ReferenceError 异常。
|
||||
|
||||
无论是 LHS 还是 RHS 都是查询一个引用,而没有查询到对应的引用时,就会得到(引用)ReferenceError 异常。
|
||||
|
||||
接下来,如果 RHS 查询到了一个变量,但是我们尝试对这个变量的值进行不合理的操作。例如对一个非函数进行函数调用,或者对对象中不存在的属性进行引用。那么引擎会抛出另外一个异常,叫做 TypeError。
|
||||
|
||||
## 闭包
|
||||
|
||||
闭包是基于词法作用域书写代码时所产生的自然结果。闭包的主要定义:
|
||||
|
||||
当函数可以记住并访问**所在的**词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
|
||||
|
||||
> JavaScript 使用的是词法作用域模型,另一种作用域模型是动态作用域。
|
||||
|
||||
仔细来看,闭包的主要定义有:
|
||||
|
||||
* 函数记住并可以访问所在的词法作用域
|
||||
* 在当前词法作用域之外执行也能继续访问所在的词法作用域
|
||||
|
||||
来看一个例子:
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
const a = 123;
|
||||
|
||||
function bar() {
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
bar();
|
||||
}
|
||||
foo();
|
||||
```
|
||||
|
||||
这段代码看起来好像符合闭包的一部分定义,虽然`bar()`函数并没有脱离当前的词法作用域执行。但是它依然记住了`foo()`的词法作用域,并能访问。
|
||||
|
||||
它确实满足闭包定义的一部分(很重要的一部分),从技术上讲,也许是,但并不能完全断定这就是闭包。通常我们所见到的与认为闭包的情况就是满足所有定义的时候:
|
||||
|
||||
```ts
|
||||
function foo() {
|
||||
const a = 321;
|
||||
|
||||
function bar() {
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
return bar;
|
||||
}
|
||||
// 同理
|
||||
// foo()();
|
||||
const baz = foo()
|
||||
baz();
|
||||
```
|
||||
|
||||
因为垃圾收集机制,当一个函数执行结束后,通常它的整个内部作用域会被销毁。当我们的`foo()`函数执行结束后,看上去它的内容不会再被使用,所以很自然的考虑会被回收。
|
||||
|
||||
但闭包的神奇之处就在这里,它会阻止这一切的发生。当`bar`被`return`出去之后,在其词法作用域的外部依然能够访问`foo()`的内部作用域。`bar`依然持有对该作用域的引用,这个引用就叫作闭包。
|
||||
|
||||
这也是经常见到说闭包会影响性能的主要原因。某些情况下,它确实会影响到性能,例如过度多的返回本不需要的函数,甚至是嵌套。这会导致本不需要的作用域没有被回收。
|
||||
|
||||
### 常见的闭包
|
||||
|
||||
上述将一个函数`return`出来的案例是最常见的闭包案例。但在我们的代码中,也有些其他非常常见的闭包。不过平时可能没有太过去注意它。
|
||||
|
||||
先来回顾一下定义:
|
||||
|
||||
无论通过何种手段将内部函数传递到词法作用域之外,它都会保留对改内部词法作用域的引用,无论在何处执行这个函数都会使其闭包。
|
||||
|
||||
```ts
|
||||
function waitAMinute(msg: string) {
|
||||
setTimeout(() => {
|
||||
console.log(msg);
|
||||
}, 1000);
|
||||
}
|
||||
waitAMinute('嘤嘤嘤');
|
||||
```
|
||||
|
||||
```ts
|
||||
function btnClick(selector: string, msg: string) {
|
||||
$(selector).click(() => {
|
||||
alert(msg);
|
||||
});
|
||||
}
|
||||
btnClick('#btn_1', 'hah');
|
||||
btnClick('#btn_2', 'got you');
|
||||
```
|
||||
|
232
source/_posts/JavaScript-语法.md
Normal file
@ -0,0 +1,232 @@
|
||||
---
|
||||
title: JavaScript-语法
|
||||
date: 2021-07-12 15:20:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: javascript-syntax
|
||||
---
|
||||
|
||||
在 JavaScript 中,有很多常见的语法仍然有很多地方容易产生困惑、造成误解。
|
||||
|
||||
## 语句与表达式
|
||||
|
||||
语句(statement)和表达式(expression)常常被混为一谈,但他们二者之间有细微的差别。语句相当于一句话,而表达式更类似于一条短语。
|
||||
|
||||
JavaScript 中表达式可以返回一个结果值:
|
||||
|
||||
```js
|
||||
let a = 3 * 6;
|
||||
const b = a;
|
||||
b;
|
||||
```
|
||||
|
||||
上述带有`let`与`const`的语句被称为“声明语句”(declaration statement),而不带声明关键词的`a = 3 * 6;`则是“赋值表达式”。
|
||||
|
||||
乍一看可能还是不能太过于区分他们之间的区别,但是有些地方会让我们强制区分他们。在现代 SPA 框架中,模板语法中通常只允许插入 JavaScript 表达式,而不允许插入语句。例如在常见的 Vue 和 React 中:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<span>{{ 3 * 6 }}</span>
|
||||
<span>{{ a }}</span>
|
||||
<span>{{ let a = 3 * 6 }}</span> // 错误
|
||||
</template>
|
||||
```
|
||||
|
||||
### 语句的结果值
|
||||
|
||||
基本上所有的语句都有一个结果值(undefined 也算)。
|
||||
|
||||
如果经常和控制台打交道,应该会发现很多时候控制台都会给我们返回一个 undefined。其实这就是语句所返回的值,只不过大部分情况下都是 undefined。
|
||||
|
||||
代码块中的返回值是最后一个语句/表达式的结果:
|
||||
|
||||
```js
|
||||
let a ;
|
||||
// undefined
|
||||
if (true) {
|
||||
a = 3 + 1;
|
||||
}
|
||||
// 4
|
||||
```
|
||||
|
||||
目前代码块语句返回的值无法被拿到:
|
||||
|
||||
```js
|
||||
b = if (true) {
|
||||
a = 3 + 1;
|
||||
}
|
||||
// VM268:1 Uncaught SyntaxError: Unexpected token 'if'
|
||||
```
|
||||
|
||||
当然也有变通的方法 evil:
|
||||
|
||||
```js
|
||||
b = eval("if (true) {a = 3 + 1;}");
|
||||
b; // 4
|
||||
```
|
||||
|
||||
除了万恶的 evil,ES7 还有一项“do 表达式”提案,目前环境还没实现:
|
||||
|
||||
```js
|
||||
b = do {
|
||||
if (true) {
|
||||
a = 3 + 1;
|
||||
}
|
||||
};
|
||||
b; // 4
|
||||
```
|
||||
|
||||
### 表达式的副作用
|
||||
|
||||
副作用通常是指执行了语句之后,除了返回值或赋值,还有其他变量等被修改。
|
||||
|
||||
最常见的副作用的表达式是函数调用:
|
||||
|
||||
```js
|
||||
const foo = () => {
|
||||
a = a + 1;
|
||||
};
|
||||
|
||||
let a = 1;
|
||||
foo(); // 结果:undefined,副作用:a 的值被修改
|
||||
```
|
||||
|
||||
### 上下文规则
|
||||
|
||||
在 JavaScript 语法规则中,有时候同样的语法在不同的情况下会有不同的解释。这些语法规则孤立起来会很难理解。
|
||||
|
||||
#### 大括号
|
||||
|
||||
大括号的作用有很多,随着 JavaScript 的演进可能会出现更多类似的情况。
|
||||
|
||||
**对象常量**
|
||||
|
||||
大括号可以使用表达式的方式定义对象:
|
||||
|
||||
```js
|
||||
const a = {
|
||||
foo: bar()
|
||||
}
|
||||
```
|
||||
|
||||
大括号内的内容被赋值给变量 a,因而它是一个对象常量。
|
||||
|
||||
**标签**
|
||||
|
||||
将上述声明语句去掉,这时的大括号可不是一个孤立的对象常量。因为这里的`{...}`被看作了一个普通的代码块。这里的`foo: bar()`被解释为标签语句。
|
||||
|
||||
```js
|
||||
{
|
||||
foo: bar()
|
||||
}
|
||||
```
|
||||
|
||||
标签语句配合 continue 和 break 语句可以实现类似于 goto 的方式进行跳转。但 goto 是一种极为糟糕的编码方式,所以 JavaScript 并不支持真正的 goto 语句。
|
||||
|
||||
#### 代码块
|
||||
|
||||
有一个强制类型转换的坑经常被提到:
|
||||
|
||||
```js
|
||||
[] + {} // "[object Object]"
|
||||
{} + [] // 0
|
||||
```
|
||||
|
||||
看上去简直就是 JavaScript 在和我们开玩笑。其实不然,上述情况并不是非常难以理解。
|
||||
|
||||
第一行是普通的强制类型转换,二者都会被强制转换字符串,而`[]`被转换为了`''`,`{}`被转换为了`"[object Object]"`,所以他们相加的最后结果就是`"[object Object]"`。
|
||||
|
||||
第二行看上去有点莫名其妙,换个位置结果就变了。这里其实`{}`并没有参与运算,而是被当作了一段独立的空代码块。真正参与运算的是`+[]`,最终被转换为数字 0。
|
||||
|
||||
#### 对象解构
|
||||
|
||||
从 ES6 开始`{...}`也可用于解构赋值。
|
||||
|
||||
```js
|
||||
const foo = () => {
|
||||
return {
|
||||
name: 'xfy',
|
||||
age: 18,
|
||||
};
|
||||
};
|
||||
|
||||
const { name, age } = foo();
|
||||
console.log(name, age);
|
||||
```
|
||||
|
||||
同理,函数形参也可以利用解构赋值。
|
||||
|
||||
#### else if 和可选代码块
|
||||
|
||||
其实`else if`并不存在于 JavaScript 语法中,只是多个`if else`只包含单条语句的时候可以省略代码块的`{...}`。
|
||||
|
||||
实际上`else if`是这样的:
|
||||
|
||||
```js
|
||||
if (a) {
|
||||
console.log(a);
|
||||
} else {
|
||||
if (b) {
|
||||
console.log(b);
|
||||
} else {
|
||||
console.log(c);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 运算符优先级
|
||||
|
||||
多数情况下,JavaScript 的数学运算符与真正的数学一样,按照真正的数学的顺序进行运算。但 JavaScript 的语句并不是数学公式,而且语句中通常会充斥着各种各样的运算符。他们也是有各自的运算优先级的。
|
||||
|
||||
### 短路
|
||||
|
||||
and 与 or 操作拥有“短路”(short circuiting)的特性,别急,我们还没走错片场。在 JavaScript 中,他们分别是`&&`和`||`运算符。
|
||||
|
||||
短路的意思是,当左边的操作数能够得出结果时,就可以忽略右边的操作数。
|
||||
|
||||
对于`&&`来说,左边的操作数为假值时,则就没有必要再判断右边的值。同理,对于`||`来说,左边的操作数为真值时,也没有必要再判断右边的操作数。
|
||||
|
||||
### 关联
|
||||
|
||||
JavaScript 的代码是从上往下执行的,而单条语句是从左往右执行的。但在特定的运算符下,某些语句需要先求得右边的值。
|
||||
|
||||
例如:
|
||||
|
||||
```js
|
||||
true ? false : true ? true : true;
|
||||
```
|
||||
|
||||
这段代码乍一看很令人头疼,但它确实说明运算符的关联最容易理解的例子。在这里,第二个三目运算符的结果会影响第一个三目运算符,所以这里要执行右关联,也就是需要先求得第二个三目运算符的结果。
|
||||
|
||||
> 虽然是右关联,但它的运算顺序还是从左到右。`a ? b : c;`还是会先执行 a 然后 b c。
|
||||
|
||||
## 自动分号
|
||||
|
||||
有时 JavaScript 会自动为代码补上分号,即分号自动插入机制(Automatic Semicolon Insertion,ASI)。
|
||||
|
||||
ASI只会在换行符处起作用,而不会在代码中间插入分号。
|
||||
|
||||
常见的情况是在 return 后自动加上,这样 return 后换行的代码就会不生效。return 语句可以跨越多行,但是其后必须右换行符以外的代码。
|
||||
|
||||
```js
|
||||
const foo = () => {
|
||||
return (
|
||||
1 + 2
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 纠错机制
|
||||
|
||||
是否应该完全依赖 ASI 来编码,这是 JavaScript 社区中最具争议性的话题之一。
|
||||
|
||||
支持的一方认为 ASI 大有裨益,能省略掉那些不必要的`;`,让代码更简洁。此外 ASI 让许多`;`变得可有可无,因此只要代码没问题,有没有`;`都一样。
|
||||
|
||||
反方则认为 ASI 机制问题太多,对于缺乏经验的初学者尤其如此,因为自动插入`;`会无意改变代码的逻辑。有些人还认为依赖 linter 这些工具找出错误,而不是依赖 JavaScript 引擎。
|
||||
|
||||
事实上,现在多数`;`可以通过 ESlint 这样的 linter 工具来在格式化代码是插入。这种方式相比较用引擎来找出错误好很多,而且它不会在运行时改变代码原有的意思。
|
||||
|
||||
## 小结
|
||||
|
||||
JavaScript 语法规则中的许多细节需要我们多花时间和精力来了解。从长远来看,这有助于更深入地掌握这门语言。
|
||||
|
@ -244,7 +244,7 @@ function x() {
|
||||
x();
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
在作用域链内部的函数可以向上方位外部函数作用域内的变量,直至全局作用域。而作用域外的函数不能访问内部函数的变量。
|
||||
|
||||
|
@ -1,3 +1,12 @@
|
||||
---
|
||||
title: MongoDB零碎笔记
|
||||
date: 2021-06-07 18:08:31
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: mongodb-notes
|
||||
index_img:
|
||||
---
|
||||
|
||||
## 数据结构
|
||||
|
||||
数据库
|
||||
@ -66,7 +75,7 @@ D(**D**urability):持久性。事务一旦提交后,所作的修改将
|
||||
- [可用性](https://zh.wikipedia.org/wiki/可用性)(**A**vailability)(每次请求都能获取到非错的响应——但是不保证获取的数据为最新数据)
|
||||
- [分区容错性](https://zh.wikipedia.org/w/index.php?title=网络分区&action=edit&redlink=1)(**P**artition tolerance)(以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。)
|
||||
|
||||

|
||||

|
||||
|
||||
### NoSQL 技术优势
|
||||
|
||||
@ -143,12 +152,12 @@ db.createCollection(name, options);
|
||||
|
||||
options 参数:
|
||||
|
||||
| 字段 | 类型 | 描述 |
|
||||
| :---------- | :--- | :----------------------------------------------------------- |
|
||||
| 字段 | 类型 | 描述 |
|
||||
| :---------- | :--- | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| capped | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 **当该值为 true 时,必须指定 size 参数。** |
|
||||
| autoIndexId | 布尔 | 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。 |
|
||||
| size | 数值 | (可选)为固定集合指定一个最大值,即字节数。 **如果 capped 为 true,也需要指定该字段。** |
|
||||
| max | 数值 | (可选)指定固定集合中包含文档的最大数量。 |
|
||||
| autoIndexId | 布尔 | 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。 |
|
||||
| size | 数值 | (可选)为固定集合指定一个最大值,即字节数。 **如果 capped 为 true,也需要指定该字段。** |
|
||||
| max | 数值 | (可选)指定固定集合中包含文档的最大数量。 |
|
||||
|
||||
### 集合其他操作
|
||||
|
||||
@ -394,16 +403,16 @@ db.user.find({age:{$type: 'number'}}).sort({age: -1})
|
||||
|
||||
表达式
|
||||
|
||||
| 表达式 | 描述 | 实例 |
|
||||
| :-------- | :--------------------------------------------- | :----------------------------------------------------------- |
|
||||
| 表达式 | 描述 | 实例 |
|
||||
| :-------- | :--------------------------------------------- | :------------------------------------------------------------------------------------ |
|
||||
| $sum | 计算总和。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}]) |
|
||||
| $avg | 计算平均值 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}]) |
|
||||
| $min | 获取集合中所有文档对应值得最小值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}]) |
|
||||
| $max | 获取集合中所有文档对应值得最大值。 | db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}]) |
|
||||
| $push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}]) |
|
||||
| $addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}]) |
|
||||
| $first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}]) |
|
||||
| $last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}]) |
|
||||
| $push | 在结果文档中插入值到一个数组中。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}]) |
|
||||
| $addToSet | 在结果文档中插入值到一个数组中,但不创建副本。 | db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}]) |
|
||||
| $first | 根据资源文档的排序获取第一个文档数据。 | db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}]) |
|
||||
| $last | 根据资源文档的排序获取最后一个文档数据 | db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}]) |
|
||||
|
||||
```js
|
||||
db.sc.aggregate([
|
@ -816,3 +816,8 @@ type Arg = SecondArg<F>; // Arg = number
|
||||
可见`[].slice`的第二个参数是 number 类型,而且在编译时便可知晓这一点。Java 能做到吗?
|
||||
|
||||
> ↑《Programming TypeScript》中文版 6.5.2 章原话。
|
||||
|
||||
## 异步
|
||||
|
||||
### 事件发射器
|
||||
|
||||
|
@ -1,4 +1,13 @@
|
||||
# 61
|
||||
---
|
||||
title: Vue3!
|
||||
date: 2021-05-05 18:08:31
|
||||
tags: Vue
|
||||
categories: 实践
|
||||
url: vue3-notes
|
||||
index_img:
|
||||
---
|
||||
|
||||
> 早期学习 Vue3 API 的笔记。
|
||||
|
||||
## setup
|
||||
|
@ -30,17 +30,17 @@ index_img: /images/Minecraft-bedrock服务端/217940907.webp
|
||||
|
||||
服务端我们可以免费的在[官方网站](<https://www.minecraft.net/en-us/download/server/bedrock/>)下载,目前只支持Windows与Ubuntu版本。在Windows上运行推荐使用Windows10/Windows Server 2016及以后的版本。
|
||||
|
||||
![2019-05-13T05:16:24.png][1]
|
||||
![2019-05-13T05:16:24.webp][1]
|
||||
|
||||
[1]: ../images/Minecraft-bedrock服务端/4100402335.png
|
||||
[1]: ../images/Minecraft-bedrock服务端/4100402335.webp
|
||||
|
||||
Windows版本与Ubuntu版本的文件几乎差不多,下载后直接解压,我们就能够看到一个可执行的`bedrock_server.exe`文件。当没有任何需求时,直接执行它就可以启动并正常使用服务端了。
|
||||
|
||||
启动后我们可以看到一个类似这样的命令提示符的界面:
|
||||
|
||||
![2019-05-15T13:18:43.png][2]
|
||||
![2019-05-15T13:18:43.webp][2]
|
||||
|
||||
[2]: ../images/Minecraft-bedrock服务端/3200057918.png
|
||||
[2]: ../images/Minecraft-bedrock服务端/3200057918.webp
|
||||
|
||||
此时的服务器端就已经启动完成了,若能直接访问服务器,就可以直接开始游戏了🥓。
|
||||
|
||||
@ -80,9 +80,9 @@ stop
|
||||
|
||||
配置文件可以修改大多数服务端的配置,主要是针对游戏服务器的修改。像是对于游戏内的具体修改并没有写在配置文件内,例如对玩家的修改以及修改世界的选项。这些操作选项需要我们手动赋予一个玩家“操作员”的权限,这样,该玩家就会有对目前游戏的整个世界的完整操作权限。
|
||||
|
||||
![2019-05-15T15:27:50.png][3]
|
||||
![2019-05-15T15:27:50.webp][3]
|
||||
|
||||
[3]: ../images/Minecraft-bedrock服务端/2032314932.png
|
||||
[3]: ../images/Minecraft-bedrock服务端/2032314932.webp
|
||||
|
||||
在游戏的目录下有个名为`permissions.json`的json文件,在默认情况下它是空的,我们可以根据帮助文件提供的格式直接赋予某个玩家权限:
|
||||
|
||||
@ -124,17 +124,17 @@ stop
|
||||
|
||||
最简单也是最直接的备份方式就是直接备份当前服务端的整个文件夹,如果这个操作太过于麻烦的话,或者说文件夹已经达到了臃肿的地步了。我们可以使用预留的备份命令,可以生成`.db`的文件用于copy。文档的详细解释:
|
||||
|
||||
| Command | Description |
|
||||
| ----------- | ------------------------------------------------------------ |
|
||||
| save hold | This will ask the server to prepare for a backup. It’s asynchronous and will return immediately. |
|
||||
| Command | Description |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| save hold | This will ask the server to prepare for a backup. It’s asynchronous and will return immediately. |
|
||||
| save query | After calling `save hold` you should call this command repeatedly to see if the preparation has finished. When it returns a success it will return a file list (with lengths for each file) of the files you need to copy. The server will not pause while this is happening, so some files can be modified while the backup is taking place. As long as you only copy the files in the given file list and truncate the copied files to the specified lengths, then the backup should be valid. |
|
||||
| save resume | When you’re finished with copying the files you should call this to tell the server that it’s okay to remove old files again. |
|
||||
| save resume | When you’re finished with copying the files you should call this to tell the server that it’s okay to remove old files again. |
|
||||
|
||||
我们可以直接使用`save hold`来生成备份文件,然后再使用`save query`来查询文件的位置。*注意:当我们的世界名称使用中文时,可能会出现在终端中文乱码的情况*,例如:
|
||||
|
||||
![2019-05-15T15:50:09.png][4]
|
||||
![2019-05-15T15:50:09.webp][4]
|
||||
|
||||
[4]: ../images/Minecraft-bedrock服务端/1339714059.png
|
||||
[4]: ../images/Minecraft-bedrock服务端/1339714059.webp
|
||||
|
||||
此时最佳解决办法就是换个世界名称。但是直接在配置文件中更换名称后,会导致重新创建一个新的世界。为了避免这个现象,达到给旧世界更换名称的操作。我们需要同时修改三个地方的名称,并保持一致:
|
||||
|
||||
|
278
source/_posts/体验至上的暗色模式.md
Normal file
@ -0,0 +1,278 @@
|
||||
---
|
||||
title: 体验至上的暗色模式
|
||||
date: 2021-07-07 18:08:31
|
||||
tags: JavaScript
|
||||
categories: 实践
|
||||
url: dark-mode-for-user-experience
|
||||
---
|
||||
|
||||
暗色模式(Dark Mode)是近些年来掀起的风潮,通常暗黑模式是一种文字为浅色、背景为深色的应用程序模式。这和近些年来手机等移动设备上更多的 OLED 屏幕也有一定关系,因为 OLED 的工作原理,大面积的深色背景可以使其功耗更低。
|
||||
|
||||
另外,暗色模式也并不是近些年来才出现的一种模式。早期的计算机显示器多数就是黑底绿字,据说是因为当时 CRT 的工作原理导致显式黑色背景更为方便。
|
||||
|
||||
但不管具体来历,从移动端到桌面端,现代操作系统都具有系统级别的颜色切换。所谓的系统级别就是可以让不同的应用程序根据系统当前的显式模式来切换自身的显式模式。
|
||||
|
||||

|
||||
|
||||
## 给网页添加暗色模式
|
||||
|
||||
可能是最近的暗色模式的风潮的影响,CSS 媒体查询也提供了一个强大的特性。[Media Queries Level 5 (csswg.org)](https://drafts.csswg.org/mediaqueries-5/) 中提出了深色模式的判断方式,CSS 媒体查询`@media (prefers-color-scheme: value)`。其中包含 light、dark 和`no-preference`三种值。
|
||||
|
||||
该媒体查询是根据系统的显式模式来进行切换的,也就是说 CSS 媒体查询通过浏览器为我们和系统建立了沟通的桥梁。例如当支持的系统的切换为深色模式时,`@media (prefers-color-scheme: dark)`就会被触发,我们可以根据修改其中的 CSS 变量,或者维护另一套 CSS 样式表来实现切换。
|
||||
|
||||
### 使用 CSS 变量
|
||||
|
||||
使用 CSS 变量的方式来实现切换的优点是代码量较少,可以快速切换到指定模式。但兼容性较差,市面上任然存在不支持 CSS 变量的浏览器。
|
||||
|
||||
```css
|
||||
/* variables */
|
||||
:root {
|
||||
--text-color: #333;
|
||||
--main-background: #fff;
|
||||
}
|
||||
|
||||
/* media query */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: rgb(255, 152, 0);
|
||||
--main-background: #333;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 条件加载样式表
|
||||
|
||||
`<link>`标签也支持使用媒体查询来进行条件加载样式表。这样的好处是不需要使用到 CSS 变量,但缺点就是需要维护两套样式表。
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<link rel="stylesheet" href="dark.css" media="(prefers-color-scheme: dark)">
|
||||
```
|
||||
|
||||
### 手动切换
|
||||
|
||||
还有种兼容性最好的切换方式,由用户手动触发对根标签的样式切换,从而达到切换暗色模式。并且还能将状态维护在 `localStorage`中,实现长期保存。这种方法并不复杂,缺点就是不能和系统进行沟通,和系统一同切换。
|
||||
|
||||
## 体验之上
|
||||
|
||||
如何才能有更好的模式切换体验呢?上述的切换方式都是要么根据系统来进行切换,要么用户没法手动进行设置。两种方式仿佛都差了点意思。
|
||||
|
||||
如果将两种方式结合到一起,那体验乃会更好。
|
||||
|
||||
将两种方式结合到一起不能仅仅使用手动模式来覆盖`prefers-color-scheme: dark`,当覆盖了之后会被存储到`localStorage`中,这样就永久的失去了自动模式。
|
||||
|
||||
这里的思路是,默认情况下遵循系统设置。当用户手动切换时覆盖系统设置,而当切换后和系统设置一致时,则恢复为系统设置。
|
||||
|
||||
### 先布局一下
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dark Mode</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="title">Hi, there👋</h1>
|
||||
<button class="switch-mode"></button>
|
||||
</body>
|
||||
<script src="index.js"></script>
|
||||
</html>
|
||||
```
|
||||
|
||||
为了测试,这里是一个简单的常规布局,分别使用了一个`<h1>`和`<button>`标签来展示和切换模式。
|
||||
|
||||
### 呈上 CSS
|
||||
|
||||
```css
|
||||
/* variables */
|
||||
:root {
|
||||
--text-color: #333;
|
||||
--main-background: #fff;
|
||||
}
|
||||
|
||||
/* media query */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* When prefers-color-scheme is dark mode (system level) */
|
||||
/* Avoid overwriting user custom color mode */
|
||||
:root:not([data-theme]) {
|
||||
--text-color: rgb(255, 152, 0);
|
||||
--main-background: #333;
|
||||
}
|
||||
}
|
||||
|
||||
/* User custom color mode is dark */
|
||||
[data-theme='dark'] {
|
||||
--text-color: rgb(255, 152, 0);
|
||||
--main-background: #333;
|
||||
}
|
||||
```
|
||||
|
||||
CSS 部分并不是特别复杂,且它主要是利用了媒体查询来与系统沟通。这里使用了一个伪类`:not()`,它的作用是不选中特定的类。当元素上有`[data-theme='dark']`属性时,利用伪类`:not([data-theme])`可以保证手动设置覆盖系统设置。通俗点来说,就是没有`[data-theme]`的元素就应用上媒体查询。
|
||||
|
||||
### 紧接着就是 JS
|
||||
|
||||
CSS 其实已经帮我们解决一大半的问题了。接下来还需要利用到 JavaScript 来覆盖系统设置与判断何时恢复系统设置(还有存储到`localStorage`)。
|
||||
|
||||
先来几个常量,方便后续直接使用。
|
||||
|
||||
```ts
|
||||
const ROOT_ELEMENT = document.documentElement;
|
||||
const STORAGE_KEY = 'color-mode';
|
||||
const DATA_THEME = 'data-theme';
|
||||
```
|
||||
|
||||
这里使用了类 C 的命名方式,他们分别是:
|
||||
|
||||
* ROOT_ELEMENT:根元素;
|
||||
* STORAGE_KEY:存储到`localStorage`中的 key;
|
||||
* DATA_THEME:Attribute name。
|
||||
|
||||
先不要着急,为了方便,再定义几个操作`localStorage`的方法:
|
||||
|
||||
```ts
|
||||
const setMode = (k: string, v: string) => {
|
||||
try {
|
||||
localStorage.setItem(k, v);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const removeMode = (k: string) => {
|
||||
try {
|
||||
localStorage.removeItem(k);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const getMode = (k: string) => {
|
||||
try {
|
||||
return localStorage.getItem(k);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
后续有多个地方需要用到同样的操作,所以利用`try{...}catch{...}`简单的封装了一下。
|
||||
|
||||
基(tou)本(lan)步骤做完了,根据之前的思路,接下来就是先判断当前文档是否处于**系统设置中的**暗色模式。
|
||||
|
||||
这里发现一个非常好用的 API `matchMedia`。它可以检查对应的媒体查询是否匹配,我们可以利用它来判断是否触发媒体查询,从而判断是否为系统设置下的暗色模式。
|
||||
|
||||
```ts
|
||||
const isDarkMode = () => {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
};
|
||||
```
|
||||
|
||||
这样就能够轻松判断是否处于系统设置下的暗色模式了。先不着急,接下来先为重置封装一个函数:
|
||||
|
||||
```ts
|
||||
const resetMode = () => {
|
||||
ROOT_ELEMENT.removeAttribute(DATA_THEME);
|
||||
removeMode(STORAGE_KEY);
|
||||
};
|
||||
```
|
||||
|
||||
该函数的作用是,当用户手动设置的目标模式与系统设置相同时,清楚 DOM 上的属性选择器与`localStorage`中的数据。恢复到系统设置中。
|
||||
|
||||
现在我们能确定是否处于系统设置中和能够清除手动设置了。接下来就是根据当前的模式判断下一次应该设置什么模式,以及是否恢复系统设置。
|
||||
|
||||
在封装下一个函数之前,得先需要创建两个对象,用于验证和反转对应的值:
|
||||
|
||||
```ts
|
||||
type validKeys = {
|
||||
[key: string]: true;
|
||||
};
|
||||
const validKeys: validKeys = {
|
||||
dark: true,
|
||||
light: true,
|
||||
};
|
||||
|
||||
type invertKeys = {
|
||||
[key: string]: 'dark' | 'light';
|
||||
};
|
||||
const inverKeys: invertKeys = {
|
||||
dark: 'light',
|
||||
light: 'dark',
|
||||
};
|
||||
```
|
||||
|
||||
现在我们来封装一个获取下次暗色模式的值:
|
||||
|
||||
```ts
|
||||
const getColorMode = () => {
|
||||
// Fetch value from localStorage
|
||||
const currentSetting = getMode(STORAGE_KEY);
|
||||
let mode: 'dark' | 'light' | undefined;
|
||||
// If has value
|
||||
if (currentSetting && validKeys[currentSetting]) {
|
||||
// Invert it, get next mode
|
||||
mode = inverKeys[currentSetting];
|
||||
} else if (currentSetting === null) {
|
||||
// If has not value, inver media query
|
||||
mode = inverKeys[isDarkMode()];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Set value to localStorage
|
||||
setMode(STORAGE_KEY, mode);
|
||||
return mode;
|
||||
};
|
||||
```
|
||||
|
||||
这个方法首先从`localStorage`中取出暗色模式的值。如果能够取到,那就说明上一次的值成功被存入,下一次的值直接取反就行了。
|
||||
|
||||
如果没有取到值,那就说明上次被恢复了系统设置,或者用户是第一次访问文档。这时下次的值应该为系统设置取反。
|
||||
|
||||
两个简单的判断之后,就能确定下次该设置什么值了
|
||||
|
||||
确定了下次该设置的值之后,就该真的去设置了。这里通过封装一个函数来为根元素设置元素选择器或者恢复为系统设置。
|
||||
|
||||
```ts
|
||||
const applyMode = (mode: string | null = getMode(STORAGE_KEY)) => {
|
||||
if (mode === isDarkMode()) {
|
||||
resetMode();
|
||||
} else if (mode && validKeys[mode]) {
|
||||
ROOT_ELEMENT.setAttribute(DATA_THEME, mode);
|
||||
} else {
|
||||
resetMode();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
这个函数相比较上一个更加简单,几个简单的判断就好了。如果下一次的模式与系统设置相同,那么就直接恢复系统设置。如果与系统设置不同,那么就要覆盖系统设置,这时就要为根元素添加对应的属性选择器。
|
||||
|
||||
如果出现其他什么奇奇怪怪的情况,就先恢复系统设置再说。
|
||||
|
||||
这里的参数使用了默认参数,从`localStorage`取出保存的模式值。这样做的目的是为了当文档第一次加载时,直接运行`applyMode()`函数,来为用户设置上次设置的模式。通俗点来说就是刷新后恢复上次的手动设置。
|
||||
|
||||
当然这也需要将暗色模式的 JavaScript 代码添加到`<head>`标签里,这样才能保证不会出现奇怪的闪屏。
|
||||
|
||||
```html
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dark Mode</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="darkMode.js"></script>
|
||||
</head>
|
||||
```
|
||||
|
||||
## Demo
|
||||
|
||||
<iframe src="https://codesandbox.io/embed/dark-mode-469vq?autoresize=1&fontsize=14&hidenavigation=1&theme=dark&view=preview"
|
||||
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
|
||||
title="Dark-Mode"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
|
||||
## 参考
|
||||
|
||||
[你好黑暗,我的老朋友 —— 为网站添加用户友好的深色模式支持 | Sukka's Blog (skk.moe)](https://blog.skk.moe/post/hello-darkmode-my-old-friend/)
|
310
source/_posts/异步JavaScript-Promise信任问题.md
Normal file
@ -0,0 +1,310 @@
|
||||
---
|
||||
title: 异步JavaScript-Promise信任问题
|
||||
date: 2021-07-16 18:50:04
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: asynchronous-javascript-trust-promise
|
||||
---
|
||||
|
||||
继前篇 [异步JavaScript-现在与将来 | 🍭Defectink](/defect/asynchronous-javascript-now-and-future.html) 对异步 JavaScript 有了个大概的了解之后,就要来真正的上手一下 Promise 了。但在了解 Promise 解决了哪些问题时,我们要先了解一下传统的回调会带来哪些问题。
|
||||
|
||||
## 回调信任问题
|
||||
|
||||
回调为什么会有信任问题?回调的本质就是等待将来执行完成的事件来执行我们所准备的函数。
|
||||
|
||||
```js
|
||||
// A
|
||||
ajax('url.1', () => {
|
||||
// B
|
||||
});
|
||||
// C
|
||||
```
|
||||
|
||||
A 与 B 部分的块是同步的,发生于现在,在 JavaScript 主程序的控制之下。但 C 程序块将延迟执行,并且由(虚构的)ajax 函数来调用。这就是一种控制转移,绝大部分情况下,这种控制转移通常不会给程序带来任何问题。
|
||||
|
||||
但实际上,回调带来的信任问题正是由这种控制反转(inversion of control)带来的。
|
||||
|
||||
### 哪些问题
|
||||
|
||||
对于回调的信任问题,包括但不限于:
|
||||
|
||||
* 调用回调过早;
|
||||
* 调用回调过晚;
|
||||
* 调用回调次数过少或或多;
|
||||
* 参数/环境值未传递;
|
||||
* 吞掉可能出现的错误或异常;
|
||||
|
||||
等。
|
||||
|
||||
人们用虚构的恶魔 Zalgo 来描述这种同步/异步的噩梦,在 [Don't release Zalgo!](https://oren.github.io/articles/zalgo/) 一文中,针对回调信任问题的描述为:
|
||||
|
||||
> When you write a function that accept a callback, make sure your function always sync or always async. don't mix the two.
|
||||
|
||||
当我们有一个接收回调的函数时,要确保该函数拥有以异步或同步的方式运行,二者不能混淆。如果该函数运行的方式无法确定,就会出现回调过早,过晚等情况。
|
||||
|
||||
这些情况会带来什么问题呢?这是一段伪代码:
|
||||
|
||||
```js
|
||||
function result(data) {
|
||||
console.log(a);
|
||||
}
|
||||
|
||||
let a = 0;
|
||||
|
||||
ajax('url.1', result);
|
||||
|
||||
a++;
|
||||
```
|
||||
|
||||
假设虚构的 ajax 函数无法确定它时同步执行(从缓存读取)还是异步执行(发送一个新的请求),这就导致了 result 函数打印 a 的值变的不确定。如果是同步的,则 a = 0;反之,a = 1;
|
||||
|
||||
这还只是多个信任问题中的一个,在 JavaScript 高速发展的今天,对于异步来说,回调函数已经不够用了。我们需要一个更好的异步编程方式,一种更同步、更顺序、更阻塞的方式来表达异步,就像我们的大脑一样。
|
||||
|
||||
## Promise
|
||||
|
||||
ES6 为我们带来了新的异步概念,即任务队列(微任务)。Promise API 就是基于微任务之上的,它能够更好的处理回调所带来的信任问题。
|
||||
|
||||
> Promise 是一个很优秀的 API,到处都能看到它的详细介绍,这里便不再多费口舌。
|
||||
|
||||
### 调用过早
|
||||
|
||||
根据上述的 Zalgo 所带来的问题,调用过早主要的根本原因就是我们无法确定回调函数是以同步的方式还是异步的方式执行的。而 Promise 本身就不必担心这个问题,根据任务队列的定义,即便是立即完成的 Promise `Promise.resolve(42)` 也无法被同步所察觉到。
|
||||
|
||||
也就是说,对一个 Promise 调用`then(...)`的时候,即使这个 Promise 已经 resolve,提供给`then(...)`的回调也总是会异步调用。因为这就是宏任务与微任务的运行方式: [任务 | 🍭Defectink](/defect/asynchronous-javascript-now-and-future.html#任务)
|
||||
|
||||
```js
|
||||
console.log(41);
|
||||
|
||||
Promise.resolve(42).then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
|
||||
console.log(43);
|
||||
// 41 43 42
|
||||
```
|
||||
|
||||
### 调用过晚
|
||||
|
||||
和调用过早类似,根据微任务的定义,一个 Promise 被 resolve 或者 reject 后,一定会在当前宏任务后添加到微任务队列。这就确保了注册在 Promise 上的回调在下个宏任务开始之前一定会被依次调用,并且他们中的任意一个都无法影响或延误对其他回调的调用。
|
||||
|
||||
```js
|
||||
p.then((val) => {
|
||||
p.then((val) => {
|
||||
console.log('C');
|
||||
});
|
||||
console.log('A');
|
||||
});
|
||||
p.then((val) => {
|
||||
console.log('B');
|
||||
});
|
||||
// A B C
|
||||
```
|
||||
|
||||
这里的三个微任务,C 永远无法打断或抢占 B,这是因为 Promise 的运作方式。
|
||||
|
||||
#### 调度技巧
|
||||
|
||||
微任务虽然给我们带来了不少好处,但也有很多调度的细微差别。如果两个 Promise p1 和 p2 都已经 resolve,那么注册在 p1 上的`then(...)`回调通常应该先于 p2 所注册的回调执行。但某些细微的场景下可能不是如此:
|
||||
|
||||
```js
|
||||
const p3 = new Promise((resolve) => {
|
||||
resolve('B');
|
||||
});
|
||||
const p1 = new Promise((resolve) => {
|
||||
resolve(p3);
|
||||
});
|
||||
const p2 = new Promise((resolve) => {
|
||||
resolve('A');
|
||||
});
|
||||
|
||||
p1.then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
p2.then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
// A B
|
||||
```
|
||||
|
||||
由于 p1 不是立即值, 而是 resolve 另一个 Promise p3。规定的行为是把 p3 展开到 p1,但是是异步的展开,所以这里的 p2 的回调先执行。
|
||||
|
||||
> 如果 p1 是`Promise.resolve()`,结果则不同,它会返回同一个 Promise。
|
||||
|
||||
### 回调未调用
|
||||
|
||||
目前没有任何办法能够阻止 Promise 向我们通知它的决议(resolve 或者 reject)。如果我们对 Promise 注册了回调的话,那么它在决议时,总是会调用回调函数(完成回调或拒绝回调中的一个)。
|
||||
|
||||
> 决议一词参考自《你不知的 JavaScript 中卷》中文版,这也是本文对 Promise resolve 或 reject 的表达词。
|
||||
|
||||
但如果回调永远不会被决议呢?Promise 为我们提供了一个竞态的高级抽象机制的 API:`Promise.race()`。
|
||||
|
||||
```js
|
||||
const timeoutPromise = (delay) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject('Timeout!');
|
||||
}, delay);
|
||||
});
|
||||
};
|
||||
|
||||
// 假设 foo 不会被决议
|
||||
// 尝试给它 3000ms
|
||||
Promise.race([foo(), timeoutPromise(3000)])
|
||||
.then((val) => {
|
||||
console.log('foo done');
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
});
|
||||
```
|
||||
|
||||
利用该 API,即便目标 Promise 永远不会被决议,我们也能为其设置超时并防止它永久阻塞程序。
|
||||
|
||||
> 这里如果 foo 是微任务死循环的话,race 也无能为力!
|
||||
|
||||
### 调用次数过少/过多
|
||||
|
||||
正常情况下的调用次数应该是 1 次,调用次数过少也就是 0 次,这与上述回调未调用是一个意思。剩下的就是调用次数过多了。
|
||||
|
||||
本质上,Promise 的定义方式使得它只能被决议一次。如果出现某种情况,我们的代码试图调用`resolve(...)`或`reject(...)`多次,或者试图二者都调用,那么这个 Promise 只会接收第一次决议,并默默的忽略后续的任何调用。
|
||||
|
||||
由于 Promise 只能被决议一次,任何通过`then(...)`注册的回调只会被调用一次。这是在链式调用的情况下,多次注册 then 回调(`p.then(); p.then()`)时,它的调用次数与注册次数相同。
|
||||
|
||||
### 未能传递参数/环境值
|
||||
|
||||
Promise 最多只能有一个决议值,完成或拒绝。
|
||||
|
||||
如果没有手动决议任何值,那么这个值就是 undefined。如果决议时传递了多个参数,剩余的参数都会被默默忽略。如果需要传递多个值,则需要封装为一个对象或数组。
|
||||
|
||||
对于环境值来说,我们提供的回调函数依然可以访问对应的作用域和闭包。
|
||||
|
||||
### 吞掉错误或异常
|
||||
|
||||
如果拒绝一个 Promise 并给出一个理由,那么这个值就会传给拒绝回调。
|
||||
|
||||
如果 Promise 在创建或决议的过程中发生了错误,那这个错误就会被捕捉,并且使这个 Promise 被拒绝。
|
||||
|
||||
```js
|
||||
const p = new Promise((resolve, reject) => {
|
||||
foo.bar(); // 错误!foo 未定义
|
||||
resolve(42);
|
||||
});
|
||||
|
||||
p.then(
|
||||
(val) => {
|
||||
console.log(val);
|
||||
},
|
||||
(err) => {
|
||||
console.log(err); // 在这里捕获错误
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
这是一个很好的防止 Zalgo 出现的机制。`then(...)`可以注册两个回调,一个成功时调用,一个出错时调用。这和以前回调函数的方式好似相同。但如果在回调期间出现的错误就无法被当前`then(...)`中的另一个失败回调所捕获,这看似防护就是错误被吞掉了。好在,现在的 Promise 还支持和`try...catch...`类似的方式防止吞掉错误。
|
||||
|
||||
```js
|
||||
const p1 = new Promise((resolve) => {
|
||||
resolve(42);
|
||||
});
|
||||
|
||||
p1.then((val) => {
|
||||
foo.bar();
|
||||
console.log(val);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('Catch in catch');
|
||||
console.log(err);
|
||||
})
|
||||
.finally(() => {
|
||||
cleanFunc();
|
||||
});
|
||||
```
|
||||
|
||||
## 是可信任的Promise吗
|
||||
|
||||
Promise 看上去解决了不少回调无法被信任的问题,那么 Promise 是完全可信的吗?在此之前,得先看另一个问题。
|
||||
|
||||
### thenable 的鸭子类型
|
||||
|
||||
> 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
|
||||
|
||||
常见的 Promise 都是由其构造函数实例化而来,或许我们可以通过`p instanceof Promise`来检查某样东西是否是真正的 Promise。但这个方法并不完善,如果 Promise 来自其他窗口或某个自己实现 Promise 的库中,我们就无法判断其是否是真正可用的 Promise。
|
||||
|
||||
例如这样的 thenable 类型对象,它可能类似于 Promise 的方式工作完全就是侥幸。
|
||||
|
||||
```js
|
||||
const p = {
|
||||
then(cb, ecb) {
|
||||
cb(42);
|
||||
},
|
||||
};
|
||||
|
||||
p.then((val) => {
|
||||
console.log(val); // 42
|
||||
});
|
||||
```
|
||||
|
||||
如果情况是这样,那么在 thenable 的对象上注册的回调都会被执行,这样的行为和 Promise 完全不一致。
|
||||
|
||||
```js
|
||||
const p = {
|
||||
then(cb, ecb) {
|
||||
cb(42);
|
||||
ecb('evil laugh');
|
||||
},
|
||||
};
|
||||
|
||||
p.then(
|
||||
(val) => {
|
||||
console.log(val); // 42
|
||||
},
|
||||
(err) => {
|
||||
console.log(err); // 同样也被执行了!
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
好在,Promise 给了我们解决办法:`Promise.resolve()`。`Promise.resolve()`可以接收任何 thenable 的值,将其解封为它的非 thenable 值。从`Promise.resolve()`得到的值将是一个真正的 Promise。
|
||||
|
||||
```js
|
||||
Promise.resolve(p).then(
|
||||
(val) => {
|
||||
console.log(val); // 42
|
||||
},
|
||||
(err) => {
|
||||
console.log(err); // 没有被执行!
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
所以在面对无法信任的 Promise 时,可以使用`Promise.resolve()`对其进行封装,将其变成真正的 Promise。
|
||||
|
||||
### 另外的调度技巧
|
||||
|
||||
在上述调度技巧中,p1 如果 resolve p3,则需要在下一个异步中将其展开。
|
||||
|
||||
```js
|
||||
const p3 = new Promise((resolve) => {
|
||||
resolve('B');
|
||||
});
|
||||
const p1 = new Promise((resolve) => {
|
||||
resolve(p3);
|
||||
});
|
||||
|
||||
p1.then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
```
|
||||
|
||||
而使用`Promise.resolve()`则会立即返回一个相同的 Promise。
|
||||
|
||||
```js
|
||||
const p3 = new Promise((resolve) => {
|
||||
resolve(42);
|
||||
});
|
||||
|
||||
const p1 = Promise.resolve(p3);
|
||||
|
||||
console.log(p1 === p3); // true
|
||||
```
|
||||
|
350
source/_posts/异步JavaScript-现在与将来.md
Normal file
@ -0,0 +1,350 @@
|
||||
---
|
||||
title: 异步JavaScript-现在与将来
|
||||
date: 2021-07-12 15:21:03
|
||||
tags: JavaScript
|
||||
categories: 笔记
|
||||
url: asynchronous-javascript-now-and-future
|
||||
---
|
||||
|
||||
所有重要的程序都需要通过这样或那样的方法来管理持续一段时间的程序行为。这可能是用户输入、从数据库或文件系统中请求数据、通过网络发送数据并等待响应等。在诸如此类的场景中,程序都需要管理这段时间间隔执行重复任务。
|
||||
|
||||
事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
|
||||
|
||||
## 分块的程序
|
||||
|
||||
可以将 JavaScript 程序写在单个`.js`文件中,但是这个程序几乎一定是由多个块组成。这些块中只有一个是现在执行,其余的则会在将来执行。最常见的块单位是函数。
|
||||
|
||||
通常,如果发送一个异步的 ajax 请求,像这样:
|
||||
|
||||
```js
|
||||
const data = ajax('some.url.1');
|
||||
|
||||
console.log(data); // 没有结果
|
||||
```
|
||||
|
||||
经常与异步打交道可能就明白是由于这段 ajax 是异步进行的,程序在 ajax 请求还未完成时就就执行了下面的打印 data 的语句。所以没有结果。
|
||||
|
||||
通常,最常见的异步代码还是回调函数:
|
||||
|
||||
```js
|
||||
ajax('some.url.1', (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
```
|
||||
|
||||
我们交给 ajax 函数一个回调函数,让其在准备完成后调用我们的回调函数就可以正常的获取到数据了。看上去很棒,至少现在是的。
|
||||
|
||||
> ajax 请求也是可以同步的,但没人会那样用的。
|
||||
|
||||
这和分块的程序有什么关系呢?来研究下下列代码:
|
||||
|
||||
```js
|
||||
const now = () => {
|
||||
return 21;
|
||||
};
|
||||
|
||||
const later = () => {
|
||||
answer = answer * 2;
|
||||
console.log('Meaning of life: 42');
|
||||
};
|
||||
|
||||
let answer = now();
|
||||
|
||||
setTimeout(later, 999);
|
||||
```
|
||||
|
||||
通常,分块的程序都是以函数为块来进行执行的。如上述代码,它有两个块(也有两个函数):一个是现在要执行的部分,另一个是将来要执行的部分。
|
||||
|
||||
很明显,now 函数是同步的被执行和赋值到的一个变量上的。但,我们还设置了一个定时器,将 later 函数于 999 毫秒后执行,也就是之后的某个时间执行。
|
||||
|
||||
任何时候,我们只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、ajax 响应等)时执行,我们就是在代码中创建了一个将来执行的块, 也由此在这个程序中引入了异步机制。像 later 函数就是被异步执行的。
|
||||
|
||||
### 异步控制台
|
||||
|
||||
目前还没有规范定义了 console 对象下的方法应该如何工作的,它们并不是 JavaScript 正式的一部分,而是由宿主环境实现的。所有不同的宿主环境实现的方式可能不同。但,某些浏览器并不会把传入像`console.log(...)`这样的内容立即输出,而是异步处理。不过正常延迟输出的情况不多见。
|
||||
|
||||
## 事件循环
|
||||
|
||||
很早之前我们就能给编写上述的异步 JavaScript 代码,但直到最近(ES6),JavaScript 才真正内建有直接的异步概念。
|
||||
|
||||
JavaScript 语言本身被设计的是单线程语言,宿主环境的实现中都提供了一种机制来处理程序中多个块的执行,且执行每个块时调用 JavaScript 引擎,这种机制被称之为事件循环。
|
||||
|
||||
JavaScript 引擎本身没有时间概念,它只是按需求执行 JavaScript 任意代码片段的环境。“事件”(JavaScript 代码执行)调度总是由包含它的环境进行。
|
||||
|
||||
例如发送一个 ajax 请求,从服务器获取一些数据,并设置了一个回调函数,用于完成请求时回调。一旦完成了网络请求,JavaScript 引起就会通知宿主环境,执行对应的回调函数。
|
||||
|
||||
什么是事件循环?
|
||||
|
||||
来看一段伪代码:
|
||||
|
||||
```js
|
||||
// 事件循环队列,先进先出
|
||||
const eventLoop = [];
|
||||
let event;
|
||||
|
||||
// while 相当于执行代码
|
||||
while (true) {
|
||||
// 每拿出一个事件进行执行,都是一次 tick
|
||||
if (eventLoop.length > 0) {
|
||||
// 拿到事件队列中的下个事件
|
||||
event = eventLoop.shift();
|
||||
// 执行下一个事件
|
||||
try {
|
||||
event();
|
||||
} catch (e) {
|
||||
reportError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这是一段只能用来说明概念的简单的伪代码。这里用一个 while 代表持续运行的循环,循环的每一轮称为一个 tick。对每个 tick 而言,如果在队列中有等待事件,那么就会从队列中摘下一个事件并执行。这些事件就是我们的回调函数。
|
||||
|
||||
常见的`setTimeout()`方法是设置异步的好方法,但它实际上并没有将我们的回调函数直接挂在事件队列上。而是设置一个定时器,当定时器到时后,环境会把回调函数放到事件队列中。这样,在未来某个时刻的 tick 就会摘下并执行这个回调,且没有办法将其直接排在队首。这也是`setTimeout()`方法精度不高的主要原因。
|
||||
|
||||
总的来说,程序通常分成了很多小块,在事件循环队列中一个接一个地执行。严格来说,和程序不直接相关的事件也可能会插入到队列中。
|
||||
|
||||
在 Node.js 中,`setImmediate()`方法会将回调函数准确的插入到所有事件循环队列最后。
|
||||
|
||||
## 并行线程
|
||||
|
||||
异步与并行虽然常常被混为一谈,但它们实际上意义完全不同。
|
||||
|
||||
计算机常见的工具是线程和进程,多个线程能够共享单个进程的内存。
|
||||
|
||||
事件循环机制将自身的工作分成一个个任务并顺序执行,不允许对共享内存进行并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。
|
||||
|
||||
多线程的交替执行与异步事件的交替调度,其颗粒度是完全不同的。
|
||||
|
||||
在单线程的 JavaScript 中,线程本身不会被中断。如果在多线程系统中,同一个程序中可能有两个不同的线程在运作,这时可能会得到很多不确定的结果。
|
||||
|
||||
```js
|
||||
let a = 20;
|
||||
|
||||
const foo = () => {
|
||||
a += 1;
|
||||
};
|
||||
|
||||
const bar = () => {
|
||||
a *= 2;
|
||||
};
|
||||
|
||||
ajax('url.1', foo);
|
||||
ajax('url.2', bar);
|
||||
```
|
||||
|
||||
根据 JavaScript 单线程与事件循环的特性,上述代码也会有不确定性。如果第一个请求先到达,那么 a 的结果就是 42,如果第二个请求先到达,a 的结果就是 41。
|
||||
|
||||
如果是多线程允许的话,事情就会变的更加微妙了。因为两个函数可能同时运行,并且共享内存中的 a,其结果的不确定性会更多。多线程编程是非常复杂的,如果不通过特殊的步骤来防止中断和交替运行,可能会得到出乎意料的不确定性行为。
|
||||
|
||||
> 上述 JavaScript 所表现的不确定性不全都是有害的。有时是无关紧要的,有时可能是我们刻意追求的结果。
|
||||
|
||||
### 完整运行
|
||||
|
||||
由于 JavaScript 的单线程特性,函数中的代码具有原子性。也就是说,函数`foo()`一旦开始执行,它的所有代码都会在`bar()`开始执行前完成,或者相反。这称之为完整运行(run-to-completion)特性。
|
||||
|
||||
虽然异步函数的先后执行顺序还是存在着不确定性。但是,这种不确定性是在函数(事件)顺序级别上,而不是多线程的语句顺序级别。
|
||||
|
||||
在 JavaScript 的特性中,这种函数顺序的不确定性就是通常所说的竞态条件(race condition)。
|
||||
|
||||
## 并发
|
||||
|
||||
通常想到并发,我们就可能与多线程联系上,因为它字面意思上理解就是多个任务同时运作(或者多个请求同时发送)。
|
||||
|
||||
在 JavaScript 中的并发通常是这样的情况:我们维护着一个状态更新列表(社交网页或新闻帖子)的网站,它有一个很常见的功能,下拉加载。当用户下拉到列表底部时,就会触发这个事件,由 JavaScript 发送网络请求来获取新的数据。如果在获取数据期间,用户频繁触发了下拉加载这个事件,就会导致更多的请求被发送(虽然实际情况中我们可能会避免这一状况)。
|
||||
|
||||
假如用户一瞬间触发了 6 个请求,而在接下来的时间里便会陆续的收到对应的 6 个响应:
|
||||
|
||||
```
|
||||
// onscroll 事件
|
||||
请求1;
|
||||
请求2;
|
||||
请求3;
|
||||
请求4;
|
||||
请求5;
|
||||
请求6;
|
||||
|
||||
// ajax 响应事件
|
||||
响应1;
|
||||
响应2;
|
||||
响应3;
|
||||
响应4;
|
||||
响应5;
|
||||
响应6;
|
||||
```
|
||||
|
||||
这是我们认为的情况,但根据 JavaScript 单线程事件循环的概念来看,实际上时间线可能是这样的:
|
||||
|
||||
```js
|
||||
onscroll, 请求1; // onscroll 事件
|
||||
onscroll, 请求2;
|
||||
响应1; // ajax 响应事件
|
||||
onscroll, 请求3;
|
||||
onscroll, 请求4;
|
||||
响应2;
|
||||
响应3;
|
||||
onscroll, 请求5;
|
||||
响应5;
|
||||
onscroll, 请求6;
|
||||
响应4;
|
||||
响应6;
|
||||
```
|
||||
|
||||
由于单线程的事件循环,JavaScript 一次只能处理一个事件,所以当在发送多个请求的间隙之间,有对应的响应到达时,JavaScript 便会去处理它。这就像在学校食堂排队一样,都得一个一个按顺序来。另外,由于不同的响应时间,响应的顺序可能也是乱序的。
|
||||
|
||||
### 非交互
|
||||
|
||||
两个或多个任务之间由异步交替进行任务时,如果这些任务不相干,则不一定需要交互。**如果进程间没有相互影响的话,不确定性是完全可以接收的。**
|
||||
|
||||
```js
|
||||
const res = {};
|
||||
|
||||
const foo = (result) => {
|
||||
res.foo = result;
|
||||
};
|
||||
|
||||
const bar = (result) => {
|
||||
res.bar = result;
|
||||
};
|
||||
|
||||
ajax('url.1', foo);
|
||||
ajax('url.2', bar);
|
||||
```
|
||||
|
||||
例如上述的伪代码,foo 和 bar 的两个回调按照什么顺序执行是不确定的,但它们是独立运行的,不会相互影响。
|
||||
|
||||
### 交互
|
||||
|
||||
现实中更常见的情况是多个任务之间需要相互进行交流,如果它们是交互的,则需要进行协调以避免竞态的出现。
|
||||
|
||||
再来看一段伪代码:
|
||||
|
||||
```js
|
||||
const res = [];
|
||||
|
||||
const foo = (result) => {
|
||||
res.push(result);
|
||||
};
|
||||
|
||||
ajax('url.1', foo);
|
||||
ajax('url.2', bar);
|
||||
```
|
||||
|
||||
这里两个回调都是对数组进行追加结果,并且存在不确定性。这就导致了`res[0]`可能是任意一个回调的结果。这种不确定性很可能就是一个竞态 Bug。
|
||||
|
||||
如果需要解决这种不确定性带来的问题,可以尝试进行协调:
|
||||
|
||||
```js
|
||||
let a, b;
|
||||
|
||||
const foo = (x) => {
|
||||
a = x * 2;
|
||||
if (a && b) {
|
||||
baz();
|
||||
}
|
||||
};
|
||||
|
||||
const bar = (y) => {
|
||||
b = y * 2;
|
||||
if (a && b) {
|
||||
baz();
|
||||
}
|
||||
};
|
||||
|
||||
const baz = () => {
|
||||
console.log(a + b);
|
||||
};
|
||||
|
||||
ajax('url.1', foo);
|
||||
ajax('url.2', bar);
|
||||
```
|
||||
|
||||
这是一种竞态(race),或者称之为门闩(latch)。
|
||||
|
||||
### 协作
|
||||
|
||||
还有一种合作方式,称为并发协作(cooperative concurrency)。这里的重点是利用异步的事件循环来将复杂繁重的任务插入到事件队列中,使得其他的并发任务有机会也在事件循环队列中被执行。
|
||||
|
||||
通俗的来说,就是分割耗时过长的同步任务到异步队列中,从而避免进程阻塞。
|
||||
|
||||
来看一段伪代码:
|
||||
|
||||
```js
|
||||
const res = [];
|
||||
|
||||
const response = (data) => {
|
||||
res = res.concat(data.map((item) => item * 3.14));
|
||||
};
|
||||
|
||||
ajax('url.1', response);
|
||||
ajax('url.2', response);
|
||||
```
|
||||
|
||||
这里假设需要从某处发送 ajax 请求来取得某些数据,并将这些数据进行一系列操作后存入 res 数组中。如果数据只有几千几百条的话,那可能不是什么问题。但如果数据是千万级别甚至更多呢?这时可能就会阻塞进程好一会,效果类似于死循环几秒钟。
|
||||
|
||||
这种情况下,我们先处理一部分的数据,让后将剩下的数据处理添加到事件循环的队列中,使其它任务也拥有处理的时间。这样就有助于防止程序运行阻塞,进而提高性能。
|
||||
|
||||
```js
|
||||
const res = [];
|
||||
|
||||
const response = (data) => {
|
||||
const chunk = data.splice(0, 1000);
|
||||
|
||||
res = res.concat(chunk.map((item) => item * 3.14));
|
||||
|
||||
if (data.length > 0) {
|
||||
setTimeout(() => {
|
||||
response(data);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
ajax('url.1', response);
|
||||
ajax('url.2', response);
|
||||
```
|
||||
|
||||
这里使用的是`setTimeout(.., 0)`的一个 hack 方式,它的大概意思就是尽早的将这个函数添加到事件循环队列,这不是一个推荐的添加到事件循环队列的方式。
|
||||
|
||||
## 任务
|
||||
|
||||
在 ES6 中,有一个新的概念建立在事件循环队列(宏任务)上,叫作**任务队列**(job queue)。现在流行的称呼也为微任务。微任务最大的影响就是 promise 的异步特性。
|
||||
|
||||
目前最好理解任务队列(微任务)的方法就是与事件循环队列(宏任务)一起理解。上述中,我们将宏任务描述为一个数组,每次从数组中取出一个任务(块)进行执行时,都称之为一次 tick。而微任务就是挂在每一次 tick 之后所进行的循环队列(常见的说法也有是挂在下次一 tick 开始之前,但这细微的差距不影响我们理解微任务)。
|
||||
|
||||
这里上一张上次看到的很好的一张图,图中的原文讲解 [Process.nextTick 和 setImmediate 的区别? - 知乎 (zhihu.com)](https://www.zhihu.com/question/23028843) 的。但同时也很好的说明了微任务(剧透一下,`process.nextTick()`就是会添加到微任务队列中)。
|
||||
|
||||
```js
|
||||
Promise.resolve('B').then((val) => {
|
||||
console.log(val);
|
||||
});
|
||||
|
||||
const a = () => {
|
||||
console.log('A');
|
||||
};
|
||||
|
||||
const c = () => {
|
||||
console.log('C');
|
||||
};
|
||||
|
||||
const d = () => {
|
||||
console.log('D');
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
d();
|
||||
}, 0);
|
||||
|
||||
a();
|
||||
c();
|
||||
// A C B D
|
||||
```
|
||||
|
||||

|
||||
|
||||
这里的函数`a()`和`c()`都是同步执行的(同步可以直接理解为第一次执行的宏任务队列),而函数`d()`是添加到宏任务的后续队列中的。Promise 的微任务则如图中一样,挂在同步(正在执行)队列后,等待队列(宏任务)之前。
|
||||
|
||||
## 小结
|
||||
|
||||
异步 JavaScript 的程序总是会至少分成两个块来运行:一个是现在运行的块;另一个是将来运行的块。尽管程序是一块一块执行的,但是所有这些块共享对程序的作用域和状态的访问,所以对状态的修改都是在之前累计的修改上进行的。
|
||||
|
||||
并发是指两个或多个事件链随着事件的发展交替执行,以至于从更高层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。
|
259
source/_posts/性能优化-useMemo and useCallback.md
Normal file
@ -0,0 +1,259 @@
|
||||
---
|
||||
title: 性能优化-useMemo and useCallback
|
||||
date: 2021-08-21 16:05:33
|
||||
tags: [JavaScript, React]
|
||||
categories: 实践
|
||||
url: useMemo-and-useCallback
|
||||
---
|
||||
|
||||
性能优化一直是一个值得考虑的问题,但更值得考虑的是什么时候该优化。如果优化不得当,对于向 React 这类成熟的框架来说,即可能会过早优化。反而花了过多的时间来降低其性能。
|
||||
|
||||
React 的 `useMemo` 和 `useCallback` 这两个 hook 基本上就是为性能优化而准备的。既然有了优化的方案,剩下的就是什么时候该做优化。对其我目前的看法就是除非性能明显降低,否则不必太早考虑去进行优化性能。对于这两个 hook 也是如此,不过他们的也有别的作用。
|
||||
|
||||

|
||||
|
||||
## 引用相等性
|
||||
|
||||
引用相等性,简单来说就是因为引用值的特殊性,导致经常看起来可能是相等的值,其本身并不相等。
|
||||
|
||||
来看一个最常见的例子:
|
||||
|
||||
```js
|
||||
{} === {} // false
|
||||
[] === [] // false
|
||||
|
||||
'xfy' === 'xfy' // true
|
||||
```
|
||||
|
||||
这是因为基本值是不可变的,两个看上去相同的基本值,他们绝大部分情况都是相同的。而每创建引用值时都会使用一个新的地址空间,这就导致了引用值的相等性问题。
|
||||
|
||||
更常见的例子是在函数中创建引用值:
|
||||
|
||||
```js
|
||||
const test = () => {
|
||||
return {
|
||||
name: 'xfy',
|
||||
};
|
||||
};
|
||||
|
||||
const a = test();
|
||||
const b = test();
|
||||
|
||||
console.log(a === b);
|
||||
```
|
||||
|
||||
在 React 的函数组件中,每个函数组件都是普通的函数,我们常常会遇到在父组件中创建一个引用值,将其传递给子组件,而子组件可以根据父组件传递的状态来重新渲染。
|
||||
|
||||
这时候就会遇到一个问题,因为引用值的特殊性,每次传递给子组件的状态与前一次都是不相同的。即使这个引用值根本没有任何变化。
|
||||
|
||||
来看一个刻意设计的例子,这是一个父组件,它会传递两个状态给子组件,分别是一个函数与数组,他们都是引用值:
|
||||
|
||||
```tsx
|
||||
import Child from "./Child";
|
||||
|
||||
export default function App() {
|
||||
const bar = () => console.log("bar executed");
|
||||
const foo = () => [1, 2, 3];
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Hello CodeSandbox</h1>
|
||||
<p>{count}</p>
|
||||
<button onClick={() => setCount(count + 1)}>Add one</button>
|
||||
<Child bar={bar} foo={foo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
而子组件则负责根据这两个状态的变化打印对应的消息,这样我们就能清楚的知道它什么时候重新渲染了。
|
||||
|
||||
```tsx
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface Props {
|
||||
bar: () => void;
|
||||
foo: number[];
|
||||
}
|
||||
|
||||
export default function Child({ bar, foo }: Props) {
|
||||
useEffect(() => {
|
||||
bar();
|
||||
console.log(foo);
|
||||
console.log('update')
|
||||
}, [bar, foo]);
|
||||
|
||||
return <div>Hello</div>;
|
||||
}
|
||||
```
|
||||
|
||||
这乍一看好像没有什么问题,但当我们点击按钮与父组件进行交互时,父组件会根据自身状态的变化重新渲染。而父组件本身是一个函数,当父组件重新渲染时,就意味着函数重新执行,它所传递给子组件的 bar 和 foo 的引用值则会被重新创建。
|
||||
|
||||
这就导致了因为引用值的特殊性,其本身没有变化,反而因为父组件的重新渲染导致了创建了新的引用值传递给子组件。子组件也就会因为 props 的变化而重新渲染。
|
||||
|
||||

|
||||
|
||||
这不是我们能想要的结果,好在 `useMemo` 和 `useCallback` 这两个 hook 可以帮我们解决这些问题。
|
||||
|
||||
问题其实很简单,父组件每次重新渲染时会创建新的引用值,导致子组件不必要的重新渲染。我们只需要利用到`useMemo` 和 `useCallback` 创建一个 memoized 的值,并根据依赖数组的变化更新其值。
|
||||
|
||||
这样当父组件重新渲染时,没有变化的引用值也就不会被重新创建,而是利用之前已经创建好的 memoized 值:
|
||||
|
||||
```tsx
|
||||
import Child from "./Child";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
export default function App() {
|
||||
const bar = useCallback(() => console.log("bar executed"), []);
|
||||
const foo = useMemo(() => [1, 2, 3], []);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>Hello CodeSandbox</h1>
|
||||
<p>{count}</p>
|
||||
<button onClick={() => setCount(count + 1)}>Add one</button>
|
||||
<Child bar={bar} foo={foo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 这是一个刻意设计的例子,所以 foo 和 bar 没有依赖,永远都不需要更新。
|
||||
|
||||
这时候,无论父组件怎么重新渲染,子组件都会只渲染一次。排除了所有的不必要渲染。
|
||||
|
||||
### Demo
|
||||
|
||||
<iframe src="https://codesandbox.io/embed/usememo-and-usecallback-lwt67?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark&view=preview"
|
||||
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
|
||||
title="useMemo and useCallback"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
|
||||
## 避免不必要渲染
|
||||
|
||||
上述解决引用值的相等问题时,其实也解决了子组件的必要渲染。同样的,利用 `useMemo` 和 `useCallback` 也可以避免另一种情况的不必要渲染。
|
||||
|
||||
这也是一个刻意设计的例子,我们有一个父组件,它负责提供状态给子组件进行展示。听上去和第一个例子很像,不同的是,这里需要复用子组件:
|
||||
|
||||
```tsx
|
||||
import CountButton from "./CountButton";
|
||||
|
||||
export default function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
const inc1 = () => {
|
||||
setCount((count) => count + 1);
|
||||
};
|
||||
const [count1, setCount1] = useState(0);
|
||||
const inc2 = () => {
|
||||
setCount1((count1) => count1 + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<CountButton count={count} onClick={inc1} />
|
||||
<CountButton count={count1} onClick={inc2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
这里将 count 和对应的更新状态的方法传递给子组件,由子组件进行展示和交互。并且子组件是复用同一个组件的,只是状态不同。
|
||||
|
||||
很明显,这里传递的更新方法 `setCount` 也是引用值,我们可以利用 `useCallback` 来创建一个其 memoized 的版本传递给子组件。
|
||||
|
||||
当然这还解决不了所有的问题,这里的子组件是被复用的:
|
||||
|
||||
```tsx
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export default function CountButton({ onClick, count }: Props) {
|
||||
useEffect(() => {
|
||||
console.log("re-render");
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<p>{count}</p>
|
||||
<button onClick={onClick}>Increase</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
这里的子组件在父组件中被复用了两次,我们期望的是,当我们和指定的那个子组件进行交互时,就只更新(重新渲染)它。因为只有它的 props 发生了变化,而另一个则不需要无谓的渲染。
|
||||
|
||||
要解决这个问题,还是需要 `useMemo` 和 `useCallback` 配合进行使用,不过这次并不完全一样:
|
||||
|
||||
```tsx
|
||||
export default function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
const inc1 = useCallback(() => {
|
||||
setCount((count) => count + 1);
|
||||
}, []);
|
||||
const [count1, setCount1] = useState(0);
|
||||
const inc2 = useCallback(() => {
|
||||
setCount1((count1) => count1 + 1);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<CountButton count={count} onClick={inc1} />
|
||||
<CountButton count={count1} onClick={inc2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
在父组件中,我们将传递的引用值进行 memoized,而子组件本身也是一个函数,我们期望它只根据 props 的变化而进行重新渲染,而不受复用的那一个影响。
|
||||
|
||||
所以这里需要对子组件进行 memo 化:
|
||||
|
||||
```tsx
|
||||
export default React.memo(function CountButton({ onClick, count }: Props) {
|
||||
useEffect(() => {
|
||||
console.log("re-render");
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<p>{count}</p>
|
||||
<button onClick={onClick}>Increase</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
通过如此,子组件就只会根据 props 的变化来重新渲染。而父组件中的引用值也利用 `useCallback` 进行 memoized,这样就避免了只更新其中一个子组件时,导致的另一个子组件也被重新渲染。
|
||||
|
||||
### Demo
|
||||
|
||||
<iframe src="https://codesandbox.io/embed/usecallback-4ggvv?expanddevtools=1&fontsize=14&hidenavigation=1&theme=dark"
|
||||
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
|
||||
title="useCallback"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
></iframe>
|
||||
|
||||
## 何时进行优化
|
||||
|
||||
上述例子都利用 `useMemo` 和 `useCallback` 对组件进行优化,以避免不必要的重新渲染。但另一个需要思考的问题是:我们何时需要手动进行优化?
|
||||
|
||||
> 性能优化总是需要代价的,但并不总是会带来益处。
|
||||
|
||||
像 React 这类成熟的框架,其性能还没有低到需要我们每时每刻都关注是否需要进行手动的优化。也就是说 React 或其他类似的框架,其实是很快的,JavaScript 在函数内创建和执行另一个简单的函数也是很快的。对于此,我们不需要对其进行额外的关注,可以把宝贵的时间花在创作内容上。
|
||||
|
||||
对于像上面例子中,复用两个简单的子组件其实并不需要用到 `useMemo` 来优化,相反这样简单的组件可能会由 `useMemo` 带来额外的开销,结果适得其反。
|
||||
|
||||
而如果上述例子中,子组件是很大的表格或者图表等。每次重新渲染都需要花费很大的开销。这时就很有必要进行 `useMemo` 了。
|
||||
|
||||
|
||||
## 参考
|
||||
|
||||
* [When to useMemo and useCallback (kentcdodds.com)](https://kentcdodds.com/blog/usememo-and-usecallback)
|
@ -177,7 +177,7 @@ window.moveBy(-50, 0);
|
||||
|
||||
Chrome与Firefox在1080p分辨率下输出的inner和outer的值:
|
||||
|
||||

|
||||

|
||||
|
||||
另外一些浏览器在`document.documentElement.clientWidth`和`document.documentElement.clientHeight`保存了页面视口的信息。在IE6中,这些属性必须是在标准模式下才有效;如果是混杂模式,就必须通过`document.body`中的`clientWidth`和`clientHeight`取得相同的信息。
|
||||
|
||||
|
323
source/_posts/现代前端的Web应用路由-为React打造一个迷你路由器.md
Normal file
@ -0,0 +1,323 @@
|
||||
---
|
||||
title: 现代前端的Web应用路由-为React打造一个迷你路由器
|
||||
date: 2021-08-23 17:28:49
|
||||
tags: [JavaScript, React]
|
||||
categories: 实践
|
||||
url: create-tiny-router-for-react
|
||||
---
|
||||
|
||||
路由不仅仅只是网络的代名词,它更像是一种表达路径的概念。与网络中的路由相似,前端中的页面路由也是带领我们前往指定的地方。
|
||||
|
||||

|
||||
|
||||
## 现代前端的 Web 应用路由
|
||||
|
||||
时代在变迁,过去,Web 应用的基本架构使用了一种不同于现代路由的方法。曾经的架构通常都是由后端生成的 HTML 模板来发送给浏览器。当我们单击一个标签导航到另一个页面时,浏览器会发送一个新的请求给服务器,然后服务器再将对应的页面渲染好发过来。也就是说,每个请求都会刷新页面。
|
||||
|
||||
自那时起,Web 服务器在设计和构造方面经历了很多发展(前端也是)。如今,JavaScript 框架和浏览器技术已经足够先进,允许我们利用 JavaScript 在客户端渲染 HTML 页面,以至于 Web 应用可以采用更独特的前后的分离机制。在第一次由服务端下发了对应的 JavaScript 代码后,后续的工作就全部交给客户端 JavaScript。而后端服务器负责发送原始数据,通常是 JSON 等。
|
||||
|
||||

|
||||
|
||||
在旧架构中,动态内容由 Web 服务器生成,服务器会在数据库中获取数据,并利用数据渲染 HTML 模板发送给浏览器。每次切换页面都会获取新的由服务端渲染的页面发送给浏览器。
|
||||
|
||||
在新架构中,服务端通常只下发主要的 JavaScript 和基本的 HTML 框架。之后页面的渲染就会由我们的 JavaScript 接管,后续的动态内容也还是由服务器在数据库中获取,但不同的是,后续数据由服务器发送原始格式(JSON等)。前端 JavaScript 由 AJAX 等技术获取到了新数据,再在客户端完成新的 HTML 渲染。
|
||||
|
||||
好像 SPA 的大概就是将原先服务端干的活交给了前端的 JavaScript 来做。事实上,确实如此。AJAX 技术的发展,带动了客户端 JavaScript 崛起,使得原先需要在服务端才能完成渲染的动态内容,现在交给 JavaScript 就可以了。
|
||||
|
||||
这么做的好处有很多:
|
||||
|
||||
* 主要渲染工作在客户端,减少服务器的压力。简单的场景甚至只需要静态服务器。
|
||||
* 新内容获取只需要少量交互,而不是服务端发送渲染好的 HTML 页面。
|
||||
* 可以利用 JavaScript 在客户端修改和渲染任意内容,同时无需刷新整个页面。
|
||||
* ……
|
||||
|
||||
当然同时也有一些缺点,目前最主要的痛点:
|
||||
|
||||
* 首屏/白屏时间:由于 HTML 内容需要客户端 JavaScript 完成渲染,前端架构以及多种因素会影响首次内容的出现时间。
|
||||
* 爬虫/SEO:由于 HTML 内容需要客户端 JavaScript 完成渲染,早期的爬虫可能不会在浏览器环境下执行 JavaScript,这就导致了根本爬取不到 HTML 内容。
|
||||
|
||||
> Google 的爬虫貌似已经可以爬取 SPA。
|
||||
|
||||
## React 的路由
|
||||
|
||||
React 是现代众多成熟的 SPA 应用框架之一,它自然也需要使用路由来切换对应的组件。React Router 是一个成熟的 React 路由组件库,它实现了许多功能,同时也非常还用。
|
||||
|
||||
首先来看下本次路由的基本工作原理,本质上很简单,我们需要一个可以渲染任意组件的父组件 `<Router />` 或者叫 `<router-view>` 之类的。然后再根据浏览器地址的变化,渲染注册路由时对应的组件即可。
|
||||
|
||||

|
||||
|
||||
### 配置文件
|
||||
|
||||
这里选择类似 Vue Router 的配置文件风格,而不是使用类似 `<Route />` 这样的 DOM 结构来注册路由。我们的目的是为了实现一个非常简单的迷你路由器,所以配置文件自然也就很简单:
|
||||
|
||||
```ts
|
||||
import { lazy } from 'react';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
component: lazy(() => import('../pages/Home')),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
component: lazy(() => import('../pages/About')),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
一个 `path` 属性,对应了浏览器的地址,或者说 `location.pathname`;一个 `component` 属性,对应了到该地址时所需要渲染的组件。
|
||||
|
||||
甚至还可以使用 `lazy()` 配合 `<Suspense>` 来实现代码分割。
|
||||
|
||||
### 展示路由
|
||||
|
||||
一个非常简单的路由就这样注册好了,接下来就是将对应的组件展示出来。我们都知道,JSX 最终会被 babel 转义为渲染函数,而一个组件的 `<Home />` 写法,基本等同于 `React.createElement(Home)`。[元素渲染 – React (reactjs.org)](https://zh-hans.reactjs.org/docs/rendering-elements.html#updating-the-rendered-element)
|
||||
|
||||
所以动态的渲染指定的组件基本上也就很容易解决,接下来的思路也就很简单了,我们需要:
|
||||
|
||||
* 一个状态:记录当前地址 `location.pathname`;
|
||||
* 根据当前地址在配置文件中寻找对应注册的组件,并将它渲染出来;
|
||||
* 一个副作用:当用户手动切换路由时,该组件需要重新渲染为对应注册的路由;
|
||||
|
||||
先不考虑切换路由的问题,前两个基本上已经就实现了:
|
||||
|
||||
```tsx
|
||||
// Router.tsx
|
||||
import React, { useState, Suspense } from 'react';
|
||||
import routes from './routes';
|
||||
|
||||
const Router: React.FC = () => {
|
||||
// 获取地址,并保存到状态中
|
||||
const [path, setPath] = useState(location.pathname);
|
||||
// 根据地址,寻找对应的组件
|
||||
const element = routes.find((route) => route.path === path)?.component;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<p>loading...</p>}>
|
||||
{/* 使用 React.createElement() 渲染组件 */}
|
||||
{element ? React.createElement(element) : void 0}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
```
|
||||
|
||||
看上去很简单,事实上,确实很简单。
|
||||
|
||||
现在我们直接从地址栏访问对应的路由,我们的 Router 组件应该就可以根据已经注册好的路由配置文件来找到正确的组件,并将其渲染出来了。
|
||||
|
||||

|
||||
|
||||
### 切换路由
|
||||
|
||||
到目前为止,我们实现了根据对应地址访问到对应组件的功能。这是一个路由必不可少的功能,但它还不能称得上是一个简单的路由器,因为它还无法处理用户手动切换的路由,也就是点击标签前往对应的页面。
|
||||
|
||||
简单梳理一下我们需要实现的功能:
|
||||
|
||||
* 一个 Link 组件,用于点击后导航到指定的地址;
|
||||
* 导航到地址后,还要修改浏览器的地址栏,并不真正的发送请求;
|
||||
* 通知 Router 组件,地址已经改变,重新渲染对应路由的组件;
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
to: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const RouterLink: React.FC<Props> = ({ to, children }: Props) => {
|
||||
/**
|
||||
* 处理点击事件
|
||||
* 创建自定义事件监听器
|
||||
* 并将 path 发送给 router
|
||||
* @param e
|
||||
*/
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
const nowPath = location.pathname;
|
||||
if (nowPath === to) return; // 原地跳转
|
||||
|
||||
history.pushState(null, '', to);
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<string>('route', {
|
||||
detail: to,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<a href="" onClick={handleClick}>
|
||||
{children}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RouterLink;
|
||||
```
|
||||
|
||||
我们将 Link 组件实际的渲染为一个 `<a></a>` 标签,这样就能模拟跳转到指定的导航了。这个组件它接收两个参数:`{ to, children }`,分别是前往的路由地址和标签的内容。标签的内容 children 就是展示在 a 标签内的文本。
|
||||
|
||||
我们需要解决的第一个问题就是点击标签后跳转到指定的导航,这里其实需要分成两个部分,第一个部分是悄悄的修改浏览器地址栏,第二个部分则是通知 Router 组件去渲染对应的组件。
|
||||
|
||||
修改地址栏很简单,利用到浏览器的 history API,可以很方便的修改 `pathName` 而不发送实际请求,这里只需要修改到第一个参数 to 即可:`history.pushState(null, '', to);`。
|
||||
|
||||
但使用 `pushState()` 并不会发出任何通知,我们需要自己实现去通知 Router 组件地址已经变化。本来像利用第三方库来实现一个 发布/订阅 的模型的,但这样这个路由器可能就没有那么迷你了。最后发现利用 HTML 的 CustomEvent 可以实现一个简单的消息订阅与发布模型。
|
||||
|
||||
HTML 自定义事件也很简单,我们在对应的 DOM 上 `dispatchEvent` 即可触发一个事件,而触发的这个事件,就是 `CustomEvent` 的实例,甚至还能传递一些信息。在这里,我们将路由地址传递过去。
|
||||
|
||||
```ts
|
||||
document.dispatchEvent(
|
||||
new CustomEvent<string>('route', {
|
||||
detail: to,
|
||||
})
|
||||
);
|
||||
```
|
||||
|
||||
而在 Router 组件中,只需要和以前一样在对应的 DOM 上去监听一个事件,这个事件就是刚刚发布的 `CustomEvent` 的实例。
|
||||
|
||||
```ts
|
||||
// Router.tsx
|
||||
const handleRoute = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
setPath(e.detail);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* 监听自定义 route 事件
|
||||
* 并根据 path 修改路由
|
||||
*/
|
||||
document.addEventListener('route', handleRoute as EventListener);
|
||||
|
||||
return () => {
|
||||
// 清除副作用
|
||||
document.removeEventListener('route', handleRoute as EventListener);
|
||||
};
|
||||
}, []);
|
||||
|
||||
```
|
||||
|
||||
在 Router 组件中,根据接收到的变化,将新的地址保存到状态中,并触发组件重新渲染。
|
||||
|
||||
这样一个最简单的 React 路由就做好了。
|
||||
|
||||
## Vue 的路由
|
||||
|
||||
与 React 同理,二者的路由切换都差不多,其主要思路还是使用自定义事件来订阅路由切换的请求。但 Vue 的具体实现与 React 还是有点不同的。
|
||||
|
||||
### 配置文件
|
||||
|
||||
路由的配置文件还是同理,不同的是,Vue 的异步组件需要在引入时同时引入一个 Loading 组件来实现 Loading 的效果:
|
||||
|
||||
```ts
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import Loading from '../components/common/Loading.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: defineAsyncComponent({
|
||||
loader: () => import('../views/Home.vue'),
|
||||
loadingComponent: Loading,
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'About',
|
||||
component: defineAsyncComponent({
|
||||
loader: () => import('../views/About.vue'),
|
||||
loadingComponent: Loading,
|
||||
}),
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
### 展示路由
|
||||
|
||||
同理,Vue 也是利用根据条件来渲染对应路由的组件。不同的是,我们可以使用模板语法来实现,也可以利用 `render()` 方法来直接渲染组件。
|
||||
|
||||
首先来看看和 React 类似的 `render()` 方法。在 Vue3 中,使用 `setup()` 方法后,可以直接返回一个 `createVNode()` 的函数,这就是 `render()` 方法。所以可以直接写 TypeScript 文件。
|
||||
|
||||
与 React 不同的地方在于,React 每次调用 `setPath(e.detail)` 存储状态时都会重新渲染组件,从而重新执行组件的函数,获取到对应的路由组件。
|
||||
|
||||
但 Vue 不同,如果我们仅仅将路由名称 `e.detail` 保存到状态,但没有实际在 VNode 中使用的话,更新状态时不会重新渲染组件的,也就是说,不会获取到对应的路由组件。所以最佳的办法就是将整个路由组件保存到状态,可保存整个组件无疑太过庞大。好在 Vue3 给了我们另一种解决方法:`shallowRef()`。它会创建一个跟踪自身 `.value` 变化的 ref,但不会使其值也变成响应式的。
|
||||
|
||||
```ts
|
||||
import { createVNode, defineComponent, shallowRef } from 'vue';
|
||||
import routes from './routes';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'RouterView',
|
||||
setup() {
|
||||
let currentPath = window.location.pathname;
|
||||
const component = shallowRef(
|
||||
routes.find((item) => item.path === currentPath)?.component ??
|
||||
'Note found'
|
||||
);
|
||||
|
||||
const handleEvent = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
currentPath = e.detail;
|
||||
component.value =
|
||||
routes.find((item) => item.path === currentPath)?.component ??
|
||||
'Note found';
|
||||
};
|
||||
|
||||
document.addEventListener('route', handleEvent as EventListener);
|
||||
|
||||
return () => createVNode(component.value);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
而使用模板语法主要是利用到了全局的 `component` 组件,其他部分与 `render()` 方法相同:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<component :is="component"></component>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUpdated, shallowRef } from 'vue';
|
||||
import routes from './routes';
|
||||
|
||||
let currentPath = window.location.pathname;
|
||||
const component = shallowRef(
|
||||
routes.find((item) => item.path === currentPath)?.component
|
||||
);
|
||||
|
||||
const handleEvent = (e: CustomEvent<string>) => {
|
||||
console.log(e.detail);
|
||||
currentPath = e.detail;
|
||||
component.value = routes.find((item) => item.path === currentPath)?.component;
|
||||
};
|
||||
|
||||
document.addEventListener('route', handleEvent as EventListener);
|
||||
|
||||
onUpdated(() => {
|
||||
console.log(component);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
如今的 JavaScript 做能做到的比以前更加强大,配合多种 HTML API,可以将曾经不可能实现的事变为现实。这个简单的迷你路由,主要的思路就是利用 HTML API 来通知 Router 组件该渲染哪个组件了。配合上 `lazy()` 方法,甚至还能实现代码分割。
|
||||
|
||||
## Demo
|
||||
|
||||
[DefectingCat/react-tiny-router: A tiny react router. (github.com)](https://github.com/DefectingCat/react-tiny-router)
|
||||
|
||||
<iframe src="https://codesandbox.io/embed/github/DefectingCat/react-tiny-router/tree/master/?fontsize=14&hidenavigation=1&theme=dark&view=preview"
|
||||
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
|
||||
title="tiny-router"
|
||||
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
|
||||
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
|
||||
></iframe>
|
558
source/_posts/自建简易FaaS平台.md
Normal file
@ -0,0 +1,558 @@
|
||||
---
|
||||
title: 自建简易 FaaS 平台
|
||||
date: 2021-07-11 12:02:45
|
||||
tags: [JavaScript,TypeScript]
|
||||
categories: 实践
|
||||
url: built-simply-faas
|
||||
---
|
||||
|
||||
近些年来,传统的 IaaS、PaaS 已经无法满足人们对资源调度的需求了。各大云厂商相继开始推出自家的 Serverless 服务。Serverless 顾名思义,它是“无服务器”服务器。不过并不是本质上的不需要服务器,而是面向开发者(客户)无需关心底层服务器资源的调度。只需要利用本身业务代码即可完成服务的运行。
|
||||
|
||||
Serverless 是近些年的一个发展趋势,它的发展离不开 FaaS 与 BaaS。这里不是着重讨论 Serverless 架构的,而是尝试利用 Node.js 来实现一个最简易的 FaaS 平台。顺便还能对 JavaScript 语言本身做进一步更深的研究。
|
||||
|
||||
Serverless 平台是基于函数作为运行单位的,在不同的函数被调用时,为了确保各个函数的安全性,同时避免它们之间的互相干扰,平台需要具有良好的隔离性。这种隔离技术通常被称之为“沙箱”(Sandbox)。在 FaaS 服务器中,最普遍的隔离应该式基于 Docker 技术实现的容器级别隔离。它不同于传统虚拟机的完整虚拟化操作系统,而且也实现了安全性以及对系统资源的隔离。
|
||||
|
||||
但在这我们尝试实现一个最简易的 FaaS 服务,不需要利用上 Docker。基于进程的隔离会更加的轻便、灵活,虽然与容器的隔离性有一定差距。
|
||||
|
||||
## 环境搭建
|
||||
|
||||
这里利用 TypeScript 来对 JavaScript 做更严格的类型检查,并使用 ESlint + Prettier 等工具规范代码。
|
||||
|
||||
初始化环境:
|
||||
|
||||
```bash
|
||||
yarn --init
|
||||
```
|
||||
|
||||
添加一些开发必要工具:
|
||||
|
||||
```bash
|
||||
yarn add typescript ts-node nodemon -D
|
||||
```
|
||||
|
||||
以及对代码的规范:
|
||||
|
||||
```js
|
||||
yarn add eslint prettier eslint-plugin-prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin -D
|
||||
```
|
||||
|
||||
当然不能忘了 Node 本身的 TypeScript lib。
|
||||
|
||||
```bash
|
||||
yarn add @types/node -D
|
||||
```
|
||||
|
||||
## 基础能力
|
||||
|
||||
在 [Nodejs多进程 | 🍭Defectink](https://www.defectink.com/defect/nodejs-multi-process.html) 一篇中,我们大概的探讨了进程的使用。这里也是类似。在进程创建时,操作系统将给该进程分配对应的虚拟地址,再将虚拟地址映射到真正的物理地址上。因此,进程无法感知真实的物理地址,只能访问自身的虚拟地址。这样一来,就可以防止两个进程互相修改数据。
|
||||
|
||||
所以,我们基于进程的隔离,就是让不同的函数运行再不同的进程中,从而保障各个函数的安全性和隔离性。具体的流程是:我们的主进程(master)来监听函数的调用请求,当请求被触发时,再启动子进程(child)执行函数,并将执行后的结果通过进程间的通信发送给主进程,最终返回到客户端中。
|
||||
|
||||
### 基于进程隔离
|
||||
|
||||
`chlid_process`是 Node.js 中创建子进程的一个函数,它有多个方法,包括 exec、execFile 和 fork。实际上底层都是通过 spawn 来实现的。这里我们使用 fork 来创建子进程,创建完成后,fork 会在子进程与主进程之间建立一个通信管道,来实现进程间的通信(IPC,Inter-Process Communication)。
|
||||
|
||||
其函数签名为:`child_process.fork(modulePath[, args][, options])`。
|
||||
|
||||
这里利用`child.process.fork`创建一个子进程,并利用`child.on`来监听 IPC 消息。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
|
||||
// Use child.on listen a message
|
||||
child.on('message', (message: string) => {
|
||||
console.log('MASTER get message:', message);
|
||||
});
|
||||
```
|
||||
|
||||
在 Node.js 中,process 对象是一个内置模块。在每个进程启动后,它都可以获取当前进程信息以及对当前进程进行一些操作。例如,发送一条消息给主进程。
|
||||
|
||||
子进程则利用 process 模块来和主进程进行通信
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
|
||||
process.send?.('this is a message from child process');
|
||||
```
|
||||
|
||||
执行这段方法后,master 就会创建一个子进程,并接收到其发来的消息。
|
||||
|
||||
```bash
|
||||
$ node master.js
|
||||
MASTER get message: this is a message from child process
|
||||
```
|
||||
|
||||

|
||||
|
||||
到此,我们就实现了主进程与子进程之间的互相通信。但是需要执行的函数通常来自于外部,所以我们需要从外部手动加载代码,再将代码放到子进程中执行,之后将执行完的结果再发送回主进程,最终返回给调用者。
|
||||
|
||||
我们可以再创建一个`func.js`来保存用户的代码片段,同时在主进程中读取这段代码,发送给子进程。而子进程中需要动态执行代码的能力。什么方式能在 JavaScript 中动态的执行一段代码呢?
|
||||
|
||||
### Devil waiting outside your floor
|
||||
|
||||
没错,这里要用到万恶的 evil。在 JavaScript 中动态的加载代码,eval 函数是最简单方便,同时也是最危险和性能最低下的方式。以至于现代浏览器都不愿意让我们使用
|
||||
|
||||
```js
|
||||
console.log(eval('2 + 2'))
|
||||
|
||||
// VM122:1 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' blob: filesystem:".
|
||||
```
|
||||
|
||||
执行来自用户的函数与普通函数略有一点区别,它与普通的函数不同,它需要利用 IPC 来返回值,而普通函数则之间 return 即可。我们不应该向用户暴露过度的内部细节,所以,用户的函数可以让他长这样:
|
||||
|
||||
```js
|
||||
// func.js
|
||||
|
||||
(event, context) => {
|
||||
return { message: 'it works!', status: 'ok ' };
|
||||
};
|
||||
```
|
||||
|
||||
eval 函数不仅可以执行一行代码片段,它还可以执行一个函数。在拿到用户的匿名函数后,我们可以将其包装成一个立即执行函数(IIFE)的字符串,然后交给 eval 函数进行执行。
|
||||
|
||||
```js
|
||||
const fn = `() => (2 + 2)`;
|
||||
const fnIIFE = `(${fn})()`;
|
||||
console.log(eval(fnIIFE));
|
||||
```
|
||||
|
||||
> 不用担心,evil 会离我们而去的。
|
||||
|
||||
这里我们使用主进程读取用户函数,并使用 IPC 发送给子进程;子进程利用 eval 函数来执行,随后再利用 IPC 将其结果返回给主进程。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs';
|
||||
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
|
||||
// Use child.on listen a message
|
||||
child.on('message', (message: unknown) => {
|
||||
console.log('Function result:', message);
|
||||
});
|
||||
|
||||
// Read the function from user
|
||||
const fn = fs.readFileSync('./src/func.js', { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = eval(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Devil crawling along your floor
|
||||
|
||||
前面我们利用 eval 函数获得了执行动态代码的能力,但与 Devil 做交易是需要付出代价的。很明显,我们付出了不小的安全性以及性能的代价。
|
||||
|
||||
甚至于用户代码能够直接修改 process,导致子进程无法退出等问题:
|
||||
|
||||
```js
|
||||
(event, context) => {
|
||||
process.exit = () => {
|
||||
console.log('process NOT exit!');
|
||||
};
|
||||
return { message: 'function is running.', status: 'ok' };
|
||||
};
|
||||
```
|
||||
|
||||
eval 函数能够访问全局变量的原因在于,它们由同一个执行其上下文创建。如果能让函数代码在单独的上下文中执行,那么就应该能够避免污染全局变量了。
|
||||
|
||||
所以我们得换一个 Devil 做交易。在 Node.js 内置模块中,由一个名为 vm 的模块。从名字就可以得出,它是一个用于创建基于上下文的沙箱机制,可以创建一个与当前进程无关的上下文环境。
|
||||
|
||||
具体方式是,将沙箱内需要使用的外部变量通过`vm.createContext(sandbox)`包装,这样我们就能得到一个 contextify 化的 sandbox 对象,让函数片段在新的上下文中访问。然后,可执行对象的代码片段。在此处执行的代码的上下文与当前进程的上下文是互相隔离的,在其中对全局变量的任何修改,都不会反映到进程中。提高了函数运行环境的安全性。
|
||||
|
||||
```js
|
||||
const vm = require('vm');
|
||||
|
||||
const x = 1;
|
||||
|
||||
const context = { x: 2 };
|
||||
vm.createContext(context); // Contextify the object.
|
||||
|
||||
const code = 'x += 40; var y = 17;';
|
||||
// `x` and `y` are global variables in the context.
|
||||
// Initially, x has the value 2 because that is the value of context.x.
|
||||
vm.runInContext(code, context);
|
||||
```
|
||||
|
||||
在我们的 FaaS 中,我们无须在外层访问新的上下文对象,只需要执行一段函数即可。因此可以通过`vm.runInNewContext(code)`方法来快速创建一个无参数的新上下文,更快速创建新的 sandbox。
|
||||
|
||||
我们只需要替换到 eval 函数即可:
|
||||
|
||||
```ts
|
||||
// child.ts
|
||||
import process from 'process';
|
||||
import vm from 'vm';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = vm.runInNewContext(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||
现在,我们实现了将函数隔离在沙箱中执行,流程如图:
|
||||
|
||||

|
||||
|
||||
但 vm 真的安全到可以随意执行来自用户的不信任代码吗?虽然相对于 eval 函数来,它隔离了上下文,提供了更加封闭的环境,但它也不是绝对安全的。
|
||||
|
||||
根据 JavaScript 对象的实现机制,所有对象都是有原型链的(类似`Object.crate(null)`除外)。因此 vm 创建的上下文中的 this 就指向是当前的 Context 对象。而 Context 对象是通过主进程创建的,其构造函数指向主进程的 Object。这样一来,通过原型链,用户代码就可以顺着原型链“爬”出沙箱:
|
||||
|
||||
```js
|
||||
import vm from 'vm';
|
||||
(event, context) => {
|
||||
vm.runInNewContext('this.constructor.constructor("return process")().exit()');
|
||||
return { message: 'function is running.', status: 'ok' };
|
||||
};
|
||||
```
|
||||
|
||||
这种情况就会导致非信任的代码调用主程序的`process.exit`方法,从而让整个程序退出。
|
||||
|
||||
也许我们可以切断上下文的原型链,利用`Object.create(null)`来为沙箱创建一个上下文。与任何 Devil 做交易都是需要付出代价的:
|
||||
|
||||
> **The `vm` module is not a security mechanism. Do not use it to run untrusted code**.
|
||||
|
||||
### Devil lying by your side
|
||||
|
||||
好在开源社区有人尝试解决这个问题,其中一个方案就是 vm2 模块。vm2 模块是利用 Proxy 特性来对内部变量进行封装的。这使得隔离的沙箱环境可以运行不受信任的代码。
|
||||
|
||||
当然,我们需要手动添加一下依赖:
|
||||
|
||||
```bash
|
||||
yarn add vm2
|
||||
```
|
||||
|
||||
另一个值得庆幸的是,代码改动也很小。我们只需要对`child.ts`简单修改即可:
|
||||
|
||||
```ts
|
||||
import process from 'process';
|
||||
import { VM } from 'vm2';
|
||||
|
||||
type fnData = {
|
||||
action: 'run';
|
||||
fn: () => unknown;
|
||||
};
|
||||
|
||||
// Listen function form master process
|
||||
process.on('message', (data: fnData) => {
|
||||
// Convert user function to IIFE
|
||||
const fnIIFE = `(${data.fn})()`;
|
||||
const result = new VM().run(fnIIFE);
|
||||
// Sent result to master process
|
||||
process.send?.({ result });
|
||||
process.exit();
|
||||
});
|
||||
```
|
||||
|
||||
## HTTP服务
|
||||
|
||||
在实现了动态执行代码片段的能力后,为了让函数能够对外提供服务,我们还需要添加一个 HTTP API。这个 API 使得用户可以根据不同的请求路径来动态的执行对应的代码,并将其结果返回给客户端。
|
||||
|
||||
这里 HTTP 服务器选用的是 Koa。
|
||||
|
||||
```bash
|
||||
yarn add koa
|
||||
```
|
||||
|
||||
当然还要有其类型
|
||||
|
||||
```bash
|
||||
yarn add @types/koa -D
|
||||
```
|
||||
|
||||
为了响应 HTTP 请求并运行我们的函数,我们需要进一步的将运行子进行的方法封装为一个异步函数,并在接收到子进程的消息后,直接 resolve 给 Koa。
|
||||
|
||||
将前面的子进程的创建、监听以及读取文件都封装进一个函数:
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import child_process from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import Koa from 'koa';
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run();
|
||||
});
|
||||
|
||||
const run = async () => {
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile('./src/func.js', { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Use child.on listen a message
|
||||
child.on('message', resolve);
|
||||
});
|
||||
};
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
现在我们的流程如下:
|
||||
|
||||

|
||||
|
||||
这样还不够,到目前为止,用户还只是请求的根路径,而我们响应的也只是同一个函数。因此我们还需要一个路由机制来支持不同的函数触发。
|
||||
|
||||
使用`ctx.request.path`就能获取到每次 GET 请求后的路径,所以这里也不用大费周章的去划分路由,直接把路径作为函数名,读取文件,执行即可。所以这里的改造就简单多了:
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
const run = async (path: string) => {
|
||||
const child = child_process.fork('./dist/child.js');
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile(`./src/func/${path}.js`, { encoding: 'utf-8' });
|
||||
// Sent to child process
|
||||
child.send({
|
||||
action: 'run',
|
||||
fn,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Use child.on listen a message
|
||||
child.on('message', resolve);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
至此,我们就实现了一个最简单的进程隔离 FaaS 方案,并提供了动态加载函数文件且执行的能力。
|
||||
|
||||
但这还不是全部,还有很多方面的问题值得去优化。
|
||||
|
||||
## 进阶优化
|
||||
|
||||
FaaS 并不只是简单的拥有动态的执行函数的能力就可以了,面对我们的还有大量的待处理问题。
|
||||
|
||||
### 进程管理
|
||||
|
||||
上述的方案看上去已经很理想了,利用子进程和沙箱防止污染主进程。但还有个主要的问题,用户的每一个请求都会创建一个新的子进程,并在执行完后再销毁。对系统来说,创建和销毁进程是一个不小的开销,且请求过多时,过多的进程也可能导致系统崩溃。
|
||||
|
||||
所以最佳的办法是通过进程池来复用进程。如下图,进程池是一种可以复用进程的概念,通过事先初始化并维护一批进程,让这批进程运行相同的代码,等待着执行被分配的任务。执行完成后不会退出,而是继续等待新的任务。在调度时,通常还会通过某种算法来实现多个进程之间任务分配的负载均衡。
|
||||
|
||||

|
||||
|
||||
早在 Node.js v0.8 中就引入了 cluster 模块。cluster 是对`child_process`模块的一层封装。通过它,我们可以创建共享服务器同一端口的子进程。
|
||||
|
||||
这时候我们就需要对`master.ts`进行大改造了。首先需要将`child_process`更换为 cluster 来管理进程,我们根创建CPU 超线程数量一半的子进程。这是为了留下多余的超线程给系统已经 Node 的事件循环来工作。顺便在每个子进程中监听对应的 HTTP 端口来启动 HTTP 服务。
|
||||
|
||||
```ts
|
||||
// master.ts
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
|
||||
const num = os.cpus().length;
|
||||
const CPUs = num > 2 ? num / 2 : num;
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 0; i < CPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
} else {
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
这里看上去有点匪夷所思,我们都知道,在操作系统中,是不允许多个进程监听同一个端口的。我们的多个子进程看上去监听的都是同一个端口!
|
||||
|
||||
实际上,在 Node.js 的 net 模块中,当当前进程是 cluster 的子进程时,存在一个特殊的处理。
|
||||
|
||||
简单来说就是,当调用 listen 方法监听端口后,它会判断是否处于 cluster 的子进程下。如果是子进程,则会向主进程发送消息,告诉主进程需要监听的端口。当主进程收到消息后,会判断指定端口是否已经被监听,如果没有,则通过端口绑定实现监听。随后,再将子进程加入一个 worker 队列,表明该子进程可以处理来自该端口的请求。
|
||||
|
||||
这样一来,实际上监听的端口的依然是主进程,然后将请求分发给 worker 队列中子进程。分发算法采用了 Round Robin 算法,即轮流处理制。我们可以通过环境变量`NODE_CLUSTER_SCHED_POLICY`或通过配置`cluster.schedulingPolicy`来指定其他的负载均衡算法。
|
||||
|
||||
总的来说,虽然我们的代码看上去是由子进程来多次监听端口,但实际上是由我们的主进程来进行监听。然后就指定的任务分发给子进程进行处理。
|
||||
|
||||
回到我们的逻辑上,由于可以直接在当前代码中判断和创建进程,我们也就不再需要`child.ts`了。子进程也可以直接在作用域中执行 run 函数了。
|
||||
|
||||
所以我们将`master.ts`完整的改造一下,最终我们就实现了基于 cluster 的多进程管理方案:
|
||||
|
||||
```ts
|
||||
import cluster from 'cluster';
|
||||
import os from 'os';
|
||||
import fs from 'fs/promises';
|
||||
import Koa from 'koa';
|
||||
import { VM } from 'vm2';
|
||||
|
||||
const num = os.cpus().length;
|
||||
const CPUs = num > 1 ? Math.floor(num / 2) : num;
|
||||
|
||||
const run = async (path: string) => {
|
||||
try {
|
||||
// Read the function from user
|
||||
const fn = await fs.readFile(`./src/func/${path}.js`, {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
// Use arrow function to handle semicolon
|
||||
const fnIIFE = `const func = ${fn}`;
|
||||
return new VM().run(`${fnIIFE} func()`);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return 'Not Found Function';
|
||||
}
|
||||
};
|
||||
|
||||
if (cluster.isMaster) {
|
||||
for (let i = 0; i < CPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
} else {
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx) => {
|
||||
ctx.response.body = await run(ctx.request.path);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
}
|
||||
```
|
||||
|
||||
### 限制函数执行时间
|
||||
|
||||
上述,我们利用多进程方案来提高整体的安全性。但是,目前还没有考虑死循环的情况。当用户编写了一个这样的函数时:
|
||||
|
||||
```js
|
||||
const loop = (event, context) => {
|
||||
while (1) {}
|
||||
return { message: 'this is function2!!!', status: 'ok ' };
|
||||
};
|
||||
```
|
||||
|
||||
我们的进程会一直为其计算下去,无法正常退出,导致资源被占用。所以我们理想的情况下就是在沙箱外限制没个函数的执行时长,当超过限定时间时,之间结束该函数。
|
||||
|
||||
好在,vm 模块赋予了我们这一强大的功能:
|
||||
|
||||
```js
|
||||
vm.runInNewContext({
|
||||
'loop()',
|
||||
{ loop, console },
|
||||
{ timeout: 5000 }
|
||||
})
|
||||
```
|
||||
|
||||
通过 timeout 参数,我们为函数的执行时间限制在 5000ms 内。当死循环的函数执行超 5s 后,随后会得到一个函数执行超时的错误信息。
|
||||
|
||||
由于 vm2 也是基于 vm 进行封装的,因此我们可以在 vm2 中使用和 vm 相同的能力。只需要小小的改动就可以实现限制函数执行时长能力:
|
||||
|
||||
```js
|
||||
return new VM({ timeout: 5000 }).run(`${fnIIFE} func()`);
|
||||
```
|
||||
|
||||
看上去不错!但 Devil 不会就这么轻易放过我们的。JavaScript 本身是单线程的语言,它通过出色的异步循环来解决同步阻塞的问题。异步能解决很多问题,但同时也能带来问题。事件循环机制目前管理着两个任务队列:事件循环队列(或者叫宏任务)与任务队列(常见的微任务)。
|
||||
|
||||
我们可以把每次的事件循环队列内的每次任务执行看作一个 tick,而任务队列就是挂在每个 tick 之后运行的。也就是说微任务只要一直在运行,或者一直在添加,那么就永远进入不到下一次 tick 了。这和同步下死循环问题一样!
|
||||
|
||||
事件循环通常包含:setTimout、setInterval和 I/O 操作等,而任务队列通常为:`process.nextTick`、Promise、MutationObserver 等。
|
||||
|
||||
VM2 也有类似 VM 的 timeout 设置,但是同样的是,它也是基于事件循环队列所设置的超时。根本来说,它无法限制任务队列中的死循环。
|
||||
|
||||
面对这个难题,考虑了很久,也导致这个项目拖了挺长一段时间的。摸索中想到了大概两个方法能够解决这个问题:
|
||||
|
||||
1. 继续使用 cluster 模块,cluster 模块没有直接的 API 钩子给我们方便的在主进程中实现计时的逻辑。我们可以考虑重写任务分发算法,在 Round Robin 算法的的基础上实现计时的逻辑。从而控制子进程,当子进程超时时,直接结束子进程的声明周期。
|
||||
2. 第二个方法是,放弃使用 cluster 模块,由我们亲自来管理进程的分发已经生命周期,从而达到对子进程设置执行超时时间的限制。
|
||||
|
||||
这两个方法都不是什么简单省事的方法,好在我们有优秀的开源社区。正当我被子进程卡主时,得知了一个名为 [Houfeng/safeify: 📦 Safe sandbox that can be used to execute untrusted code. (github.com)](https://github.com/Houfeng/safeify) 的项目。它属于第二种解决办法,对`child_process`的手动管理,从而实现对子进程的完全控制,且设置超时时间。
|
||||
|
||||
虽然上述写的 cluster 模块的代码需要重构,并且我们也不需要 cluster 模块了。利用 safeify 就可以进行对子进程的管理了。
|
||||
|
||||
所以这里对 Koa 的主进程写法就是最常见的方式,将控制和执行函数的逻辑抽离为一个 middleware,交由路由进行匹配:
|
||||
|
||||
```ts
|
||||
import Koa from 'koa';
|
||||
import runFaaS from './middleware/faas';
|
||||
import logger from 'koa-logger';
|
||||
import OPTION from './option';
|
||||
import router from './routers';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import cors from './middleware/CORS';
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(logger());
|
||||
app.use(bodyParser());
|
||||
app.use(cors);
|
||||
// 先注册路由
|
||||
app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
// 路由未匹配到的则运行函数
|
||||
app.use(runFaaS);
|
||||
|
||||
console.log(`⚡[Server]: running at http://${OPTION.host}:${OPTION.port} !`);
|
||||
|
||||
export default app.listen(OPTION.port);
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
我的简易 FaaS 基本上到这里就告一段落了,对 Devil 的最后针扎就是限制函数的异步执行时间。实际上还有一些可以优化的点。例如对函数执行资源的限制,即便我们对函数的执行时间有了限制,但在函数死循环的几秒钟,它还是占有了我们 100% 的 CPU。如果多个进程的函数都会占满 CPU 的执行,那么到最后服务器的资源可能会被消耗殆尽。
|
||||
|
||||
针对这个情况也有解决办法:在 Linux 系统上可以使用 CGroup 来对 CPU 和系统其他资源进程限制。其实 safeify 中也有了对 CGroup 的实现,但我最终没有采用作用这个方案,因为在 Docker 环境中,资源本身已经有了一定的限制,而且 Container 中大部分系统文件都是 readonly 的,CGroup 也不好设置。
|
||||
|
||||
还有一个优化的地方就是可以给函数上下文提供一些内置的可以函数,模仿添加 BaaS 的实现,添加一个常用的服务。不过最终这个小功能也没有实现,因为(懒)这本来就是一个对 FaaS 的简单模拟,越是复杂安全性的问题也会随着增加。
|
||||
|
||||
## 推荐
|
||||
|
||||
无利益相关推荐:
|
||||
|
||||
目前市面上大部分对于 Serverless 的书籍都是研究其架构的,对于面向前端的 Serverless 书籍不是很常见。而《前端 Serverless:面向全栈的无服务器架构实战》就是这样一本针对我们前端工程师的书籍,从 Serverless 的介绍,到最后的上云实践,循序渐进。
|
||||
|
||||
本篇也大量参考其中。
|
||||
|
||||

|
||||
|
||||
## 把玩
|
||||
|
||||
[FaaS](https://demo.defectink.com/faas/#/)
|
@ -1,7 +1,9 @@
|
||||
---
|
||||
title: about
|
||||
title: 关于
|
||||
date: 2020-02-29 13:07:34
|
||||
layout: about
|
||||
layout: docs
|
||||
seo_title: 关于
|
||||
bottom_meta: false
|
||||
---
|
||||
|
||||

|
||||
|
5
source/categories/index.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: category
|
||||
index: true
|
||||
title: 所有分类
|
||||
---
|
@ -1,178 +1,27 @@
|
||||
img {
|
||||
border-radius: 0.5rem;
|
||||
/* transition: all 0.5s;
|
||||
-webkit-transition: all 0.5;
|
||||
-ms-transition: all 0.5; */
|
||||
.l_header #wrapper .nav-main .title {
|
||||
display: flex;
|
||||
}
|
||||
.l_header #wrapper .nav-main .title img {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
/* img:hover {
|
||||
transition: all 0.5s;
|
||||
-webkit-transition: all 0.5s;
|
||||
-ms-transition: all 0.5s;
|
||||
transform: scale(1.05);
|
||||
} */
|
||||
|
||||
.index-img img {
|
||||
border-radius: 0.66rem !important;
|
||||
@media screen and (max-width: 500px) {
|
||||
.l_header .container,
|
||||
.l_header #wrapper .nav-main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/*ul and ol*/
|
||||
/* .markdown-body a {
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
color: #00f4e8;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.markdown-body li:hover {
|
||||
text-shadow: 3px 3px 2px rgba(47, 47, 47, 0.341);
|
||||
}
|
||||
.markdown-body ol {
|
||||
counter-reset: xxx 0 !important;
|
||||
}
|
||||
.markdown-body ol li:before {
|
||||
content: counter(xxx, decimal) '.' !important;
|
||||
counter-increment: xxx 1 !important;
|
||||
position: absolute;
|
||||
font-family: 'Comic Sans MS', 'Open Sans', 'Microsoft Yahei',
|
||||
'Microsoft Yahei', -apple-system, sans-serif !important;
|
||||
color: #000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
opacity: 0.5;
|
||||
line-height: 1.33;
|
||||
text-shadow: 4px 4px 1px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
}
|
||||
.markdown-body ol li:hover:before {
|
||||
-webkit-transform: scale(2);
|
||||
-ms-transform: scale(2);
|
||||
transform: scale(2);
|
||||
opacity: 1;
|
||||
text-shadow: 2px 2px 1px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.1s;
|
||||
transition: 0.1s;
|
||||
}
|
||||
.markdown-body ol li {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding: 0 0 0 2.1em;
|
||||
margin: 0 0 0 10px;
|
||||
text-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.12s;
|
||||
transition: 0.12s;
|
||||
}
|
||||
.markdown-body ul li:before {
|
||||
position: absolute;
|
||||
content: '\2022';
|
||||
font-family: Arial;
|
||||
color: #000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
opacity: 0.5;
|
||||
line-height: 1;
|
||||
text-shadow: 4px 4px 1px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
}
|
||||
.markdown-body ul li:hover:before {
|
||||
-webkit-transform: scale(2);
|
||||
-ms-transform: scale(2);
|
||||
transform: scale(2);
|
||||
opacity: 1;
|
||||
text-shadow: 2px 2px 1px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.1s;
|
||||
transition: 0.1s;
|
||||
}
|
||||
.markdown-body ul li {
|
||||
list-style: none;
|
||||
position: relative;
|
||||
padding: 0 0 0 1.5em;
|
||||
margin: 0 0 0 10px;
|
||||
text-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
|
||||
-webkit-transition: 0.12s;
|
||||
transition: 0.12s;
|
||||
} */
|
||||
|
||||
.note.note-warning {
|
||||
background-color: rgba(163, 243, 241, 0.631);
|
||||
border-color: #ff81c0 !important;
|
||||
/* 代码块适配暗色模式 */
|
||||
:root {
|
||||
--code-color: #fafafa;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: #a2f1f1;
|
||||
/* text-shadow: 0px 0px 25px; */
|
||||
.hljs {
|
||||
background: var(--code-color) !important;
|
||||
transition: all 0.5s ease !important;
|
||||
}
|
||||
|
||||
/* 调整导航栏字体大小 */
|
||||
.navbar .nav-item .nav-link {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.modal-dialog .modal-content {
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
/* 图片圆角 */
|
||||
.markdown-body p > img,
|
||||
.markdown-body p > a > img {
|
||||
border-radius: 0.425rem;
|
||||
}
|
||||
|
||||
/* 文章内标题背景 */
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
transparent 60%,
|
||||
rgba(189, 202, 219, 0.3) 0
|
||||
)
|
||||
no-repeat;
|
||||
width: auto;
|
||||
display: table;
|
||||
background-size: 90%;
|
||||
}
|
||||
|
||||
.markdown-body h1,
|
||||
.markdown-body h2 {
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 0px solid #eaecef !important;
|
||||
}
|
||||
|
||||
/* 浪漫雅园!! */
|
||||
/* @font-face {
|
||||
font-family: "LMYY";
|
||||
src: url("https://cdn.defectink.com/static/fonts/yayuan.woff")
|
||||
}
|
||||
* {
|
||||
font-family: "LMYY" !important;
|
||||
} */
|
||||
/* 代码块里的字太小 */
|
||||
/* .markdown-body pre code {
|
||||
font-size: 100% !important;
|
||||
} */
|
||||
|
||||
/* 滚动条,webkit赛高! */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background-color: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: darkgray;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.v[data-class='v'] .vwrap .vedit {
|
||||
position: relative;
|
||||
padding-top: 10px;
|
||||
background: no-repeat right bottom
|
||||
url(https://cdn.defectink.com/static/images/iloli.gif);
|
||||
background-size: 15%;
|
||||
[data-user-color-scheme='dark'] {
|
||||
--code-color: #c7c7c7;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 84 KiB |
4
source/friends/index.md
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
layout: friends
|
||||
title: 小伙伴们
|
||||
---
|
BIN
source/images/JavaScript零碎笔记/image-20200331133634029.png
Normal file
After Width: | Height: | Size: 257 KiB |
BIN
source/images/JavaScript零碎笔记/image-20200331134656244.png
Normal file
After Width: | Height: | Size: 376 KiB |
BIN
source/images/JavaScript零碎笔记/image-20200331135348855.png
Normal file
After Width: | Height: | Size: 364 KiB |
BIN
source/images/JavaScript零碎笔记/image-20200331195638452.png
Normal file
After Width: | Height: | Size: 491 KiB |
BIN
source/images/JavaScript零碎笔记/image-20200401112039274.png
Normal file
After Width: | Height: | Size: 569 KiB |
BIN
source/images/JavaScript零碎笔记/image-20200427212659150.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
source/images/JavaScript零碎笔记/image-20201201114428274.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
source/images/JavaScript零碎笔记/image-20201201114430208.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
source/images/JavaScript零碎笔记/image-20201201115114508.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 128 KiB |
BIN
source/images/img/mona.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
source/images/useMemo and useCallback/hooks.webp
Normal file
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 4.9 KiB |
1
source/images/为React造一个迷你路由器/Web架构.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-08-23T03:10:20.595Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="3TGQTE9oRjaT-yreGNVk" version="14.6.13" type="device"><diagram id="dEYAjTxxdtpzwiRtR6KX" name="第 1 页">7Vxbc5s4FP41egyDLiB4BMfeTqbd7Yx3ptt96VCb2Gwdk8WkcfbX7xFIGITs2A7YaZxMHkAS0uGcT+cqjOjgbv1bFt3PP6XTeIGIPV0jeo0IwYwQJP7t6VPZ4mGnbJhlyVQO2jSMk/9i2WjL1odkGq8aA/M0XeTJfbNxki6X8SRvtEVZlj42h92mi+aq99EsbjWMJ9Gi3folmeZz2Ypdf9PxIU5mc7m0R3jZcRepwfJNVvNomj7WmugQ0UGWpnl5dbcexAvBPMWX8rnRlt6KsCxe5vs84F9//fY0+8a+/Pi4unlKsq/LH39fyVl+RosH+cJo6KBgiAK4cJHnoZCjIUdBiIIRGo5QOBCN0BU6yIMWDwW8aHGQ76PAU2OAPe4C6Aq/Z3A1E1dXsNCHPz99VEMGg/G4ur4Zmx8QS8HKYimGQlgtKNYcFXQBFRh5QKmPPIrCa0GFJNlFPvTaoivAgli48K/Fa5XCyJ+UhLP0YTmNBZMwrPk4T/J4fB9NRO8jYBra5vndQnavfsT5ZC5vJO/iLI/XW4WCK1HDHonTuzjPnmCIesCW6JDbA1N5/1gDmyvb5jWc+bItkvieVVNvIAAXEgUHIIIYEAHMBBmV7A0EPytpv4iZs0W0EuTbOmOrrWJ3w2XKm1wmntvmMjFw2e2Ly9TMZaeArCt4DXsQuBwGyKevC7IueR6yvoGX1OmJl8zISwCnBOoQeeHrZmFl7c7FQsfIQm8olLvUqW5Hm753XvrkvLx0DbwsrWTJwhCFw9fGQtxgIeX+eVnotVmoMyxeTgPh3sHdRJiRZNLkUbxO8r+kBRHXX8W15ci763Wt6/pJ3SyB+L/qN7WnxO3mseJOPWeQRzxtuZXPSqPGbcfAbdWWxYsoT342pzeJQK7wOU1g4R26x7G4JshV+pBNYvlg3aV8Zi5M23PlUTaL89ZcILzoqTbsXgxYbSfb8XWyXYs4O6nTH8FUfwQuSjo2UK3kcTx6fYMCAN81FPp0h5GX/u210BYwMiw170j4se2n9O0Amz/XlESepT/iQbpIM2hZpksYGd4mi4XWFC2S2VLsIoBnDO2hUCUJBECB7LhLplOxjFErbfTWlo3wQsXEmxLkuKWYPMNWIX25bdjgt71rpp40E7Wx1ZFiAmr0qXrSS9QmByklQhrj+9FI2OAiv8O2J9gyiYAXg5YSbaKeIAsEW4dZUqDMOoEhxYaw5FDYNlIJvxSuOG45V8fhitt+U+XgflQhZ2aCt9LFWp7oCTBlCM8uCVNEsz6ap7Q3prAWMtraRJ1hykzwdkwRI139YopfNqY0KOhT7I8p3S5pE3WGKTPB2zGFjXT1iylTFFlk4so0kggMsQgVIU4UlRmOfCaqITJbV9ZNIFSkRfUGF5EmjAmQx/bIjl5GOEmcM4eTauKGkAtJikJaOw1QybYmyXY6Acb4YZFXCEVqQRbdZCmtlLsramHhSJb4YGpi34z/+P1CcUCVI3I2HBjKsC8yIFWwZvk+bwRslKpgbkvIBjef4yyBFxNi+/XMEfN8y2WaE8AtVfF7cVBme3sZpa7sgEJCd9BQMTk+LCZXkLqyLRsCijqmXM5Ogyl460JsO/glQ5tSKrv4ek6MOq2UwZFuuKMVtqmznxveGTpNdez38yPnOz/CVKrvfOdHug6GlOLBDZ1D9s08Wq7L6pqu0F/PKjtxdyqVpY61vWqVpR+hwTqA9lVZzNGyPl4/mQN9HexQDds9RG2kg8p1p9bathiuO4AF+umrsdZkT+if1aOkWl6C8COhT11tIj2g6Ar6elmenyCxSgwJi26gD8p+o7AFiLFlY+8YDV6ZEmUCKnOCBTPfclhEXNvifu3PafoNBFt+vf9IhPvMcvzWNFWczSx7+yIdoZ+2Xo30j35qyOR0FMEzQmtQBeg6bxyqDFOLuxpyOGxRu/o7MsNMNAXMGOtHAWNmXKdfCHaeRNooYF6qx7oC5id0oV+3a0xcrT7A6XHwZI6Wl3RIP/DU1lEE70sXoycogNDeEl8iKeo23WnPwy/xJ2ACn51HSXfoZJfJnNfiZbvH5sR0L9vZMyd28C7i2q7wTxBgUnPCTVYDfQTBtIdNGS4uqkHgcR3wLc/bLP7Qfb64sg047636Q80fsGwTk7igskucHGYy4+oNVDmQVQXCMl36bArzbUq6yiwpSXsGSdOTStr0nU1793IUDMR/mbGGxl2p61q9/7I3NtESflVRpS5udlJxmz4Fau/eQrignYVwXQmAYvfeRD+j8SRL7vNLEaGWZGZqivPtWEM1Q4jQLj7mKIyq+GKjpWGVCIN/orVh116IOPVw2zEUp067I82nqrQdKQ/DCEU6RP6osK9cHJMyHLeR5cedVc8SJryGF3VaC1aq7fFiXqELSvs/Mn36+0aRom18x6C7yUmdMgVLw1dcQUMDyINYRsM8Kmw26AQiS9xijH+xn3NhLcRzTK63KcTsT8qH5cwkj7ce2y1kpH4mhGgCQoROo9i7nbSkCT3uxIu/3/5qmdrWBysdheytiXo+xqIOjTdgUJ4muU0LQjd4cP99SFXH1ar4vRpxxhLT+/WmU51AKbY6L3S/K47ahq68AJUhFwCCyzXkE5ehCmhT4IS3PQN+UkVg+rCzMwS4KkB7R8A2BFDGz4wAUxam7envMPjVITKQb9G7K4njiePXZe7GDwUeNjO3szkXmsDBdrOM5hucQuZ0ghG43fwSV2lWNr9nRof/Aw==</diagram></mxfile>
|
3
source/images/为React造一个迷你路由器/Web架构.svg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
source/images/为React造一个迷你路由器/image-20210823154009498.webp
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
source/images/为React造一个迷你路由器/router.png
Normal file
After Width: | Height: | Size: 504 KiB |
BIN
source/images/为React造一个迷你路由器/router.psd
Normal file
BIN
source/images/为React造一个迷你路由器/router.webp
Normal file
After Width: | Height: | Size: 10 KiB |
1
source/images/为React造一个迷你路由器/迷你路由器.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-08-23T08:52:55.443Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.9.6 Chrome/89.0.4389.128 Electron/12.0.16 Safari/537.36" etag="msaHTrqSDVVvf8JIMCGx" version="14.9.6" type="device"><diagram id="XTkkDmfpvXEnlI4-h4sE" name="第 1 页">7VtJk+I2FP41qkoOUJbl9WizZA49qclMpZIcjS3A0wYRIwaYX58nL3gTYNqY6e7MpbGfZEl+79P3FrkRGa0Ov8XeZvmRBTRCqhIcEBkjVTU1G/4KwTEVGIaRChZxGKQiXAi+hN9pJlQy6S4M6LbSkTMW8XBTFfpsvaY+r8i8OGb7arc5i6qzbrwFbQi++F7UlP4VBnyZSbFhFw0faLhYZlNbqpk2rLy8c/Ym26UXsH1JRCaIjGLGeHq1OoxoJHSX6yV9bnqm9bSwmK55mwf44PPXA9U/brbj2e/sQP5U/xgP9HSUb160y144Wyw/5hpYxGy3QcSFn3VAxWAY7rbPlPvL7CYbhMacHmRW8Wb5YEpz1fikC8AQZSvK4yN0yQGUD5Xh5zTEvrCGZmSyZckQWo4gL0PA4jR2oSS4yPR0g87IdZ1VdbVfhpx+2Xi+aN3DNgHZkq+im1RZVtl5U57V4wDjH603TaI3I4Jp3TmDNysr0Ph3x/KGwTYhBQc6YGtzKBrhaiF+P7MdpzE0o4mJXBdZGppo4sI18glgvekc6RMNe4Gyec0oPGbPdMQiFoNkzdZULCeMoprIi8LFGm59MA4sgrjCdCHwh5M1rMIgENNIUVDgRHkUELBBKhtKa+4nTCS4UPuCBZZx0I240GS4QBMDOQpybXFhjZEzQRMLuSZypgIpNmAEiyYXhKq4sKfIJkmTk4BIR84Y2R1g9XIaKCMNqWQ+n6u+34AltATGzNCNHvFiVPkXK028EGxI8GL2BhizO2B0KZFQz+dDP6Yep5OIrkBtv9D099efTNJkkpwSctdsNKFxoo2HUAmRuebUbruoMFkqicKCKExkOWJfC6JQkIVL1i51kzxoIQv4xEm4YiJ4Q1CEjSw7uQAacWWk0RwThOUV1kAFrw7x7jnTl7B2IUarMUrgUWsuZRTDt+hsXgJkROe8RxgRXGeYpkOSocjqDUXG9QCPBpAlZLc0mrH9pBC4iQAa8g0sROvAEdmI2N+Rt92G/tUYUEzxAj3DMtku9mmLbcK9eEEvDYjPWK5kGl1imVwW08jj4bfqW8jMlc3wiYUJfefAsOwqMOoeJX3R7Kly5lMbSCO1geoElCqiMRBYzDuWum1Eh+35BWtKbR6tkpHBRTpiAc2TTjug9Q7eUJWHT7YgNwicRGgEYZIiCZZaekWB5SdvRqMq6tu7vJjCcrM0Uji4zBQwuO4ifdw1B73AA+czKmVITFWrusBuwM+7sPl8S3kNO3dBC5YlYdKEv7+Unii1uIFIQkoZ45/ykrtTvozx7xFRfoD19Z8svNwzp3A4H+FZEl+sXWD8+8f6LYotZV/MYr5kC7b2orJDvhAcFw88MbbJhF8p58esFuntOKvq/qXuOd97V/2zKTdKR3daD9fr3vROThDr1Wmwol90zvX+5gN8Zl8JpDNjO/4e9vuxGmL/uO2vdLeUcb5m+BSun38WDS8iIz+2aZmU9VclVF+fIwCNxse/yzf/iJGHen47PmQzpXfH7K53B5KFM9fzO0Vu9Ns8TVcPob4+B5G/c+cSwJLF4XcgkVZFgBOelKGi1DBlqPYVVCV3n2gcwssLUjnHDhdxh28JUVogjMgB9pgCgk6qyNHrR1ttCwh6zS+eDs7uHDvpNahrinJ5XVqtv3alv13rbz9iL50vynYtUJjIUpPzHV1UKsB5izLsFLmjpDBrJYVZA9nmDU79TVYqriTcA6AP0zYrpsfddlj/lQpVVqm4W11LSWr1pqhiOXqCnymyreRiJIpd/2O0KEONEPWNgaV5hrzkXHwi5Ii51GlAV2wY77zhJtoBAU+9NFN7bEp2We+6pVeVTixUj7k165Ext3rjQcgrKb60jWHaRsnqmVOQ1tuhmxH6q+9DvjtNvoowko8hEj9qj5JCP1yMBUumn1AAOb5rQrxa6MeEVL+aGaivnhLt2zbvli7Ehw6VJOaWQ8tS8mLZajl5GYh05lpOLM1e2m/2152IYMWocXu9YtY2E8EY10ZS2qUidztulGXG94rmbRfZOCEfBTkjSVg2SYJ7uJZ82tXkrhbnl92/8XqbjGdfCQFV26p+ldGV8KoHpP3TXx48lXF6yb29k9rujSEmboaYsm+CXxBhwm3xaXtq1OL/A8jkPw==</diagram></mxfile>
|
3
source/images/为React造一个迷你路由器/迷你路由器.svg
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
source/images/体验至上的暗色模式/image-20210615152851647.webp
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
source/images/体验至上的暗色模式/image-20210615153023484.webp
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
source/images/全栈之路-首个GraphQL API/image-20210721230252762.png
Normal file
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 20 KiB |
BIN
source/images/自建简易FaaS/book.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
1
source/images/自建简易FaaS/主进程与子进程间的通信.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-06-29T02:27:11.849Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="adkdUVyCXITzHjuNtrhD" version="14.6.13" type="device"><diagram id="OXZfFdEjUsCswzGT0AAY" name="Page-1">5Zddb5swFIZ/jS9XEUwIuQwJ3ap9KFqUbr2aXDgFKwYz4zRpf/3sYBMIVOmmdps2RUL2a3NsP++xTRCe5/u3gpTZR54AQ66T7BFeINedeFP11MJDLfi+XwupoEktjY7Cij6CER2jbmkCVaej5JxJWnbFmBcFxLKjESH4rtvtjrPuqCVJoSesYsL66heayKxWA3dy1N8BTTM78sg3C74l8SYVfFuY8QpeQN2SExvGrLHKSMJ3rfFwhPBccC7rUr6fA9NULbH6vcsnWpspCyjkc17wN1ef8CiCdbKePK5E9n59c/PGRLknbGtQoMhDYYDCEEXqeYmmqjBBswAFoVmHfLDYdhmVsCpJrOs7lRkIh5nMmaqNVJFUZW3WHd2DmkXYn7IdH4SE/SkblW3Ac5DiQXUxrU3KmEzDFu+u5ZuRspZlViMmU9Im8hGZKhhqP0HQ7RM8xQRFMtNJqmoxI1VFY8WikkTIvtzC9yQtSDrZ3GfVYjEeYGE1AYxIet/dA0OAzAhLTtVMGisa9NaK8fRi3A1S8a2IwbzXzs2TUJ5zNpTClYLshTp41iz9123E5238A9nu4b8s24PzmPTqqDpaP5BbYEteUUl5oZpuuZQ87zKzfWeMprqP5BoqMbVYgQNx2CuCb2DOGVe1xeGMVWJGSj1kvk/1nXRBHrcCLmJ1NX27o3oyYamTBUR0r+JUZkTVxGwg5GJvoX8vZNbY7Zrl9c3CQd8s77XMmg4c7mM0W6Cp84zDXa1adt0atKEN1Eg9/05tzmmSHBwa2lKH21RvoIXzUr6c30TewCZyX8sXe4WdGBNMULhAkY+mYxQ6/5Efwe/zIxfX66++s9yNPruba59OiuX3gY+gq+X83+XfnEuWP3Zfi7+qHj9w66v6+P8BRz8A</diagram></mxfile>
|
3
source/images/自建简易FaaS/主进程与子进程间的通信.svg
Normal file
After Width: | Height: | Size: 7.9 KiB |
1
source/images/自建简易FaaS/在隔离的沙箱中执行函数.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-06-29T03:35:10.250Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="z65VeRThHICAR922zABT" version="14.6.13" type="device"><diagram id="OXZfFdEjUsCswzGT0AAY" name="Page-1">3Vhbb9sgGP01PC6KjW95jJ1krXbLFqVdnyZqU5vFNi4mt/76gY0T36K0VbtuVaUIDvgDzvkuUAC9ZPeRoSz6QgMcA30Y7ACcAF23jZH4lcC+BCzLKoGQkaCEtCOwIA9YgUOFrkmA88ZETmnMSdYEfZqm2OcNDDFGt81pdzRurpqhEHeAhY/iLnpNAh6VqKPbR/wCkzCqVtYsdeBb5K9CRtepWi+lKS5HElSZUWfMIxTQbW09OAXQY5TyspXsPBxLVivGyu9mJ0YPW2Y45Y/5wFpdfoXaFC+Dpf2wYNGn5c3NB2Vlg+K1ogJMDeA6wHXBVPzOwEg0bDB2gOOqc/B9Rds2IhwvMuTL/lZ4BoBuxJNY9DTRRHlWinVHdljswu1uuVofM453bW6Et2GaYM72YooaPbiM8jRY0but6aagqCZZhSHlKeHB8pEy0VCsPYFBvctgmyacBmPppKLnxyjPiS+4yDlivAvX6DvJFg4a3tzlqsaF2cNFhTEcI042zRjoI0itMKdE7OQgxYH6SgpzNDCbRnK6Zj5W39V9s2XKGJ41JegKMe+YKjQ7HP35MsLzMr6BtxvwH/N25zxN8nREpNbP6BbHc5oTTmgqhm4p5zRpclbNHccklHM4laQi1fMFcZgVscLoCns0pqI3KXKsACOUySWTXShr0gA9rBke+KI0/bojcjNuJp0Fs+lG2MnVimIorgwBHRoT+fdCqQlqDbEMpysWdLpiGa8l1qgnuZtgPAGj4SOSuzg1b6rVK0OdUAV19GvLnJAgKBTqC6mimsoAmgxfKIjMVhBBvaOL0RNE+mvpUpWwljCODdwJmFpgZAJ3+H710FtJzdTNv6ZHwq6WP63hfKv90FdXFrHT+X3PJehy7r1f/qHxdvHQy/9zr1D/0V1JN0aD6k1UlQetxedjL0t9tuyWrRO3JcEh2temFRUyf9q2YeOZIRql0efexno9ouc2JgrWeFY8S0SmFJXLKjKlJbOmfLG4wLXeb8jqrRJ2uBfWvNd+pZCdee765vs3tLm+13+T6/h7dgF7UqZH04LutgZNLs7coU/w0sPe6Wrfzm5WlyqrhyrNeTJVont8vpeuf/zvCJz+AQ==</diagram></mxfile>
|
3
source/images/自建简易FaaS/在隔离的沙箱中执行函数.svg
Normal file
After Width: | Height: | Size: 10 KiB |
1
source/images/自建简易FaaS/用户通过HTTP触发函数的流程.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-06-29T07:28:51.615Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="bG6XJZwkzevC7-8nKnGR" version="14.6.13" type="device"><diagram id="OXZfFdEjUsCswzGT0AAY" name="Page-1">3Vhbc+MmGP01PDZjCd38aPmyyTTtepvNrS8dIhGLRhIOwrGdX1+QwJYQmXizzu424xmP+IQ/4JzzXTCA42LziaFl9gdNcQ7cQboBcAJcN/SG4lsato0hCILGsGAkbUzO3nBBnrEyDpR1RVJcdSZySnNOll1jQssSJ7xjQ4zRdXfaPc27qy7RAvcMFwnK+9ZrkvKssUZuuLefYrLI9MpOoA58h5KHBaOrUq1X0hI3bwqk3agzVhlK6bq1HpwCOGaU8uap2IxxLlHViDW/m73wdrdlhkt+yA+Ch7M/oTPFl+ll+HzBst8vb29/U16eUL5SUICpB+IIxDGYiu8ZGIqHEIwiEMXqHHyrYVtnhOOLJUrkeC2UAWCc8SIXI0c8omrZkHVPNljsIu5vWa+PGccbExuhNkwLzNlWTFFvd5JRSoMa3nWLN2XKWpRpG1JKWew87yETDwq1b0DQ7SNowoTLdCRFKkZJjqqKJAKLiiPG++YWfC+ihdOOmvtYtbDwLVhoG8M54uSpGwM2gNQKc0rETnZU7KDXVPjDE7/rpKIrlmD1u7Y2DVfe4FVXAq4F5j1XNWe7o7+dRvg6jT9B7R78xdQevQ6TPB0RqfUc3eF8TivCCS3FqzvKOS26mOm5o5ws5BxOJahIjRIBHGZ1rDD6gMc0p2I0qXOsMGZoKZcsNgtZk07Q84rhk0SUpn/uidxMvJRiwWz6JPxUakXxKteOgAu9ifwcKTVBp0OWF/XJglGfLO+9yBpakrsPRhMwHByQ3MWpeZctKw1tQJWpx59Jc0HStGbIFlJ1NZUBNBkcKYh8I4ig2+PFswSR+1686BJmEBOFIJ6AaQCGPogHH5cP10hqvuv/MD4KdnV5Ewzma+cv9+EqIGE5f7Q0QWfz8cfFH3o/Lx6s+L+1hfof9UquNzzRdyJdHhwDz0ObJZuv0PD1QrckMETb1rS6Qlbftm3YuWaIh8bpW7sxqyIs3ZgoWKNZfS0RmVJUrqDOlIHMmvLGEoM4+Lgh6xolbNcXttQbvlPIzsbx6vbLZ/R0/ej+S67zL8tTaEmZY1rWcJscdLF4pYc+RrU3s1vQhyqwQOVE3w8VQue3j8H1zdVn9ml08/fM2Z6fWa/YIRh6su0SEo5EIxZqdYfSEjsg6mfAX0nLPTYsnL2sZV3ut1qjQY8gWyY+hpatBB1w9fuuG7xRgg7C6gdVJSOtiGB5e0ny3W7cDQxfRyxJxlJuFBy1JFlV4vVUcvr16/wDh6mpDf/dwlQM93+DNnzt/2WG0/8A</diagram></mxfile>
|
3
source/images/自建简易FaaS/用户通过HTTP触发函数的流程.svg
Normal file
After Width: | Height: | Size: 13 KiB |
1
source/images/自建简易FaaS/通过子进程动态运行函数代码片段.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-06-29T02:26:14.538Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="Pt02c8ytWje2NVdAXfDv" version="14.6.13" type="device"><diagram id="OXZfFdEjUsCswzGT0AAY" name="Page-1">3Vhbb9owFP41lraHIcidRwJ0m3YRWkW3Pk1uYhKLJGaOKdBfv+PEDrmh0qo3VUjI/uwc2993zvFJkDlN95853sQ/WEgSZAzDPTJnyDBcawz/EjiUgOM4JRBxGpbQ6Ahc0juiwKFCtzQkeWOiYCwRdNMEA5ZlJBANDHPOds1pK5Y0V93giHSAywAnXfQ3DUVcop7hHvEvhEaxXnnkqAPf4GAdcbbN1HoZy0g5kmJtRp0xj3HIdrX1zDkyp5wxUbbS/ZQkklXNWPncxYnRasucZOKcB5z115/maE6W4dK9u+Txt+X19Sdl5RYnW0UFmlvI95Dvozn8X6AxNFw08ZDnq3OIg6ZtF1NBLjc4kP0deAYy/VikCfRG0MT5phRrRfcEduF3t6zXJ1yQfZsb8DbCUiL4Aaao0cpllKeZmt5dTTcFxTXJNIaVp0SV5SNl0FCsPYBBo8tgmyaShRPppNALEpznNAAucoG56MI1+k6yRcKGN3e5qnFh93ChMU4SLOhtMwb6CFIrLBiFnVRSVNRrKezxwG4aydmWB0Q9V/fNlilreK8poCsiomOq0Kw6+uNlNO+X8RW83TLfmLd799MkT0chtX7HNyRZsJwKyjIYumFCsLTJmZ47SWgk5wgmScWqFwBxhBexwtmaTFnCoDcrciyAMd7IJdN9JO+kAb7bcjII4Gr6u6JyM/5GOgvh81uwk6sVYSjRhpBhWjP5e6LUZI4aYlleVyzT64plPZdY457kbqPJDI2HZyR3OLVoqtUrQ51QBXX0a8uc0jAsFOoLqeI2lQE0Gz5RENmtILK6ulg9QWQ8ly76CmsJ47nIn6G5g8Y28ofvVw+jldRsw34xPVJ+tfzjDBe70S9jfeVQN1v86ymCvi6mb5r/Dtk9kpzkv/J/HQ+m8br8P7aE6tRKrcLoLFJeplYyrPFAvxPp62HU4vPcYqnPltuydaJaAg7xoTatuCHzh23bbLxmQKM0+thqrNcjeqoxuLAmF8VrCWRKuLmcIlM6MmvKNxYf+c77DVmjdYVVdWHNe92XDFmrIxCBNiAfVtsskBXfx/erhqUvLJ1Ana4a46dRA7rHLwRldB0/wJjz/w==</diagram></mxfile>
|
3
source/images/自建简易FaaS/通过子进程动态运行函数代码片段.svg
Normal file
After Width: | Height: | Size: 10 KiB |
1
source/images/自建简易FaaS/通过进程池管理子进程.drawio
Normal file
@ -0,0 +1 @@
|
||||
<mxfile host="Electron" modified="2021-06-29T07:47:39.383Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.6.13 Chrome/89.0.4389.128 Electron/12.0.7 Safari/537.36" etag="wq473Edj50IbV8qj7lGz" version="14.6.13" type="device"><diagram id="OXZfFdEjUsCswzGT0AAY" name="Page-1">5Vptc6I6FP41+bgOEAjwUaxuO9t7172urb1f7qSQClckNsSq/fUb3lRIduy2uHbtOOPACSThec5zzkkAwN58/ZnhRfgXDUgMDC1YA3gBDMPVLPGfGTaFwakMUxYFhUnfGUbRMymNWmldRgFJaxdySmMeLepGnyYJ8XnNhhmjq/plDzSuj7rAUyIZRj6OZettFPCwfArD3tkvSTQNq5F15BYt99ifTRldJuV4CU1I0TLHVTflM6YhDuhqbzzYB7DHKOXF0XzdI3GGaoVYcd/gJ63bKTOS8JfcEOrQuR5dIjSeLK/IaDAhHvtUEfCE42WJRTlbvqnAyR+PZL1oAHqrMOJktMB+1roS3iBsIZ/H4kwXh/Ksyok+EcbJuvn4wqEInRPONuKSstWE5aRKZ4KVM6121NioMIV7rFjlbbh0hum25x0q4qAERg0Sml39DfU+GQdj+3nEwi/ju7tPuoxR3wSeAzwP9MX/ALjiwAZdBzieBN8BwHC6KDz6IVpnILeB4FZXFYKVD+4h6Ggygs6xEDQOexlJgm6mZHHmxzhNI19gkXLMuGx+ib+RoCZ5Gas9LCwFFpWNkRjz6KkeKFQAlSMMaSRmsqUCNqlAWscy6r2kdMl8Ut64r+BmX+7hvgRgU8KlvnLWtg//eiKdw0RmbhqJ0HqN70k8pGnEI5qIpnvKOZ3X2auu7cbRNLuG00wduDzzBaeE5W7A6Iz0aEzF2UUeY4UxxItsyPl6muWkDn5eMtLxRWr67yHKJuMtMhQI6z+JftJyRNEUVx0BA5oX2a8l1UG9Ro/pyKqDjuxp5tHiliK49y3g2MC7AH0EXAt4msSfeFpeZ0kJ/z6QpUnirUnvPAqCnBlVTKynmVb4qMvFMuQ8YiqUb7TAx5zdjCdIG670f4zZDYrsZPioyCNXw9754g+beRzCjmacloLXJqI/KOMYpttBqB6J9AaeL804qr7sRl8/yTgCQ7zZuywPxumvTRvWKlpxUHT62oym9AioiJEO6A7y4k4ES1HcoTxYoixwZnWfBzx0vqrdumGDhFr1fSTJDnre8u7bV/x0+2j8H93G3xaXUBE1ezTJ4X7TQqVRCPTc7NfWAsapBz5F4kEKCHXn7RBifH33iG4nN1/Z5+7k34G+ub5SLmBs4JrZikW4tiPWMHbl9XZm8XTgyJHxfHy8ImRT+S6SCFJF6DZ8XEmQIgh9kPVRM9yIRc3rU1W1GNpuHDT6ajFVNYYyHNRqqlJ6iSl5yeX378MzlmnTN6zfJ1PlgsqVCFhRNhNInS0FptlIZYq9uGMV8OoNS0Uue1Md0AJIlqEdBOlYG5ZqkORlznuvmSzjdDWTGkM5J5+72K1m3XpyscsZ7+Ril95OoFOL3frjxC4tkJxTix19OLFLmV3hx79X7Pb7E7uU2U8udvkFzHsXu5TZTy72j1fGS5n91GI32i7jFRwEOA3z29vK/FU5sl3zyzujuquIBjo6Goxyoa/4IqHY0Ou+11d9rfPkNvdmZJ5acnZxuvuAp9jm2X0fBfs/AA==</diagram></mxfile>
|
3
source/images/自建简易FaaS/通过进程池管理子进程.svg
Normal file
After Width: | Height: | Size: 20 KiB |
17
source/js/dist/xfy.dev.js
vendored
@ -1,17 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
$(document).ready(function () {
|
||||
// 通过on选择动态DOM
|
||||
// 获取焦点时修改背景动画
|
||||
$("#comments").on("focus", ".v[data-class=v] .vwrap .vedit", function () {
|
||||
$(this).animate({
|
||||
"background-size": "0%"
|
||||
});
|
||||
}); // 失焦时添加回来
|
||||
|
||||
$("#comments").on("blur", ".v[data-class=v] .vwrap .vedit", function () {
|
||||
$(this).animate({
|
||||
"background-size": "20%"
|
||||
});
|
||||
});
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
$(document).ready(function () {
|
||||
// 通过on选择动态DOM
|
||||
// 获取焦点时修改背景动画
|
||||
$("#comments").on("focus", ".v[data-class=v] .vwrap .vedit", function () {
|
||||
$(this).animate({"background-size":"0%"});
|
||||
});
|
||||
// 失焦时添加回来
|
||||
$("#comments").on("blur", ".v[data-class=v] .vwrap .vedit", function () {
|
||||
$(this).animate({"background-size":"15%"});
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: pgp
|
||||
title: post
|
||||
date: 2020-02-23 17:03:44
|
||||
comment: 'valine'
|
||||
index: true
|
||||
---
|
||||
|
||||
<div class="markdown-body">
|
||||
|
5
source/tags/index.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
layout: tag
|
||||
index: true
|
||||
title: 所有标签
|
||||
---
|